From 2b248bf65c616905c3e865e609494d6f34865d23 Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 14:53:37 +0200 Subject: [PATCH 1/8] Add Australian GST regime support. Made-with: Cursor --- data/regimes/au.json | 126 ++++++++++++++++++++++++++++ data/schemas/tax/regime-code.json | 4 + examples/au/invoice-au-au.yaml | 41 +++++++++ examples/au/out/invoice-au-au.json | 93 +++++++++++++++++++++ regimes/au/README.md | 32 +++++++ regimes/au/au.go | 91 ++++++++++++++++++++ regimes/au/au_test.go | 33 ++++++++ regimes/au/bill_invoice.go | 70 ++++++++++++++++ regimes/au/bill_invoice_test.go | 130 +++++++++++++++++++++++++++++ regimes/au/tax_categories.go | 59 +++++++++++++ regimes/au/tax_identity.go | 57 +++++++++++++ regimes/au/tax_identity_test.go | 80 ++++++++++++++++++ regimes/regimes.go | 1 + 13 files changed, 817 insertions(+) create mode 100644 data/regimes/au.json create mode 100644 examples/au/invoice-au-au.yaml create mode 100644 examples/au/out/invoice-au-au.json create mode 100644 regimes/au/README.md create mode 100644 regimes/au/au.go create mode 100644 regimes/au/au_test.go create mode 100644 regimes/au/bill_invoice.go create mode 100644 regimes/au/bill_invoice_test.go create mode 100644 regimes/au/tax_categories.go create mode 100644 regimes/au/tax_identity.go create mode 100644 regimes/au/tax_identity_test.go diff --git a/data/regimes/au.json b/data/regimes/au.json new file mode 100644 index 000000000..c83508717 --- /dev/null +++ b/data/regimes/au.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "Australia" + }, + "description": { + "en": "Australia's indirect tax system is administered by the Australian\nTaxation Office (ATO). Goods and Services Tax (GST) applies at a\nstandard rate of 10%, with zero-rated exports and exempt supplies for\nspecific transactions such as certain financial services and residential\nrent.\n\nBusinesses are identified by an Australian Business Number (ABN), an\n11-digit identifier used for GST registration and invoicing. Australian\ntax invoices must show the supplier's details and ABN, and invoices of\nAUD 1,000 or more must also identify the customer. Electronic invoicing\nis aligned with the Peppol PINT A-NZ specification." + }, + "sources": [ + { + "title": { + "en": "ATO - GST" + }, + "url": "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst" + }, + { + "title": { + "en": "ATO - Tax invoices" + }, + "url": "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst/tax-invoices" + }, + { + "title": { + "en": "Peppol PINT A-NZ BIS" + }, + "url": "https://docs.peppol.eu/poac/aunz/pint-aunz/bis/" + }, + { + "title": { + "en": "ATO - eInvoicing" + }, + "url": "https://www.ato.gov.au/businesses-and-organisations/invoicing-and-using-accounting-software/einvoicing" + } + ], + "time_zone": "Australia/Sydney", + "country": "AU", + "currency": "AUD", + "tax_scheme": "GST", + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], + "categories": [ + { + "code": "GST", + "name": { + "en": "GST" + }, + "title": { + "en": "Goods and Services Tax" + }, + "keys": [ + { + "key": "standard", + "name": { + "en": "Standard" + } + }, + { + "key": "zero", + "name": { + "en": "Zero" + } + }, + { + "key": "exempt", + "name": { + "en": "Exempt" + }, + "no_percent": true + }, + { + "key": "outside-scope", + "name": { + "en": "Outside scope" + }, + "no_percent": true + } + ], + "rates": [ + { + "rate": "general", + "keys": [ + "standard" + ], + "name": { + "en": "General rate" + }, + "values": [ + { + "since": "2000-07-01", + "percent": "10%" + } + ] + }, + { + "rate": "zero", + "keys": [ + "zero" + ], + "name": { + "en": "Zero rate" + }, + "values": [ + { + "since": "2000-07-01", + "percent": "0%" + } + ] + } + ], + "sources": [ + { + "title": { + "en": "ATO - GST" + }, + "url": "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst" + } + ] + } + ] +} \ No newline at end of file diff --git a/data/schemas/tax/regime-code.json b/data/schemas/tax/regime-code.json index c4b6e938a..d0b7abb81 100644 --- a/data/schemas/tax/regime-code.json +++ b/data/schemas/tax/regime-code.json @@ -17,6 +17,10 @@ "const": "AT", "title": "Austria" }, + { + "const": "AU", + "title": "Australia" + }, { "const": "BE", "title": "Belgium" diff --git a/examples/au/invoice-au-au.yaml b/examples/au/invoice-au-au.yaml new file mode 100644 index 000000000..717a69b75 --- /dev/null +++ b/examples/au/invoice-au-au.yaml @@ -0,0 +1,41 @@ +$schema: https://gobl.org/draft-0/bill/invoice +$regime: AU +uuid: 2d301f3b-370a-4e15-a554-a8e05f61e93b +currency: AUD +series: "2026" +code: "AU0001" +issue_date: "2026-04-03" + +supplier: + name: Example Supplier Pty Ltd + tax_id: + country: AU + code: "51824753556" + addresses: + - street: George Street + number: "100" + locality: Sydney + region: NSW + code: "2000" + country: AU + +customer: + name: Example Customer Pty Ltd + addresses: + - street: Collins Street + number: "200" + locality: Melbourne + region: VIC + code: "3000" + country: AU + +lines: + - quantity: "1" + item: + name: Software engineering services + price: "900.00" + unit: h + taxes: + - cat: GST + rate: general + key: standard diff --git a/examples/au/out/invoice-au-au.json b/examples/au/out/invoice-au-au.json new file mode 100644 index 000000000..cffa8ac5e --- /dev/null +++ b/examples/au/out/invoice-au-au.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "4d8afaf12759b4ca824799073709d20282467f48829766514b9ee94012ef14cc" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "AU", + "uuid": "2d301f3b-370a-4e15-a554-a8e05f61e93b", + "type": "standard", + "series": "2026", + "code": "AU0001", + "issue_date": "2026-04-03", + "currency": "AUD", + "supplier": { + "name": "Example Supplier Pty Ltd", + "tax_id": { + "country": "AU", + "code": "51824753556" + }, + "addresses": [ + { + "street": "George Street", + "locality": "Sydney", + "region": "NSW", + "code": "2000", + "country": "AU" + } + ] + }, + "customer": { + "name": "Example Customer Pty Ltd", + "addresses": [ + { + "street": "Collins Street", + "locality": "Melbourne", + "region": "VIC", + "code": "3000", + "country": "AU" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "Software engineering services", + "price": "900.00", + "unit": "h" + }, + "sum": "900.00", + "taxes": [ + { + "cat": "GST", + "key": "standard", + "rate": "general", + "percent": "10%" + } + ], + "total": "900.00" + } + ], + "totals": { + "sum": "900.00", + "total": "900.00", + "taxes": { + "categories": [ + { + "code": "GST", + "rates": [ + { + "key": "standard", + "base": "900.00", + "percent": "10%", + "amount": "90.00" + } + ], + "amount": "90.00" + } + ], + "sum": "90.00" + }, + "tax": "90.00", + "total_with_tax": "990.00", + "payable": "990.00" + } + } +} \ No newline at end of file diff --git a/regimes/au/README.md b/regimes/au/README.md new file mode 100644 index 000000000..bef3d3407 --- /dev/null +++ b/regimes/au/README.md @@ -0,0 +1,32 @@ +# Australia (`AU`) + +Australia uses a Goods and Services Tax (GST) system administered by the Australian Taxation Office (ATO). GOBL models the Australian regime with a 10% standard GST rate, support for zero-rated supplies, ABN validation, and invoice validation rules for supplier and customer tax identifiers. + +## Public Documentation + +- [ATO - GST](https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst) +- [ATO - Tax invoices](https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst/tax-invoices) +- [ABR - ABN format](https://abr.business.gov.au/Help/AbnFormat) +- [Peppol PINT A-NZ BIS](https://docs.peppol.eu/poac/aunz/pint-aunz/bis/) + +## Tax Identity (ABN) + +Australian businesses are commonly identified by an Australian Business Number (ABN). The ABN is 11 digits long, usually written with spaces for display, but normalized in GOBL without separators. + +Validation follows the ABR checksum algorithm: + +| Position | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Weight | 10 | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | + +1. Subtract 1 from the first digit. +2. Multiply each digit by its weight. +3. Sum the products. +4. The total must be divisible by 89. + +## GST + +| Rate Name | GOBL Rate Key | Percent | Since | +| --- | --- | --- | --- | +| General rate | `standard` / `general` | 10% | 2000-07-01 | +| Zero rate | `zero` / `zero` | 0% | 2000-07-01 | diff --git a/regimes/au/au.go b/regimes/au/au.go new file mode 100644 index 000000000..b457328bf --- /dev/null +++ b/regimes/au/au.go @@ -0,0 +1,91 @@ +package au + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/pkg/here" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegimeDef(New()) +} + +// New instantiates a new Australian regime. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: l10n.AU.Tax(), + Currency: currency.AUD, + TaxScheme: tax.CategoryGST, + Name: i18n.String{ + i18n.EN: "Australia", + }, + Description: i18n.String{ + i18n.EN: here.Doc(` + Australia's indirect tax system is administered by the Australian + Taxation Office (ATO). Goods and Services Tax (GST) applies at a + standard rate of 10%, with zero-rated exports and exempt supplies for + specific transactions such as certain financial services and residential + rent. + + Businesses are identified by an Australian Business Number (ABN), an + 11-digit identifier used for GST registration and invoicing. Australian + tax invoices must show the supplier's details and ABN, and invoices of + AUD 1,000 or more must also identify the customer. Electronic invoicing + is aligned with the Peppol PINT A-NZ specification. + `), + }, + Sources: []*cbc.Source{ + { + Title: i18n.NewString("ATO - GST"), + URL: "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst", + }, + { + Title: i18n.NewString("ATO - Tax invoices"), + URL: "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst/tax-invoices", + }, + { + Title: i18n.NewString("Peppol PINT A-NZ BIS"), + URL: "https://docs.peppol.eu/poac/aunz/pint-aunz/bis/", + }, + { + Title: i18n.NewString("ATO - eInvoicing"), + URL: "https://www.ato.gov.au/businesses-and-organisations/invoicing-and-using-accounting-software/einvoicing", + }, + }, + TimeZone: "Australia/Sydney", + Corrections: []*tax.CorrectionDefinition{ + { + Schema: bill.ShortSchemaInvoice, + Types: []cbc.Key{ + bill.InvoiceTypeCreditNote, + }, + }, + }, + Validator: Validate, + Normalizer: Normalize, + Categories: taxCategories(), + } +} + +// Validate checks the document type and determines if it can be validated. +func Validate(doc any) error { + switch obj := doc.(type) { + case *bill.Invoice: + return validateBillInvoice(obj) + case *tax.Identity: + return validateTaxIdentity(obj) + } + return nil +} + +// Normalize will attempt to clean the object passed to it. +func Normalize(doc any) { + switch obj := doc.(type) { + case *tax.Identity: + tax.NormalizeIdentity(obj) + } +} diff --git a/regimes/au/au_test.go b/regimes/au/au_test.go new file mode 100644 index 000000000..e265f4049 --- /dev/null +++ b/regimes/au/au_test.go @@ -0,0 +1,33 @@ +package au_test + +import ( + "testing" + + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/regimes/au" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + t.Parallel() + + regime := au.New() + + assert.Equal(t, l10n.AU.Tax(), regime.Country) + assert.Equal(t, currency.AUD, regime.Currency) + assert.Equal(t, tax.CategoryGST, regime.TaxScheme) + assert.Equal(t, "Australia", regime.Name.String()) + assert.NotEmpty(t, regime.Categories) + assert.NotNil(t, regime.Validator) + assert.NotNil(t, regime.Normalizer) +} + +func TestRegimeValidation(t *testing.T) { + t.Parallel() + + regime := au.New() + require.NoError(t, regime.Validate()) +} diff --git a/regimes/au/bill_invoice.go b/regimes/au/bill_invoice.go new file mode 100644 index 000000000..964d18833 --- /dev/null +++ b/regimes/au/bill_invoice.go @@ -0,0 +1,70 @@ +package au + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +var customerABNThreshold = num.MakeAmount(100000, 2) + +func validateBillInvoice(inv *bill.Invoice) error { + return validation.ValidateStruct(inv, + validation.Field(&inv.Supplier, + validation.By(validateBillInvoiceSupplier), + validation.Skip, + ), + validation.Field(&inv.Customer, + validation.When( + requiresCustomerABN(inv), + validation.Required, + ).Else( + validation.Skip, + ), + validation.By(validateBillInvoiceCustomer(inv)), + validation.Skip, + ), + ) +} + +func validateBillInvoiceSupplier(value any) error { + party, ok := value.(*org.Party) + if !ok || party == nil { + return nil + } + return validation.ValidateStruct(party, + validation.Field(&party.Name, + validation.Required, + validation.Skip, + ), + validation.Field(&party.TaxID, + validation.Required, + tax.RequireIdentityCode, + validation.Skip, + ), + ) +} + +func validateBillInvoiceCustomer(inv *bill.Invoice) validation.RuleFunc { + return func(value any) error { + party, ok := value.(*org.Party) + if !ok || party == nil || !requiresCustomerABN(inv) { + return nil + } + return validation.ValidateStruct(party, + validation.Field(&party.TaxID, + validation.Required, + tax.RequireIdentityCode, + validation.Skip, + ), + ) + } +} + +func requiresCustomerABN(inv *bill.Invoice) bool { + return inv != nil && + inv.Totals != nil && + inv.Totals.TotalWithTax.Compare(customerABNThreshold) >= 0 +} diff --git a/regimes/au/bill_invoice_test.go b/regimes/au/bill_invoice_test.go new file mode 100644 index 000000000..225796f1e --- /dev/null +++ b/regimes/au/bill_invoice_test.go @@ -0,0 +1,130 @@ +package au_test + +import ( + "testing" + "time" + + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/currency" + "github.com/invopop/gobl/l10n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func validInvoice() *bill.Invoice { + return &bill.Invoice{ + Regime: tax.WithRegime("AU"), + Series: "2026", + Code: "AU0001", + IssueDate: cal.MakeDate(2026, time.April, 3), + Currency: currency.AUD, + Supplier: &org.Party{ + Name: "Example Supplier Pty Ltd", + TaxID: &tax.Identity{ + Country: l10n.AU.Tax(), + Code: "51824753556", + }, + Addresses: []*org.Address{ + { + Street: "George Street", + Number: "100", + Locality: "Sydney", + State: "NSW", + Code: "2000", + Country: l10n.AU.ISO(), + }, + }, + }, + Customer: &org.Party{ + Name: "Example Customer Pty Ltd", + Addresses: []*org.Address{ + { + Street: "Collins Street", + Number: "200", + Locality: "Melbourne", + State: "VIC", + Code: "3000", + Country: l10n.AU.ISO(), + }, + }, + }, + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(1, 0), + Item: &org.Item{ + Name: "Software engineering services", + Price: num.NewAmount(90000, 2), + Unit: org.UnitHour, + }, + Taxes: tax.Set{ + { + Category: tax.CategoryGST, + Rate: tax.RateGeneral, + }, + }, + }, + }, + } +} + +func TestInvoiceValidation(t *testing.T) { + t.Parallel() + + t.Run("valid invoice under threshold", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + }) + + t.Run("valid invoice at threshold with customer ABN", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Lines[0].Item.Price = num.NewAmount(100000, 2) + inv.Customer.TaxID = &tax.Identity{ + Country: l10n.AU.Tax(), + Code: "53004085616", + } + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + }) + + t.Run("invoice at threshold without customer ABN", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Lines[0].Item.Price = num.NewAmount(100000, 2) + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "tax_id") + }) + + t.Run("nil supplier", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier = nil + require.NoError(t, inv.Calculate()) + require.Error(t, inv.Validate()) + }) + + t.Run("missing supplier tax ID", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier.TaxID = nil + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "tax_id") + }) + + t.Run("missing supplier name", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier.Name = "" + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "name") + }) +} diff --git a/regimes/au/tax_categories.go b/regimes/au/tax_categories.go new file mode 100644 index 000000000..0e5a0e443 --- /dev/null +++ b/regimes/au/tax_categories.go @@ -0,0 +1,59 @@ +package au + +import ( + "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/num" + "github.com/invopop/gobl/tax" +) + +func taxCategories() []*tax.CategoryDef { + return []*tax.CategoryDef{ + { + Code: tax.CategoryGST, + Name: i18n.String{ + i18n.EN: "GST", + }, + Title: i18n.String{ + i18n.EN: "Goods and Services Tax", + }, + Sources: []*cbc.Source{ + { + Title: i18n.NewString("ATO - GST"), + URL: "https://www.ato.gov.au/businesses-and-organisations/gst-excise-and-indirect-taxes/gst", + }, + }, + Retained: false, + Keys: tax.GlobalGSTKeys(), + Rates: []*tax.RateDef{ + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateGeneral, + Name: i18n.String{ + i18n.EN: "General rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2000, 7, 1), + Percent: num.MakePercentage(10, 2), + }, + }, + }, + { + Keys: []cbc.Key{tax.KeyZero}, + Rate: tax.RateZero, + Name: i18n.String{ + i18n.EN: "Zero rate", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2000, 7, 1), + Percent: num.PercentageZero, + }, + }, + }, + }, + }, + } +} diff --git a/regimes/au/tax_identity.go b/regimes/au/tax_identity.go new file mode 100644 index 000000000..ab1aa94db --- /dev/null +++ b/regimes/au/tax_identity.go @@ -0,0 +1,57 @@ +package au + +import ( + "errors" + "strconv" + "strings" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +const abnLength = 11 + +var abnWeights = []int{10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19} + +// validateTaxIdentity performs validation specific to Australian tax IDs. +func validateTaxIdentity(tID *tax.Identity) error { + if tID == nil { + return nil + } + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, + validation.By(validateABN), + validation.Skip, + ), + ) +} + +// validateABN checks Australian Business Numbers (ABNs). +// Reference: https://abr.business.gov.au/Help/AbnFormat +func validateABN(value any) error { + code, _ := value.(cbc.Code) + normalized := strings.ReplaceAll(strings.ToUpper(code.String()), " ", "") + if normalized == "" { + return nil + } + if len(normalized) != abnLength { + return errors.New("invalid length") + } + if _, err := strconv.Atoi(normalized); err != nil { + return errors.New("invalid characters, expected numeric") + } + + sum := 0 + for i, r := range normalized { + digit := int(r - '0') + if i == 0 { + digit-- + } + sum += digit * abnWeights[i] + } + if sum%89 != 0 { + return errors.New("invalid checksum") + } + return nil +} diff --git a/regimes/au/tax_identity_test.go b/regimes/au/tax_identity_test.go new file mode 100644 index 000000000..18d6fc345 --- /dev/null +++ b/regimes/au/tax_identity_test.go @@ -0,0 +1,80 @@ +package au_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/au" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestValidateTaxIdentity(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code cbc.Code + expectedErr string + }{ + {name: "valid ABN from ATO", code: "51824753556"}, + {name: "valid ABN with spaces", code: "51 824 753 556"}, + {name: "second valid ABN", code: "53004085616"}, + {name: "empty ABN", code: ""}, + {name: "too short", code: "1234567890", expectedErr: "invalid length"}, + {name: "too long", code: "123456789012", expectedErr: "invalid length"}, + {name: "invalid checksum", code: "11111111111", expectedErr: "invalid checksum"}, + {name: "contains non-numeric characters", code: "5182475355A", expectedErr: "invalid characters, expected numeric"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "AU", Code: tt.code} + + err := au.Validate(tID) + + if tt.expectedErr == "" { + assert.NoError(t, err) + } else if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.expectedErr) + } + }) + } + + t.Run("nil identity", func(t *testing.T) { + var tID *tax.Identity + assert.NoError(t, au.Validate(tID)) + }) +} + +func TestNormalizeTaxIdentity(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input cbc.Code + expected cbc.Code + }{ + {name: "spaces stripped", input: "51 824 753 556", expected: "51824753556"}, + {name: "country prefix stripped", input: "AU51824753556", expected: "51824753556"}, + {name: "already normalized", input: "51824753556", expected: "51824753556"}, + {name: "empty code", input: "", expected: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "AU", Code: tt.input} + + au.Normalize(tID) + + assert.Equal(t, tt.expected, tID.Code) + }) + } + + t.Run("nil identity", func(t *testing.T) { + assert.NotPanics(t, func() { + var tID *tax.Identity + au.Normalize(tID) + }) + }) +} diff --git a/regimes/regimes.go b/regimes/regimes.go index ff3cb73a8..a9a774813 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -8,6 +8,7 @@ import ( _ "github.com/invopop/gobl/regimes/ae" _ "github.com/invopop/gobl/regimes/ar" _ "github.com/invopop/gobl/regimes/at" + _ "github.com/invopop/gobl/regimes/au" _ "github.com/invopop/gobl/regimes/be" _ "github.com/invopop/gobl/regimes/br" _ "github.com/invopop/gobl/regimes/ca" From dd5c155607d7510a77996d2ef37ee18996ce8cca Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 18:46:59 +0200 Subject: [PATCH 2/8] fix: align Australian GST invoice rules and docs --- regimes/au/README.md | 20 ++++++++++++-- regimes/au/au.go | 25 ++++++++++-------- regimes/au/au_test.go | 32 ++++++++++++++++++++++ regimes/au/bill_invoice.go | 37 +++++++++++++++++++------- regimes/au/bill_invoice_test.go | 47 ++++++++++++++++++++++++++++----- 5 files changed, 132 insertions(+), 29 deletions(-) diff --git a/regimes/au/README.md b/regimes/au/README.md index bef3d3407..cecd7ba6b 100644 --- a/regimes/au/README.md +++ b/regimes/au/README.md @@ -1,6 +1,6 @@ # Australia (`AU`) -Australia uses a Goods and Services Tax (GST) system administered by the Australian Taxation Office (ATO). GOBL models the Australian regime with a 10% standard GST rate, support for zero-rated supplies, ABN validation, and invoice validation rules for supplier and customer tax identifiers. +Australia uses a Goods and Services Tax (GST) system administered by the Australian Taxation Office (ATO). GOBL models the Australian regime with a 10% standard GST rate, support for GST-free and input-taxed supplies through the generic GST model, ABN validation, and invoice validation rules for supplier and customer identification. ## Public Documentation @@ -26,7 +26,23 @@ Validation follows the ABR checksum algorithm: ## GST +Australian GST distinguishes between taxable supplies, GST-free supplies, and input-taxed supplies. GOBL keeps Australia on the shared GST model and maps those concepts as follows: + +| Australian concept | GOBL key / rate | GST treatment | +| --- | --- | --- | +| Taxable supply | `standard` / `general` | 10% GST | +| GST-free supply | `zero` / `zero` | 0% GST | +| Input-taxed supply | `exempt` | No GST charged; used as the generic mapping for input-taxed treatment | +| Outside scope / non-taxable | `outside-scope` | Not part of the GST calculation | + | Rate Name | GOBL Rate Key | Percent | Since | | --- | --- | --- | --- | | General rate | `standard` / `general` | 10% | 2000-07-01 | -| Zero rate | `zero` / `zero` | 0% | 2000-07-01 | +| GST-free rate | `zero` / `zero` | 0% | 2000-07-01 | + +## Tax Invoices + +- Suppliers must include their details and ABN. +- Invoices of AUD 1,000 or more must identify the customer. +- Self-billed invoices must identify the customer regardless of amount. +- In this implementation pass, customer identification is satisfied by the customer's name; an AU ABN may also be included but is not required when the name is present. diff --git a/regimes/au/au.go b/regimes/au/au.go index b457328bf..81e90f86d 100644 --- a/regimes/au/au.go +++ b/regimes/au/au.go @@ -25,18 +25,20 @@ func New() *tax.RegimeDef { }, Description: i18n.String{ i18n.EN: here.Doc(` - Australia's indirect tax system is administered by the Australian - Taxation Office (ATO). Goods and Services Tax (GST) applies at a - standard rate of 10%, with zero-rated exports and exempt supplies for - specific transactions such as certain financial services and residential - rent. + Australia's indirect tax system is administered by the Australian + Taxation Office (ATO). Goods and Services Tax (GST) applies at a + standard rate of 10%. Supplies may also be GST-free (for example, many + exports) or input-taxed (for example, certain financial supplies and + residential rent). In GOBL's generic GST model, GST-free supplies map to + the zero key and input-taxed supplies map to the exempt key. - Businesses are identified by an Australian Business Number (ABN), an - 11-digit identifier used for GST registration and invoicing. Australian - tax invoices must show the supplier's details and ABN, and invoices of - AUD 1,000 or more must also identify the customer. Electronic invoicing - is aligned with the Peppol PINT A-NZ specification. - `), + Businesses are identified by an Australian Business Number (ABN), an + 11-digit identifier used for GST registration and invoicing. Australian + tax invoices must show the supplier's details and ABN, and invoices of + AUD 1,000 or more, or self-billed invoices, must also identify the + customer. Electronic invoicing is aligned with the Peppol PINT A-NZ + specification. + `), }, Sources: []*cbc.Source{ { @@ -62,6 +64,7 @@ func New() *tax.RegimeDef { Schema: bill.ShortSchemaInvoice, Types: []cbc.Key{ bill.InvoiceTypeCreditNote, + bill.InvoiceTypeDebitNote, }, }, }, diff --git a/regimes/au/au_test.go b/regimes/au/au_test.go index e265f4049..f12f3755c 100644 --- a/regimes/au/au_test.go +++ b/regimes/au/au_test.go @@ -1,8 +1,11 @@ package au_test import ( + "encoding/json" + "strings" "testing" + "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/regimes/au" @@ -23,6 +26,19 @@ func TestNew(t *testing.T) { assert.NotEmpty(t, regime.Categories) assert.NotNil(t, regime.Validator) assert.NotNil(t, regime.Normalizer) + assert.Len(t, regime.Corrections, 1) + assert.Equal(t, []string{ + bill.InvoiceTypeCreditNote.String(), + bill.InvoiceTypeDebitNote.String(), + }, []string{ + regime.Corrections[0].Types[0].String(), + regime.Corrections[0].Types[1].String(), + }) + assert.Contains(t, regime.Description.String(), "GST-free") + assert.Contains(t, regime.Description.String(), "input-taxed") + assert.True(t, strings.Contains(regime.Description.String(), "exempt key")) + assert.NotNil(t, regime.CategoryDef(tax.CategoryGST)) + assert.NotNil(t, regime.CategoryDef(tax.CategoryGST).KeyDef(tax.KeyExempt)) } func TestRegimeValidation(t *testing.T) { @@ -31,3 +47,19 @@ func TestRegimeValidation(t *testing.T) { regime := au.New() require.NoError(t, regime.Validate()) } + +func TestCorrectionOptionsSchema(t *testing.T) { + t.Parallel() + + inv := validInvoice() + require.NoError(t, inv.Calculate()) + + out, err := inv.CorrectionOptionsSchema() + require.NoError(t, err) + + data, err := json.Marshal(out) + require.NoError(t, err) + + assert.Contains(t, string(data), `"const":"credit-note"`) + assert.Contains(t, string(data), `"const":"debit-note"`) +} diff --git a/regimes/au/bill_invoice.go b/regimes/au/bill_invoice.go index 964d18833..e6bf8cb81 100644 --- a/regimes/au/bill_invoice.go +++ b/regimes/au/bill_invoice.go @@ -2,13 +2,14 @@ package au import ( "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" "github.com/invopop/gobl/org" "github.com/invopop/gobl/tax" "github.com/invopop/validation" ) -var customerABNThreshold = num.MakeAmount(100000, 2) +var customerIdentificationThreshold = num.MakeAmount(100000, 2) func validateBillInvoice(inv *bill.Invoice) error { return validation.ValidateStruct(inv, @@ -18,7 +19,7 @@ func validateBillInvoice(inv *bill.Invoice) error { ), validation.Field(&inv.Customer, validation.When( - requiresCustomerABN(inv), + requiresCustomerIdentification(inv), validation.Required, ).Else( validation.Skip, @@ -42,6 +43,20 @@ func validateBillInvoiceSupplier(value any) error { validation.Field(&party.TaxID, validation.Required, tax.RequireIdentityCode, + validation.By(validateBillInvoiceSupplierTaxID), + validation.Skip, + ), + ) +} + +func validateBillInvoiceSupplierTaxID(value any) error { + tID, ok := value.(*tax.Identity) + if !ok || tID == nil { + return nil + } + return validation.ValidateStruct(tID, + validation.Field(&tID.Country, + validation.In(l10n.AU.Tax()), validation.Skip, ), ) @@ -50,21 +65,25 @@ func validateBillInvoiceSupplier(value any) error { func validateBillInvoiceCustomer(inv *bill.Invoice) validation.RuleFunc { return func(value any) error { party, ok := value.(*org.Party) - if !ok || party == nil || !requiresCustomerABN(inv) { + if !ok || party == nil || !requiresCustomerIdentification(inv) { return nil } return validation.ValidateStruct(party, - validation.Field(&party.TaxID, + validation.Field(&party.Name, validation.Required, - tax.RequireIdentityCode, validation.Skip, ), ) } } -func requiresCustomerABN(inv *bill.Invoice) bool { - return inv != nil && - inv.Totals != nil && - inv.Totals.TotalWithTax.Compare(customerABNThreshold) >= 0 +func requiresCustomerIdentification(inv *bill.Invoice) bool { + if inv == nil { + return false + } + if inv.HasTags(tax.TagSelfBilled) { + return true + } + return inv.Totals != nil && + inv.Totals.TotalWithTax.Compare(customerIdentificationThreshold) >= 0 } diff --git a/regimes/au/bill_invoice_test.go b/regimes/au/bill_invoice_test.go index 225796f1e..6f3c8bb6a 100644 --- a/regimes/au/bill_invoice_test.go +++ b/regimes/au/bill_invoice_test.go @@ -81,25 +81,40 @@ func TestInvoiceValidation(t *testing.T) { require.NoError(t, inv.Validate()) }) - t.Run("valid invoice at threshold with customer ABN", func(t *testing.T) { + t.Run("valid invoice at threshold with customer name only", func(t *testing.T) { t.Parallel() inv := validInvoice() inv.Lines[0].Item.Price = num.NewAmount(100000, 2) - inv.Customer.TaxID = &tax.Identity{ - Country: l10n.AU.Tax(), - Code: "53004085616", - } require.NoError(t, inv.Calculate()) require.NoError(t, inv.Validate()) }) - t.Run("invoice at threshold without customer ABN", func(t *testing.T) { + t.Run("invoice at threshold without customer", func(t *testing.T) { t.Parallel() inv := validInvoice() inv.Lines[0].Item.Price = num.NewAmount(100000, 2) + inv.Customer = nil require.NoError(t, inv.Calculate()) err := inv.Validate() - assert.ErrorContains(t, err, "tax_id") + assert.ErrorContains(t, err, "customer") + }) + + t.Run("self billed invoice under threshold with customer name", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.SetTags(tax.TagSelfBilled) + require.NoError(t, inv.Calculate()) + require.NoError(t, inv.Validate()) + }) + + t.Run("self billed invoice under threshold without customer", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.SetTags(tax.TagSelfBilled) + inv.Customer = nil + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "customer") }) t.Run("nil supplier", func(t *testing.T) { @@ -127,4 +142,22 @@ func TestInvoiceValidation(t *testing.T) { err := inv.Validate() assert.ErrorContains(t, err, "name") }) + + t.Run("supplier tax ID must be australian", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier.TaxID.Country = "US" + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "country") + }) + + t.Run("supplier tax ID must be valid ABN", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier.TaxID.Code = "11111111111" + require.NoError(t, inv.Calculate()) + err := inv.Validate() + assert.ErrorContains(t, err, "invalid checksum") + }) } From d00c6011de839eb6aff3cbf7fe712782caa06729 Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 18:52:37 +0200 Subject: [PATCH 3/8] fix: align AU example address fields with JSON output --- examples/au/invoice-au-au.yaml | 4 ++-- examples/au/out/invoice-au-au.json | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/au/invoice-au-au.yaml b/examples/au/invoice-au-au.yaml index 717a69b75..b25f9c3d7 100644 --- a/examples/au/invoice-au-au.yaml +++ b/examples/au/invoice-au-au.yaml @@ -13,7 +13,7 @@ supplier: code: "51824753556" addresses: - street: George Street - number: "100" + num: "100" locality: Sydney region: NSW code: "2000" @@ -23,7 +23,7 @@ customer: name: Example Customer Pty Ltd addresses: - street: Collins Street - number: "200" + num: "200" locality: Melbourne region: VIC code: "3000" diff --git a/examples/au/out/invoice-au-au.json b/examples/au/out/invoice-au-au.json index cffa8ac5e..90fb5f843 100644 --- a/examples/au/out/invoice-au-au.json +++ b/examples/au/out/invoice-au-au.json @@ -4,7 +4,7 @@ "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", "dig": { "alg": "sha256", - "val": "4d8afaf12759b4ca824799073709d20282467f48829766514b9ee94012ef14cc" + "val": "9b83035fda7d9f82243097be6bfdfc321d431f25d4d355b2a00700ed5c9b6687" } }, "doc": { @@ -24,6 +24,7 @@ }, "addresses": [ { + "num": "100", "street": "George Street", "locality": "Sydney", "region": "NSW", @@ -36,6 +37,7 @@ "name": "Example Customer Pty Ltd", "addresses": [ { + "num": "200", "street": "Collins Street", "locality": "Melbourne", "region": "VIC", @@ -90,4 +92,4 @@ "payable": "990.00" } } -} \ No newline at end of file +} From 758c3acaa64553769f73008420ec53df9725b9be Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 18:53:44 +0200 Subject: [PATCH 4/8] test(au): add package doc and parallelize tax ID subtests --- regimes/au/au.go | 1 + regimes/au/tax_identity_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/regimes/au/au.go b/regimes/au/au.go index 81e90f86d..b6b53bb77 100644 --- a/regimes/au/au.go +++ b/regimes/au/au.go @@ -1,3 +1,4 @@ +// Package au provides models for dealing with Australia. package au import ( diff --git a/regimes/au/tax_identity_test.go b/regimes/au/tax_identity_test.go index 18d6fc345..0cf942da1 100644 --- a/regimes/au/tax_identity_test.go +++ b/regimes/au/tax_identity_test.go @@ -29,6 +29,7 @@ func TestValidateTaxIdentity(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tID := &tax.Identity{Country: "AU", Code: tt.code} err := au.Validate(tID) @@ -63,6 +64,7 @@ func TestNormalizeTaxIdentity(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tID := &tax.Identity{Country: "AU", Code: tt.input} au.Normalize(tID) From a26e159711244c4d9cc8e3a0a1ca5bd5cd077285 Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 19:28:09 +0200 Subject: [PATCH 5/8] chore(au): added changelog & updated regime definition --- CHANGELOG.md | 4 ++++ data/regimes/au.json | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3319455c..27a7351d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- `au`: Added the Australian GST regime. + ## [v0.309.0] - 2026-04-01 ### Added diff --git a/data/regimes/au.json b/data/regimes/au.json index c83508717..7a7748e3a 100644 --- a/data/regimes/au.json +++ b/data/regimes/au.json @@ -4,7 +4,7 @@ "en": "Australia" }, "description": { - "en": "Australia's indirect tax system is administered by the Australian\nTaxation Office (ATO). Goods and Services Tax (GST) applies at a\nstandard rate of 10%, with zero-rated exports and exempt supplies for\nspecific transactions such as certain financial services and residential\nrent.\n\nBusinesses are identified by an Australian Business Number (ABN), an\n11-digit identifier used for GST registration and invoicing. Australian\ntax invoices must show the supplier's details and ABN, and invoices of\nAUD 1,000 or more must also identify the customer. Electronic invoicing\nis aligned with the Peppol PINT A-NZ specification." + "en": "Australia's indirect tax system is administered by the Australian\nTaxation Office (ATO). Goods and Services Tax (GST) applies at a\nstandard rate of 10%. Supplies may also be GST-free (for example, many\nexports) or input-taxed (for example, certain financial supplies and\nresidential rent). In GOBL's generic GST model, GST-free supplies map to\nthe zero key and input-taxed supplies map to the exempt key.\n\nBusinesses are identified by an Australian Business Number (ABN), an\n11-digit identifier used for GST registration and invoicing. Australian\ntax invoices must show the supplier's details and ABN, and invoices of\nAUD 1,000 or more, or self-billed invoices, must also identify the\ncustomer. Electronic invoicing is aligned with the Peppol PINT A-NZ\nspecification." }, "sources": [ { @@ -40,7 +40,8 @@ { "schema": "bill/invoice", "types": [ - "credit-note" + "credit-note", + "debit-note" ] } ], From e8cf718a424f3f3fa58678de560ad944b2451581 Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 21:39:20 +0200 Subject: [PATCH 6/8] fix: removed explicit zero RateDef --- data/regimes/au.json | 15 --------------- regimes/au/README.md | 7 +++---- regimes/au/tax_categories.go | 13 ------------- 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/data/regimes/au.json b/data/regimes/au.json index 7a7748e3a..1ba25325a 100644 --- a/data/regimes/au.json +++ b/data/regimes/au.json @@ -97,21 +97,6 @@ "percent": "10%" } ] - }, - { - "rate": "zero", - "keys": [ - "zero" - ], - "name": { - "en": "Zero rate" - }, - "values": [ - { - "since": "2000-07-01", - "percent": "0%" - } - ] } ], "sources": [ diff --git a/regimes/au/README.md b/regimes/au/README.md index cecd7ba6b..1b411ca32 100644 --- a/regimes/au/README.md +++ b/regimes/au/README.md @@ -1,6 +1,6 @@ # Australia (`AU`) -Australia uses a Goods and Services Tax (GST) system administered by the Australian Taxation Office (ATO). GOBL models the Australian regime with a 10% standard GST rate, support for GST-free and input-taxed supplies through the generic GST model, ABN validation, and invoice validation rules for supplier and customer identification. +Australia uses a Goods and Services Tax (GST) system administered by the Australian Taxation Office (ATO). GOBL models the Australian regime with a 10% standard GST rate, support for GST-free and input-taxed supplies through the generic GST key model, ABN validation, and invoice validation rules for supplier and customer identification. ## Public Documentation @@ -28,17 +28,16 @@ Validation follows the ABR checksum algorithm: Australian GST distinguishes between taxable supplies, GST-free supplies, and input-taxed supplies. GOBL keeps Australia on the shared GST model and maps those concepts as follows: -| Australian concept | GOBL key / rate | GST treatment | +| Australian concept | GOBL key / handling | GST treatment | | --- | --- | --- | | Taxable supply | `standard` / `general` | 10% GST | -| GST-free supply | `zero` / `zero` | 0% GST | +| GST-free supply | `zero` | 0% GST through the shared GST zero key | | Input-taxed supply | `exempt` | No GST charged; used as the generic mapping for input-taxed treatment | | Outside scope / non-taxable | `outside-scope` | Not part of the GST calculation | | Rate Name | GOBL Rate Key | Percent | Since | | --- | --- | --- | --- | | General rate | `standard` / `general` | 10% | 2000-07-01 | -| GST-free rate | `zero` / `zero` | 0% | 2000-07-01 | ## Tax Invoices diff --git a/regimes/au/tax_categories.go b/regimes/au/tax_categories.go index 0e5a0e443..5d14b2cbf 100644 --- a/regimes/au/tax_categories.go +++ b/regimes/au/tax_categories.go @@ -40,19 +40,6 @@ func taxCategories() []*tax.CategoryDef { }, }, }, - { - Keys: []cbc.Key{tax.KeyZero}, - Rate: tax.RateZero, - Name: i18n.String{ - i18n.EN: "Zero rate", - }, - Values: []*tax.RateValueDef{ - { - Since: cal.NewDate(2000, 7, 1), - Percent: num.PercentageZero, - }, - }, - }, }, }, } From c26a8ecbe44ef4170e2a133b3f5f50c7e452a41e Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Sat, 4 Apr 2026 22:00:45 +0200 Subject: [PATCH 7/8] test(au): cover nil invoice validation --- regimes/au/bill_invoice.go | 3 +++ regimes/au/bill_invoice_test.go | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/regimes/au/bill_invoice.go b/regimes/au/bill_invoice.go index e6bf8cb81..7d8a183b3 100644 --- a/regimes/au/bill_invoice.go +++ b/regimes/au/bill_invoice.go @@ -12,6 +12,9 @@ import ( var customerIdentificationThreshold = num.MakeAmount(100000, 2) func validateBillInvoice(inv *bill.Invoice) error { + if inv == nil { + return nil + } return validation.ValidateStruct(inv, validation.Field(&inv.Supplier, validation.By(validateBillInvoiceSupplier), diff --git a/regimes/au/bill_invoice_test.go b/regimes/au/bill_invoice_test.go index 6f3c8bb6a..2bd0cd0f9 100644 --- a/regimes/au/bill_invoice_test.go +++ b/regimes/au/bill_invoice_test.go @@ -10,6 +10,7 @@ import ( "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/au" "github.com/invopop/gobl/tax" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,6 +75,12 @@ func validInvoice() *bill.Invoice { func TestInvoiceValidation(t *testing.T) { t.Parallel() + t.Run("nil invoice", func(t *testing.T) { + t.Parallel() + var inv *bill.Invoice + require.NoError(t, au.Validate(inv)) + }) + t.Run("valid invoice under threshold", func(t *testing.T) { t.Parallel() inv := validInvoice() From 1e2da5a7116845fc16e7341fd432f010563c8c7c Mon Sep 17 00:00:00 2001 From: Alberto Puliga Date: Tue, 7 Apr 2026 07:48:08 +0200 Subject: [PATCH 8/8] fix(tests): fix test coverage and dead branch --- regimes/au/bill_invoice.go | 8 +------- regimes/au/bill_invoice_test.go | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/regimes/au/bill_invoice.go b/regimes/au/bill_invoice.go index 7d8a183b3..87ef40adc 100644 --- a/regimes/au/bill_invoice.go +++ b/regimes/au/bill_invoice.go @@ -53,10 +53,7 @@ func validateBillInvoiceSupplier(value any) error { } func validateBillInvoiceSupplierTaxID(value any) error { - tID, ok := value.(*tax.Identity) - if !ok || tID == nil { - return nil - } + tID := value.(*tax.Identity) return validation.ValidateStruct(tID, validation.Field(&tID.Country, validation.In(l10n.AU.Tax()), @@ -81,9 +78,6 @@ func validateBillInvoiceCustomer(inv *bill.Invoice) validation.RuleFunc { } func requiresCustomerIdentification(inv *bill.Invoice) bool { - if inv == nil { - return false - } if inv.HasTags(tax.TagSelfBilled) { return true } diff --git a/regimes/au/bill_invoice_test.go b/regimes/au/bill_invoice_test.go index 2bd0cd0f9..91d2c13da 100644 --- a/regimes/au/bill_invoice_test.go +++ b/regimes/au/bill_invoice_test.go @@ -124,6 +124,13 @@ func TestInvoiceValidation(t *testing.T) { assert.ErrorContains(t, err, "customer") }) + t.Run("regime validator with nil supplier party", func(t *testing.T) { + t.Parallel() + inv := validInvoice() + inv.Supplier = nil + require.NoError(t, au.Validate(inv)) + }) + t.Run("nil supplier", func(t *testing.T) { t.Parallel() inv := validInvoice()