* feat(voucher): add create voucher and correct entry previews; update commit methods * feat: add support for pending operations in API key scopes and OAuth client management - Introduced new API key scopes for reading and approving pending operations. - Updated the scope groups to include pending operations. - Added new tools for listing and managing pending operations. - Implemented OAuth client registration and revocation endpoints. - Created a UI panel for managing OAuth clients, including registration and revocation. - Added tests for pending operations tools and OAuth allowlist functionality. - Implemented a database migration for OAuth client registrations with appropriate policies and constraints. * feat: Implement OAuth client registration rate limiting and enhance security measures - Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks. - Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained. - Updated error responses to be uniform across different types of redirect URI validation failures. - Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided. - Improved handling of high-risk pending operations, requiring explicit confirmation for approvals. - Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail. - Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks. * feat: add recurring invoice scheduling functionality - Implemented recurring invoice schedules with a new database schema. - Created API routes for managing recurring invoices (GET and POST). - Added cron job to automatically generate invoices based on schedules. - Developed service functions for computing next run dates and executing schedules. - Added tests for the new functionality, including validation and success cases. - Introduced error handling for various scenarios in the invoice creation process. * feat: refine VAT rate validation and enhance recurring invoice handling
144 lines
7.6 KiB
SQL
144 lines
7.6 KiB
SQL
-- Migration: recurring_invoice_schedules — Återkommande fakturor (v1)
|
|
--
|
|
-- Why this exists: Users with subscription-style billing (retainers, hyror,
|
|
-- abonnemang) repeatedly create the same invoice on a fixed day each month.
|
|
-- This table stores an invoice template plus a monthly cadence. A daily cron
|
|
-- (/api/invoices/recurring/cron) finds schedules whose next_run_date <= today
|
|
-- and spawns a real invoice via the standard invoice creation pipeline.
|
|
--
|
|
-- Scope v1 (locked in via planning):
|
|
-- - Monthly cadence only (day_of_month 1-31; clamped to last day of month
|
|
-- in cron's computeNextRunDate, schedule retains original day_of_month).
|
|
-- - No end_date / max_runs — schedule runs until user pauses or deletes.
|
|
-- - Per-schedule auto_send flag: true = create + send email immediately,
|
|
-- false = create as draft for manual review.
|
|
|
|
-- ============================================================
|
|
-- recurring_invoice_schedules — the template + cadence
|
|
-- ============================================================
|
|
|
|
CREATE TABLE public.recurring_invoice_schedules (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
company_id UUID NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
-- Customer is RESTRICT so deleting a customer with active schedules raises a
|
|
-- clear FK error rather than silently nuking the schedules. Surface as a
|
|
-- Swedish error via lib/errors/get-error-message.ts on the customer delete
|
|
-- API route; the user then pauses/deletes the schedule first.
|
|
customer_id UUID NOT NULL REFERENCES public.customers(id) ON DELETE RESTRICT,
|
|
-- Human-readable name shown in the list view (e.g. "Månadsretainer Acme AB").
|
|
name TEXT NOT NULL CHECK (length(name) > 0),
|
|
-- Day of month (1-31). Values >28 are clamped to the last day of shorter
|
|
-- months by computeNextRunDate; the original day_of_month is preserved so
|
|
-- a 31-day schedule jumps back to 31 in months that have it.
|
|
day_of_month SMALLINT NOT NULL CHECK (day_of_month BETWEEN 1 AND 31),
|
|
-- Payment terms (days). due_date = invoice_date + payment_terms_days.
|
|
-- Net-30 is the SME default; 0-90 covers practical range without being
|
|
-- arbitrary.
|
|
payment_terms_days SMALLINT NOT NULL DEFAULT 30 CHECK (payment_terms_days BETWEEN 0 AND 90),
|
|
currency TEXT NOT NULL DEFAULT 'SEK',
|
|
-- Free-text fields mirroring the manual invoice form, applied to each
|
|
-- generated faktura.
|
|
your_reference TEXT,
|
|
our_reference TEXT,
|
|
notes TEXT,
|
|
-- false: create as draft so the user reviews + sends manually.
|
|
-- true: render PDF, send via email extension, flip status to 'sent',
|
|
-- create journal entry on accrual. If email extension not configured
|
|
-- or customer has no email, falls back to draft + sets
|
|
-- last_run_warning.
|
|
auto_send BOOLEAN NOT NULL DEFAULT false,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused')),
|
|
-- Date the schedule should next produce an invoice. Cron filter:
|
|
-- next_run_date <= today AND status='active'. Recomputed after every
|
|
-- successful run.
|
|
next_run_date DATE NOT NULL,
|
|
-- last_run_at + last_invoice_id provide idempotency. Cron skips if
|
|
-- last_run_at::date >= today, so retries within the same UTC day don't
|
|
-- double-spawn.
|
|
last_run_at TIMESTAMPTZ,
|
|
last_invoice_id UUID REFERENCES public.invoices(id) ON DELETE SET NULL,
|
|
-- Free-text Swedish warning surfaced in the UI when the most recent run
|
|
-- couldn't fully complete (e.g. email extension disabled). Cleared on
|
|
-- next successful run.
|
|
last_run_warning TEXT,
|
|
generated_count INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_ris_company ON public.recurring_invoice_schedules (company_id);
|
|
CREATE INDEX idx_ris_customer ON public.recurring_invoice_schedules (customer_id);
|
|
-- Partial index for cron's primary query: active schedules due to run.
|
|
CREATE INDEX idx_ris_due ON public.recurring_invoice_schedules (next_run_date)
|
|
WHERE status = 'active';
|
|
|
|
ALTER TABLE public.recurring_invoice_schedules ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "recurring_invoice_schedules_select" ON public.recurring_invoice_schedules
|
|
FOR SELECT USING (company_id IN (SELECT public.user_company_ids()));
|
|
CREATE POLICY "recurring_invoice_schedules_insert" ON public.recurring_invoice_schedules
|
|
FOR INSERT WITH CHECK (company_id IN (SELECT public.user_company_ids()));
|
|
CREATE POLICY "recurring_invoice_schedules_update" ON public.recurring_invoice_schedules
|
|
FOR UPDATE USING (company_id IN (SELECT public.user_company_ids()))
|
|
WITH CHECK (company_id IN (SELECT public.user_company_ids()));
|
|
CREATE POLICY "recurring_invoice_schedules_delete" ON public.recurring_invoice_schedules
|
|
FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
|
|
|
|
CREATE TRIGGER recurring_invoice_schedules_updated_at
|
|
BEFORE UPDATE ON public.recurring_invoice_schedules
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ============================================================
|
|
-- recurring_invoice_schedule_items — template line items
|
|
-- ============================================================
|
|
|
|
CREATE TABLE public.recurring_invoice_schedule_items (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
schedule_id UUID NOT NULL REFERENCES public.recurring_invoice_schedules(id) ON DELETE CASCADE,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
description TEXT NOT NULL CHECK (length(description) > 0),
|
|
quantity NUMERIC(12, 4) NOT NULL CHECK (quantity > 0),
|
|
unit TEXT NOT NULL DEFAULT 'st',
|
|
unit_price NUMERIC(14, 2) NOT NULL,
|
|
-- NULL = inherit the customer's default VAT rate at spawn time. The cron
|
|
-- resolves this via lib/invoices/vat-rules.ts so a customer who later
|
|
-- becomes VAT-validated picks up the new rate automatically on the next
|
|
-- run.
|
|
vat_rate NUMERIC(5, 2) CHECK (vat_rate IS NULL OR (vat_rate >= 0 AND vat_rate <= 100)),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_risi_schedule ON public.recurring_invoice_schedule_items (schedule_id, sort_order);
|
|
|
|
ALTER TABLE public.recurring_invoice_schedule_items ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Items inherit access from the parent schedule via EXISTS-join so we
|
|
-- don't have to duplicate company_id on the child rows.
|
|
CREATE POLICY "recurring_invoice_schedule_items_select" ON public.recurring_invoice_schedule_items
|
|
FOR SELECT USING (EXISTS (
|
|
SELECT 1 FROM public.recurring_invoice_schedules s
|
|
WHERE s.id = schedule_id
|
|
AND s.company_id IN (SELECT public.user_company_ids())
|
|
));
|
|
CREATE POLICY "recurring_invoice_schedule_items_insert" ON public.recurring_invoice_schedule_items
|
|
FOR INSERT WITH CHECK (EXISTS (
|
|
SELECT 1 FROM public.recurring_invoice_schedules s
|
|
WHERE s.id = schedule_id
|
|
AND s.company_id IN (SELECT public.user_company_ids())
|
|
));
|
|
CREATE POLICY "recurring_invoice_schedule_items_update" ON public.recurring_invoice_schedule_items
|
|
FOR UPDATE USING (EXISTS (
|
|
SELECT 1 FROM public.recurring_invoice_schedules s
|
|
WHERE s.id = schedule_id
|
|
AND s.company_id IN (SELECT public.user_company_ids())
|
|
));
|
|
CREATE POLICY "recurring_invoice_schedule_items_delete" ON public.recurring_invoice_schedule_items
|
|
FOR DELETE USING (EXISTS (
|
|
SELECT 1 FROM public.recurring_invoice_schedules s
|
|
WHERE s.id = schedule_id
|
|
AND s.company_id IN (SELECT public.user_company_ids())
|
|
));
|
|
|
|
NOTIFY pgrst, 'reload schema';
|