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.
90 lines
3.9 KiB
SQL
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;
|