Approval Workflow
The approval workflow API endpoints are implemented in Phase 2. The status state machine and transitions are already enforced in Phase 1.
After finalization, invoices enter the approval workflow where managers review and approve or decline them.
Approval Flow
Status Transitions
Approve
Transitions: needs_review → approved
- Uploads PDF to Cloudflare R2 (best-effort, before transaction — failure doesn't block approval)
- Sets
approved_atto current timestamp - Sets
approved_byto the approving user - Stores
r2_storage_keyandr2_public_urlif R2 upload succeeds - Logs a
status_changedevent - Triggers async PlanFact shipment sync (goroutine, 30s timeout — stores
planfact_shipment_id+planfact_deal_idon success, logsplanfact_sync_failedevent on failure) - Sends Telegram notification with R2 PDF link when available (background goroutine)
Decline
Transitions: needs_review → declined
- Sets
declined_atto current timestamp - Sets
declined_byto the declining user - Records
decline_reasonexplaining why - Logs a
status_changedevent with reason in metadata
Send
Transitions: approved → sent
- Sets
sent_atto current timestamp - Logs a
status_changedevent - Local (UZS): via Didox.uz (Phase 4)
- International: via email with PDF attachment (Phase 4)
Accept
Transitions: sent → accepted
- Logs a
status_changedevent
Reject
Transitions: sent → rejected
- Sets
rejected_atto current timestamp - Sets
rejected_byto the rejecting user - Records
reject_reasonexplaining why (required — enforced by RejectModal in frontend) - Logs a
status_changedevent with reason in metadata
Mark as Paid
Transitions: accepted → paid
- Sets
paid_atto current timestamp - Logs a
status_changedevent
Regenerate (after decline, reject, or approve)
Transitions: declined → draft, rejected → draft, approved → draft
- Re-fetches latest worklog data from ClickHouse
- Recalculates financials
- Resets status to
draftfor re-editing - Clears
approved_at,approved_by, andhtml_overridefields - Preserves edit history from previous version
PlanFact Sync Retry
If the async PlanFact sync fails during approval (e.g., PlanFact API is down), it can be retried manually:
POST /api/v1/invoices/:id/sync-planfact
- Only works on
approvedorsentinvoices - Runs synchronously (returns when sync completes or fails)
- Frontend shows a "Retry" button in the Integrations card for unsynced invoices
- Returns 400 if PlanFact service is not configured or invoice is in wrong status
Telegram Notifications (Phase 2)
Optional Telegram bot integration for approval notifications:
On approval, the backend sends a Telegram notification in a background goroutine:
- Fetches the invoice PDF from the database
- If PDF is available, sends it as a document attachment with a caption
- If PDF is unavailable or document send fails, falls back to a text message
- Notification failures are logged but do not affect the approval result
Telegram configuration via environment variables (INVOICE_TELEGRAM_BOT_TOKEN, INVOICE_TELEGRAM_CHAT_ID). When both are set, the Notifier is initialized at startup and injected into the invoice service. When either is empty, notifications are silently disabled (nil notifier).
Review Checklist
When reviewing an invoice, managers typically verify:
- Line items match work performed — correct issues listed
- Hours are reasonable — no unexpectedly high or low values
- Overtime is justified (SUP) — hours genuinely exceeded the limit
- Rates are correct — multipliers applied appropriately
- Total amount is accurate — matches financial calculations
- Client details are correct — right company, right period