Skip to main content

Invoice Lifecycle

Every invoice follows a strict state machine from creation to payment. Status transitions are enforced by the backend — invalid transitions are rejected with a 409 Conflict error.

State Diagram

Status Definitions

StatusEditableDescription
draftYesInvoice created, all fields editable. Line items can be added, modified, or removed.
needs_reviewNoFinalized with PDF generated. Awaiting manager approval.
approvedNoApproved by manager. Ready to be sent to client.
declinedNoDeclined by manager with reason. Can be returned to draft for rework.
sentNoSent to client via Didox (UZS) or email (international).
acceptedNoClient accepted the invoice.
rejectedNoClient rejected the invoice. Can be returned to draft.
paidNoPayment received. Terminal state.

Allowed Transitions

var AllowedTransitions = map[string][]string{
"draft": {"needs_review"},
"needs_review": {"approved", "declined"},
"declined": {"draft"},
"approved": {"draft", "sent"},
"sent": {"accepted", "rejected"},
"rejected": {"draft"},
"accepted": {"paid"},
}

Transition Details

Generate → Draft

The invoice is created by pre-filling data from ClickHouse (worklogs, client info) and saved to PostgreSQL with status = 'draft'. Line items have source = 'auto' and is_auto_generated = true.

Finalize: Draft → Needs Review

  1. Validate all required fields are present
  2. Render the HTML invoice template (Russian or English)
  3. Send HTML to Gotenberg (convert.prxm.uz) for PDF conversion
  4. Store PDF binary in the pdf_data column
  5. Record the html_template_hash for change detection
  6. Transition status to needs_review
  7. Log a status_changed event

Approve / Decline: Needs Review → Approved / Declined

  • Approve: Sets approved_at and approved_by. (Phase 2 adds Telegram notification.)
  • Decline: Sets declined_at, declined_by, and decline_reason.

Regenerate: Declined / Rejected / Approved → Draft

Re-fetches fresh worklog data from ClickHouse, recalculates financials, and resets the invoice to draft. Previous edits are preserved in invoice_edit_history. When regenerating from approved, clears approved_at, approved_by, and html_override.

Send: Approved → Sent

  • Local (UZS): Sent via Didox.uz e-invoicing (Phase 4)
  • International: Sent as PDF email attachment (Phase 4)
  • Sets sent_at timestamp

Accept / Reject: Sent → Accepted / Rejected

  • Accept: Logs a status_changed event.
  • Reject: Sets rejected_at, rejected_by, and reject_reason. Reason is required (enforced by frontend RejectModal). Rejected invoices can be regenerated back to draft.

Mark Paid: Accepted → Paid

Terminal state. Sets paid_at timestamp. Triggers PlanFact sync (Phase 3).

Event Tracking

Every status transition is logged in the invoice_events table:

{
"event_type": "status_changed",
"from_status": "draft",
"to_status": "needs_review",
"actor": "[email protected]",
"metadata": { "pdf_generated": true }
}

Event types include: created, status_changed, regenerated, line_items_updated, invoice_updated.