nxtgauge-backend-rust/crates/db/migrations/20260721040000_create_invoices.up.sql
Ashwin Kumar Sivakumar 63fd3f5135 feat: generate a GST invoice automatically after a successful Tracecoin/PayU purchase
crates/invoice (InvoiceService, GST computation, HTML rendering) was
fully built but never wired to anything and never had its tables —
invoices/invoice_line_items/billing_profiles/invoice_number_seq never
existed in any active migration (same root cause as everything else
this session: only in scripts/init-db.sql, which the real db-migrate
job never runs). Created them, matching InvoiceService's actual
columns exactly rather than init-db.sql's older, simpler invoices
shape.

Fixed a real bug in InvoiceService::create while at it: four money
fields (total, and three line-item amounts) were bound as i64 against
columns/read-models that are i32 everywhere else — would have failed
every insert with a Postgres type mismatch the first time this code
ever actually ran against a real table.

Wired invoice generation into apps/payments' PayU verify_payment
handler (the actual success callback) — right after the wallet is
credited, a one-line-item GST invoice is generated from the purchased
package and PayU's billing fields (firstname/email/phone), using a
new INVOICE_SELLER_* env-configurable seller identity. Generation
failures are logged, not surfaced to the buyer, since the payment and
wallet credit have already succeeded by that point.

Also added the missing user-facing endpoints to fetch what got
generated: GET /api/payments/invoices (list) and
GET /api/payments/invoices/{id} (detail + line items) — previously
only admin-side invoice viewing existed.

NOTE: the frontend (nxtgauge-frontend-solid, a separate repo) has an
existing invoice-viewing page at src/routes/dashboard/wallet/invoices/
but it calls /wallet/me/invoices (no /api/ prefix) via a different,
apparently-dead API helper (api.get, not apiFetch) that every other
live page avoids — same dead-code pattern as the earlier apps/leads
discovery. The live purchase flow (CreditsPage.tsx) has no invoice UI
at all yet. Not fixed here since it's out of this repo's scope this
session — flagging for a frontend pass.
2026-07-21 03:02:47 +05:30

90 lines
3.9 KiB
SQL

-- Backs crates/invoice (InvoiceService, BillingProfileRepo) — GST-compliant
-- invoice generation for Tracecoin/package purchases. Never created by any
-- active migration (same root cause as everything else fixed this session):
-- init-db.sql has a much simpler, older `invoices` shape that doesn't match
-- what InvoiceService actually reads/writes (no GST breakdown, seller/
-- customer snapshot, or line items), so it wasn't used as the reference here
-- — the schema below matches crates/invoice/src/lib.rs's Invoice/LineItem
-- structs and crates/invoice/src/service.rs's queries exactly.
CREATE TABLE IF NOT EXISTS invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_number VARCHAR(50) NOT NULL UNIQUE,
payment_id UUID NOT NULL REFERENCES payments(id),
user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'ISSUED',
currency VARCHAR(10) NOT NULL DEFAULT 'INR',
invoice_type VARCHAR(50) NOT NULL DEFAULT 'TRACECOIN_PURCHASE',
subtotal INTEGER NOT NULL,
discount_amount INTEGER NOT NULL DEFAULT 0,
cgst_rate DOUBLE PRECISION NOT NULL DEFAULT 0,
cgst_amount INTEGER NOT NULL DEFAULT 0,
sgst_rate DOUBLE PRECISION NOT NULL DEFAULT 0,
sgst_amount INTEGER NOT NULL DEFAULT 0,
igst_rate DOUBLE PRECISION NOT NULL DEFAULT 0,
igst_amount INTEGER NOT NULL DEFAULT 0,
total INTEGER NOT NULL,
reverse_charge BOOLEAN NOT NULL DEFAULT false,
seller_name VARCHAR(255) NOT NULL,
seller_address TEXT NOT NULL,
seller_gstin VARCHAR(20),
seller_pan VARCHAR(20),
seller_state_code VARCHAR(10),
place_of_supply_state VARCHAR(10),
customer_name VARCHAR(255),
customer_email VARCHAR(255),
customer_phone VARCHAR(20),
customer_billing_address TEXT,
customer_gstin VARCHAR(20),
customer_state_code VARCHAR(10),
discount_label VARCHAR(255),
notes TEXT,
pdf_object_key VARCHAR(500),
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
paid_at TIMESTAMPTZ,
voided_at TIMESTAMPTZ,
voided_by_user_id UUID REFERENCES users(id),
void_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_invoices_user_id ON invoices(user_id);
CREATE INDEX IF NOT EXISTS idx_invoices_payment_id ON invoices(payment_id);
CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
CREATE TABLE IF NOT EXISTS invoice_line_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
line_number INTEGER NOT NULL,
description VARCHAR(500) NOT NULL,
hsn_sac_code VARCHAR(20),
quantity DOUBLE PRECISION NOT NULL DEFAULT 1,
unit_price_inr INTEGER NOT NULL,
line_subtotal_inr INTEGER NOT NULL,
tax_rate DOUBLE PRECISION NOT NULL DEFAULT 0,
line_tax_inr INTEGER NOT NULL DEFAULT 0,
line_total_inr INTEGER NOT NULL,
metadata JSONB
);
CREATE INDEX IF NOT EXISTS idx_invoice_line_items_invoice_id ON invoice_line_items(invoice_id);
-- Default billing details a user has on file, used to prefill invoices
-- without asking again on every purchase (BillingProfileRepo).
CREATE TABLE IF NOT EXISTS billing_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
legal_name VARCHAR(255) NOT NULL,
email VARCHAR(255),
phone VARCHAR(20),
gstin VARCHAR(20),
pan VARCHAR(20),
billing_address TEXT NOT NULL,
state_code VARCHAR(10),
is_default BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_billing_profiles_user_id ON billing_profiles(user_id);
-- InvoiceService::create allocates invoice numbers from this sequence
-- (crates/invoice/src/service.rs: `SELECT nextval('invoice_number_seq')`).
CREATE SEQUENCE IF NOT EXISTS invoice_number_seq;