feat(boc): v1.0 - Complete Business Operations Center

- Go backend API with full CRUD for all modules
- Rust analytics service with parallel processing
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables
- Redis cache, Kafka event streaming
- WebSocket hub, automation engine
- PDF generation, Resend email integration
- JWT auth, multi-tenant
- Docker Compose deployment
- Nginx reverse proxy

Refs: BOC-001
This commit is contained in:
Bernt (LandveX AI)
2026-07-12 13:21:10 +00:00
commit 67a69ab073
1130 changed files with 18263 additions and 0 deletions
@@ -0,0 +1,525 @@
-- BOC Initial Schema
-- Business Operations Center — Full schema for all modules
-- Created: 2026-07-12
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Core: Tenants (multi-tenant support)
CREATE TABLE IF NOT EXISTS boc_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
domain TEXT,
settings JSONB DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Core: Users
CREATE TABLE IF NOT EXISTS boc_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
email TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
avatar_url TEXT,
settings JSONB DEFAULT '{}',
last_login TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, email)
);
-- Core: Audit log (immutable)
CREATE TABLE IF NOT EXISTS boc_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES boc_users(id),
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
old_value JSONB,
new_value JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- CRM: Customers
CREATE TABLE IF NOT EXISTS boc_customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
company TEXT,
org_number TEXT,
address JSONB,
status TEXT NOT NULL DEFAULT 'lead',
source TEXT,
tags TEXT[] DEFAULT '{}',
metadata JSONB DEFAULT '{}',
assigned_to UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- CRM: Customer interactions
CREATE TABLE IF NOT EXISTS boc_customer_interactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- call, email, meeting, note, task
direction TEXT, -- inbound, outbound
subject TEXT,
content TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- CRM: Contacts (people within customer orgs)
CREATE TABLE IF NOT EXISTS boc_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
title TEXT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Sales: Deals
CREATE TABLE IF NOT EXISTS boc_deals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id),
contact_id UUID REFERENCES boc_contacts(id),
name TEXT NOT NULL,
description TEXT,
value DECIMAL(15,2) NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'open',
stage TEXT NOT NULL DEFAULT 'prospect',
probability INTEGER NOT NULL DEFAULT 0,
expected_close DATE,
actual_close TIMESTAMPTZ,
won_reason TEXT,
lost_reason TEXT,
tags TEXT[] DEFAULT '{}',
metadata JSONB DEFAULT '{}',
assigned_to UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Sales: Deal timeline / activities
CREATE TABLE IF NOT EXISTS boc_deal_activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
deal_id UUID REFERENCES boc_deals(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- call, email, meeting, proposal, note, stage_change
description TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Sales: Products/Services
CREATE TABLE IF NOT EXISTS boc_products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
sku TEXT,
price DECIMAL(15,2) NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
unit TEXT DEFAULT 'piece',
is_recurring BOOLEAN DEFAULT FALSE,
billing_period TEXT, -- monthly, quarterly, yearly
status TEXT NOT NULL DEFAULT 'active',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Sales: Deal line items
CREATE TABLE IF NOT EXISTS boc_deal_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
deal_id UUID REFERENCES boc_deals(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id),
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL DEFAULT 0,
discount DECIMAL(5,2) DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Finance: Invoices
CREATE TABLE IF NOT EXISTS boc_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id),
deal_id UUID REFERENCES boc_deals(id),
invoice_number TEXT NOT NULL,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(15,2) DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'draft',
due_date DATE,
paid_at TIMESTAMPTZ,
paid_amount DECIMAL(15,2) DEFAULT 0,
notes TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Finance: Invoice items
CREATE TABLE IF NOT EXISTS boc_invoice_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
invoice_id UUID REFERENCES boc_invoices(id) ON DELETE CASCADE,
description TEXT NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_rate DECIMAL(5,2) DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Finance: Payments
CREATE TABLE IF NOT EXISTS boc_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
invoice_id UUID REFERENCES boc_invoices(id),
amount DECIMAL(15,2) NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
method TEXT, -- bank_transfer, card, cash, stripe, etc
reference TEXT,
status TEXT NOT NULL DEFAULT 'completed',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Finance: Expenses
CREATE TABLE IF NOT EXISTS boc_expenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
category TEXT NOT NULL,
description TEXT NOT NULL,
amount DECIMAL(15,2) NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
vendor TEXT,
receipt_url TEXT,
status TEXT NOT NULL DEFAULT 'pending',
approved_by UUID REFERENCES boc_users(id),
approved_at TIMESTAMPTZ,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Finance: Budgets
CREATE TABLE IF NOT EXISTS boc_budgets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
fiscal_year INTEGER NOT NULL,
category TEXT,
amount DECIMAL(15,2) NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
spent DECIMAL(15,2) DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- HR: Employees
CREATE TABLE IF NOT EXISTS boc_employees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES boc_users(id),
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
department TEXT,
position TEXT,
employment_type TEXT DEFAULT 'full_time',
salary DECIMAL(15,2),
currency TEXT DEFAULT 'USD',
start_date DATE,
end_date DATE,
manager_id UUID REFERENCES boc_employees(id),
address JSONB,
bank_info JSONB,
documents JSONB DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- HR: Time off / Leave
CREATE TABLE IF NOT EXISTS boc_leaves (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
employee_id UUID REFERENCES boc_employees(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- vacation, sick, parental, unpaid
start_date DATE NOT NULL,
end_date DATE NOT NULL,
days DECIMAL(4,1) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
approved_by UUID REFERENCES boc_users(id),
approved_at TIMESTAMPTZ,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- HR: Timesheets
CREATE TABLE IF NOT EXISTS boc_timesheets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
employee_id UUID REFERENCES boc_employees(id) ON DELETE CASCADE,
date DATE NOT NULL,
hours DECIMAL(4,2) NOT NULL DEFAULT 0,
project TEXT,
task TEXT,
description TEXT,
status TEXT NOT NULL DEFAULT 'draft',
approved_by UUID REFERENCES boc_users(id),
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, employee_id, date)
);
-- Legal: Contracts
CREATE TABLE IF NOT EXISTS boc_contracts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
title TEXT NOT NULL,
counterparty TEXT NOT NULL,
type TEXT NOT NULL, -- service, employment, nda, partnership, etc
status TEXT NOT NULL DEFAULT 'draft',
value DECIMAL(15,2),
currency TEXT DEFAULT 'USD',
start_date DATE,
end_date DATE,
renewal_date DATE,
document_url TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Legal: Contract reminders
CREATE TABLE IF NOT EXISTS boc_contract_reminders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
contract_id UUID REFERENCES boc_contracts(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- renewal, expiration, payment, review
due_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Marketing: Campaigns
CREATE TABLE IF NOT EXISTS boc_campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL, -- email, social, content, event, ad
status TEXT NOT NULL DEFAULT 'draft',
budget DECIMAL(15,2),
spent DECIMAL(15,2) DEFAULT 0,
currency TEXT DEFAULT 'USD',
start_date DATE,
end_date DATE,
metrics JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Marketing: Content items
CREATE TABLE IF NOT EXISTS boc_content (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
campaign_id UUID REFERENCES boc_campaigns(id),
title TEXT NOT NULL,
type TEXT NOT NULL, -- blog, social, email, video, whitepaper
status TEXT NOT NULL DEFAULT 'draft',
publish_at TIMESTAMPTZ,
published_at TIMESTAMPTZ,
url TEXT,
metrics JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Support: Tickets
CREATE TABLE IF NOT EXISTS boc_tickets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id),
contact_id UUID REFERENCES boc_contacts(id),
subject TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
priority TEXT NOT NULL DEFAULT 'medium',
category TEXT,
source TEXT, -- email, chat, phone, web
assigned_to UUID REFERENCES boc_users(id),
resolved_at TIMESTAMPTZ,
resolution TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Support: Ticket comments
CREATE TABLE IF NOT EXISTS boc_ticket_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
ticket_id UUID REFERENCES boc_tickets(id) ON DELETE CASCADE,
content TEXT NOT NULL,
is_internal BOOLEAN DEFAULT FALSE,
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Automation: Workflows
CREATE TABLE IF NOT EXISTS boc_workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
trigger_type TEXT NOT NULL, -- schedule, event, webhook, manual
trigger_config JSONB DEFAULT '{}',
actions JSONB NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'active',
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
run_count INTEGER DEFAULT 0,
fail_count INTEGER DEFAULT 0,
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Automation: Workflow runs
CREATE TABLE IF NOT EXISTS boc_workflow_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
workflow_id UUID REFERENCES boc_workflows(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'running',
input JSONB DEFAULT '{}',
output JSONB DEFAULT '{}',
error TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- Automation: Scheduled jobs
CREATE TABLE IF NOT EXISTS boc_scheduled_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
cron_expr TEXT NOT NULL,
timezone TEXT DEFAULT 'UTC',
job_type TEXT NOT NULL, -- report, reminder, sync, cleanup, backup
job_config JSONB DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
run_count INTEGER DEFAULT 0,
fail_count INTEGER DEFAULT 0,
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Automation: Scheduled job runs
CREATE TABLE IF NOT EXISTS boc_scheduled_job_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
job_id UUID REFERENCES boc_scheduled_jobs(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'running',
output JSONB DEFAULT '{}',
error TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON boc_audit_log(tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_customers_tenant ON boc_customers(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_customers_assigned ON boc_customers(assigned_to);
CREATE INDEX IF NOT EXISTS idx_interactions_customer ON boc_customer_interactions(customer_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_deals_tenant ON boc_deals(tenant_id, status, stage);
CREATE INDEX IF NOT EXISTS idx_deals_customer ON boc_deals(customer_id);
CREATE INDEX IF NOT EXISTS idx_deals_assigned ON boc_deals(assigned_to);
CREATE INDEX IF NOT EXISTS idx_deals_expected_close ON boc_deals(expected_close);
CREATE INDEX IF NOT EXISTS idx_invoices_tenant ON boc_invoices(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_invoices_due ON boc_invoices(due_date);
CREATE INDEX IF NOT EXISTS idx_expenses_tenant ON boc_expenses(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_employees_tenant ON boc_employees(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_leaves_employee ON boc_leaves(employee_id, start_date);
CREATE INDEX IF NOT EXISTS idx_timesheets_employee ON boc_timesheets(employee_id, date);
CREATE INDEX IF NOT EXISTS idx_contracts_tenant ON boc_contracts(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_contracts_renewal ON boc_contracts(renewal_date);
CREATE INDEX IF NOT EXISTS idx_tickets_tenant ON boc_tickets(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON boc_tickets(assigned_to);
CREATE INDEX IF NOT EXISTS idx_workflows_tenant ON boc_workflows(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_tenant ON boc_scheduled_jobs(tenant_id, status);
-- Functions
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Triggers for updated_at
DO $$
BEGIN
CREATE TRIGGER update_boc_tenants_updated_at BEFORE UPDATE ON boc_tenants FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_users_updated_at BEFORE UPDATE ON boc_users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_customers_updated_at BEFORE UPDATE ON boc_customers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_deals_updated_at BEFORE UPDATE ON boc_deals FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_invoices_updated_at BEFORE UPDATE ON boc_invoices FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_expenses_updated_at BEFORE UPDATE ON boc_expenses FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_budgets_updated_at BEFORE UPDATE ON boc_budgets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_employees_updated_at BEFORE UPDATE ON boc_employees FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_leaves_updated_at BEFORE UPDATE ON boc_leaves FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_timesheets_updated_at BEFORE UPDATE ON boc_timesheets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_contracts_updated_at BEFORE UPDATE ON boc_contracts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_campaigns_updated_at BEFORE UPDATE ON boc_campaigns FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_content_updated_at BEFORE UPDATE ON boc_content FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_tickets_updated_at BEFORE UPDATE ON boc_tickets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_workflows_updated_at BEFORE UPDATE ON boc_workflows FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_scheduled_jobs_updated_at BEFORE UPDATE ON boc_scheduled_jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
EXCEPTION WHEN duplicate_object THEN
-- Triggers already exist, ignore
END;
$$;
@@ -0,0 +1,437 @@
-- BOC Schema Extension: Quotes, Orders, Suppliers, Inventory
-- Created: 2026-07-12
-- ============================================
-- SALES: Quotes (Offert)
-- ============================================
CREATE TABLE IF NOT EXISTS boc_quotes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE,
contact_id UUID REFERENCES boc_contacts(id),
quote_number TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, accepted, rejected, expired
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(15,2) DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
valid_until DATE,
accepted_at TIMESTAMPTZ,
converted_to_order_id UUID,
notes TEXT,
terms TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_quote_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
quote_id UUID REFERENCES boc_quotes(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id),
description TEXT NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_rate DECIMAL(5,2) DEFAULT 0,
discount DECIMAL(5,2) DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- SALES: Orders
-- ============================================
CREATE TABLE IF NOT EXISTS boc_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE,
quote_id UUID REFERENCES boc_quotes(id),
order_number TEXT NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft', -- draft, confirmed, processing, shipped, delivered, cancelled
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(15,2) DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
delivery_date DATE,
shipped_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
tracking_number TEXT,
notes TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
order_id UUID REFERENCES boc_orders(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id),
description TEXT NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_rate DECIMAL(5,2) DEFAULT 0,
discount DECIMAL(5,2) DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
delivered_qty DECIMAL(10,2) DEFAULT 0,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- PURCHASE: Suppliers
-- ============================================
CREATE TABLE IF NOT EXISTS boc_suppliers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT,
phone TEXT,
org_number TEXT,
address JSONB,
payment_terms TEXT DEFAULT '30 days',
bank_account TEXT,
bankgiro TEXT,
postgiro TEXT,
currency TEXT DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'active',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- PURCHASE: Purchase Orders
-- ============================================
CREATE TABLE IF NOT EXISTS boc_purchase_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
supplier_id UUID REFERENCES boc_suppliers(id) ON DELETE CASCADE,
po_number TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, confirmed, received, invoiced, paid
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(15,2) DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
expected_delivery DATE,
received_at TIMESTAMPTZ,
notes TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_purchase_order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
po_id UUID REFERENCES boc_purchase_orders(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id),
description TEXT NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit_price DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_rate DECIMAL(5,2) DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
received_qty DECIMAL(10,2) DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- PURCHASE: Supplier Invoices (Leverantörsfakturor)
-- ============================================
CREATE TABLE IF NOT EXISTS boc_supplier_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
supplier_id UUID REFERENCES boc_suppliers(id),
po_id UUID REFERENCES boc_purchase_orders(id),
invoice_number TEXT NOT NULL,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_amount DECIMAL(15,2) DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'draft', -- draft, received, approved, paid, disputed
due_date DATE,
paid_at TIMESTAMPTZ,
paid_amount DECIMAL(15,2) DEFAULT 0,
ocr_number TEXT,
notes TEXT,
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- INVENTORY: Stock / Warehouse
-- ============================================
CREATE TABLE IF NOT EXISTS boc_warehouses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
location TEXT,
address JSONB,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id) ON DELETE CASCADE,
warehouse_id UUID REFERENCES boc_warehouses(id),
quantity DECIMAL(10,2) NOT NULL DEFAULT 0,
reserved_qty DECIMAL(10,2) DEFAULT 0,
reorder_point DECIMAL(10,2) DEFAULT 0,
reorder_qty DECIMAL(10,2) DEFAULT 0,
unit_cost DECIMAL(15,2) DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, product_id, warehouse_id)
);
CREATE TABLE IF NOT EXISTS boc_inventory_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
product_id UUID REFERENCES boc_products(id),
warehouse_id UUID REFERENCES boc_warehouses(id),
type TEXT NOT NULL, -- in, out, adjustment, transfer
quantity DECIMAL(10,2) NOT NULL,
reference_type TEXT, -- order, po, adjustment
reference_id TEXT,
notes TEXT,
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- RECURRING: Subscriptions & Recurring Invoices
-- ============================================
CREATE TABLE IF NOT EXISTS boc_subscription_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
product_id UUID REFERENCES boc_products(id),
interval TEXT NOT NULL DEFAULT 'monthly', -- weekly, monthly, quarterly, yearly
interval_count INTEGER DEFAULT 1,
price DECIMAL(15,2) NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
trial_days INTEGER DEFAULT 0,
setup_fee DECIMAL(15,2) DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE,
plan_id UUID REFERENCES boc_subscription_plans(id),
status TEXT NOT NULL DEFAULT 'active', -- active, paused, cancelled, expired
start_date DATE NOT NULL,
end_date DATE,
trial_end DATE,
current_period_start DATE,
current_period_end DATE,
price DECIMAL(15,2) NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_recurring_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
customer_id UUID REFERENCES boc_customers(id),
subscription_id UUID REFERENCES boc_subscriptions(id),
plan_id UUID REFERENCES boc_subscription_plans(id),
invoice_number TEXT,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL DEFAULT 'pending', -- pending, generated, sent, paid, failed
scheduled_date DATE NOT NULL,
generated_at TIMESTAMPTZ,
sent_at TIMESTAMPTZ,
error TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- EXPENSES: Receipts & OCR
-- ============================================
CREATE TABLE IF NOT EXISTS boc_receipts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
employee_id UUID REFERENCES boc_employees(id),
expense_id UUID REFERENCES boc_expenses(id),
image_url TEXT NOT NULL,
ocr_text TEXT,
ocr_data JSONB DEFAULT '{}', -- extracted: amount, date, vendor, category
ocr_confidence DECIMAL(5,2) DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending', -- pending, processed, failed
processed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- PAYROLL: Basic structure
-- ============================================
CREATE TABLE IF NOT EXISTS boc_payroll_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
pay_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'draft', -- draft, processing, approved, paid
total_gross DECIMAL(15,2) DEFAULT 0,
total_tax DECIMAL(15,2) DEFAULT 0,
total_net DECIMAL(15,2) DEFAULT 0,
total_employer_tax DECIMAL(15,2) DEFAULT 0,
currency TEXT DEFAULT 'USD',
metadata JSONB DEFAULT '{}',
created_by UUID REFERENCES boc_users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_payroll_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
payroll_run_id UUID REFERENCES boc_payroll_runs(id) ON DELETE CASCADE,
employee_id UUID REFERENCES boc_employees(id),
gross_salary DECIMAL(15,2) NOT NULL DEFAULT 0,
tax_deduction DECIMAL(15,2) DEFAULT 0,
social_fees DECIMAL(15,2) DEFAULT 0,
pension DECIMAL(15,2) DEFAULT 0,
other_deductions DECIMAL(15,2) DEFAULT 0,
net_salary DECIMAL(15,2) DEFAULT 0,
hours_worked DECIMAL(5,2) DEFAULT 0,
vacation_days_used DECIMAL(4,1) DEFAULT 0,
sick_days DECIMAL(4,1) DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- BANK: Accounts & Transactions
-- ============================================
CREATE TABLE IF NOT EXISTS boc_bank_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
bank_name TEXT NOT NULL,
account_number TEXT NOT NULL,
iban TEXT,
bic TEXT,
currency TEXT DEFAULT 'USD',
balance DECIMAL(15,2) DEFAULT 0,
is_default BOOLEAN DEFAULT FALSE,
status TEXT NOT NULL DEFAULT 'active',
last_sync TIMESTAMPTZ,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_bank_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
account_id UUID REFERENCES boc_bank_accounts(id) ON DELETE CASCADE,
transaction_date DATE NOT NULL,
amount DECIMAL(15,2) NOT NULL,
currency TEXT DEFAULT 'USD',
description TEXT,
counterparty TEXT,
reference TEXT,
external_id TEXT,
status TEXT NOT NULL DEFAULT 'unmatched', -- unmatched, matched, reconciled
matched_to_type TEXT, -- invoice, expense, payroll
matched_to_id UUID,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- PROJECTS
-- ============================================
CREATE TABLE IF NOT EXISTS boc_projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
customer_id UUID REFERENCES boc_customers(id),
status TEXT NOT NULL DEFAULT 'active', -- active, completed, on_hold, cancelled
budget DECIMAL(15,2) DEFAULT 0,
spent DECIMAL(15,2) DEFAULT 0,
currency TEXT DEFAULT 'USD',
start_date DATE,
end_date DATE,
manager_id UUID REFERENCES boc_employees(id),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_project_times (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
project_id UUID REFERENCES boc_projects(id) ON DELETE CASCADE,
employee_id UUID REFERENCES boc_employees(id),
date DATE NOT NULL,
hours DECIMAL(4,2) NOT NULL DEFAULT 0,
description TEXT,
billable BOOLEAN DEFAULT TRUE,
hourly_rate DECIMAL(15,2) DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS boc_project_expenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE,
project_id UUID REFERENCES boc_projects(id) ON DELETE CASCADE,
expense_id UUID REFERENCES boc_expenses(id),
amount DECIMAL(15,2) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_quotes_tenant ON boc_quotes(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_quotes_customer ON boc_quotes(customer_id);
CREATE INDEX IF NOT EXISTS idx_orders_tenant ON boc_orders(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_orders_customer ON boc_orders(customer_id);
CREATE INDEX IF NOT EXISTS idx_suppliers_tenant ON boc_suppliers(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_po_tenant ON boc_purchase_orders(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_supplier_invoices_tenant ON boc_supplier_invoices(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_inventory_product ON boc_inventory(product_id, warehouse_id);
CREATE INDEX IF NOT EXISTS idx_inventory_movements ON boc_inventory_movements(product_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON boc_subscriptions(customer_id, status);
CREATE INDEX IF NOT EXISTS idx_recurring_invoices ON boc_recurring_invoices(scheduled_date, status);
CREATE INDEX IF NOT EXISTS idx_receipts_status ON boc_receipts(status);
CREATE INDEX IF NOT EXISTS idx_payroll_runs ON boc_payroll_runs(period_start, status);
CREATE INDEX IF NOT EXISTS idx_bank_transactions ON boc_bank_transactions(account_id, transaction_date DESC);
CREATE INDEX IF NOT EXISTS idx_projects_tenant ON boc_projects(tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_project_times ON boc_project_times(project_id, date);
-- Triggers for updated_at
DO $$
BEGIN
CREATE TRIGGER update_boc_quotes_updated_at BEFORE UPDATE ON boc_quotes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_orders_updated_at BEFORE UPDATE ON boc_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_suppliers_updated_at BEFORE UPDATE ON boc_suppliers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_purchase_orders_updated_at BEFORE UPDATE ON boc_purchase_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_supplier_invoices_updated_at BEFORE UPDATE ON boc_supplier_invoices FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_inventory_updated_at BEFORE UPDATE ON boc_inventory FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_subscription_plans_updated_at BEFORE UPDATE ON boc_subscription_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_subscriptions_updated_at BEFORE UPDATE ON boc_subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_payroll_runs_updated_at BEFORE UPDATE ON boc_payroll_runs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_bank_accounts_updated_at BEFORE UPDATE ON boc_bank_accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_boc_projects_updated_at BEFORE UPDATE ON boc_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
EXCEPTION WHEN duplicate_object THEN
-- Triggers already exist, ignore
END;
$$;