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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
102 lines
4.9 KiB
Markdown
102 lines
4.9 KiB
Markdown
# 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 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.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
|