-- Meshkee CMS — invoices (platform / business billing) -- --------------------------------------------------------------------------- -- enums -- --------------------------------------------------------------------------- DO $$ BEGIN CREATE TYPE invoice_owner_scope AS ENUM ('platform', 'business'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE invoice_status AS ENUM ( 'draft', 'issued', 'paid', 'cancelled' ); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- --------------------------------------------------------------------------- -- invoice item templates (predefined line items) -- --------------------------------------------------------------------------- CREATE TABLE invoice_item_templates ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, owner_scope invoice_owner_scope NOT NULL, business_id BIGINT, title VARCHAR(255) NOT NULL, duration VARCHAR(100), worktime VARCHAR(100), description TEXT, price NUMERIC(12, 2) NOT NULL DEFAULT 0, discounted_price NUMERIC(12, 2), sort_order INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT invoice_item_templates_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE, CONSTRAINT invoice_item_templates_title_nonempty CHECK (char_length(trim(title)) > 0), CONSTRAINT invoice_item_templates_price_non_negative CHECK (price >= 0), CONSTRAINT invoice_item_templates_discounted_non_negative CHECK (discounted_price IS NULL OR discounted_price >= 0), CONSTRAINT invoice_item_templates_scope_business_check CHECK ( (owner_scope = 'platform' AND business_id IS NULL) OR (owner_scope = 'business' AND business_id IS NOT NULL) ) ); CREATE INDEX idx_invoice_item_templates_owner_scope ON invoice_item_templates (owner_scope, sort_order); CREATE INDEX idx_invoice_item_templates_business_id ON invoice_item_templates (business_id, sort_order) WHERE business_id IS NOT NULL; CREATE TRIGGER invoice_item_templates_set_updated_at BEFORE UPDATE ON invoice_item_templates FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- invoices -- --------------------------------------------------------------------------- CREATE TABLE invoices ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, business_id BIGINT NOT NULL, owner_scope invoice_owner_scope NOT NULL DEFAULT 'platform', issuer_business_id BIGINT, status invoice_status NOT NULL DEFAULT 'issued', notes TEXT, issued_by BIGINT, issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT invoices_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE, CONSTRAINT invoices_issuer_business_id_fkey FOREIGN KEY (issuer_business_id) REFERENCES businesses (id) ON DELETE SET NULL, CONSTRAINT invoices_issued_by_fkey FOREIGN KEY (issued_by) REFERENCES users (id) ON DELETE SET NULL, CONSTRAINT invoices_scope_issuer_check CHECK ( (owner_scope = 'platform' AND issuer_business_id IS NULL) OR (owner_scope = 'business' AND issuer_business_id IS NOT NULL) ) ); CREATE INDEX idx_invoices_business_created ON invoices (business_id, created_at DESC); CREATE INDEX idx_invoices_owner_scope ON invoices (owner_scope, created_at DESC); CREATE INDEX idx_invoices_issuer_business_id ON invoices (issuer_business_id, created_at DESC) WHERE issuer_business_id IS NOT NULL; CREATE INDEX idx_invoices_status ON invoices (status); CREATE TRIGGER invoices_set_updated_at BEFORE UPDATE ON invoices FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- invoice line items -- --------------------------------------------------------------------------- CREATE TABLE invoice_items ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, invoice_id BIGINT NOT NULL, template_id BIGINT, title VARCHAR(255) NOT NULL, duration VARCHAR(100), worktime VARCHAR(100), description TEXT, price NUMERIC(12, 2) NOT NULL DEFAULT 0, discounted_price NUMERIC(12, 2), sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT invoice_items_invoice_id_fkey FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE, CONSTRAINT invoice_items_template_id_fkey FOREIGN KEY (template_id) REFERENCES invoice_item_templates (id) ON DELETE SET NULL, CONSTRAINT invoice_items_title_nonempty CHECK (char_length(trim(title)) > 0), CONSTRAINT invoice_items_price_non_negative CHECK (price >= 0), CONSTRAINT invoice_items_discounted_non_negative CHECK (discounted_price IS NULL OR discounted_price >= 0) ); CREATE INDEX idx_invoice_items_invoice_id ON invoice_items (invoice_id, sort_order); CREATE INDEX idx_invoice_items_template_id ON invoice_items (template_id) WHERE template_id IS NOT NULL; CREATE TRIGGER invoice_items_set_updated_at BEFORE UPDATE ON invoice_items FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- --------------------------------------------------------------------------- -- permissions (for future business dashboard use) -- --------------------------------------------------------------------------- INSERT INTO permissions (name, slug, group_name, description) VALUES ('View invoices', 'invoices.read', 'invoices', 'View invoices'), ('Create invoices', 'invoices.create', 'invoices', 'Create invoices'), ('Update invoices', 'invoices.update', 'invoices', 'Update invoices'), ('Delete invoices', 'invoices.delete', 'invoices', 'Delete invoices'), ('View invoice templates', 'invoice_templates.read', 'invoices', 'View invoice item templates'), ('Manage invoice templates', 'invoice_templates.manage', 'invoices', 'Create, update, and delete invoice item templates') ON CONFLICT (slug) DO NOTHING; INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r JOIN permissions p ON p.slug LIKE 'invoices.%' OR p.slug LIKE 'invoice_templates.%' WHERE r.slug IN ('business_owner', 'owner', 'admin') ON CONFLICT DO NOTHING;