158 lines
6.8 KiB
Text
158 lines
6.8 KiB
Text
|
|
-- ──────────────────────────────────────────────────────────────────────────
|
||
|
|
-- 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);
|