Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
4.9 KiB
Unapproved Time Blocks Recurring Invoices Design
Date: 2026-04-12
Slug: unapproved-time-blocks-recurring-invoices
Summary
Recurring invoice windows that contain billable time entries which are not yet approved should be treated as not invoice-ready. Instead of silently billing partial work or burying the problem in preview flows, Automatic Invoices should split those windows into a dedicated Needs Approval section and prevent generation until the underlying time is approved.
Current-State Findings
Code review in this worktree showed:
- Contract hourly billing already filters
time_entries.approval_status = 'APPROVED'inpackages/billing/src/lib/billing/billingEngine.ts. - Unresolved/non-contract time billing also filters
time_entries.approval_status = 'APPROVED'in the same billing engine. rolloverUnapprovedTime(...)explicitly targetsDRAFT,SUBMITTED, andCHANGES_REQUESTEDentries after recurring invoice generation.
So the primary issue is less “the core billing query intentionally bills unapproved time” and more:
- recurring bulk invoicing does not clearly surface windows that are blocked by unapproved billable time,
- operators lack a dedicated queue for approval-dependent windows, and
- generation should still re-check the approval rule server-side in case the UI is stale.
Product Rule
A recurring invoice window is blocked when it contains at least one uninvoiced time entry that would otherwise be billable for that window but whose approval_status !== 'APPROVED'.
This blocks the entire invoice window, even if the same window also contains fixed, license, usage, or other non-time charges.
UX
Automatic Invoices layout
Split the recurring invoice experience into two sections:
-
Needs Approval
- Shown above Ready to Invoice.
- Contains grouped invoice windows that are otherwise billable but blocked by unapproved time.
- Shows client, service period, invoice window, and
X unapproved entries. - Includes a
Review Approvalsaction. - Rows are informational/actionable only: no generation, no selection.
-
Ready to Invoice
- Contains only windows that are fully invoiceable now.
- Existing preview/generate behaviors remain available.
Operator messaging
- Section helper text should explain that these windows are blocked because billable time is not yet approved.
- Needs Approval rows should show blocked counts, not blocked hours, because entry count is more actionable for approvers.
- After bulk generation, the screen may still summarize how many windows remain in Needs Approval, but blocked windows should not be mixed into Ready to Invoice selection.
Detection Rules
Approval blockers must be computed with the same effective billing semantics as invoice selection:
- Contract hourly time: use the same service-period timing, client/work-item scope, service matching, and assignment logic used for approved-billing selection, but search for non-approved entries.
- Unresolved/non-contract time: only treat unresolved time as a blocker when unresolved/non-contract time is actually part of the recurring selection scope.
- Ignore already invoiced entries.
- Treat any non-approved status as blocking, not only today’s named workflow states.
A client having unrelated unapproved time elsewhere must not block unrelated invoice windows.
Enforcement
Read path
The recurring due-work read path should classify windows with approval blockers and include blocker metadata in the row/candidate models.
Generate path
Invoice generation must re-run the blocker check immediately before invoice creation and reject blocked windows with a descriptive error such as:
This invoice window is blocked because it contains 7 unapproved time entries.
That protects against stale tabs, direct API calls, and approval-state races.
Implementation Shape
Likely touchpoints:
packages/billing/src/actions/billingAndTax.tspackages/billing/src/components/billing-dashboard/AutomaticInvoices.tsxpackages/billing/src/actions/invoiceGeneration.tspackages/billing/src/actions/recurringBillingRunActions.tspackages/types/src/interfaces/recurringTiming.interfaces.ts- Possibly shared recurring due-work helpers / billing-engine helpers for reusable blocker detection
Edge Cases
- Mixed fixed + hourly window with one matching unapproved time entry: block the whole window.
- Window becomes approved after manager action: it moves from Needs Approval to Ready to Invoice on refresh.
- UI showed ready earlier but approval status changed before generate: server rejects generation.
Testing Focus
- Due-work classification between Needs Approval and Ready to Invoice
- Window-specific blocker semantics vs unrelated client time
- Server-side rejection of blocked generation attempts
- Automatic Invoices rendering and non-selectability of blocked rows
- Transition from blocked to ready after approval