Accounts receivable (Phase 2): design
Status: decided under the user’s “complete phase 2” goal (decisions D1–D20 below); the
compliance-sensitive choices are listed at the end for the owner to confirm.
Roadmap: Phase 2, “AR: customer role, sales invoices, credit notes, receipts, aging, statements”.
Architecture: §18.2 item 3 (party roles), §19 Phase 2 table (AR row), §5.5 (semantics), §10 (ledger),
§11 / §11A.6 (MyInvois is the sales side’s obligation).
Template: the merged AP design (2026-09-26-accounts-payable-design.md, D1–D12, plan Rulings P1–P48).
AR mirrors it wherever the mirror is honest; every place it does not is a decision below.
Migrations: 00310–00329 (this item uses 00310–00314). E2E lane 1: pg 56412, minio 59110,
api 3110, vite 5192.
In: the customer master, sales invoices and customer credit notes shaped for MyInvois and frozen once issued, output SST recorded per line with the tax code snapshotted on the line, customer receipts (including money received on account), append-only allocation of receipts and credit notes to invoices, customer balances, AR aging, customer statements of account, a printable invoice / credit note, and the web screens for all of it.
Out, each its own item or later:
- MyInvois submission, validation, status polling, retry, cancellation, QR code — the MyInvois item (reads this spec’s §“Hand-off to MyInvois”).
- SST-02 return — the SST-02 item (reads §“How SST-02 reads AR”).
- Bad-debt write-off and SST bad-debt relief (D17).
- Debit notes, refund notes and customer refunds (D6, D9) — a debit is a new invoice in v1; a refund of a credit balance waits for its own item. — superseded: see debit notes.
- Quotation → SO → DO → invoice (Phase 4), item master / price lists (Phase 4 inventory), recurring invoices, dunning / reminders (no mailer yet), emailing invoices or statements.
- Credit limits and credit control.
- Linking one party as both supplier and customer (D1).
Decisions
Section titled “Decisions”D1. Customer is a role on party
Section titled “D1. Customer is a role on party”AP D1 already made party role-based and gave it the shared fields (tin, registration_no,
country_code, structured address, email, phone). AR adds:
party.registration_type∈brn | nric | passport | army(nullable; a type needs a number). MyInvois names the buyer’s identification by type;registration_noalone does not say whether it is an SSM number or an NRIC. Vocabulary values are strings.party.sst_registration_no(nullable; NULL means not SST-registered — the MyInvois item maps NULL to the “NA” the payload wants).party_customer (org_id, party_id PK, code, payment_terms_days, currency, default_revenue_account_id, default_tax_code_id, is_active, revision, …).codeunique per org. No bank details: AR never pays a customer in v1.
A new customer always creates a new party row, as AP does. Attaching the customer role to an
existing supplier’s party would make edits on one screen change the other’s record; that is a
deliberate feature, and it belongs with the import/migration tooling item that will also merge
duplicate parties. Out of v1.
D2. The control account already exists: ar, “Trade receivables” 1540
Section titled “D2. The control account already exists: ar, “Trade receivables” 1540”The chart has carried control kind ar since Phase 1 (account_control_kind_known, IsSubledgerKind),
and the MY pack seeds 1540 Trade receivables with it; nothing posts to it yet. So AR needs no new
control kind, no chart change and no provisioning step for existing organizations — unlike AP, which
had to add ap_trade because ap was taken by expense claims. Every journal line on an ar account
carries the customer’s party_id; a drift test asserts Σ open items per customer (base) = the ar
control balance per party.
D3. One platform package and one module; invoice and credit note share a table
Section titled “D3. One platform package and one module; invoice and credit note share a table”internal/platform/receivables (+ receivablestest) owns every AR rule; internal/modules/receivables
is the HTTP shell — AP Ruling P1’s shape. sales_invoice.kind ∈ {sales_invoice, customer_credit_note}
equals the workflow kind and the journal source_kind (AP P2); lines share sales_invoice_line.
D4. The sales document is shaped for MyInvois and frozen once issued (the Ruling)
Section titled “D4. The sales document is shaped for MyInvois and frozen once issued (the Ruling)”Header (all on sales_invoice):
- customer, currency,
fx_rate(required when ≠ base, exactly 1 at base), payment terms (defaulted from the customer), an optionalcustomer_reference(the buyer’s PO number), totalsnet_total,tax_total,gross_amount(= net + tax, a named CHECK) andgross_base. - Set at issue, never before:
reference(the e-Invoice number, D5),issued_at(the issuance date-time MyInvois asks for),invoice_date,due_on, and two snapshots:- seller —
seller_name,seller_tin,seller_registration_no,seller_sst_no,seller_msic,seller_address, copied from the company profile; - buyer —
buyer_name,buyer_tin,buyer_registration_type,buyer_registration_no,buyer_sst_no,buyer_email,buyer_phone,buyer_address_line1/2/city/postcode/state,buyer_country_code, copied from the customer’s party. A frozen document must say what it said on the day, whatever later happens to the customer record or the company profile. A named CHECK ties them:issued_at,reference,invoice_date,due_on,buyer_name,seller_nameare all NULL or all set.
- seller —
- A credit note also carries
original_invoice_id(required) andreason(required) — D6.
Lines (sales_invoice_line): description, classification_code (three digits, required; the web
defaults to 022 “Others”), quantity (> 0, NUMERIC(19,10) / money.Rate, the expense-claim
precedent), optional unit_code (UN/ECE Rec 20, e.g. C62, H87), unit_price, discount_amount,
net_amount = round-to-cents(quantity × unit price) − discount, account_id (an income account),
the tax code and its snapshot (D7), tax_amount, optional tax_exemption_reason, net_base,
tax_base, analytic shares (project revenue).
Money on a sales document is to the sen: net_amount, tax_amount, discount_amount have a named
cents CHECK. Two kernel helpers round once, half away from zero: Amount.MulRateCents (price ×
quantity) and Amount.PercentCents (net × rate %). Tax is computed, never entered — unlike AP D8,
where the supplier rounds its own way; on our own invoice we are the one rounding.
Frozen: the draft guard triggers (AP 00302’s shape) refuse any change to the header’s content
columns and to any line once the instance’s semantic is not draft. The issue snapshot is written in
the issuing transaction before workflow.Transition. An invoice is still a draft then; a credit note is
issued from pending_approval (D6), so the header guard has one narrow exception: a pending document
may take its issue stamp once (issued_at NULL → set) and only with its 14 content columns unchanged
(kind, customer, original invoice, reason, customer reference, currency, rate, terms, the four totals,
instance, preparer). Lines stay frozen from submission. Corrected only by credit note; a draft can
be withdrawn.
D5. Issuing: no approval step, number and date at issue, the issued state is final
Section titled “D5. Issuing: no approval step, number and date at issue, the issued state is final”Seeded sales_invoice graph:
draft ──issue (reporter)──▶ issued (accrual, control ar, terminal) └──withdraw (reporter, reason)──▶ cancelled ("Withdrawn", terminal)- No approval before issue. A sales invoice records revenue the customer will see and dispute if
wrong; the control that matters is on the correction (credit notes, D6) and on receipts’ voids (D8).
An administrator who wants a review step adds one in the workflow editor: the kind is an
accrual-only kind (the validator’s rule 3 already permits a pending step before the accrual), and
every draft save reports the base gross with
workflow.SetAmount, so amount bands work unchanged. - The number is allocated at issue,
INV-<FY>-NNNNfromnumbering(scopesales_invoice, period = fiscal year, AP P12), so the issued series is gapless — a withdrawn draft never consumed a number. Drafts are identified by id and shown as “Draft”. - Invoice date = issue date = posting date (
clock.TodayUTC), because the engine posts on the transition’s business date. This removes, for AR, the cut-off question AP left open (“posting date is the approval date, not the document date”). No back-dating in v1. due_on= invoice date + the document’s payment terms.issuedis terminal. The Ruling allows correction only by credit note, so v1 has no void edge, and the validator requires a non-terminal state to have an exit. MyInvois cancellation (the issuer’s short window after validation) and “rejected by LHDN” handling need a way out ofissued; that is a v2 of this seeded graph owned by the MyInvois item (§“Hand-off to MyInvois”).
D6. Customer credit note: tied to its invoice, approved by a second person
Section titled “D6. Customer credit note: tied to its invoice, approved by a second person”Seeded customer_credit_note graph (AP’s document graph minus the void):
draft ──submit (reporter)──▶ pending_approval ──issue (target, OWNER seat)──▶ issued (accrual, terminal) │ └──reject (advance, reason)──▶ rejected ──revise──▶ draft └──withdraw (reporter, reason)──▶ cancelled- Approval is required. A credit note reduces revenue, output tax and a customer’s debt: it is how
a diverted receipt is hidden. The preparer cannot issue it (the engine’s
self_approvalrefusal, AP P9); the seeded grant names the OWNER seat; administrators re-grant. - It must name an issued invoice of the same customer (
original_invoice_id— MyInvois requires the original e-Invoice reference) and areason(record text). It takes the original’s currency and rate (not editable), so offsetting it never leaves an unposted FX difference (AP P23). - Cap: Σ gross of the invoice’s issued credit notes (this one included) ≤ the invoice’s gross — checked at submit and again at issue under the original invoice’s row lock.
- At issue it allocates itself to the original invoice, up to that invoice’s outstanding amount
(posts nothing: both sides are the customer’s, on
ar). A remainder (the invoice was already paid) stays as the customer’s credit, allocatable to other invoices of the same customer, currency and rate (D10). Refunding it is out of v1. - No debit notes in v1: an extra charge is a new invoice. No refund notes (MyInvois type “refund”). — superseded: see debit notes.
D7. Output SST: per line, tax code snapshotted, posted at issue
Section titled “D7. Output SST: per line, tax code snapshotted, posted at issue”- Every sales line requires a tax code of usage
salesorboth(AP amendment 1’stax_code.usage; the MY pack’sST10 ST5 SVT6 SVT8 EXEMPT OSare allsales). A seller that is not SST-registered picksOS(out of scope). MyInvois’s tax type is mandatory on every line, so a line without a code would be unsubmittable. - The code must be active on the issue date (checked on every save against today, and again at issue).
- Snapshot on the line, written on every draft save and frozen with the line:
tax_code_id,tax_code(its code),tax_pack_kind(sales_tax | service_tax | exempt | out_of_scope, the pack’s own classification),tax_rate. Onlyrateis immutable ontax_code(tax_code_immutable_guard); code, pack kind and account can be edited later, and the return must report what the invoice said. This is what the roadmap’s SST-02 note asked for, done on the sales side. tax_amount=PercentCents(net, tax_rate). A code with a positive rate must name an account (2140 SST payablein the pack): output tax is never absorbed (422tax_code_accountotherwise).- Posting at issue (D11): Cr income per line (net), Cr the tax code’s account per line (tax), each line
carrying
tax_code_id; Drar(gross) withparty_id. A credit note posts the mirror. tax_exemption_reasonis stored when given and not interpreted: whether it is required for anexemptline is a MyInvois rule, andpack_kindhas meaning only insidecountrypack(Ruling R27).
D8. Customer receipts
Section titled “D8. Customer receipts”Kind customer_receipt, cash_only:
draft ──record (reporter)──▶ received (payout, non-terminal) ──void (target, OWNER seat, reason)──▶ voided └──withdraw (reporter, reason)──▶ cancelled- Fields: customer, deposit account, receipt date (≤ today), bank reference, currency, rate, amount,
and planned allocations to the customer’s issued invoices (AP P20: planned in
customer_receipt_linewhile a draft, effectivear_allocationrows when it entersreceived). - No approval to record. Money arrived; the bank statement is the evidence, and the bank reconciliation item will match it. The control is on the reversal: voiding a receipt (a bounced cheque, a wrong customer) needs the OWNER seat, a reason, and reverses its allocations.
- Where the money lands: an active postable account that is either control kind
bankor the default account of an activecashjournal (so1520 Bankand1510 Cash in handboth work). The entry posts on that account’s journal (AP P25’sjournalFor).GET /receivables/deposit-accountslists them, because a Finance user may not holdaccounting.ledger.read-level knowledge of journals. - Payout channels are not reused. A payout channel describes how the organization pays a person (payee fields sealed per member); a receipt needs only the account the money reached. No “payment method” field in v1 (it is optional in MyInvois).
- Number
RCT-<FY>-NNNN, allocated when it entersreceived(the same rule as D5). - The engine posts on the recording date;
receipt_dateis the document’s date. This is AP’s open “posting date” question, and stays open (receipts can be recorded from yesterday’s statement).
D9. Money received on account (advances, overpayments)
Section titled “D9. Money received on account (advances, overpayments)”A receipt need not be fully allocated when it is in the base currency. The remainder posts as a
credit on ar for the customer (the customer’s credit balance) and is allocated to invoices later; that
allocation posts nothing (both sides on ar, same party, rate 1). A foreign-currency receipt must be
fully allocated when recorded (AP D4’s rule): allocating a foreign remainder later to an invoice at a
different rate would leave a realised FX difference nothing posts.
A customer credit balance is shown on ar rather than a separate “customer deposits” liability: it is
what AutoCount and most SME ledgers do, the balance sheet still shows it (a credit balance within
receivables), and it keeps allocation posting-free. Refunding a credit balance is out of v1.
D10. Allocation is append-only; balances, aging and statements are derived
Section titled “D10. Allocation is append-only; balances, aging and statements are derived”ar_allocation mirrors ap_allocation after AP Ruling P21: invoice_id, exactly one of receipt_id /
credit_note_id, generated source_kind, amount, base_amount, currency, effective_on,
reverses_id, created_by/at. No UPDATE/DELETE grant; a reversal is a mirroring row, once. A guard
trigger locks the invoice (and the source) and refuses over-allocation, cross-customer, cross-currency,
cross-rate (credit notes) and an unposted target.
- Receipt allocations made at recording are mirrored in the receipt’s journal entry (Cr
arper invoice at the invoice’s frozen base); for a foreign receipt they are undone only by voiding the receipt (AP P22). For a base-currency receipt any allocation may be reversed on its own (it moves an amount between the invoice and the customer’s credit; rate 1, nothing to post). - Credit-note allocations are reversible on their own.
- Invoice outstanding = gross − Σ
ar_allocation.amountfor it; a credit note’s or receipt’s remainder likewise.
D11. Posting rules
Section titled “D11. Posting rules”receivables.Rules(): AccrualRule{sales_invoice}, AccrualRule{customer_credit_note},
ReceiptRule{} (payout), registered from internal/modules/receivables/boot.go. The accrual rule posts
on the sales journal in the document currency with the frozen bases (AP P13: no FX line on an
accrual). The receipt rule posts Dr deposit account (amount at the receipt’s rate), Cr ar per
allocation at the invoice’s frozen base, Cr ar the remainder (base currency only); ledger.Post adds
the fx_difference line on fx_gain_loss when the bases differ — that line is the realised FX, as in
AP D9. Reversal (a voided receipt) is the engine’s mirror. No rule compares a state name.
D12. FX
Section titled “D12. FX”AP D9 and P13 unchanged: currency + rate per document; bases frozen per line; realised FX on the receipt. A credit note inherits its invoice’s currency and rate (D6). No unrealised revaluation in v1.
D13. AR aging
Section titled “D13. AR aging”A report in reports, like AP aging (AP P4, P31): GET /reports/ar-aging?as_of under reports.read,
and CSV/XLSX/PDF through POST /reports/exports with report = 'ar_aging' under reports.export.
Open items as of a date: issued invoices (positive), credit-note remainders and on-account receipt
remainders (negative, bucketed from their own date), a document counting when its posting entry is
dated on or before the date and no reversal of it is, allocations when effective_on ≤ the date. Days
past due and the buckets are AP’s own helpers (payables.BucketFor, payables.BucketAmounts) —
reused, not copied; a neutral home for them is warranted only if a third aging appears.
GET /receivables/customers/{id}/balance is the same for one customer, by currency.
D14. Customer statements of account
Section titled “D14. Customer statements of account”A statement is built from the ledger, not from the documents: every normal journal line on an
ar account for the customer’s party in one currency, between two dates, with the opening balance
before the first date and a running balance — so the statement cannot disagree with the books. Its
closing balance equals the customer’s aging total in that currency as of the end date (a test holds
it). The statement also carries the aging buckets at its end date, in that currency. Dates are
entry dates (when it hit the books), which for a receipt may differ from its receipt date (D8).
- JSON:
GET /receivables/customers/{id}/statement?from&to¤cyunderreceivables.invoice.read. - File:
report = 'customer_statement'withcustomer_id,from,to,currencythrough the existing export pipeline (CSV/XLSX/PDF), underreports.export. - English column titles (record language, §11A.3), as every existing export.
D15. A printable invoice and credit note
Section titled “D15. A printable invoice and credit note”GET /receivables/invoices/{id}/pdf renders an issued document synchronously with the existing
render PDF writer: the seller and buyer snapshots, number, dates, currency and rate as the page’s
header fields; the lines as the table; net, tax per tax code, and total rows. It is a commercial copy,
not MyInvois’s visual representation — that needs the validated UUID and QR code, which the
MyInvois item adds to this same endpoint. A draft has no PDF (409).
D16. Permissions and roles
Section titled “D16. Permissions and roles”receivables.customer.read, receivables.customer.manage, receivables.invoice.read,
receivables.invoice.create, receivables.invoice.transition, receivables.receipt.read,
receivables.receipt.create, receivables.receipt.transition. Seeded: Owner and Finance get all eight
(new organizations through provisioning.RegisterRolePermissions; existing ones through one migration,
the 00305 pattern). There is no key that must stay Owner-only: the second-person controls (credit
note issue, receipt void) are workflow grants on the OWNER seat, which administrators configure.
AR aging and statement files reuse reports.read / reports.export.
D17. Write-off and bad debt are out of v1
Section titled “D17. Write-off and bad debt are out of v1”ar is a sub-ledger kind, so no manual journal can clear an invoice, and a credit note is the wrong
tool for a bad debt (it tells the customer and MyInvois the sale was reduced, and reduces output tax).
Until the write-off item lands, an uncollectible invoice stays open in aging; the year-end impairment an
auditor requires is a manual journal to an allowance account (not a control account), which already
works. SST bad-debt relief is part of the write-off item.
D18. Duplicates and identity
Section titled “D18. Duplicates and identity”No supplier-number duplicate check applies (we number our own documents). A customer’s code is unique
per org. A buyer TIN is checked with taxrecord’s shape when present; MyInvois’s general TINs for
consolidated B2C and foreign buyers (EI00000000010, EI00000000020) fit that shape.
Buyer TIN and identification are not required to issue in v1 (below-threshold organizations are
exempt from e-Invoicing and must still invoice); the MyInvois item adds the organization’s “e-Invoice
in scope” setting that makes them required at issue (owner to confirm).
D19. Never deleted; retention
Section titled “D19. Never deleted; retention”Issued documents are never edited or deleted (D4, D5). party_customer, sales_invoice,
sales_invoice_line, customer_receipt, customer_receipt_line are amendable (no DELETE grant);
ar_allocation is appendOnly (no UPDATE, DELETE). Seven years, like every record (§11A.2).
D20. Every write is audited; issuing announces itself
Section titled “D20. Every write is audited; issuing announces itself”Every mutation writes audit_log last (AP P33). Entering the issued state publishes
receivables.EventDocumentIssued (DocumentIssued{OrgID, DocumentID, Kind, Reference}) on the
transaction’s event buffer, dispatched after commit — the documents package’s EventUploaded pattern.
No listener exists in v1.
Hand-off to MyInvois
Section titled “Hand-off to MyInvois”What the MyInvois item can rely on, and what it owns.
The data contract. An issued document is a sales_invoice row with issued_at IS NOT NULL. Its
e-Invoice type follows from kind (invoice / credit note); its number is reference; its issuance
date-time is issued_at; currency and exchange rate are currency / fx_rate; seller and buyer
parties are the snapshot columns (D4), never the live customer or company rows; a credit note’s
original e-Invoice is original_invoice_id → that row’s reference. Lines give classification,
description, quantity, measurement (unit_code), unit price, discount, subtotal (net_amount), tax
type (from tax_pack_kind via a mapping the MY pack owns), tax rate, tax amount, exemption reason. All
of it is frozen by trigger. receivables.GetDocument(ctx, tx, id) reads a document with its lines.
The hook. Two, by design: EventDocumentIssued after commit for promptness, and the durable
condition “issued and no submission row” for correctness (an event is lost if the process dies between
commit and dispatch). The MyInvois item’s worker should sweep the condition; the event only wakes it.
What it owns, and should not put on sales_invoice:
- its own table (e.g.
einvoice_submission (org_id, document_id, uuid, long_id, status, validated_at, …)), so the freeze trigger never needs an exception; - the organization’s “e-Invoice in scope” setting and the stricter issue-time checks it implies (buyer
TIN / identification / address / contact required,
tax_exemption_reasonfor exempt lines, state and country codes); - the LHDN code tables (classification, unit, state, country, tax type) and validation against them — v1 checks only their shape;
- v2 of the
sales_invoiceandcustomer_credit_noteseeded graphs:issuedbecomes non-terminal with an owner-granted, reason-required, reversing exit for cancellation within LHDN’s window and for documents LHDN rejected as invalid — and a provisioning step that moves existing organizations onto it; - the QR code and validated UUID on the PDF endpoint (D15);
- the seller fields the company profile does not yet carry (business activity description; tourism tax number), which the seller snapshot will then include.
Unsure, for that item to settle: how long after the issuance date-time LHDN accepts a submission (we believe 72 hours); whether issue and submission must therefore be one action; whether advance payments (D9) must themselves be e-Invoiced on receipt.
How SST-02 reads AR
Section titled “How SST-02 reads AR”- Sales tax (goods): Σ
net_base,tax_baseofsales_invoice_linegrouped bytax_pack_kindandtax_rate, for documents whose accrual journal entry is dated in the taxable period (the same “posted as of” join the aging uses); credit-note lines subtract.tax_code/tax_code_ididentify the code without ambiguity — no reliance onaccount = tax_code.account_id. - Service tax: we believe service tax becomes due when payment is received (or 12 months after the
service if unpaid) — not verified. If so, SST-02 needs the paid proportion of each invoice in the
period:
ar_allocationrows (effective_onin the period, receipts and credit notes told apart bysource_kind) against the invoice’s gross give it; the invoice’s service-tax lines give the tax. - Exempt and out-of-scope sales are reported from the same lines by
tax_pack_kind. - Nothing in SST-02 needs to read journal lines to tell tax from revenue on the sales side.
Permissions summary, API, web
Section titled “Permissions summary, API, web”API (all in api/openapi.yaml first): customers list/get/create/update, balance, statement;
deposit accounts; sales documents list (kind, customer, state, currency, open), get, create, update
draft, transition, allocations, PDF; allocations create (credit note or receipt remainder → invoice) and
reverse; receipts list/get/create/update draft/transition; /reports/ar-aging; exports ar_aging,
customer_statement. Money and quantity are strings on the wire.
Web: three items appended to the sidebar’s Accounting group (Customers, Sales invoices, Receipts), following AP P35 and the nav’s append-only rule; credit notes are a tab of Sales invoices; AR aging is a tab of Reports; a statement (with its export buttons) is a section of the customer drawer. en/ms/zh.
Tests that must exist
Section titled “Tests that must exist”- RLS for every new table (the existing drift test, once they exist); retention lists.
- Freeze: an issued document’s header and lines refuse UPDATE by name (presence: a draft accepts it).
- Issue: number allocated only at issue and gapless across a withdrawn draft; snapshots written; the document date and entry date are the business date.
- Posting: invoice, credit note, receipt (base, foreign with FX, part on account) balanced on the right
accounts,
party_idon everyarline,tax_code_idon every tax line. - Tax snapshot survives an edit of the tax code’s code / pack kind.
- Credit note: needs a same-customer issued invoice; cap refused over; self-issue refused (presence: Owner issues); auto-allocation; remainder allocatable only at the same rate.
- Receipts: foreign remainder refused (presence: base-currency remainder accepted); void reverses allocations and the entry; reversal rules by currency.
- Allocation guard: over-allocation, cross-customer, double reversal refused.
- Sub-ledger = control per customer after a mixed sequence (drift test, watched failing once).
- Statement closing = aging total for the customer and currency.
- Aging boundaries (0, 1, 30, 31, 90, 91 days) and a past “as of”.
- The export CHECK accepts every report the export package knows (drift test — a later migration that
rewrites
report_export_report_knownwithoutar_agingfails the build). - e2e: customer → invoice issued → aging shows it → receipt → aging shows zero → statement lists both.
For the owner to confirm
Section titled “For the owner to confirm”- No approval to issue a sales invoice (the preparer issues); credit notes need a second person. Alternative: a review step on invoices — one more click per invoice; an administrator can add it. Confirmed (2026-09-29): issuing overstates rather than drains assets; the control that matters is on reductions (credit note, void), and those stay second-person.
- Invoice number allocated at issue (gapless issued series). Alternative: at draft creation, as AP does — gaps from withdrawn drafts, which a tax auditor asks about. Confirmed (2026-09-29): a gapless issued series is what auditors and MyInvois expect.
- Invoice date = issue date = posting date; no back-dating. Alternative: a document date earlier than issue — posts into a period that may be closing and may conflict with MyInvois’s timing rule. Confirmed (2026-09-29): matches MyInvois’s issue-time rule and keeps the invoice out of closing periods.
- Receipts post without approval; voiding needs the OWNER seat. Alternative: approve every receipt — slower cash application for a risk the bank reconciliation also catches. Confirmed (2026-09-29): cash in carries low risk; the void is the control point and stays second-person.
- Money on account allowed in MYR only, held as a credit on Trade receivables (not a “customer deposits” liability). Alternative: a separate liability account — cleaner presentation, but every later allocation must post. Unsure whether advance payments need their own e-Invoice. Confirmed (2026-09-29): year-end reclassification of credit balances is a presentation adjustment. Needs outside authority: whether an advance payment needs its own e-Invoice — a tax agent, against the LHDN e-Invoice Specific Guideline (tracked as EINV-10).
- Output SST recognised at issue for both sales and service tax; the SST-02 item decides service tax’s due-on-payment timing from allocations. Unsure of the service-tax time-of-supply rule. Confirmed (2026-09-29): the statutory figure comes from the return (SST D5); 2140’s timing difference is reconciled by the drift test.
- Credit note needs its original invoice and a reason, may not exceed it, and inherits its rate; no debit or refund notes in v1. Unsure whether Malaysian rules require a specific credit-note wording. Confirmed (2026-09-29): additional charges go on a new invoice; the prescribed-particulars check is folded into SST-3’s tax-agent review.
- Buyer TIN / identification not required to issue until the MyInvois item adds “e-Invoice in scope”. Risk: an in-scope organization issues without them and must credit and reissue. Confirmed (2026-09-29): superseded — the MyInvois lane closed the risk: when in scope, the guard requires TIN, ID, phone and address before issue.
- No bad-debt write-off or SST bad-debt relief in v1; uncollectible invoices stay in aging; year-end impairment by manual journal to an allowance account. Confirmed (2026-09-29): the allowance method is MFRS-compliant; relief goes through SST-02 manual adjustment (SST-5).
- Classification code defaults to
022(“Others”) and is shape-checked only — the LHDN code list has not been verified here. Confirmed (2026-09-29): superseded — already checked against the SDK’s code list at issue when in scope, not only shape-checked. Re-verify the list at go-live (DEPLOY_NOTES). - The invoice PDF is a commercial copy, not MyInvois’s visual representation (no UUID/QR yet). Confirmed (2026-09-29): superseded — done by MyInvois D15; the PDF prints UUID, validation link and QR once valid.
- AR aging under
reports.read, as AP aging (AP’s own open decision). Ruled: O-9 — every Phase 2 report’s export and on-screen route needs the owning module’s read key. - Statements are ledger-based and dated by entry date, in English. Alternative: document dates, or a Malay statement layout. Confirmed (2026-09-29): for AR, entry date equals invoice date (item 3), so the two bases coincide; English is the record language.