PSA/ee/docs/plans/2026-04-12-unapproved-time-blocks-recurring-invoices-design.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

102 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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'` in `packages/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 targets `DRAFT`, `SUBMITTED`, and `CHANGES_REQUESTED` entries after recurring invoice generation.
So the primary issue is less “the core billing query intentionally bills unapproved time” and more:
1. recurring bulk invoicing does not clearly surface windows that are blocked by unapproved billable time,
2. operators lack a dedicated queue for approval-dependent windows, and
3. 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:
1. **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 Approvals` action.
- Rows are informational/actionable only: no generation, no selection.
2. **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 todays 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.ts`
- `packages/billing/src/components/billing-dashboard/AutomaticInvoices.tsx`
- `packages/billing/src/actions/invoiceGeneration.ts`
- `packages/billing/src/actions/recurringBillingRunActions.ts`
- `packages/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