{
  "spec_file": "docs/specs/2026-05-31-recurring-invoices.md",
  "audited_at_commit": "e307875",
  "summary": "Recurring-invoices marked COMPLETED but UI route unwired, schedule/date math wrong, tier/module gates partial, and cron lacks completion notifications.",
  "findings": [
    {
      "id": "ri-001",
      "severity": "P0",
      "title": "Recurring templates UI route not mounted",
      "detail": "Spec §/invoices/recurring tab+page; apps/zync-app/src/routes/invoices/recurring.tsx exists but apps/zync-app/src/modules/invoices.tsx:25-57 registers only index/reconcile/new/:id — no recurring route. Nav links to /invoices/recurring (nav-model.ts:177) match :id and load InvoiceDetailPage instead.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-002",
      "severity": "P1",
      "title": "computeNextDate monthly day-31 overflows month",
      "detail": "Spec §Next date calculation / Day clamping requires computeNextDate('2026-01-31','monthly',31) → '2026-02-28'; packages/db/src/queries/recurring-invoices.ts:85-106 uses setUTCMonth(+1) on Jan 31 which rolls to Mar 31 before clamping.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-003",
      "severity": "P1",
      "title": "computeInitialNextDate ignores frequency_day snap",
      "detail": "Spec §POST create + task plan Task 3 require first generation on/after start_date snapped to frequency_day; packages/db/src/queries/recurring-invoices.ts:115-126 loops from raw startDate (e.g. monthly day15 start 2026-06-20 returns 2026-06-20 not 2026-06-15; start 2026-06-01 with today 2026-06-17 skips June 15 entirely).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-004",
      "severity": "P1",
      "title": "Weekly schedule never snaps to frequency_day",
      "detail": "Spec §Template form day-of-week (0–6) for weekly; computeInitialNextDate/computeNextDate in packages/db/src/queries/recurring-invoices.ts:81-84 only add 7 days — weekly frequencyDay ignored on initial and advance.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-005",
      "severity": "P1",
      "title": "generate-now due date uses today not generation date",
      "detail": "Spec §payment_terms_days: due = generation date + N; apps/zync-api/src/routes/invoices/recurring.ts:284-285 sets dueDate from new Date() while cron correctly uses template.nextGenerationDate (recurring-invoice-generator.ts:97-99).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-006",
      "severity": "P1",
      "title": "UI create form cannot pass API validation",
      "detail": "Spec §Template form line items required; apps/zync-app/src/routes/invoices/recurring.tsx:173 submits lineItems:[] but createRecurringInvoiceSchema requires min(1) line (packages/db/src/queries/recurring-invoices.ts:43) — POST always 400.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-007",
      "severity": "P1",
      "title": "Business tier gate only on POST create",
      "detail": "Spec §Tier Availability: Free/Starter blocked; apps/zync-api/src/routes/invoices/recurring.ts:101-111 tier check only in POST / — GET list/detail, PATCH, DELETE, generate-now lack requireTier('business') so sub-Business tenants can use API.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-008",
      "severity": "P1",
      "title": "recurring-invoices router missing invoices module gate",
      "detail": "Task plan Task 4 requires requireModuleEnabled('invoices') on router; invoice routes use it (invoices/index.ts:114) but apps/zync-api/src/routes/invoices/recurring.ts:54 only authMiddleware — module-disabled tenants can hit recurring endpoints.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-009",
      "severity": "P1",
      "title": "Generated invoices filtered view absent",
      "detail": "Spec §Generated Invoices View /invoices?recurring_template_id={id}; no recurring_template_id in listInvoicesSchema (packages/db/src/queries/invoices.ts:97-121), InvoiceListFilter (apps/zync-app/src/api/invoices.ts:129-138), or InvoicePage — grep recurring_template_id in apps/zync-app returns zero matches.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-010",
      "severity": "P1",
      "title": "No OWNER notification on template auto-complete",
      "detail": "Spec §Auto-complete: notify tenant OWNER when status→completed; apps/zync-api/src/routes/cron/recurring-invoice-generator.ts:149-150 calls advanceRecurringTemplate but never createNotification (contrast subscription-trial-check.ts:110).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-011",
      "severity": "P1",
      "title": "View generated invoices row action missing",
      "detail": "Spec §Row actions 'View generated invoices — filters invoice list'; apps/zync-app/src/routes/invoices/recurring.tsx TemplateRow:385-442 has Edit/Generate/Pause/Cancel only — no navigation to filtered invoice list.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-012",
      "severity": "P2",
      "title": "Cron scheduled at 05:00 UTC not 06:00",
      "detail": "Spec §Cron Trigger 0 6 * * *; apps/zync-api/src/cron/runner.ts:26-28 maps recurring-invoice-generator to '0 5 * * *' daily-0500 group.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-013",
      "severity": "P2",
      "title": "Cloudflare cron triggers disabled in wrangler",
      "detail": "Spec §daily Cloudflare Cron Trigger; apps/zync-api/wrangler.toml:257-267 [triggers] crons block commented (CF plan limit) — scheduled() never fires unless external dispatch-group daily-0500 is configured.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-014",
      "severity": "P2",
      "title": "GET list lacks cursor/limit pagination",
      "detail": "Spec §GET /api/recurring-invoices cursor, limit default 50; apps/zync-api/src/routes/invoices/recurring.ts:67-82 returns full {templates} array via listRecurringInvoiceTemplates with no cursor/limit/buildPaginated.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-015",
      "severity": "P2",
      "title": "Idempotency guard uses next_generation_date not period start",
      "detail": "Spec §Duplicate generation guard + task plan periodStart(); hasInvoiceForPeriod called with template.nextGenerationDate (recurring-invoice-generator.ts:67, recurring.ts:265) instead of weekly/monthly/quarterly/year period-start logic.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-016",
      "severity": "P2",
      "title": "auto_charge request ignored without warning",
      "detail": "Spec §auto_charge v2 placeholder must return warning when client sets auto_charge; createRecurringInvoiceSchema omits auto_charge (recurring-invoices.ts:39-52), POST strips to false (recurring.ts:131) with no warning field in 201 response.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-017",
      "severity": "P2",
      "title": "Generated invoice source is retainer not manual",
      "detail": "Task plan Task 3/source semantics source='manual'; cron and generate-now call createInvoice with source:'retainer' (recurring-invoice-generator.ts:110, recurring.ts:295).",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-018",
      "severity": "P2",
      "title": "GET /:id/invoices not standard paginated invoice list",
      "detail": "Spec §GET /api/recurring-invoices/:id/invoices PaginatedResponse<InvoiceObject>; apps/zync-api/src/routes/invoices/recurring.ts:347-348 returns raw {invoices} from listInvoicesForTemplate (id, invoiceNumber, status, createdAt only) without cursor/serializeInvoice.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-019",
      "severity": "P2",
      "title": "List filter param customer_id vs customerId",
      "detail": "Spec §GET query param customer_id; apps/zync-api/src/routes/invoices/recurring.ts:74 reads customerId — snake_case clients silently ignored.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-020",
      "severity": "P2",
      "title": "Template form missing spec fields and previews",
      "detail": "Spec §Template Create/Edit: searchable customer select, line item editor, VAT rate, preview panel, next-3-dates panel; apps/zync-app/src/routes/invoices/recurring.tsx:141-335 has Customer ID text input, hardcoded vatRate 0.18, no line editor, no preview/next-dates UI.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-021",
      "severity": "P2",
      "title": "List table missing Customer column and DataTable",
      "detail": "Spec §Table columns Template name|Customer|Frequency|Next date|Status|Generated|Actions; recurring.tsx TemplateRow:366-377 shows title/frequency/next/count only — no customer name, not DataTable, no cursor pagination.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-022",
      "severity": "P2",
      "title": "No @zync/types recurring serializer package",
      "detail": "Task plan Task 2 packages/types/src/recurring-invoice.ts + serializeRecurringTemplate with snake_case wire objects; types package has recurring-tasks only (packages/types/src/index.ts:58-59) — API returns raw Drizzle camelCase rows.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-023",
      "severity": "P2",
      "title": "idx_rit_tenant index missing status column",
      "detail": "Spec §Data Model CREATE INDEX idx_rit_tenant ON (tenant_id, status); packages/db/migrations/0006_sleepy_molten_man.sql:457 creates idx_rit_tenant ON (tenant_id) only.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-024",
      "severity": "P2",
      "title": "currency column lacks CHECK constraint",
      "detail": "Task plan Task 1 DDL CHECK (currency IN ('ILS','USD','EUR')); packages/db/migrations/0006_sleepy_molten_man.sql:167 has currency text DEFAULT 'ILS' with no currency check on recurring_invoice_templates.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-025",
      "severity": "P2",
      "title": "generate-now inactive template returns 422 not 409",
      "detail": "Spec §POST generate-now 409 if status != active; apps/zync-api/src/routes/invoices/recurring.ts:245-246 returns 422.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-026",
      "severity": "P2",
      "title": "generate-now response missing completed field and wrong status",
      "detail": "Spec §POST generate-now 200 {invoiceId, completed}; apps/zync-api/src/routes/invoices/recurring.ts:328 returns 201 {invoiceId} only.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-027",
      "severity": "P2",
      "title": "DELETE cancel returns 204 empty body",
      "detail": "Spec §DELETE 200 {status:'cancelled'}; apps/zync-api/src/routes/invoices/recurring.ts:227 returns 204 No Content.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-028",
      "severity": "P2",
      "title": "Sub-Business create returns 402 not 403",
      "detail": "Spec §Tier / template limit 403; apps/zync-api/src/routes/invoices/recurring.ts:107-110 returns 402 Upgrade required for tier below Business.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-029",
      "severity": "P2",
      "title": "Cron hardcodes IL country for send/VAT",
      "detail": "apps/zync-api/src/routes/cron/recurring-invoice-generator.ts:120,139 passes 'IL' to createInvoice/sendInvoice with comment 'production would look up from tenant' — tenant country not resolved.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-030",
      "severity": "P2",
      "title": "Cron advances template when line_items empty",
      "detail": "Spec §build invoice from template line_items; recurring-invoice-generator.ts:87-93 skips invoice but still advanceRecurringTemplate — silent skip of generation with schedule advance.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-031",
      "severity": "P2",
      "title": "No centralized generateInvoiceFromTemplate/runRecurringInvoiceJob",
      "detail": "Task plan Task 3 exports generateInvoiceFromTemplate + runRecurringInvoiceJob; generation logic duplicated inline in cron (recurring-invoice-generator.ts:64-162) and generate-now route (recurring.ts:274-322) — drift risk on VAT/due-date/linking.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-032",
      "severity": "P2",
      "title": "UI currency select includes unsupported GBP",
      "detail": "Schema/API allow ILS|USD|EUR only; apps/zync-app/src/routes/invoices/recurring.tsx:300-305 offers GBP option — POST validation 400.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "ri-033",
      "severity": "P2",
      "title": "No recurring-invoices API tests",
      "detail": "Task plan acceptance criteria for rate limit, tier limit, idempotency; grep recurring-invoice in apps/zync-api/test returns no matches.",
      "fix_direction": "code_to_spec"
    }
  ]
}
