Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions examples/my/invoice-my.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
$schema: https://gobl.org/draft-0/bill/invoice
$version: v0.214.1
$regime: MY
currency: MYR

series: INV
code: "2025-0001"
issue_date: "2025-04-24"

supplier:
tax_id:
country: "MY"
code: "201901234567"
name: "Tech Solutions Sdn Bhd"
alias: "TechSol"
emails:
- addr: "billing@techsol.my"
addresses:
- street: "123 Jalan Bukit Bintang"
locality: "Kuala Lumpur"
region: "Wilayah Persekutuan"
code: "55100"
country: "MY"

customer:
name: "Innovatech Berhad"
tax_id:
country: "MY"
code: "202001234567"
emails:
- addr: "accounts@innovatech.my"
addresses:
- street: "456 Jalan Tun Razak"
locality: "Kuala Lumpur"
region: "Wilayah Persekutuan"
code: "50400"
country: "MY"

lines:
- quantity: 1
item:
name: "Cloud Hosting Services - April 2025"
price: 50000
unit: "unit"
taxes:
- cat: service-tax
percent: "6.0%"

- quantity: 2
item:
name: "IT Support - Onsite Visit"
price: 15000
unit: "h"
taxes:
- cat: service-tax
percent: "6.0%"
111 changes: 111 additions & 0 deletions examples/my/out/invoice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"$schema": "https://gobl.org/draft-0/bill/invoice",
"$regime": "MY",
"type": "standard",
"series": "INV",
"code": "2025-0001",
"issue_date": "2025-04-24",
"currency": "MYR",
"supplier": {
"name": "Tech Solutions Sdn Bhd",
"alias": "TechSol",
"tax_id": {
"country": "MY",
"code": "201901234567"
},
"addresses": [
{
"street": "123 Jalan Bukit Bintang",
"locality": "Kuala Lumpur",
"region": "Wilayah Persekutuan",
"code": "55100",
"country": "MY"
}
],
"emails": [
{
"addr": "billing@techsol.my"
}
]
},
"customer": {
"name": "Innovatech Berhad",
"tax_id": {
"country": "MY",
"code": "202001234567"
},
"addresses": [
{
"street": "456 Jalan Tun Razak",
"locality": "Kuala Lumpur",
"region": "Wilayah Persekutuan",
"code": "50400",
"country": "MY"
}
],
"emails": [
{
"addr": "accounts@innovatech.my"
}
]
},
"lines": [
{
"i": 1,
"quantity": "1",
"item": {
"name": "Cloud Hosting Services - April 2025",
"price": "50000.00",
"unit": "unit"
},
"sum": "50000.00",
"taxes": [
{
"cat": "service-tax",
"percent": "6.0%"
}
],
"total": "50000.00"
},
{
"i": 2,
"quantity": "2",
"item": {
"name": "IT Support - Onsite Visit",
"price": "15000.00",
"unit": "h"
},
"sum": "30000.00",
"taxes": [
{
"cat": "service-tax",
"percent": "6.0%"
}
],
"total": "30000.00"
}
],
"totals": {
"sum": "80000.00",
"total": "80000.00",
"taxes": {
"categories": [
{
"code": "service-tax",
"rates": [
{
"base": "80000.00",
"percent": "6.0%",
"amount": "4800.00"
}
],
"amount": "4800.00"
}
],
"sum": "4800.00"
},
"tax": "4800.00",
"total_with_tax": "84800.00",
"payable": "84800.00"
}
}
37 changes: 37 additions & 0 deletions examples/my/test-wrong-invoice-my.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
$schema: https://gobl.org/draft-0/bill/invoice
$version: v0.214.1
$regime: MY
currency: MYR

series: INV
code: "2025-0002"
issue_date: "2025-04-24"

supplier:
name: "Tech Solutions Sdn Bhd"
alias: "TechSol"
emails:
- addr: "billing@techsol.my"
addresses:
- street: "123 Jalan Bukit Bintang"
locality: "Kuala Lumpur"
region: "Wilayah Persekutuan"
code: "55100"
country: "MY"

customer:
name: "Innovatech Berhad"
tax_id:
country: "MY"
code: "INVALID"
emails:
- addr: "accounts@innovatech.my"
addresses:
- street: "456 Jalan Tun Razak"
locality: "Kuala Lumpur"
region: "Wilayah Persekutuan"
code: "50400"
country: "MY"

# Only 0 lines → should trigger validation error
lines: []
67 changes: 67 additions & 0 deletions regimes/my/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 🇲🇾 GOBL Malaysia Tax Regime

Find example MY GOBL files in the [`examples`](../../examples/my) (uncalculated documents) and [`examples/out`](../../examples/my/out) (calculated envelopes) subdirectories.

## Malaysia-specific Requirements

### Tax IDs

In Malaysia, companies and taxable entities are primarily identified using:

- **Business Registration Number**: A 12-digit number issued upon company registration. Example: `201901234567`
- **Service Tax / Sales Tax Registration Numbers**: For companies registered under SST (Sales and Service Tax), tax IDs may appear in alphanumeric formats such as:
- `SST1234567890`
- `W10-12345678-123`
- These identifiers may include letters and hyphens.

During the normalization process of Tax Identities, GOBL will automatically **uppercase** Malaysian tax IDs for consistency.

A Malaysian company's `org.Party` definition inside an invoice may look like:

```json
{
"tax_id": {
"country": "MY",
"code": "201901234567"
},
"name": "Tech Solutions Sdn Bhd",
"addresses": [
{
"street": "123 Jalan Bukit Bintang",
"locality": "Kuala Lumpur",
"region": "Wilayah Persekutuan",
"code": "55100",
"country": "MY"
}
],
"emails": [
{
"addr": "billing@techsol.my"
}
],
"identities": [
{
"type": "SST",
"code": "SST1234567890"
}
]
}
```

**Notes:**
- The primary `tax_id` field must always be filled.
- Additional tax identifiers like SST numbers can be included in the `identities` array for clarity or regulatory needs.
- Both numeric-only and alphanumeric tax IDs are accepted, following Malaysian Customs regulations.
- The logic to validate or normalize values in the identities array (e.g., SST numbers) has not been implemented yet.

## Disclaimer

> This implementation of the Malaysian tax regime (`MY`) for GOBL has been developed as part of a technical exercise and is intended for **demonstration and testing purposes only**. While every effort has been made to align with publicly available SST regulations and business practices in Malaysia, this module is **not certified for production use** and may not cover all legal, fiscal, or e-invoicing requirements.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary.

>
> Users are advised to consult with a certified tax advisor or local regulatory authority before using this module in any commercial or legal context.


Sources:
- [Royal Malaysian Customs Department - Sales and Service Tax](https://mysst.customs.gov.my/)


73 changes: 73 additions & 0 deletions regimes/my/invoice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package my_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/invopop/gobl/bill"
"github.com/invopop/gobl/cal"
"github.com/invopop/gobl/num"
"github.com/invopop/gobl/org"
"github.com/invopop/gobl/tax"
)

func TestInvoiceValidation(t *testing.T) {
t.Run("standard invoice", func(t *testing.T) {
inv := testInvoiceStandard(t)
require.NoError(t, inv.Calculate())
require.NoError(t, inv.Validate())
})

t.Run("missing supplier tax ID", func(t *testing.T) {
inv := testInvoiceStandard(t)
inv.SetRegime("MY")
inv.Supplier.TaxID = nil
require.NoError(t, inv.Calculate())
err := inv.Validate()
assert.ErrorContains(t, err, "supplier: (tax_id: cannot be blank.)")
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test appears to be present for requiring the customer.

}

// testInvoiceStandard provides a valid minimal Malaysian invoice
func testInvoiceStandard(t *testing.T) *bill.Invoice {
t.Helper()

return &bill.Invoice{
Regime: tax.WithRegime("MY"),
Currency: "MYR",
Code: "INV-0001",
IssueDate: cal.MakeDate(2025, 4, 24),
Supplier: &org.Party{
Name: "Tech Solutions Sdn Bhd",
TaxID: &tax.Identity{
Country: "MY",
Code: "201901234567",
},
},
Customer: &org.Party{
Name: "Innovatech Berhad",
TaxID: &tax.Identity{
Country: "MY",
Code: "202001234567",
},
},
Lines: []*bill.Line{
{
Quantity: num.MakeAmount(1, 0),
Item: &org.Item{
Name: "Cloud Hosting Services",
Price: num.NewAmount(10000, 2), // 100.00 MYR
Unit: org.UnitHour,
},
Taxes: tax.Set{
{
Category: "service-tax",
Percent: num.NewPercentage(6, 1),
},
},
},
},
}
}
Loading
Loading