feat: add flexible billing frequency support for invoices
Assessment groups can now define billing frequency (monthly, quarterly, annual) with configurable due months and due day. Invoice generation respects each group's schedule - only generating invoices when the selected month is a billing month for that group. Adds a generation preview showing which groups will be billed, period tracking on invoices, and billing period context in the payments UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
57
db/migrations/011-invoice-billing-frequency.sql
Normal file
57
db/migrations/011-invoice-billing-frequency.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Migration 011: Add billing frequency support to invoices
|
||||
-- Adds due_months and due_day to assessment_groups
|
||||
-- Adds period_start, period_end, assessment_group_id to invoices
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_schema TEXT;
|
||||
BEGIN
|
||||
FOR v_schema IN
|
||||
SELECT schema_name FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'tenant_%'
|
||||
LOOP
|
||||
-- Add due_months and due_day to assessment_groups
|
||||
EXECUTE format('
|
||||
ALTER TABLE %I.assessment_groups
|
||||
ADD COLUMN IF NOT EXISTS due_months INTEGER[] DEFAULT ''{1,2,3,4,5,6,7,8,9,10,11,12}'',
|
||||
ADD COLUMN IF NOT EXISTS due_day INTEGER DEFAULT 1
|
||||
', v_schema);
|
||||
|
||||
-- Add period tracking and assessment group link to invoices
|
||||
EXECUTE format('
|
||||
ALTER TABLE %I.invoices
|
||||
ADD COLUMN IF NOT EXISTS period_start DATE,
|
||||
ADD COLUMN IF NOT EXISTS period_end DATE,
|
||||
ADD COLUMN IF NOT EXISTS assessment_group_id UUID
|
||||
', v_schema);
|
||||
|
||||
-- Backfill due_months based on existing frequency values
|
||||
EXECUTE format('
|
||||
UPDATE %I.assessment_groups
|
||||
SET due_months = CASE frequency
|
||||
WHEN ''quarterly'' THEN ''{1,4,7,10}''::INTEGER[]
|
||||
WHEN ''annual'' THEN ''{1}''::INTEGER[]
|
||||
ELSE ''{1,2,3,4,5,6,7,8,9,10,11,12}''::INTEGER[]
|
||||
END
|
||||
WHERE due_months IS NULL OR due_months = ''{1,2,3,4,5,6,7,8,9,10,11,12}''
|
||||
AND frequency != ''monthly''
|
||||
', v_schema);
|
||||
|
||||
-- Backfill period_start/period_end for existing invoices (all monthly)
|
||||
EXECUTE format('
|
||||
UPDATE %I.invoices
|
||||
SET period_start = invoice_date,
|
||||
period_end = (invoice_date + INTERVAL ''1 month'' - INTERVAL ''1 day'')::DATE
|
||||
WHERE period_start IS NULL AND invoice_type = ''regular_assessment''
|
||||
', v_schema);
|
||||
|
||||
-- Backfill assessment_group_id on existing invoices from units
|
||||
EXECUTE format('
|
||||
UPDATE %I.invoices i
|
||||
SET assessment_group_id = u.assessment_group_id
|
||||
FROM %I.units u
|
||||
WHERE i.unit_id = u.id AND i.assessment_group_id IS NULL
|
||||
', v_schema, v_schema);
|
||||
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -204,7 +204,10 @@ CREATE TABLE IF NOT EXISTS %I.assessment_groups (
|
||||
special_assessment DECIMAL(10,2) DEFAULT 0.00,
|
||||
unit_count INTEGER DEFAULT 0,
|
||||
frequency VARCHAR(20) DEFAULT ''monthly'',
|
||||
due_months INTEGER[] DEFAULT ''{1,2,3,4,5,6,7,8,9,10,11,12}'',
|
||||
due_day INTEGER DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)', v_schema);
|
||||
@@ -244,6 +247,9 @@ CREATE TABLE IF NOT EXISTS %I.invoices (
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
amount_paid DECIMAL(10,2) DEFAULT 0.00,
|
||||
status VARCHAR(20) DEFAULT ''draft'',
|
||||
period_start DATE,
|
||||
period_end DATE,
|
||||
assessment_group_id UUID,
|
||||
journal_entry_id UUID,
|
||||
sent_at TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
@@ -443,10 +449,10 @@ END LOOP;
|
||||
-- ============================================================
|
||||
-- 4b. Seed Assessment Groups
|
||||
-- ============================================================
|
||||
EXECUTE format('INSERT INTO %I.assessment_groups (name, description, regular_assessment, special_assessment, unit_count) VALUES
|
||||
(''Single Family Homes'', ''Standard single family detached homes (Units 1-20)'', 350.00, 0.00, 20),
|
||||
(''Patio Homes'', ''Medium-sized patio homes (Units 21-35)'', 425.00, 0.00, 15),
|
||||
(''Estate Lots'', ''Large estate lots (Units 36-50)'', 500.00, 75.00, 15)
|
||||
EXECUTE format('INSERT INTO %I.assessment_groups (name, description, regular_assessment, special_assessment, unit_count, frequency, due_months, due_day) VALUES
|
||||
(''Single Family Homes'', ''Standard single family detached homes (Units 1-20)'', 350.00, 0.00, 20, ''monthly'', ''{1,2,3,4,5,6,7,8,9,10,11,12}'', 15),
|
||||
(''Patio Homes'', ''Medium-sized patio homes (Units 21-35)'', 1275.00, 0.00, 15, ''quarterly'', ''{1,4,7,10}'', 1),
|
||||
(''Estate Lots'', ''Large estate lots (Units 36-50)'', 6000.00, 900.00, 15, ''annual'', ''{3}'', 1)
|
||||
', v_schema);
|
||||
|
||||
-- ============================================================
|
||||
|
||||
Reference in New Issue
Block a user