🧾

INV

P2 · Invoicing & AR Planned · P2 design phase Owner: CFO / Finance

Multi-currency invoicing drafted from TIME entries — Stripe (USD/EUR) + Wise (multi) + VietQR (VND) + Vietnamese e-invoice (Decree 123) + AR aging + dunning automation.

INV is the AR (Accounts Receivable) side of finance — the place where billable work becomes a sent invoice, a tracked payment, an aged balance, or a reconciled receipt. The architecture is composition-first: invoices draft automatically from TIME entries per engagement (per (FR pending)), multi-currency (VND, USD, SGD, EUR) with daily SBV rate snapshots, payment collection through Stripe (USD/EUR), Wise (multi-currency), and the Vietnamese domestic stack (VietQR / Napas247 / MoMo / ZaloPay / VNPay) for VND. For Vietnamese tenants, the vn-vat-invoice skill generates Decree-123-compliant e-invoices in the GDT XML format (Circular 78); the schema embeds Mẫu 01/GTGT line items with MST validation via vn-mst-validate. The CUO/CFO-skill drafts dunning emails on overdue but never sends them without human approval. PRD §9.16 locks the FRs; SRS §4.16 binds them to verification methods.

INV is the AR plane. Invoices begin life as a draft auto-populated from TIME entries against an engagement's billable rules; a human reviews + sends; the customer pays through one of four payment rails (Stripe, Wise, domestic VND PSP, manual bank transfer); the receipt matches back to the invoice (cash application); the GL takes the revenue. The Vietnamese-context specifics — Decree 123 e-invoice schema, Circular 78 GDT format, Mẫu 01/GTGT, MST validation, VietQR collection — are wrapped in three reusable CyberSkill skills (vn-vat-invoice, vn-mst-validate, vn-bank-transfer) so the same primitives serve INV here and any external SaaS that adopts the cyberskill-vn collection.

Status
Planned
P2 · design phase
Est. LoC
~5,800
Rust (axum) + skill bridges
Planned tests
100+
incl. webhook idempotency + currency rounding
Currencies
VND · USD · SGD · EUR
SBV daily snapshot
Payment rails
Stripe · Wise · VietQR · manual
+ MoMo/ZaloPay/VNPay (P3)
VN e-invoice
Decree 123
via vn-vat-invoice skill
Depends on
TIME · CRM · AUTH
+ skills · S3 · NATS
Feeds
CRM · GL · BRAIN audit
payment status events
1

Why INV exists

Without an invoicing module, the gap between "did the work" and "got paid" lives in a spreadsheet, a Stripe dashboard, and somebody's memory. Multi-currency makes that gap worse (which rate? when? rounded how?). Vietnamese e-invoice requirements make it worse still (which Mẫu? which line item code? was the MST validated?). INV closes that gap with a single source of truth — one invoice row per billable event, one payment row per receipt, one aging report per close, and one webhook handler per payment rail — and exposes the same primitives as reusable skills so the patterns generalise.

⏱→🧾
From TIME to invoice

Engagements carry billable rules (rate, currency, cap). TIME entries per engagement aggregate into invoice line items. Invoice draft is a one-click read.

🇻🇳
Vietnamese-grade compliance

Decree 123 + Circular 78 GDT format. Mẫu 01/GTGT line items. MST validation on customer + tenant. VietQR collection. cyberskill-vn skills wrap the lot.

📬
Cash application + dunning

Webhooks from Stripe/Wise/banks match incoming receipts to outstanding invoices. CUO drafts dunning emails on overdue; human sends.

INV is also part of the CyberSkill margin story: faster collection compresses DSO; multi-rail payment options improve customer experience; e-invoice automation avoids the 3-day round trip via the old VN paper-stamp process. Each is a small win that compounds.

2

What it does — 5W1H2C5M

Every cell traces back to PRD §9.16 and SRS §4.16.

AxisQuestionAnswer
5W · WhatWhat is INV?An AR + multi-currency invoicing + payment integration service. Drafts invoices from TIME entries; sends; tracks payments; ages receivables; reconciles cash to invoices; routes Vietnamese e-invoices to GDT via the vn-vat-invoice skill.
5W · WhoWho uses INV?CFO / Finance: close, AR aging, dunning approval. Project lead / Account Manager: invoice draft preview + send. Customer: receives invoice + pays. CUO/CFO-skill: drafts dunning + payment-status narrative.
5W · WhenWhen does INV act?(a) Engagement milestone or month-end → invoice draft. (b) Send invoice → email/portal. (c) Payment webhook → cash application. (d) Overdue tick → dunning. (e) Quarter-end → AR aging report. (f) Year-end → 1099-style summaries.
5W · WhereWhere does it run?P2: single region (SG-1) backed by AWS RDS Postgres. Per-tenant data-residency for VN tenants (vn-hanoi-1) by P3.
5W · WhyWhy a separate module?Because AR-ledger drift and tax-compliance surprise are existential to small companies. INV is the one source of truth for "what did we bill, what did they pay, who owes what".
1H · HowHow does it work?Postgres for invoice + payment + reminder rows. Stripe / Wise webhooks → cash_application engine matches receipts to invoices by metadata + amount. VietQR generated via vn-bank-transfer skill; bank confirms via screen-scrape OR open-banking API (Napas247 P3). Vietnamese e-invoice via vn-vat-invoice skill → POST to GDT.
2C · CostCost budget?P2: ~$40/month (RDS + Fargate + Stripe/Wise pass-through fees). 50-tenant: ~$150/month. Per-invoice cost: ~$0.02 + payment processor fee.
2C · ConstraintsConstraints?(a) Vietnam Decree 123 + Circular 78 — e-invoice format. (b) Currency rounding policy (VND has no decimals; round to nearest 1,000 for display). (c) Per-engagement billable rules drive line items ((FR pending)). (d) Approved invoices are append-only ((FR pending) SRS); corrections via credit note.
5M · MaterialsStack?Rust 1.81 · axum · sqlx · PostgreSQL 16 · stripe-rust · wise sdk · typst / tectonic for PDF · async-graphql · cyberskill-vn skills · AWS S3 (immutable archive).
5M · MethodsMethod choices?Idempotent webhooks (Stripe idempotency-key + DB UNIQUE on external_event_id). VND rate snapshot daily from SBV REST endpoint. Multi-rail collection abstracted behind PaymentRail trait. Append-only invoice rows; credit-note for corrections.
5M · MachinesDeployment?Fargate in SG-1. Multi-AZ Postgres RDS. S3 immutable bucket for PDFs.
5M · ManpowerWho maintains?CFO (R for close + dunning approval) + 0.25 FTE eng.
5M · MeasurementHow measured?KPIs: DSO (days sales outstanding); cash-application match rate; e-invoice GDT acceptance rate; dunning effectiveness.
3

Architecture

INV composes three external systems (Stripe, Wise, Vietnamese payment stack), one e-invoice issuer (GDT via the vn-vat-invoice skill), and one rate source (SBV daily snapshot) into a single AR ledger. The diagram below shows the canonical invoice → payment lifecycle.

graph TB subgraph CLIENTS ["Clients"] PM["Project Lead / AM (SPA)"] CFO["CFO (close + dunning)"] CUST["Customer
(invoice + payment portal)"] CUO["🤖 CUO/CFO-skill
drafts + narrates"] end subgraph EDGE ["Edge"] AR["Apollo Router
JWT + RBAC"] end subgraph INV ["INV service (Rust · axum)"] GQL["GraphQL subgraph
Invoice · Payment"] REST["REST admin
send · void · credit"] MCP["MCP tool catalogue"] DRAFT["draft.rs
from TIME entries"] PDF["pdf_renderer.rs
tectonic deterministic"] SEND["send.rs
email + portal link"] WEBHOOK["webhook.rs
Stripe · Wise · bank"] CASH["cash_application.rs
match receipt → invoice"] AGING["aging.rs
0-30 · 31-60 · 61-90 · 90+"] DUN["dunning.rs
drafts only · human send"] FX["fx.rs
SBV daily snapshot"] end subgraph SKILLS ["cyberskill-vn (skill bridges)"] VAT["vn-vat-invoice
Mẫu 01/GTGT · GDT XML"] MST["vn-mst-validate
customer + tenant MST"] BANK["vn-bank-transfer
VietQR generator"] end subgraph STORES ["Stores"] PG[("PostgreSQL
invoice · line_item
payment · credit_note
reminder · rate_snapshot")] S3[("AWS S3
invoice PDFs · receipts
immutable bucket")] end subgraph EXT ["External rails"] STR["Stripe (USD/EUR)"] WISE["Wise (multi-currency)"] VQR["VietQR / Napas247 (VND)"] GDT["🇻🇳 GDT e-invoice
Tổng cục Thuế"] SBV["SBV daily rate API"] end subgraph SINKS ["Audit + events"] BRAIN["🧠 BRAIN
inv.* lifecycle"] NATS["📡 NATS
inv.invoice.sent · inv.payment.received"] CRM["🏢 CRM
payment-status sync"] OBS["👁 OBS"] end PM --> AR CFO --> AR CUST --> AR CUO --> AR AR --> GQL AR --> REST AR --> MCP REST --> DRAFT DRAFT --> PDF PDF --> VAT VAT --> GDT REST --> MST REST --> BANK REST --> SEND WEBHOOK --> CASH CASH --> PG STR -.webhook.-> WEBHOOK WISE -.webhook.-> WEBHOOK VQR -.webhook.-> WEBHOOK FX -.daily.-> SBV REST --> PG GQL --> PG PDF --> S3 AGING --> PG DUN --> SEND REST --> BRAIN REST --> NATS NATS --> CRM INV --> OBS classDef planned fill:#a7f3d0,stroke:#064e3b classDef store fill:#f5f3ff,stroke:#7c3aed classDef ext fill:#fef6e0,stroke:#9c750a classDef sink fill:#e8d4c2,stroke:#45210e class GQL,REST,MCP,DRAFT,PDF,SEND,WEBHOOK,CASH,AGING,DUN,FX,VAT,MST,BANK planned class PG,S3 store class STR,WISE,VQR,GDT,SBV ext class BRAIN,NATS,CRM,OBS sink

Internal components

ComponentPath (planned)Responsibility
invoice.rsservices/inv/src/invoice.rsInvoice CRUD. Append-only after status="sent"; corrections via credit_note.
draft.rsservices/inv/src/draft.rsAuto-populate draft from TIME entries per engagement + billable rules.
line_item.rsservices/inv/src/line_item.rsPer-invoice line items; aggregates billable rules; supports VN VAT category codes.
payment.rsservices/inv/src/payment.rsPayment row writer. Idempotent by external_event_id.
credit_note.rsservices/inv/src/credit_note.rsCredit-note + refund. Always tied to an invoice; reduces outstanding balance.
pdf_renderer.rsservices/inv/src/pdf_renderer.rsDeterministic PDF via tectonic. Tenant brand applied via per-tenant template.
cash_application.rsservices/inv/src/cash_application.rsMatch incoming receipt → outstanding invoice by metadata + amount. Manual override UI for ambiguous cases.
webhook.rsservices/inv/src/webhook.rsStripe + Wise + bank webhook handlers. Signature verification; idempotency-key check.
aging.rsservices/inv/src/aging.rsAR aging bucket report: 0–30 / 31–60 / 61–90 / 90+ days.
dunning.rsservices/inv/src/dunning.rsDrafts dunning emails (T+1, T+7, T+30 of overdue). Sends ONLY after CFO/AM approval.
fx.rsservices/inv/src/fx.rsDaily SBV rate snapshot. Cached per day; new invoices use today's snapshot.
vn_vat_bridge.rsservices/inv/src/vn_vat_bridge.rsBridge to vn-vat-invoice skill — Mẫu 01/GTGT generation + GDT submission.
mst_bridge.rsservices/inv/src/mst_bridge.rsBridge to vn-mst-validate skill — customer + tenant MST validation before send.
bank_bridge.rsservices/inv/src/bank_bridge.rsBridge to vn-bank-transfer skill — VietQR generator.
audit_bridge.rsservices/inv/src/audit_bridge.rsLifecycle rows to BRAIN.
nats_bridge.rsservices/inv/src/nats_bridge.rsEvent publisher — sent/paid/overdue events for CRM + KPI dashboards.
migrations/services/inv/migrations/sqlx migrations. UNIQUE on external_event_id; append-only on invoice after status=sent.
4

Data model

Invoice + line items + payment + credit note are the core rows. Reminders are a side-table. FX rate snapshots are daily. Webhook event IDs carry a UNIQUE constraint so duplicate webhooks are no-ops.

erDiagram TENANT ||--o{ CUSTOMER : "bills" CUSTOMER ||--o{ INVOICE : "owes" ENGAGEMENT ||--o{ INVOICE : "drafted from" INVOICE ||--o{ LINE_ITEM : "contains" INVOICE ||--o{ PAYMENT : "settled by" INVOICE ||--o{ CREDIT_NOTE : "corrected by" INVOICE ||--o{ REMINDER : "dunned by" PAYMENT_METHOD ||--o{ PAYMENT : "via" RATE_SNAPSHOT ||--o{ INVOICE : "issued at rate" INVOICE ||--o| VN_EINVOICE : "Vietnamese variant" TIME_ENTRY ||--o{ LINE_ITEM : "source of" CUSTOMER { uuid id PK uuid tenant_id FK string display_name string country "VN | SG | US | …" string mst_or_taxid "Vietnamese MST or other" string email string billing_address string default_currency string default_payment_method } ENGAGEMENT { uuid id PK uuid tenant_id FK uuid customer_id FK string title jsonb billable_rules "rate, currency, cap, schedule" string status "active | paused | closed" } INVOICE { uuid id PK uuid tenant_id FK uuid customer_id FK uuid engagement_id FK string number "INV-2026-04-001" string currency decimal subtotal decimal vat decimal total decimal outstanding date issued_at date due_at string status "draft | sent | viewed | paid | overdue | late | escalated | void" uuid rate_snapshot_id FK string pdf_s3_uri bytea pdf_sha256 timestamp sent_at } LINE_ITEM { uuid id PK uuid invoice_id FK string description decimal quantity string unit "hr | day | flat" decimal unit_price decimal amount decimal vat_rate "0 | 5 | 8 | 10 (VN)" string vn_category_code "Mẫu 01/GTGT category" } PAYMENT { uuid id PK uuid invoice_id FK string external_event_id UK "Stripe payment_intent | Wise transfer | bank txn" string rail "stripe | wise | vietqr | manual" decimal amount string currency timestamp received_at string status "received | matched | reconciled | reversed" jsonb raw_payload "for audit" } CREDIT_NOTE { uuid id PK uuid invoice_id FK decimal amount string reason date issued_at uuid issued_by FK } REMINDER { uuid id PK uuid invoice_id FK string kind "T+1 | T+7 | T+30 | escalation" string status "drafted | approved | sent" string draft_body uuid approved_by FK timestamp sent_at } PAYMENT_METHOD { uuid id PK uuid customer_id FK string rail string token_encrypted "stripe customer | wise recipient | bank account" bool is_default } RATE_SNAPSHOT { uuid id PK date snapshot_date string from_currency string to_currency decimal rate string source "SBV | ECB | fixer" } VN_EINVOICE { uuid invoice_id PK string ma_so_thue_seller "tenant MST" string ma_so_thue_buyer "customer MST" string mau_so "01/GTGT" string ky_hieu "issuance series" string so_seri "invoice serial" string gdt_status "draft | submitted | accepted | rejected" string gdt_message_id bytea xml_payload "Circular 78 format" timestamp submitted_at } TIME_ENTRY { uuid id PK uuid engagement_id FK uuid member_id FK decimal hours date worked_at string description bool invoiced "set true when line_item.amount fully covers" }

Payment-rail comparison (P2 launch set)

RailCurrenciesTypical feeSettlementWebhookUse case
StripeUSD, EUR (multi-currency intent supported)2.9% + $0.30T+2signed (HMAC)Default for international customers; cards.
Wise40+ incl. VND, SGD, USD, EUR0.4–1% mid-marketT+0 to T+2polling + webhook (P3)Cross-border + local-rail collection.
VietQR / Napas247VND~5,000 VND / txninstant (24/7)bank API (BIDV, MB, VCB)Vietnamese B2B + B2C; preferred for VN customers.
Manual bank transferanybank-dependentT+1 to T+5none (manual reconciliation)Fallback when other rails unavailable.
MoMo / ZaloPay / VNPay (P3)VND1.5–2.5%instantsignedVN consumer wallets — added at P3.
5

API surface

Three surfaces: GraphQL subgraph for invoice/payment reads, REST admin for send/credit/void operations, MCP tool catalogue for the CUO/CFO-skill (draft dunning, narrate aging — never send without human approval).

GraphQL subgraph

extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@requiresScopes"])

type Invoice @key(fields: "id") {
  id: ID!
  number: String!
  customer: Customer!
  engagement: Engagement
  currency: String!
  subtotal: Money!
  vat: Money!
  total: Money!
  outstanding: Money!
  issuedAt: Date!
  dueAt: Date!
  status: InvoiceStatus!
  lineItems: [LineItem!]!
  payments: [Payment!]! @requiresScopes(scopes: [["inv.payment_read"]])
  pdfUrl: String!  # pre-signed S3
}

type Payment @key(fields: "id") {
  id: ID!
  invoiceId: ID!
  rail: PaymentRail!
  amount: Money!
  receivedAt: DateTime!
  status: PaymentStatus!
}

type CreditNote @key(fields: "id") {
  id: ID!
  invoiceId: ID!
  amount: Money!
  reason: String!
  issuedAt: Date!
}

type AgingReport {
  bucket0_30: Money!
  bucket31_60: Money!
  bucket61_90: Money!
  bucket90Plus: Money!
  total: Money!
  byCustomer: [CustomerAging!]!
}

enum InvoiceStatus { DRAFT SENT VIEWED PAID OVERDUE LATE ESCALATED VOID }
enum PaymentStatus { RECEIVED MATCHED RECONCILED REVERSED }
enum PaymentRail { STRIPE WISE VIETQR MOMO ZALOPAY VNPAY MANUAL }

type Query {
  invoice(id: ID!): Invoice
  invoices(customerId: ID, status: InvoiceStatus, since: Date): [Invoice!]!
  agingReport(asOf: Date): AgingReport!
    @requiresScopes(scopes: [["inv.aging_read"]])
}

type Mutation {
  draftInvoice(input: DraftInvoiceInput!): Invoice!
  sendInvoice(id: ID!): Invoice!
    @requiresScopes(scopes: [["inv.send"]])
  voidInvoice(id: ID!, reason: String!): Invoice!
    @requiresScopes(scopes: [["inv.void"]])
  issueCreditNote(input: CreditNoteInput!): CreditNote!
    @requiresScopes(scopes: [["inv.credit"]])
  approveDunning(reminderId: ID!): Reminder!
    @requiresScopes(scopes: [["inv.dunning_approve"]])
}

REST + webhook surface

MethodPathPurpose
POST/admin/invoice/draftDraft from TIME entries against engagement billable rules.
POST/admin/invoice/{id}/sendSend invoice — render PDF, validate MST (VN), submit e-invoice to GDT (VN), email customer.
POST/admin/invoice/{id}/voidVoid invoice. Append-only — sets status=void; does NOT delete.
POST/admin/invoice/{id}/credit-noteIssue credit note (partial or full).
GET/admin/aging?as_of=YYYY-MM-DDAR aging report.
POST/admin/dunning/{reminder_id}/approveCFO/AM approves CUO-drafted dunning email; sends.
POST/webhook/stripeStripe webhook — HMAC verified; cash_application triggered.
POST/webhook/wiseWise webhook (P3); polling fallback P2.
POST/webhook/bank/{bank_code}Bank API webhook (BIDV, MB, VCB) for VietQR receipts.
GET/portal/invoice/{token}Customer-facing invoice + payment options page.
POST/internal/fx/snapshotDaily SBV rate snapshot job.
POST/admin/cash-application/manualManual override — link a payment to an invoice when auto-match fails.

MCP tool catalogue (CUO/CFO-skill)

Tool nameInputsOutputsAnnotations
cyberos.inv.draft_invoiceengagement_id, perioddraft Invoice (not sent)readwrite (draft only) · human-confirm
cyberos.inv.list_overduesince?Invoice[]readonly · scope=inv.aging_read
cyberos.inv.draft_dunninginvoice_idemail draftreadwrite (draft only) · human-confirm before send
cyberos.inv.explain_agingas_of?narrative AR reportreadonly
cyberos.inv.summarise_payment_statusinvoice_idnarrativereadonly
cyberos.inv.validate_mstmst_string{valid, taxpayer_info}readonly · bridges vn-mst-validate skill
cyberos.inv.generate_vietqramount, bank, accountQR data + imagereadonly · bridges vn-bank-transfer skill
6

Key flows

Flow 1 — Draft invoice from TIME entries

sequenceDiagram autonumber participant PM as Project Lead (SPA) participant I as INV /admin/invoice/draft participant T as ⏱ TIME (read entries) participant E as Engagement (billable rules) participant FX as fx.rs (rate snapshot) participant PG as PostgreSQL PM->>I: draftInvoice {engagement_id, period:"2026-04"} I->>E: read billable rules (rate, currency, cap) I->>T: read uninvoiced time entries for engagement+period T-->>I: 47 entries · 168 hrs I->>FX: read today's snapshot if cross-currency I->>I: aggregate into line items (per role / per project phase) I->>PG: INSERT invoice status="draft"
+ line_item rows PG-->>I: invoice_id I-->>PM: draft invoice ready for review Note over PM: PM previews PDF, may edit lines
before "send".

Flow 2 — Send (with VN e-invoice via vn-vat-invoice skill)

sequenceDiagram autonumber participant PM as PM participant I as INV /admin/invoice/{id}/send participant MST as 🛠 vn-mst-validate skill participant VAT as 🛠 vn-vat-invoice skill participant GDT as 🇻🇳 GDT (Tổng cục Thuế) participant PDF as pdf_renderer.rs participant S3 as AWS S3 (immutable) participant EM as Email service participant C as Customer participant B as 🧠 BRAIN participant N as 📡 NATS PM->>I: sendInvoice {id} I->>MST: validate tenant + customer MST alt VN tenant I->>VAT: generate Mẫu 01/GTGT XML (Circular 78) VAT-->>I: signed XML + PDF I->>GDT: POST e-invoice GDT-->>I: gdt_message_id · status=accepted I->>I: persist vn_einvoice row else non-VN tenant I->>PDF: render PDF (tenant brand) PDF-->>I: pdf bytes end I->>S3: archive PDF (immutable bucket) I->>I: status="sent" · sent_at = now I->>EM: send invoice email (+ payment portal link) EM-->>C: email delivered I->>B: audit "inv.invoice.sent" I->>N: publish inv.invoice.sent Note over I,N: CRM subscribes to NATS for payment-status sync.

Flow 3 — Stripe payment received + cash application

sequenceDiagram autonumber participant STR as Stripe participant W as INV /webhook/stripe participant CASH as cash_application.rs participant PG as PostgreSQL participant N as 📡 NATS participant CRM as 🏢 CRM (subscriber) STR->>W: POST webhook payment_intent.succeeded
(HMAC signed) W->>W: verify signature W->>PG: INSERT payment (UNIQUE external_event_id) alt new event W->>CASH: match payment to invoice
(by metadata.invoice_id OR amount + customer) alt single match CASH->>PG: invoice.outstanding -= amount
set status="paid" if outstanding=0 CASH->>N: publish inv.payment.received CRM-->>N: subscriber → updates account record else ambiguous CASH->>PG: payment.status="received" (unmatched) CASH->>N: publish inv.payment.unmatched (alerts AM/CFO) end else duplicate event W->>W: idempotent no-op end

Flow 4 — VietQR collection (VN customer paying via bank)

sequenceDiagram autonumber participant PM as PM participant I as INV send.rs participant BANK as 🛠 vn-bank-transfer skill participant C as Customer (VN) participant B247 as Napas247 bank (BIDV/MB/…) participant W as INV /webhook/bank/{code} participant CASH as cash_application.rs PM->>I: sendInvoice {id} (currency=VND) I->>BANK: generate VietQR
{account, amount, addInfo: "INV-2026-04-001"} BANK-->>I: QR string + image I->>C: email with VietQR image + amount + addInfo C->>B247: scan QR · approve transfer B247-->>I: webhook "transfer received" I->>W: route to webhook handler W->>CASH: match by addInfo (invoice number) + amount CASH->>CASH: idempotent match · update invoice CASH->>I: status="paid" Note over CASH: matching strictness:
(invoice_number in addInfo) AND (amount within ±1,000 VND tolerance)

Flow 5 — Dunning automation (drafts only · human send)

sequenceDiagram autonumber participant CRON as Daily cron participant AGE as aging.rs participant DUN as dunning.rs participant CUO as 🤖 CUO/CFO-skill participant CFO as CFO participant EM as Email participant C as Customer CRON->>AGE: tick AGE->>AGE: identify invoices T+1, T+7, T+30 overdue AGE-->>DUN: 3 candidate reminders loop per reminder DUN->>CUO: cyberos.inv.draft_dunning {invoice_id} CUO-->>DUN: draft email (tone adapts to T+1/T+7/T+30) DUN->>DUN: INSERT reminder status="drafted" end DUN->>CFO: notify "3 reminders awaiting your review" CFO->>DUN: approveDunning {reminder_id} (per reminder) DUN->>EM: send email DUN->>DUN: reminder status="sent" EM-->>C: email delivered Note over CUO,CFO: CUO NEVER sends without human approval.
OWASP LLM08 "excessive agency" mitigated.
7

Invoice lifecycle

An invoice traverses up to eight states. Once sent, the invoice row is append-only — corrections happen via credit_note or by voiding and reissuing.

stateDiagram-v2 [*] --> Draft: drafted from TIME Draft --> Sent: PM clicks send · PDF rendered · GDT submitted (VN) Sent --> Viewed: customer opens portal link Viewed --> Paid: full payment matched Sent --> Paid: full payment matched (skipped portal) Sent --> Overdue: past due_at Viewed --> Overdue: past due_at Overdue --> Late: T+7 reminder sent Late --> Escalated: T+30 reminder · CFO escalation Escalated --> Paid: late payment received Paid --> [*] Sent --> Void: void with reason (issues credit note if partial paid) Draft --> Void: discarded Void --> [*]

AR aging buckets

BucketDays past dueDefault action
Current≤ 0None — not yet due
0–301–30T+1 friendly reminder · T+7 firmer reminder
31–6031–60T+30 escalation · CFO sees in close report
61–9061–90AM phone call · CFO weekly review
90+91+Legal counsel consultation · write-off policy review
8

Functional Requirements

The CyberOS FR catalogue is being rebuilt one feature at a time via the open fr-author Agent Skill.

Previous FR enumerations were archived 2026-05-14 and are no longer reflected on this page. PRD/SRS narrative remains authoritative for the spec; specific FRs land here as they are re-authored.

9

Non-Functional Requirements

PRD §11.2.1 (performance) and §11.2.4 (reliability) bind on INV — especially payment reconciliation correctness.

NFR IDConcernTargetMeasurement
N(FR pending)Invoice generation p95≤ 2 sbench/draft.rs · k6 load
N(FR pending)PDF render p95 (deterministic)≤ 600 msbench/pdf.rs
N(FR pending)VN e-invoice GDT submission p95≤ 5 s (network bound)k6 against staging GDT
N(FR pending)Payment-webhook idempotency100% (zero duplicate side-effects)chaos test: replay webhook 100×
N(FR pending)Cash-application auto-match rate≥ 90% (the rest manual)OBS dashboard · monthly review
N(FR pending)Webhook signature verification100% (zero unsigned acceptance)integration test injects unsigned webhook
N(FR pending)VND rounding correctness0 sub-1,000 drift / 1k invoicesproperty test
N(FR pending)INV availability≥ 99.5%SLO monitor
N(FR pending)Invoice PDF durability (10-year)0 lost objectsS3 immutable + quarterly inventory
N(FR pending)Customer payment method tokens encryptedKMS-wrappedschema inspection · CI gate
N(FR pending)PCI scope minimisation (no card data stored)= 0 raw PAN columnsschema audit · Stripe handles PCI
10

Dependencies

INV reads from TIME, CRM, and AUTH; bridges to three skills (vn-vat-invoice, vn-mst-validate, vn-bank-transfer); integrates with four external rails (Stripe, Wise, VietQR/Napas247, GDT). Events flow to CRM via NATS.

graph LR subgraph upstream ["INV depends on"] AUTH["🔐 AUTH"] TIME["⏱ TIME
entries · billable"] CRM["🏢 CRM
customer records"] BRAIN["🧠 BRAIN"] OBS["👁 OBS"] NATS["📡 NATS"] S3["🗂 S3 (immutable)"] end subgraph skills ["Skill bridges"] VAT["🛠 vn-vat-invoice"] MST["🛠 vn-mst-validate"] BANK["🛠 vn-bank-transfer"] end subgraph external ["External rails"] STR["Stripe"] WISE["Wise"] VQR["VietQR / Napas247"] GDT["🇻🇳 GDT"] SBV["SBV rate API"] end INV["🧾 INV"] subgraph downstream ["INV feeds"] CRM2["🏢 CRM
payment status"] GL["📊 GL
revenue recognition"] BRAIN2["🧠 BRAIN
audit"] CUO["🤖 CUO/CFO-skill"] end AUTH --> INV TIME --> INV CRM --> INV BRAIN --> INV OBS --> INV NATS --> INV S3 --> INV VAT --> INV MST --> INV BANK --> INV STR --> INV WISE --> INV VQR --> INV GDT --> INV SBV --> INV INV --> CRM2 INV --> GL INV --> BRAIN2 INV --> CUO classDef planned fill:#a7f3d0,stroke:#064e3b classDef shipped fill:#f5ede6,stroke:#45210e classDef ext fill:#fef6e0,stroke:#9c750a class INV planned class AUTH,TIME,CRM,CRM2,GL,CUO,OBS,NATS planned class BRAIN,BRAIN2,S3,VAT,MST,BANK shipped class STR,WISE,VQR,GDT,SBV ext
11

Compliance scope

INV is the tax-and-financial-records front door. Decree 123, Circular 78, and 10-year retention are non-negotiable for VN tenants.

Regulation / standardArticle / clauseINV feature that satisfies it
Decree 123/2020/NĐ-CPArt. 7 — Electronic invoice mandatevn-vat-invoice skill generates GDT-compliant e-invoice for every VN tenant invoice.
Circular 78/2021/TT-BTCSchema — GDT XML formatSkill emits Circular 78 XML; submits to GDT via authenticated endpoint.
Decree 119/2018/NĐ-CPArt. 4 — E-invoice retentionS3 immutable bucket; 10-year retention lock.
Vietnam VAT Law (Law 13/2008)Art. 8 — VAT rate categoriesLine items carry VAT category code: 0%, 5%, 8%, 10% (Vietnamese categories).
Vietnam Tax Administration Law (2019)Art. 33 — MSTMST validated (tenant + customer) via vn-mst-validate before submission.
Vietnam PDPL (Law 91/2025)Art. 14 — DSARCustomer invoice history exportable as DSAR bundle.
GDPR (EU 2016/679)Art. 6 — Lawful processingCustomer billing data processed on contractual basis.
PCI DSSCardholder data scope minimisationNo raw PAN stored; Stripe handles card data; INV stores only Stripe customer tokens.
ISO/IEC 27001:2022A.8.10 — Information deletionInvoice rows append-only; void via status flag, not delete.
ISO/IEC 27001:2022A.5.13 — Information labellingCustomer + invoice records classified confidential; access scoped.
SOC 2 Type IICC8.1 — Change managementAppend-only ledger; credit notes audited.
VN Accounting Standards (VAS)VAS 14 — RevenueRevenue recognition triggered by invoice.status="sent"; GL bridge per accrual.
SBV Circular 35/2018FX reportingDaily rate snapshot from SBV; cross-border invoice carries snapshot reference.
12

Risk entries

INV's risks are largely about reconciliation correctness and webhook spoofing. The cash-application false-match risk is the highest single-incident risk.

IDRiskLikelihoodImpactOwnerMitigation
R-INV-001Webhook spoofing leads to false payment recordLowHighCTOHMAC signature verification on every webhook; unsigned events rejected; integration test injects unsigned payload.
R-INV-002Duplicate webhook causes double cash-applicationMediumMediumCTOUNIQUE on external_event_id; idempotent INSERT; chaos test replays webhook 100×.
R-INV-003Cash-application false match (wrong invoice)MediumMediumCFOStrict matching: metadata.invoice_id OR (amount + customer + ±1k VND tolerance); ambiguous cases land in manual-override queue.
R-INV-004GDT e-invoice submission rejected (Mẫu format drift)MediumMediumCTO + CFOvn-vat-invoice skill versioned; CI test against GDT sandbox; alert on rejected status.
R-INV-005Currency rounding drift on cross-currency invoiceMediumLowCFORound-half-to-even at line-item level; VND no decimals; property test asserts deterministic.
R-INV-006SBV rate API outage blocks new cross-currency invoicesMediumLowCTOLast-known-good cached; on outage, use yesterday's rate + flag invoice for review.
R-INV-007CUO sends dunning email without approvalLowMediumCSOMCP tool annotated destructive=false; reminder.status="drafted" cannot transition to "sent" without approval_id; CI verifies.
R-INV-008Stripe webhook secret leakedLowHighCSOSecret in AWS Secrets Manager; rotation every 90 days; access audited.
R-INV-009Invoice number gap (regulatory issue in VN)MediumMediumCFOPer-tenant sequence with gap-detection job; void invoices retain number; audit shows void/issued chain.
R-INV-01010-year retention violated by S3 lifecycle bugLowHighCTOImmutable bucket; lifecycle policy review at deploy; quarterly inventory.
13

KPIs

INV health rolls up into 9 KPIs across AR efficiency, reconciliation accuracy, and compliance.

KPIFormulaSourceTarget
DSO (Days Sales Outstanding)(AR balance / sales) × period daysINV DB≤ 35 days
Auto-match rateauto-matched payments / totalcash_application≥ 90%
Invoice generation p95histogramOBS≤ 2 s
GDT e-invoice acceptance rateaccepted / submittedvn_einvoice≥ 99.5%
Dunning effectiveness(invoices paid within 14d of dunning) / dunning sentINV DB≥ 60%
Aging 90+ as % of totalaging_90_plus / total_outstandingaging_report≤ 5%
Manual cash-application queue depthunmatched payments > 24hOBS≤ 2 at any time
Webhook spoofing attempts blockedsignature-failed countOBStracked; alert on bursts
Invoice number gap incidentsdetected gaps / periodcron gap-check= 0 unexplained
14

RACI matrix

INV is operationally owned by CFO; AM drives per-engagement billing; CFO approves dunning; CTO owns reliability.

ActivityCEOCFOAM/PLCTOCSODPO
Engagement billable-rules setupICA/RIII
Invoice draft → sendICA/RIII
Cash application (manual override)IA/RCIII
AR aging reviewIA/RCIII
Dunning approvalIA/RCIII
VN e-invoice integration (GDT)ICIA/RCI
Webhook secret rotationIIIRAI
10-year retention auditIRIACC
DSAR (customer scope)ICRICA

R Responsible · A Accountable · C Consulted · I Informed.

15

Planned CLI surface

Admin CLI cyberos-inv for CFO/AM.

1. Draft an invoice

$ cyberos-inv invoice draft \
    --engagement acme-q2-2026 \
    --period 2026-04

[draft]   reading TIME entries for engagement acme-q2-2026 (2026-04)
[time]    47 entries · 168 hours
[engagement] rate=$150/hr · currency=USD · cap=N/A
[draft]   subtotal: $25,200.00
[draft]   vat:      $0.00 (USD invoice · no VN VAT)
[draft]   total:    $25,200.00
[invoice] INV-2026-04-001 · status=draft
[audit]   brain seq=16001

2. Send an invoice (VN tenant · e-invoice)

$ cyberos-inv invoice send --id INV-2026-04-005

[mst]     validating tenant MST: 0301234567 ✓
[mst]     validating customer MST: 0307654321 ✓
[vn-vat]  generating Mẫu 01/GTGT XML (Circular 78)
[gdt]     submitting to Tổng cục Thuế…
[gdt]     accepted · message_id = GDT-2026-04-9F3E2A1B
[pdf]     deterministic render via tectonic
[s3]      archived to s3://inv/2026/04/INV-2026-04-005.pdf (immutable 10y)
[email]   sent to ke-toan@acme.vn
[nats]    inv.invoice.sent published
[status]  sent
[audit]   brain seq=16011

3. Generate VietQR for an invoice

$ cyberos-inv vietqr --invoice INV-2026-04-005

[vn-bank-transfer skill]
  account:    0307654321 @ BIDV
  amount:     45,000,000 VND
  addInfo:    INV-2026-04-005
  qr:         00020101021238…6304abcd
[image]     vietqr-INV-2026-04-005.png (240×240)
[expected_settlement] instant via Napas247

4. AR aging report

$ cyberos-inv aging --as-of 2026-05-14

[AR aging report · 2026-05-14]

bucket          amount (USD eq)   invoices
  current       $48,200            12
  0-30 overdue  $12,400             3
  31-60         $ 4,800             1
  61-90         $   800             1
  90+           $   200             1
  ─────────    ────────────       ───
  total         $66,400            18

[top overdue]
  INV-2026-02-003 acme corp        $4,800   45 days
  INV-2025-12-007 globex industries $800   71 days
  INV-2025-11-012 initech           $200  120 days

5. Approve dunning emails

$ cyberos-inv dunning list --status drafted

[3 reminders awaiting approval]
  rem-id    invoice          kind   amount   age
  R-001     INV-2026-04-001  T+7    $25,200   8d
  R-002     INV-2026-03-007  T+30   $ 8,400  34d
  R-003     INV-2025-11-012  esc.   $   200 120d

$ cyberos-inv dunning approve --reminder R-001 --cfo-sign

[approved]  cfo: hoa@cyberskill.com ✓
[sent]      email delivered to ar@acme.com
[audit]     brain seq=16031

6. Manual cash application

$ cyberos-inv cash-app match \
    --payment pay_01HZJ8… \
    --invoice INV-2026-04-005 \
    --reason "addInfo missing but bank ref matches"

[match]    payment pay_01HZJ8… → INV-2026-04-005
[update]   invoice.outstanding 45M VND → 0
[update]   invoice.status     sent → paid
[nats]     inv.payment.received published
[audit]    brain seq=16041

7. Issue credit note

$ cyberos-inv credit-note issue \
    --invoice INV-2026-04-001 \
    --amount 1200 \
    --currency USD \
    --reason "scope reduction agreed with customer"

[credit-note] CN-2026-04-001 issued
[invoice]     INV-2026-04-001 outstanding: $25,200 → $24,000
[audit]       brain seq=16051
16

Phase status & estimates

Status
Planned
P2 design phase
Est. LoC (Rust)
~5,800
services/inv + skill bridges
Planned tests
100+
incl. webhook idempotency + currency rounding
External libs
~14
axum · sqlx · stripe · tectonic · async-graphql · cyberskill-vn
CLI subcommands
~20 planned
cyberos-inv entrypoint
P2 budget
~$40/mo
+ Stripe/Wise pass-through fees
CapabilityStatus
Invoice draft from TIME entriesplanned · P2
Multi-currency (VND/USD/SGD/EUR) + SBV snapshotplanned · P2
Stripe (USD/EUR) integration + webhookplanned · P2
Wise integration (multi-currency)planned · P2
VietQR / Napas247 collectionplanned · P2
Vietnamese e-invoice (Decree 123 + Circular 78)planned · P2
MST validation (vn-mst-validate skill)skill shipped
vn-vat-invoice + vn-bank-transfer skillsskills shipped
AR aging reportplanned · P2
Dunning automation (drafts only · human send)planned · P2
Cash application (auto-match + manual override)planned · P2
Credit-note + void workflowplanned · P2
Webhook idempotency + signature verificationplanned · P2
Customer payment portal (token-link)planned · P2
Recurring invoice schedule (subscription)planned · P3
MoMo / ZaloPay / VNPay railsplanned · P3
Open-banking integration (Napas247 real-time)planned · P3
17

References

  • PRD §9.16 — INV module FRs ((FR pending) through (FR pending)).
  • PRD §19.16 — INV architecture posture, multi-rail composition.
  • PRD §11.2.1 — Performance NFRs (invoice generation p95).
  • PRD §11.2.4 — Reliability NFRs (webhook idempotency, cash application).
  • SRS §4.16 — Formal (FR pending) through (FR pending) with verification methods.
  • Decree 123/2020/NĐ-CP — Vietnamese e-invoice mandate.
  • Circular 78/2021/TT-BTC — GDT XML schema for e-invoices.
  • Decree 119/2018/NĐ-CP — E-invoice retention (10 years).
  • Vietnam VAT Law (Law 13/2008/QH12) — VAT category codes.
  • Vietnam Tax Administration Law (2019) — MST regulations.
  • Vietnam PDPL (Law 91/2025) — DSAR; customer billing data.
  • SBV Circular 35/2018 — Daily FX rate disclosure obligations.
  • VAS 14 — Revenue — Vietnamese Accounting Standards revenue recognition.
  • PCI DSS 4.0 — Card data scope; INV stays out of scope by using Stripe.
  • cyberskill-vn collection — vn-vat-invoice · vn-mst-validate · vn-bank-transfer skills.
  • Architecture context: services.html#inv.