nxtgauge-backend-rust/crates/db/migrations/20260627020000_invoice_hardening.up.sql.skip

158 lines
6.8 KiB
Text
Raw Normal View History

-- ──────────────────────────────────────────────────────────────────────────
-- Invoice hardening and feature additions
-- ──────────────────────────────────────────────────────────────────────────
-- Adds:
-- * GST breakdown (CGST/SGST for intra-state, IGST for inter-state)
-- * Seller and customer billing details
-- * Currency, place-of-supply, invoice-type, notes
-- * paid_at / voided_at / void_reason for the full lifecycle
-- * A new `invoice_line_items` table so each invoice can hold multiple
-- lines (one per package, adjustment, discount, etc.) — required for
-- GST-compliant Indian e-invoicing.
-- * A `billing_profiles` table to store reusable customer billing
-- details (so an invoice doesn't have to capture them on every sale).
-- * A `pdf_object_key` column for the generated PDF stored in B2.
-- * CHECK constraints to keep numerics sane.
-- ──────────────────────────────────────────────────────────────────────────
-- 1. Invoice lifecycle enum
ALTER TABLE invoices
DROP CONSTRAINT IF EXISTS invoices_status_valid;
ALTER TABLE invoices
ADD CONSTRAINT invoices_status_valid
CHECK (status IN ('DRAFT', 'ISSUED', 'PAID', 'VOID', 'REFUNDED'));
-- 2. Currency
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS currency CHAR(3) NOT NULL DEFAULT 'INR';
-- 3. Invoice type
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS invoice_type VARCHAR(20) NOT NULL DEFAULT 'TAX_INVOICE';
ALTER TABLE invoices
DROP CONSTRAINT IF EXISTS invoices_invoice_type_valid;
ALTER TABLE invoices
ADD CONSTRAINT invoices_invoice_type_valid
CHECK (invoice_type IN ('TAX_INVOICE', 'BILL_OF_SUPPLY', 'CREDIT_NOTE', 'DEBIT_NOTE'));
-- 4. GST breakdown (percentages as basis points * 100 to keep numerics)
-- Stored as numeric so admin tools can display "9% / 9%" or "18% / 18%".
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS cgst_rate NUMERIC(5,2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS cgst_amount INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS sgst_rate NUMERIC(5,2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS sgst_amount INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS igst_rate NUMERIC(5,2) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS igst_amount INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS place_of_supply_state VARCHAR(2),
ADD COLUMN IF NOT EXISTS seller_state VARCHAR(2),
ADD COLUMN IF NOT EXISTS reverse_charge BOOLEAN NOT NULL DEFAULT false;
-- 5. Seller (Nxtgauge) details
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS seller_name VARCHAR(255) NOT NULL DEFAULT 'Nxtgauge Technologies Pvt. Ltd.',
ADD COLUMN IF NOT EXISTS seller_address TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS seller_gstin VARCHAR(15),
ADD COLUMN IF NOT EXISTS seller_pan VARCHAR(10),
ADD COLUMN IF NOT EXISTS seller_state_code VARCHAR(2);
-- 6. Customer billing details (snapshot at the time of issue)
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS customer_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS customer_email VARCHAR(255),
ADD COLUMN IF NOT EXISTS customer_phone VARCHAR(20),
ADD COLUMN IF NOT EXISTS customer_billing_address TEXT,
ADD COLUMN IF NOT EXISTS customer_gstin VARCHAR(15),
ADD COLUMN IF NOT EXISTS customer_state_code VARCHAR(2);
-- 7. Discount / adjustment fields
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS discount_amount INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS discount_label VARCHAR(255),
ADD COLUMN IF NOT EXISTS notes TEXT;
-- 8. Lifecycle timestamps
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS paid_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS voided_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS voided_by_user_id UUID REFERENCES users(id),
ADD COLUMN IF NOT EXISTS void_reason TEXT,
ADD COLUMN IF NOT EXISTS pdf_object_key VARCHAR(500),
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
-- Backfill `updated_at` on existing rows.
UPDATE invoices SET updated_at = COALESCE(updated_at, issued_at, NOW());
-- 9. CHECKs
ALTER TABLE invoices
DROP CONSTRAINT IF EXISTS invoices_amounts_nonneg;
ALTER TABLE invoices
ADD CONSTRAINT invoices_amounts_nonneg
CHECK (
subtotal >= 0
AND gst_amount >= 0
AND total >= 0
AND cgst_amount >= 0
AND sgst_amount >= 0
AND igst_amount >= 0
AND discount_amount >= 0
);
ALTER TABLE invoices
DROP CONSTRAINT IF EXISTS invoices_total_matches;
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_matches
CHECK (total = subtotal - discount_amount + gst_amount);
-- 10. Line items
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 TEXT NOT NULL,
hsn_sac_code VARCHAR(20),
quantity NUMERIC(12,3) NOT NULL DEFAULT 1,
unit_price_inr INTEGER NOT NULL,
line_subtotal_inr INTEGER NOT NULL,
tax_rate NUMERIC(5,2) NOT NULL DEFAULT 0,
line_tax_inr INTEGER NOT NULL DEFAULT 0,
line_total_inr INTEGER NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(invoice_id, line_number)
);
CREATE INDEX IF NOT EXISTS idx_invoice_line_items_invoice
ON invoice_line_items(invoice_id);
-- 11. Customer billing profiles
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(15),
pan VARCHAR(10),
billing_address TEXT NOT NULL,
state_code VARCHAR(2),
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
ON billing_profiles(user_id);
-- At most one default per user
CREATE UNIQUE INDEX IF NOT EXISTS uq_billing_profiles_default_per_user
ON billing_profiles(user_id)
WHERE is_default = true;
-- 12. Invoice sequence for human-readable numbers
CREATE SEQUENCE IF NOT EXISTS invoice_number_seq START 100000;
-- 13. Indexes on invoices
CREATE INDEX IF NOT EXISTS idx_invoices_status_issued_at
ON invoices(status, issued_at DESC);
CREATE INDEX IF NOT EXISTS idx_invoices_payment_id
ON invoices(payment_id);