books/backend/Books.Api/Database/Migrations/011_Customers.sql
Nicolaj Hartmann 1f75c5d791 Add all backend domain, commands, repositories, and tests
This commit includes all previously untracked backend files:

Domain:
- Accounts, Attachments, BankConnections, Customers
- FiscalYears, Invoices, JournalEntryDrafts
- Orders, Products, UserAccess

Commands & Handlers:
- Full CQRS command structure for all domains

Repositories:
- PostgreSQL repositories for all read models
- Bank transaction and ledger repositories

GraphQL:
- Input types, scalars, and types for all entities
- Mutations and queries

Infrastructure:
- Banking integration (Enable Banking client)
- File storage, Invoicing, Reporting, SAF-T export
- Database migrations (003-029)

Tests:
- Integration tests for GraphQL endpoints
- Domain tests
- Invoicing and reporting tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:19:42 +01:00

84 lines
3.6 KiB
SQL

-- 011_Customers.sql
-- Creates customer read model tables and number series for invoicing support
-- Supports B2B (business with CVR) and B2C (private) customers
-- Each customer gets a sub-ledger account under 1900 Debitorer
-- Customer read models
CREATE TABLE IF NOT EXISTS customer_read_models (
-- EventFlow standard columns
aggregate_id VARCHAR(255) PRIMARY KEY,
create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_aggregate_sequence_number INT NOT NULL DEFAULT 1,
-- Business columns (snake_case)
company_id VARCHAR(255) NOT NULL,
customer_number VARCHAR(20) NOT NULL,
customer_type VARCHAR(20) NOT NULL, -- 'Business' or 'Private'
name VARCHAR(255) NOT NULL,
cvr VARCHAR(8), -- Required for Business, optional for Private
address VARCHAR(500),
postal_code VARCHAR(10),
city VARCHAR(100),
country VARCHAR(2) NOT NULL DEFAULT 'DK',
email VARCHAR(255),
phone VARCHAR(50),
payment_terms_days INT NOT NULL DEFAULT 30,
default_revenue_account_id VARCHAR(255),
sub_ledger_account_id VARCHAR(255) NOT NULL, -- Auto-created 1900-XXXX account
is_active BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT fk_customer_company
FOREIGN KEY (company_id) REFERENCES company_read_models(aggregate_id) ON DELETE CASCADE,
CONSTRAINT chk_customer_type
CHECK (customer_type IN ('Business', 'Private')),
CONSTRAINT chk_payment_terms
CHECK (payment_terms_days >= 0 AND payment_terms_days <= 365)
);
-- Unique customer number per company
CREATE UNIQUE INDEX IF NOT EXISTS idx_customer_number
ON customer_read_models(company_id, customer_number);
-- Lookup by company
CREATE INDEX IF NOT EXISTS idx_customer_company
ON customer_read_models(company_id);
-- Lookup by CVR (for B2B customers)
CREATE INDEX IF NOT EXISTS idx_customer_cvr
ON customer_read_models(cvr) WHERE cvr IS NOT NULL;
-- Search by name
CREATE INDEX IF NOT EXISTS idx_customer_name
ON customer_read_models(company_id, name);
-- Filter active customers
CREATE INDEX IF NOT EXISTS idx_customer_active
ON customer_read_models(company_id, is_active) WHERE is_active = TRUE;
-- Number series for sequential numbering (customers, invoices, credit notes)
-- Uses atomic UPSERT for thread-safe number generation
CREATE TABLE IF NOT EXISTS number_series (
id SERIAL PRIMARY KEY,
company_id VARCHAR(255) NOT NULL,
sequence_key VARCHAR(100) NOT NULL, -- e.g., 'customer', 'invoice-2024', 'creditnote-2024'
last_number INT NOT NULL DEFAULT 0,
prefix VARCHAR(20), -- e.g., 'INV-2024-', 'CN-2024-', ''
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(company_id, sequence_key),
CONSTRAINT fk_number_series_company
FOREIGN KEY (company_id) REFERENCES company_read_models(aggregate_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_number_series_company
ON number_series(company_id);
-- Comments for documentation
COMMENT ON TABLE customer_read_models IS 'Customer master data supporting B2B and B2C customers';
COMMENT ON COLUMN customer_read_models.customer_type IS 'Business (requires CVR) or Private (no CVR required)';
COMMENT ON COLUMN customer_read_models.sub_ledger_account_id IS 'Auto-created sub-ledger account (1900-XXXX) for customer receivables';
COMMENT ON COLUMN customer_read_models.payment_terms_days IS 'Default payment terms in days, used to calculate invoice due dates';
COMMENT ON TABLE number_series IS 'Sequential number generation for customers, invoices, and credit notes (Momsloven §52)';
COMMENT ON COLUMN number_series.sequence_key IS 'Identifies the sequence: customer, invoice-YYYY, creditnote-YYYY';