Skip to content

SST-02 return generation (Phase 2): design

Status: decided under the user’s “complete phase 2” goal (2026-09-27); decisions D1–D16 below. Compliance-sensitive choices are repeated, one line each, in For the owner to confirm. Roadmap: Phase 2, “SST-02 return generation” (and its note: “a purchase invoice’s tax lines are told apart from its expense lines only by account = tax_code.account_id”). Architecture: §11 (country pack; nothing outside countrypack names SST — CountryPack.StatutoryReports and FilingPeriods were anticipated there), §10.3 (posting engine), §5.5 (semantics), §11A (retention, record language, traceability), §19 Phase 2 table (“SST-02 return generation from tax codes”). Builds on: AR (2026-09-27-accounts-receivable-design.md, §“How SST-02 reads AR”, D7, D10 — the tax snapshot on every sales line and ar_allocation), AP (2026-09-26-accounts-payable-design.md D8 and amendment 1 — tax_code.usage), WHT (2026-09-27-withholding-tax-design.md D1 — usage withholding), the MY country pack (internal/platform/countrypack/my/my.go). Migrations: 00400–00409 (this design uses 00400–00403). E2E lane 8: ERP_STACK=erp-l8, pg 56492, minio 59180, api 3180, vite 5199.

Not reviewed by a Malaysian tax agent or by the Royal Malaysian Customs Department (RMCD). The SST rules below — the taxable period, the due date, when service tax becomes due, what the SST-02 form asks for and in which part — are written from general knowledge of the Sales Tax Act 2018, the Service Tax Act 2018 and RMCD practice, not verified against the Acts, the regulations, RMCD’s guides or the current SST-02 form. Every form reference below is marked unverified. Rules are seeded as country-pack data wherever they can be, so a tax agent can correct them without a code change. Re-verify before a paying customer relies on this.

What the law asks (as understood; see For the owner to confirm)

Section titled “What the law asks (as understood; see For the owner to confirm)”
  • A registered manufacturer charges sales tax (5% / 10%, some goods at specific rates) on taxable goods it sells; it is due when the goods are sold (document basis).
  • A registered service provider charges service tax (6% / 8%) on taxable services; it is due when payment is received, and if payment is not received within 12 months of the invoice, on the day after that 12-month period (payment basis).
  • Both are declared on one return, SST-02, per taxable period — normally two months, aligned to the registrant’s own cycle (RMCD assigns it at registration; a different length can be approved). The return and the tax are due by the last day of the month after the period ends. A nil return is still filed.
  • A registered person that acquires imported taxable services declares and pays service tax on them in its SST-02. Bad-debt relief and some other deductions/adjustments are claimed on the return.
  • There is no input tax credit in SST (unlike GST): tax paid on purchases is cost.
  • Filing is on MySST, RMCD’s portal, by typing figures into the online form. We know of no published file format for uploading SST-02 figures, so v1 produces a figures sheet for manual entry (D12).

In (v1):

  • The organization’s SST registration for returns: effective date, the end of its first taxable period, period length (default two months), the SST payable account, and the SST registration number (kept on the company profile, edited here).
  • Taxable periods derived from that registration, each with its due date and status.
  • The return computed from the sales documents’ frozen tax snapshots (AR D7): sales tax by rate (document basis), service tax by rate (payment basis with the 12-month rule), exempt and out-of-scope sales (information), plus manual adjustments (imported taxable services, other tax payable, deductions) each with a reason.
  • A return record: a draft recomputes live; finalising freezes every figure down to the source document (append-only lines); an amendment is a new version of the same period; a finalised return that no longer matches the ledger is flagged stale.
  • Payment of the tax as its own workflow kind (preparer ≠ approver): Dr SST payable / Cr bank through a registered posting rule; outstanding and overdue per period.
  • A figures sheet export (CSV / XLSX / PDF through reporting/export) laid out by form part, for manual entry on MySST, and a reconciliation line against the SST payable account.
  • Web: a Tax returns screen (registration, periods, the return drawer, payments). en/ms/zh.

Out (each named, with where it goes):

  • MySST upload or API submission — no published format we can specify honestly (D12).
  • Purchase-side computation and a purchase-line tax snapshot — not needed in v1 (D6).
  • Sales tax exemptions for manufacturers’ purchases (Schedule A/B/C), specific-rate goods, goods exported, own-use valuation — reported through adjustments or not at all; a later item with a tax agent.
  • Bad-debt relief as a computed figure — AR has no write-off yet (AR D17); v1 takes it as a manual deduction.
  • Late-payment penalty — flagged (overdue), never computed or posted (D11).
  • Refund of a credit position (an amendment below what was paid) — shown, claimed outside.
  • Deregistration and a change of taxable period after the first finalised return (D2).
  • Group/B2B service-tax relief, tourism tax, digital-service-tax for foreign providers.
  • SST-02A (the return a non-registered person files for imported services) — not our registrant.

D1. A generic tax-return engine; the SST-02 layout is country-pack data

Section titled “D1. A generic tax-return engine; the SST-02 layout is country-pack data”

Architecture §11 forbids anything outside internal/platform/countrypack from naming SST. So:

  • internal/platform/countrypack gains TaxReturnDef (data) and an optional interface ReturnPack { TaxReturn() TaxReturnDef } a pack implements when its country has a periodic indirect-tax return. Validate checks it. The MY pack implements it with the SST-02 layout (countrypack/my/taxreturn.go): return code sst02, period length 2, due one month after the period, and the fields below.
  • internal/platform/taxreturn (+ taxreturntest) is the engine: registration, periods, computation, return versions, payments, the posting rule. It knows pack kinds, bases and field keys, never “SST”. internal/modules/taxreturns is the HTTP shell.
  • A field (ReturnField) is: Key, PackKind (which line snapshots feed it; empty for a manual field), Basis (document | payment | manual), PaymentLimitMonths (payment basis only), Sign (+1 adds to tax payable, −1 deducts, 0 information only), ByRate (rows per rate), FormRef (where it goes on the form) and Names (en/ms/zh). The pack also says FormRefsVerified (false for MY, rendered as “(unverified)” beside every reference).

MY fields, in sheet order (form references unverified — our best reading of the SST-02 layout):

Key Pack kind Basis Sign Form reference (unverified)
sales_tax sales_tax document +1, by rate Part B — sales tax: value of taxable goods sold and tax, per rate
service_tax service_tax payment, 12 months +1, by rate Part C — service tax: value of taxable services and tax, per rate
imported_services — manual +1 Part C — imported taxable services
other_payable — manual +1 Part B/C — other tax payable (own use, disposal)
deduction — manual −1 Part B/C — deductions (bad-debt relief, other)
exempt exempt document 0 Part D — value of exempt goods / services
out_of_scope out_of_scope document 0 not on the form — shown for reconciliation

Tax payable = Σ Sign × tax over fields. A pack kind no field names (a code created with a new pack kind) is reported in an Unmapped section and blocks finalising (409 unmapped_tax_kind), so a new kind can never silently drop out of the return.

D2. The registration: one per organization, frozen once a return is filed

Section titled “D2. The registration: one per organization, frozen once a return is filed”

tax_registration (org_id, return_code, effective_from, first_period_end, period_months, tax_account_id, revision), unique per (org_id, return_code).

  • effective_from — the registration’s effective date (any day). first_period_end — the last day of the first taxable period, a month end, on or after effective_from and within period_months of it (named CHECKs). Users read both off RMCD’s registration approval.
  • period_months 1–12, defaulted from the pack (2).
  • tax_account_id — the SST payable account the payment debits. Defaulted to the one account the org’s active positive-rate sales codes name (2140 in the pack); must be an active postable liability with no control kind other than sst_payable (Phase 2 completion O-7: 2140 carries it, so no manual line can move it).
  • The return files the registration’s own single SST number (Phase 2 completion O-4, 00431: tax_registration.registration_no). The company profile’s sst_registration_no — the field the AR seller snapshot and MyInvois read — may hold two numbers A;B (SST-R14); a return is filed under one. The registration PUT takes one number, stores it, and writes it through company.Update (audited) unless the profile holds two, which it keeps. Finalise freezes the number onto the return. A registration migrated from a two-number profile has no number until an administrator picks it; finalising refuses (registration_no_missing) until then.
  • effective_from, first_period_end, period_months refuse UPDATE once any return of the registration is finalised (trigger tax_registration_period_frozen): every filed period was cut from them. Changing the cycle later (RMCD approval) or deregistering is out of v1.

Period 0 is [effective_from, first_period_end]; period k starts the day after period k−1 ends and ends at the month end period_months months later. due_on = the last day of the month after period_end. One pure function (taxreturn.Periods(reg, through)); a return row stores its own period_start, period_end, due_on (checked against the function when created). GET /tax-returns/periods lists every period from effective_from through the one containing today, newest first, each with its latest return’s status, tax payable, paid, outstanding and overdue (outstanding > 0 or no finalised return, and today > due_on).

D4. Figures come from the sales documents’ tax snapshots; the ledger is the cross-check

Section titled “D4. Figures come from the sales documents’ tax snapshots; the ledger is the cross-check”

The return reads sales_invoice_line.tax_pack_kind / tax_rate / net_base / tax_base (AR D7) — the snapshot frozen at issue, which says what the invoice said whatever later happens to the tax code. It does not tell tax from revenue by reading journal lines: that is exactly the ambiguity the roadmap note warns about, and the snapshot removes it.

  • Document basis (sales_tax, exempt, out_of_scope): lines of issued sales invoices whose invoice_date is in the period, plus lines of issued customer credit notes dated in the period, negative. invoice_date is the posting date (AR D5), so the return and the ledger cut periods identically. Amounts are base (RM).
  • Withholding codes (WHT D1) never appear on sales lines; the engine filters by the field’s PackKind, never by account, so a withholding line on 2160 could not leak in even if one existed.
  • Reconciliation (D13) compares the return with the movement on the SST payable account.

D5. Service tax: payment basis, computed as a cumulative difference

Section titled “D5. Service tax: payment basis, computed as a cumulative difference”

For each issued invoice with payment-basis lines, due(I, d) is the service tax attributable up to day d, per rate:

  • net(I, d) = the invoice’s payment-basis lines − those of its issued credit notes dated ≤ d (taxable and tax, base, per rate);
  • grossNet(I, d) = invoice gross − Σ its credit notes’ gross dated ≤ d (document currency);
  • paid(I, d) = Σ allocations to I dated ≤ d that are payments: receipt allocations, and allocations of a credit note that is not one of I’s own (a credit balance from another invoice applied here is the customer’s money). I’s own credit notes are already in grossNet;
  • if d ≥ invoice_date + PaymentLimitMonths → due = net (the 12-month rule); else if grossNet ≤ 0 → due = net; else due = net × min(1, max(0, paid / grossNet)), rounded once to the sen per invoice per rate, half away from zero.

The period’s figure is due(I, period_end) − due(I, period_start − 1), summed over invoices. It telescopes: the periods of an invoice add up to exactly its net service tax, a later credit note after full payment shows as a negative in its own period, and a partial payment attributes its share.

Dating a payment. A receipt allocation is dated GREATEST(receipt.receipt_date, invoice.invoice_date) — money is received on the receipt date, but cannot pay an invoice before it exists; AR’s effective_on is the recording date and would push tax a period late when a receipt is recorded after month end. A reversal (voided receipt, reversed allocation) and a credit-note allocation are dated effective_on, the day they happened — so a bounced cheque reduces the period it bounced in, not an already-filed one.

ponytail: every issued payment-basis invoice dated ≤ period end is loaded on each computation — fine for SME volumes (thousands); an invoice fully attributed marker is the upgrade if it is not.

D6. No purchase-side snapshot or migration in v1

Section titled “D6. No purchase-side snapshot or migration in v1”

SST has no input credit, so no purchase figure enters the tax payable except imported taxable services (and, for manufacturers, exemption-schedule values, out of v1). Imported services are foreign suppliers’ invoices; nothing on purchase_invoice_line says “imported taxable service” today (the P* codes are domestic tax charged by a supplier, D8 of AP). v1 takes them as a manual adjustment (D9) with a reason — the preparer knows which foreign invoices they are. purchase_invoice_line.tax_code_id already names the code without the account ambiguity; if a later item computes imported services from purchases, it adds the snapshot (tax_pack_kind, tax_rate) then, together with a reverse-charge code and its posting (Dr expense / Cr 2140). The roadmap note is answered: the sales side has its snapshot (AR D7); the purchase side is not read.

AP’s open decision “may a purchase code name 2140”: recommend no. A purchase code naming 2140 would debit the SST payable account with tax that is not creditable, understating the ledger’s liability against the return. The reconciliation (D13) shows the gap if it happens.

D7. The return record: draft → finalised; amendments are new versions

Section titled “D7. The return record: draft → finalised; amendments are new versions”

tax_return (registration_id, period_start, period_end, due_on, version, status, amends_id, amendment_reason, …), status ∈ draft | finalised | discarded.

  • POST /tax-returns {period_start} creates a draft. If the period already has a finalised version, the draft is an amendment of the latest one (amends_id), which requires a reason (record text) and copies that version’s adjustments. version numbers every row of the period, discarded drafts included. At most one draft per period (partial unique index).
  • A draft stores only its adjustments; its figures are computed on every read.
  • Finalise (POST …/finalise {revision}) is allowed only after the period has ended (today > period_end), with no unmapped kinds, and with a non-blank registration number. In one transaction it computes, writes tax_return_line rows (one per field, rate and source document or adjustment), and snapshots registration_no, total_tax, tax_payable, finalised_by/at. Lines are append-only (no UPDATE/DELETE grant; inserts refused unless the return is a draft); a finalised row refuses every UPDATE (trigger tax_return_frozen). The finalised return is what was filed.
  • No second person to finalise. Finalising posts nothing and moves no money; the declaration is signed by the registrant on MySST. The money is controlled at payment (D10).
  • A draft may be discarded (reason); nothing is deleted.

D8. A finalised return that no longer matches is stale, never silently corrected

Section titled “D8. A finalised return that no longer matches is stale, never silently corrected”

A receipt dated inside a filed period, or a credit note’s effect on it, changes what the period should have said. Reading a finalised return recomputes it live and returns stale: true with the live tax payable and the per-field differences; the drawer shows “The ledger now differs from this filed return by RM x — file an amendment”. Late items are not carried into the next period automatically: attribution stays by date (D4, D5), and correcting a filed period is an amendment of that period, which is how we understand RMCD handles errors (unverified).

D9. Manual adjustments, pack-defined, with a reason

Section titled “D9. Manual adjustments, pack-defined, with a reason”

tax_return_adjustment (return_id, field_key, taxable_amount, tax_amount, reason, withdrawn_at): only the pack’s manual fields; amounts ≥ 0 (the field’s Sign gives direction), tax > 0; reason is record text (en/ms). Editable only while the return is a draft (trigger); replaced wholesale on each save (old rows withdrawn, AP P16). Imported services post themselves (Phase 2 completion fix round 1, 00434): the pack marks the field Accrues, each of its adjustments names an active, postable expense account no module owns (account_id), and finalising posts taxreturn’s own accrual Dr <that account> / Cr the registration's tax account (the sst_payable control), dated the period’s last day (or, when that day’s fiscal period is closed, the first open date after it, the reason written on the entry — O-12’s rule), source tax_return/the version’s id. An amendment first reverses the entry of the version it replaces, then posts its own, so 2140 carries only the filed version’s accrual; a discarded draft posts nothing. Other manual fields change the return only. No manual journal can reach 2140 (O-7); the go-live balance enters as an opening entry (opening: true, once, while the account has no lines).

D10. Paying the tax is its own workflow kind

Section titled “D10. Paying the tax is its own workflow kind”

tax_return_payment, cash_only, AP’s payment graph: draft → pending_approval → paid (payout) → voided, rejected → draft, cancelled from draft; Owner seat granted on paid and voided; the engine’s self_approval keeps the preparer off it.

  • Header: reference (TXP-<FY>-NNNN), return_id (a finalised version), bank_account_id (an active postable bank control account whose currency is null or base), payment_date (≤ today), bank_reference (the MySST/FPX reference), amount (base currency, > 0).
  • amount ≤ the period’s outstanding = the latest finalised version’s tax payable − Σ amounts of the period’s payments in paid — checked at submit and again at approval under a FOR UPDATE lock on the period’s latest finalised return (so two approvals serialize). Part payments are allowed.
  • Payout rule PaymentPayoutRule: Dr registration tax_account_id, Cr bank, currency = base, journal = the bank account’s journal (journalFor, AP P25). Void mirrors it (the engine).
  • Why not reuse a kind: AP’s payment settles invoices and WHT’s remittance settles deductions; a tax return payment settles a period. The graph is AP’s, the rule is ten lines.

D11. Outstanding, overdue and credit positions; no penalty

Section titled “D11. Outstanding, overdue and credit positions; no penalty”

A period is overdue when today > due_on and it has no finalised return or outstanding > 0. Outstanding < 0 (an amendment reduced the tax below what was paid) is shown as “credit — claim from RMCD”; nothing posts. A negative tax payable on a return is filed as a credit position the same way. The late-payment penalty is RMCD’s to assess (we believe it is a percentage stepping up every 30 days, capped — unverified); it is not computed, and is paid as an ordinary expense.

D12. The export is a figures sheet for manual MySST entry

Section titled “D12. The export is a figures sheet for manual MySST entry”

Report tax_return in reporting/export (CSV / XLSX / PDF), param return_id: the header (legal name, SST number, period, due date, version, status, finalised by/at, “figures as computed at …”), then one row per field and rate — form reference (with “(unverified)”), label (English, with the Malay label beneath in PDF/XLSX), taxable value, tax — the tax payable, the adjustments with reasons, and the reconciliation. A draft’s sheet is stamped DRAFT — not filed; a superseded or discarded version’s says not the filed version, in its title and on every PDF page’s footer (O-4). It is a transcription aid; the sheet says “Enter these figures on MySST; this file is not an upload format”. Per-document detail is available as JSON (GET /tax-returns/{id}/lines) and in the drawer.

D13. Reconciliation against the SST payable account

Section titled “D13. Reconciliation against the SST payable account”

On every return: the base movement on tax_account_id in the period (credits − debits, excluding tax_return_payment entries; the return’s own imported-services accruals and their reversals count for the period of the version they came from, by source, whatever their date; a go-live opening_balance entry is shown on its own line, not in the movement — Phase 2 completion fix round 3 N-6) against the expected movement = the tax on sales documents dated in the period as issued (sales + service tax lines’ tax_base, credit notes negative — what AR posts at issue, before the payment-basis deferral) + Σ Sign × tax of the adjustments. Difference = movement − expected is expected to be zero; a non-zero difference means a manual journal, an adjustment not journaled (or journaled elsewhere), or a purchase code posting to 2140 — it is shown, not blocked. The payment-basis deferral (service tax charged in the period but not yet due) is shown beside it as its own line. Deferred service tax still sits in 2140 until it becomes due; that is the timing difference AR D7 (output tax posted at issue) creates.

taxreturns.return.read (registration, periods, returns, lines, payments), taxreturns.return.manage (registration incl. the SST number, drafts, adjustments, discard), taxreturns.return.finalise, taxreturns.payment.create (prepare and edit a draft payment), taxreturns.payment.transition (the transition endpoint; authority still from grants). Owner and Finance get all five — new organizations through provisioning.RegisterRolePermissions, existing ones through 00402 (the 00305 pattern). The export is reports.export (AP P4).

Field labels are pack data (Names, en/ms/zh), served with each field as labels{en,ms,zh}; the web shows the current locale. They are statutory-form vocabulary shipped with the pack, like seeded workflow labels, not org-editable names; Validate requires all three locales.

tax_registration, tax_return, tax_return_adjustment, tax_return_payment are amendable (no DELETE grant); tax_return_line is appendOnly (no UPDATE, DELETE). Seven years (§11A.2). Every mutation writes audit_log last (AP P33).

Migration 00400_tax_return.sql:

  • tax_registration — as D2; composite FK to account; CHECKs tax_registration_first_period_month_end, tax_registration_first_period_after_start, tax_registration_first_period_within, tax_registration_period_months_range; trigger tax_registration_period_frozen.
  • tax_return — as D7; CHECKs tax_return_period_order, tax_return_status_known, tax_return_amendment_shape (amends_id IS NULL ⇔ no reason; a reason is non-blank), tax_return_finalised_complete (finalised ⇔ registration_no, total_tax, tax_payable, finalised_by, finalised_at set), tax_return_discarded_complete, record-language CHECKs; unique (org_id, registration_id, period_start, version); partial unique index one draft per period; trigger tax_return_frozen.
  • tax_return_adjustment — trigger tax_return_adjustment_draft_only.
  • tax_return_line — (return_id, field_key, rate, source_kind ∈ sales_invoice | customer_credit_note | adjustment, source_id, reference, document_date, taxable_amount, tax_amount), rate NUMERIC(19,10) nullable (non-rate fields); composite FK to sales_invoice for document sources; trigger tax_return_line_draft_only.

Migration 00401_tax_return_payment.sql: tax_return_payment + draft guard (header fields editable only while the workflow instance’s semantic is draft, reusing public.payables_semantic). Migration 00402_tax_return_grants.sql: Owner and Finance keys. Migration 00403_report_export_tax_return.sql: report_export_report_known gains tax_return (with the union of every report on the branch at build time — program ruling).

All tables: org_id, ENABLE + FORCE RLS, policy org_isolation, (org_id, id) unique, composite FKs, named constraints, pinned trigger functions.

  • GET|PUT /tax-returns/registration — TaxRegistration {return_code, effective_from, first_period_end, period_months, tax_account_id, registration_no, revision, frozen}; 404 no_registration; 404 no_tax_return when the org’s country pack has none.
  • GET /tax-returns/periods — TaxPeriod[] {period_start, period_end, due_on, latest{id, version, status}, tax_payable, paid, outstanding, overdue, stale}.
  • POST /tax-returns {period_start, amendment_reason?} → TaxReturn.
  • GET /tax-returns/{id} — TaxReturn {… finalisable, sections[{field_key, labels, form_ref, form_ref_verified, sign, rows[{rate, taxable_amount, tax_amount, documents}], taxable_amount, tax_amount}], unmapped[], total_tax, tax_payable, paid, outstanding, overdue, stale, live_tax_payable, reconciliation{…}, adjustments[], versions[], payments[]}.
  • GET /tax-returns/{id}/lines?field_key — per-document detail (frozen, or live for a draft).
  • PUT /tax-returns/{id}/adjustments {revision, adjustments[]}; POST /tax-returns/{id}/finalise {revision}; POST /tax-returns/{id}/discard {reason}.
  • GET|POST /tax-returns/payments, GET|PUT /tax-returns/payments/{id}, POST /tax-returns/payments/{id}/transitions.
  • Export enum gains tax_return; ExportParams.return_id.

Error codes: 422 field=… rule=month_end|first_period_within|period_months|tax_account| registration_no|not_a_period|adjustment_field|adjustment_amount|record_language|payment_date_future| bank_account|amount_over_outstanding; 409 registration_frozen, draft_exists, amendment_reason_required (422 field), period_not_ended, unmapped_tax_kind, registration_no_missing, not_draft, stale_revision, return_not_finalised.

  • Tax returns nav item (Accounting group, taxreturns.return.read) → /tax-returns, two tabs: Periods (registration card with Set up / Edit, a DataTable of periods — period, due, status tag, tax payable, paid, outstanding, overdue/stale tags) and Payments (a DataTable of payments with state filter).
  • Return drawer: sections with rate rows (documents count opens the line detail), unmapped warning, adjustments (edit dialog while draft), reconciliation, stale banner, versions, payments, actions Create draft / Amend (reason) / Finalise (ConfirmDialog: “Finalise the return for ? Its figures are frozen; corrections are amendments.”) / Discard, ExportMenu (report tax_return), Record payment (dialog: bank account, date, reference, amount prefilled with outstanding) and the payment’s transitions.
  • Pack: Validate refuses a return def with an unknown basis, a duplicate key, a missing locale, a payment basis without a limit (presence: the MY def passes).
  • Periods: 2-month cycle from a mid-month effective date; 1-month and 3-month cycles; due dates across a year end and February; a first period not ending on a month end refused.
  • Attribution (pure, table-driven): full payment; two part payments in two periods; credit note before payment; credit note after full payment (negative); 12-month rule with and without later payment; voided receipt dated later; cross-invoice credit counted as payment, own credit note not; a foreign-currency invoice (fraction in document currency, amounts in base); sum over periods = net tax.
  • Compute (DB): sales tax at 10% and 5% in the period, credit note negative, an invoice outside the period excluded (presence: inside included); exempt/out-of-scope information only; a withholding code excluded by kind; an unmapped kind blocks finalising (presence: mapped finalises).
  • Schema guards (raw SQL as erp_app): finalised return UPDATE refused; line INSERT on a finalised return refused; line UPDATE/DELETE refused; adjustment write on a finalised return refused; registration period change refused after a finalised return — each after the accepted case.
  • Lifecycle: finalise before period end refused (presence: after); two drafts refused; amendment needs a reason and copies adjustments; stale detected after a backdated receipt (presence: fresh is not).
  • Payment: posts Dr 2140 / Cr bank; over-outstanding refused at submit and at approval (two payments, second refused); self-approval refused; void reverses and restores outstanding.
  • Drift: after a mixed sequence with only document-basis codes, the reconciliation difference is zero; posting an adjustment’s journal to the wrong account makes it non-zero (watched failing).
  • Export: the sheet renders for a draft (stamped) and a finalised return; report_export_report_known accepts tax_return (the export package’s own CHECK drift test).
  • e2e (lane 8): registration → an issued invoice with ST10 and SVT8 lines and a part receipt → the current period’s draft shows sales tax and the paid share of service tax → the previous (ended) period: draft, an imported-services adjustment, finalise, export CSV → payment → a second user approves → the period shows Paid, outstanding 0.
  • AR must be merged first. 00400 references sales_invoice; the computation reads sales_invoice, sales_invoice_line (tax_pack_kind, tax_rate, net_base, tax_base, withdrawn_at), customer_receipt.receipt_date and ar_allocation exactly as AR’s plan defines them; fixtures build on receivablestest.Env. Only Task 1 (pack data, periods — no DB) can run before AR lands.
  • WHT: relies on WHT codes never appearing on sales lines and on tax_code.usage gaining withholding — the engine filters by pack kind, so it needs nothing from WHT.
  • MyInvois / self-billed: the self-billed item covers imported services on the e-Invoice side; if it introduces a reverse-charge code, a later SST-02 revision can compute imported_services from it instead of the adjustment.
  • Report export CHECK is rebuilt by several lanes; 00403 carries the union at build time.
  • AP open decision “may a purchase code name 2140”: this design recommends no (D6).
  1. Service tax on payment basis with the 12-month rule (D5), payments dated by receipt date; alternative (tax at invoice date, as the ledger posts it) is simpler but, as we understand the Act, over-declares unpaid invoices early. Confirmed (2026-09-29): this is the Service Tax Act 2018 time-of-supply rule; the tax agent’s item 3 review re-reads it.
  2. Taxable period = two months from the registration’s own cycle; due = last day of the following month — from memory of the Acts; if RMCD’s rule differs, it is pack data (period length) or one function (due date). Confirmed (2026-09-29): matches the Acts’ default period; a varied period (by RMCD approval) is pack data.
  3. SST-02 form part references are unverified and item numbers are not seeded; a tax agent should map each field to the current form — a data change in countrypack/my/taxreturn.go. Needs outside authority: a licensed tax agent maps every field to the current SST-02 (and SST-02A) form from the RMCD MySST guide. Until then, every export keeps the “(unverified)” stamp.
  4. No MySST upload file — a figures sheet for manual entry; if RMCD publishes an upload format, it is a new export projection. Confirmed (2026-09-29): MySST takes online entry; there is no published upload format to target.
  5. Imported taxable services, bad-debt relief, own use and other deductions are manual adjustments with a reason; they must be journaled separately. Alternative (computed from purchases with a reverse-charge code) costs an AP posting change and a purchase-line snapshot. Confirmed (2026-09-29): imported services now post at finalise; computing them from purchases waits for reverse-charge codes.
  6. Purchase codes should not name 2140 (AP open decision) — recommended no; the reconciliation shows it if one does. Changed (2026-09-29, D3): now refused, not only recommended — a purchase tax code cannot name 2140/sst_payable (and a non-withholding code cannot name 2160/wht_payable).
  7. Credit notes reduce the period they are issued in (sales tax) or via the payment-basis formula (service tax); we are unsure whether RMCD requires them on a separate line. Confirmed (2026-09-29): the separate-line question goes to item 3’s tax-agent mapping.
  8. Late items are not carried forward; a filed period that changes is flagged stale and must be amended. Alternative: carry into the next return — we believe RMCD expects an amendment. Confirmed (2026-09-29): RMCD corrects by amendment (SST-02A).
  9. Finalising needs no second person; paying needs one (preparer ≠ approver). Single-Owner orgs cannot approve their own payment (the AP open decision applies). Confirmed (2026-09-29): finalise moves no money; single-Owner payment is covered by D1’s X1.
  10. No penalty computed; overdue is flagged; a credit position is shown, refunds are claimed outside the system. Confirmed (2026-09-29): the penalty rate is RMCD’s assessment, not the taxpayer’s computation.
  11. The SST registration screen edits the company profile’s SST number — one field for invoices and returns; alternative is a separate copy that can drift from the invoices. Confirmed (2026-09-29): as designed.
  12. Base currency is MYR for an SST-registered organization; a non-MYR base would report base amounts that are not RM. Changed (2026-09-29, D3): now refused, not only assumed — creating an SST registration is refused when base currency is not MYR (shared with WHT’s base-currency change, X5).
  13. Deregistration and period changes after the first filed return are out of v1; the fields freeze. Confirmed (2026-09-29): as designed.