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 bankthrough 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.
Decisions
Section titled “Decisions”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/countrypackgainsTaxReturnDef(data) and an optional interfaceReturnPack { TaxReturn() TaxReturnDef }a pack implements when its country has a periodic indirect-tax return.Validatechecks it. The MY pack implements it with the SST-02 layout (countrypack/my/taxreturn.go): return codesst02, 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/taxreturnsis 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(+1adds to tax payable,−1deducts,0information only),ByRate(rows per rate),FormRef(where it goes on the form) andNames(en/ms/zh). The pack also saysFormRefsVerified(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 aftereffective_fromand withinperiod_monthsof it (named CHECKs). Users read both off RMCD’s registration approval.period_months1–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-ratesalescodes name (2140in the pack); must be an active postable liability with no control kind other thansst_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’ssst_registration_no— the field the AR seller snapshot and MyInvois read — may hold two numbersA;B(SST-R14); a return is filed under one. The registrationPUTtakes one number, stores it, and writes it throughcompany.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_monthsrefuse UPDATE once any return of the registration is finalised (triggertax_registration_period_frozen): every filed period was cut from them. Changing the cycle later (RMCD approval) or deregistering is out of v1.
D3. Periods are derived, never stored
Section titled “D3. Periods are derived, never stored”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 whoseinvoice_dateis in the period, plus lines of issued customer credit notes dated in the period, negative.invoice_dateis 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 toIdated ≤dthat are payments: receipt allocations, and allocations of a credit note that is not one ofI’s own (a credit balance from another invoice applied here is the customer’s money).I’s own credit notes are already ingrossNet;- if
d ≥ invoice_date + PaymentLimitMonths→due = net(the 12-month rule); else ifgrossNet ≤ 0→due = net; elsedue = 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.versionnumbers 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, writestax_return_linerows (one per field, rate and source document or adjustment), and snapshotsregistration_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 (triggertax_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 postablebankcontrol 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 inpaid— checked at submit and again at approval under aFOR UPDATElock on the period’s latest finalised return (so two approvals serialize). Part payments are allowed.- Payout rule
PaymentPayoutRule:Drregistrationtax_account_id,Crbank, 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.
D14. Permissions
Section titled “D14. Permissions”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).
D15. Labels
Section titled “D15. Labels”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.
D16. Retention and audit
Section titled “D16. Retention and audit”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).
Data model
Section titled “Data model”Migration 00400_tax_return.sql:
tax_registration— as D2; composite FK toaccount; CHECKstax_registration_first_period_month_end,tax_registration_first_period_after_start,tax_registration_first_period_within,tax_registration_period_months_range; triggertax_registration_period_frozen.tax_return— as D7; CHECKstax_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_atset),tax_return_discarded_complete, record-language CHECKs; unique(org_id, registration_id, period_start, version); partial unique index one draft per period; triggertax_return_frozen.tax_return_adjustment— triggertax_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 tosales_invoicefor document sources; triggertax_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.
API (in api/openapi.yaml first)
Section titled “API (in api/openapi.yaml first)”GET|PUT /tax-returns/registration—TaxRegistration {return_code, effective_from, first_period_end, period_months, tax_account_id, registration_no, revision, frozen}; 404no_registration; 404no_tax_returnwhen 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, aDataTableof periods — period, due, status tag, tax payable, paid, outstanding, overdue/stale tags) and Payments (aDataTableof 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(reporttax_return), Record payment (dialog: bank account, date, reference, amount prefilled with outstanding) and the payment’s transitions.
Tests that must exist
Section titled “Tests that must exist”- Pack:
Validaterefuses 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_knownacceptstax_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.
Dependencies on other Phase 2 items
Section titled “Dependencies on other Phase 2 items”- AR must be merged first.
00400referencessales_invoice; the computation readssales_invoice,sales_invoice_line(tax_pack_kind,tax_rate,net_base,tax_base,withdrawn_at),customer_receipt.receipt_dateandar_allocationexactly as AR’s plan defines them; fixtures build onreceivablestest.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.usagegainingwithholding— 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_servicesfrom it instead of the adjustment. - Report export CHECK is rebuilt by several lanes;
00403carries the union at build time. - AP open decision “may a purchase code name 2140”: this design recommends no (D6).
For the owner to confirm
Section titled “For the owner to confirm”- 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.
- 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.
- 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. - 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.
- 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.
- 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). - 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.
- 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).
- 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.
- 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.
- 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.
- 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).
- Deregistration and period changes after the first filed return are out of v1; the fields freeze. Confirmed (2026-09-29): as designed.