Migración desde workspace-v2/projects/template-saas/apps/database Este repositorio es parte del estándar multi-repo v2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
175 lines
5.2 KiB
PL/PgSQL
175 lines
5.2 KiB
PL/PgSQL
-- ============================================
|
|
-- TEMPLATE-SAAS: Invoices & Payments
|
|
-- Schema: billing
|
|
-- Version: 1.0.0
|
|
-- ============================================
|
|
|
|
CREATE TABLE billing.invoices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
subscription_id UUID REFERENCES billing.subscriptions(id),
|
|
|
|
-- Invoice number
|
|
invoice_number VARCHAR(50) UNIQUE NOT NULL,
|
|
|
|
-- Stripe
|
|
stripe_invoice_id VARCHAR(255) UNIQUE,
|
|
|
|
-- Status
|
|
status billing.invoice_status DEFAULT 'draft' NOT NULL,
|
|
|
|
-- Amounts
|
|
subtotal DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
|
tax DECIMAL(10, 2) DEFAULT 0,
|
|
discount DECIMAL(10, 2) DEFAULT 0,
|
|
total DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
|
amount_paid DECIMAL(10, 2) DEFAULT 0,
|
|
amount_due DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
|
|
-- Dates
|
|
invoice_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
due_date DATE,
|
|
paid_at TIMESTAMPTZ,
|
|
|
|
-- Period
|
|
period_start DATE,
|
|
period_end DATE,
|
|
|
|
-- PDF
|
|
invoice_pdf_url VARCHAR(500),
|
|
hosted_invoice_url VARCHAR(500),
|
|
|
|
-- Customer info at time of invoice
|
|
customer_name VARCHAR(255),
|
|
customer_email VARCHAR(255),
|
|
billing_address JSONB,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
notes TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
|
);
|
|
|
|
-- Invoice line items
|
|
CREATE TABLE billing.invoice_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
|
|
|
|
-- Description
|
|
description VARCHAR(500) NOT NULL,
|
|
quantity INT DEFAULT 1,
|
|
|
|
-- Pricing
|
|
unit_amount DECIMAL(10, 2) NOT NULL,
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
|
|
-- Period (for subscription items)
|
|
period_start DATE,
|
|
period_end DATE,
|
|
|
|
-- Stripe
|
|
stripe_invoice_item_id VARCHAR(255),
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
|
);
|
|
|
|
-- Payments
|
|
CREATE TABLE billing.payments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
invoice_id UUID REFERENCES billing.invoices(id),
|
|
|
|
-- Stripe
|
|
stripe_payment_intent_id VARCHAR(255) UNIQUE,
|
|
stripe_charge_id VARCHAR(255),
|
|
|
|
-- Amount
|
|
amount DECIMAL(10, 2) NOT NULL,
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
|
|
-- Status
|
|
status billing.payment_status DEFAULT 'pending' NOT NULL,
|
|
|
|
-- Payment method
|
|
payment_method_type VARCHAR(50), -- 'card', 'bank_transfer', etc.
|
|
payment_method_last4 VARCHAR(4),
|
|
payment_method_brand VARCHAR(50),
|
|
|
|
-- Failure info
|
|
failure_code VARCHAR(100),
|
|
failure_message TEXT,
|
|
|
|
-- Dates
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
succeeded_at TIMESTAMPTZ,
|
|
failed_at TIMESTAMPTZ,
|
|
refunded_at TIMESTAMPTZ,
|
|
|
|
-- Refund info
|
|
refund_amount DECIMAL(10, 2),
|
|
refund_reason VARCHAR(500)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_invoices_tenant ON billing.invoices(tenant_id);
|
|
CREATE INDEX idx_invoices_subscription ON billing.invoices(subscription_id);
|
|
CREATE INDEX idx_invoices_status ON billing.invoices(status);
|
|
CREATE INDEX idx_invoices_stripe ON billing.invoices(stripe_invoice_id);
|
|
CREATE INDEX idx_invoice_items_invoice ON billing.invoice_items(invoice_id);
|
|
CREATE INDEX idx_payments_tenant ON billing.payments(tenant_id);
|
|
CREATE INDEX idx_payments_invoice ON billing.payments(invoice_id);
|
|
CREATE INDEX idx_payments_stripe ON billing.payments(stripe_payment_intent_id);
|
|
|
|
-- RLS
|
|
ALTER TABLE billing.invoices ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE billing.invoice_items ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE billing.payments ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY invoices_tenant_isolation ON billing.invoices
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
CREATE POLICY invoice_items_tenant_isolation ON billing.invoice_items
|
|
USING (invoice_id IN (
|
|
SELECT id FROM billing.invoices
|
|
WHERE tenant_id = current_setting('app.current_tenant_id', true)::UUID
|
|
));
|
|
|
|
CREATE POLICY payments_tenant_isolation ON billing.payments
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Trigger
|
|
CREATE TRIGGER trg_invoices_updated_at
|
|
BEFORE UPDATE ON billing.invoices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION billing.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_payments_updated_at
|
|
BEFORE UPDATE ON billing.payments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION billing.update_updated_at();
|
|
|
|
-- Invoice number sequence
|
|
CREATE SEQUENCE billing.invoice_number_seq START 1000;
|
|
|
|
-- Function to generate invoice number
|
|
CREATE OR REPLACE FUNCTION billing.generate_invoice_number()
|
|
RETURNS VARCHAR AS $$
|
|
BEGIN
|
|
RETURN 'INV-' || TO_CHAR(NOW(), 'YYYYMM') || '-' || LPAD(nextval('billing.invoice_number_seq')::TEXT, 6, '0');
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Comments
|
|
COMMENT ON TABLE billing.invoices IS 'Invoices generated for billing';
|
|
COMMENT ON TABLE billing.invoice_items IS 'Line items within an invoice';
|
|
COMMENT ON TABLE billing.payments IS 'Payment records and attempts';
|