diff --git a/CHANGELOG.md b/CHANGELOG.md index 583df2854..6e76c5b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `eu-en16931-v2017`: BR-32 validation requiring taxes on document-level discounts. - `pl-favat-v3`: Tax combos with a non-Polish country are normalized as outside scope (category 8). +- `jp`: New tax regime for Japan with Consumption Tax (10%/8%), Corporate Number validation, and reverse charge support. ### Changed @@ -55,7 +56,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `bill`: Invoice, Order, Payment, and Delivery now normalize Notes fields - `bill`: Invoice and Order now normalize Attachments fields - ## [v0.307.0] - 2026-01-27 ### Added diff --git a/data/regimes/jp.json b/data/regimes/jp.json new file mode 100644 index 000000000..5feb67f05 --- /dev/null +++ b/data/regimes/jp.json @@ -0,0 +1,164 @@ +{ + "$schema": "https://gobl.org/draft-0/tax/regime-def", + "name": { + "en": "Japan", + "ja": "日本" + }, + "description": { + "en": "Japan's tax system is administered by the National Tax Agency (NTA).\nThe Consumption Tax (消費税, Shōhizei) functions as a VAT and applies\nat standard (10%) and reduced (8%) rates since October 2019.\n\nBusinesses are identified by a 13-digit Corporate Number (法人番号,\nHōjin Bangō). The Qualified Invoice System (インボイス制度), effective\nsince October 2023, requires registered businesses to include a\nRegistration Number (T + Corporate Number) on invoices for buyers\nto claim input tax credits.\n\nCredit notes are supported as Qualified Return Invoices (適格返還請求書)\nfor returns, discounts, and rebates." + }, + "sources": [ + { + "title": { + "en": "National Tax Agency - Consumption Tax" + }, + "url": "https://www.nta.go.jp/english/taxes/consumption_tax/01.htm" + } + ], + "time_zone": "Asia/Tokyo", + "country": "JP", + "currency": "JPY", + "tax_scheme": "VAT", + "scenarios": [ + { + "schema": "bill/invoice", + "list": [ + { + "tags": [ + "reverse-charge" + ], + "note": { + "key": "legal", + "src": "reverse-charge", + "text": "Reverse Charge" + } + } + ] + } + ], + "corrections": [ + { + "schema": "bill/invoice", + "types": [ + "credit-note" + ] + } + ], + "categories": [ + { + "code": "VAT", + "name": { + "en": "CT", + "ja": "消費税" + }, + "title": { + "en": "Consumption Tax", + "ja": "消費税" + }, + "keys": [ + { + "key": "standard", + "name": { + "en": "Standard" + } + }, + { + "key": "zero", + "name": { + "en": "Zero" + } + }, + { + "key": "reverse-charge", + "name": { + "en": "Reverse charge" + }, + "no_percent": true + }, + { + "key": "exempt", + "name": { + "en": "Exempt" + }, + "no_percent": true + }, + { + "key": "export", + "name": { + "en": "Export" + }, + "no_percent": true + }, + { + "key": "intra-community", + "name": { + "en": "Intra-community" + }, + "no_percent": true + }, + { + "key": "outside-scope", + "name": { + "en": "Outside scope" + }, + "no_percent": true + } + ], + "rates": [ + { + "rate": "general", + "keys": [ + "standard" + ], + "name": { + "en": "General Rate", + "ja": "標準税率" + }, + "desc": { + "en": "Applies to most goods and services unless specified otherwise.", + "ja": "特に定めがない限り、ほとんどの商品・サービスに適用されます。" + }, + "values": [ + { + "since": "2019-10-01", + "percent": "10%" + }, + { + "since": "2014-04-01", + "percent": "8%" + } + ] + }, + { + "rate": "reduced", + "keys": [ + "standard" + ], + "name": { + "en": "Reduced Rate", + "ja": "軽減税率" + }, + "desc": { + "en": "Applies to food and non-alcoholic beverages (excluding dining out) and newspaper subscriptions (2+ per week).", + "ja": "飲食料品(外食を除く)および定期購読の新聞(週2回以上発行)に適用されます。" + }, + "values": [ + { + "since": "2019-10-01", + "percent": "8%" + } + ] + } + ], + "sources": [ + { + "title": { + "en": "National Tax Agency - Consumption Tax", + "ja": "国税庁 - 消費税" + }, + "url": "https://www.nta.go.jp/english/taxes/consumption_tax/01.htm" + } + ] + } + ] +} \ No newline at end of file diff --git a/data/schemas/tax/regime-code.json b/data/schemas/tax/regime-code.json index c4b6e938a..8e8313422 100644 --- a/data/schemas/tax/regime-code.json +++ b/data/schemas/tax/regime-code.json @@ -73,6 +73,10 @@ "const": "IT", "title": "Italy" }, + { + "const": "JP", + "title": "Japan" + }, { "const": "MX", "title": "Mexico" diff --git a/examples/jp/invoice-jp-credit-note.yaml b/examples/jp/invoice-jp-credit-note.yaml new file mode 100644 index 000000000..2c22a80f2 --- /dev/null +++ b/examples/jp/invoice-jp-credit-note.yaml @@ -0,0 +1,45 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "4b128d92-7b1a-4e3c-a5f6-2d8a0e5c9b17" +currency: "JPY" +type: credit-note +issue_date: "2025-03-20" +series: "CN" +code: "2025-001" + +preceding: + - type: standard + issue_date: "2025-03-15" + series: "INV" + code: "2025-001" + reason: "Returned defective items" + +supplier: + tax_id: + country: "JP" + code: "9700150098417" + name: "Sakura Tech Co., Ltd." + addresses: + - street: "Marunouchi 1-9-2" + locality: "Chiyoda-ku" + region: "Tokyo" + code: "100-0005" + country: "JP" +customer: + tax_id: + country: "JP" + code: "5050005005266" + name: "Fuji Solutions K.K." + addresses: + - street: "Umeda 3-1-1" + locality: "Kita-ku" + region: "Osaka" + code: "530-0001" + country: "JP" +lines: + - quantity: 5 + item: + name: "Bento lunch boxes (catering)" + price: "1080" + taxes: + - cat: VAT + rate: reduced diff --git a/examples/jp/invoice-jp-reverse-charge.yaml b/examples/jp/invoice-jp-reverse-charge.yaml new file mode 100644 index 000000000..b2d787dca --- /dev/null +++ b/examples/jp/invoice-jp-reverse-charge.yaml @@ -0,0 +1,38 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "7c94e8d1-3f52-4a6b-b9e0-1d7f3c8a2e45" +currency: "JPY" +issue_date: "2025-03-15" +series: "INV" +code: "2025-002" +$tags: + - reverse-charge +supplier: + tax_id: + country: "JP" + code: "9700150098417" + name: "Sakura Tech Co., Ltd." + addresses: + - street: "Marunouchi 1-9-2" + locality: "Chiyoda-ku" + region: "Tokyo" + code: "100-0005" + country: "JP" +customer: + name: "Acme Cloud Inc." + tax_id: + country: "US" + code: "123456789" + addresses: + - street: "100 Market St" + locality: "San Francisco" + region: "CA" + code: "94105" + country: "US" +lines: + - quantity: 1 + item: + name: "IT consulting services" + price: "300000" + taxes: + - cat: VAT + key: reverse-charge diff --git a/examples/jp/invoice-jp-standard.yaml b/examples/jp/invoice-jp-standard.yaml new file mode 100644 index 000000000..b37bca23c --- /dev/null +++ b/examples/jp/invoice-jp-standard.yaml @@ -0,0 +1,43 @@ +$schema: "https://gobl.org/draft-0/bill/invoice" +uuid: "3aea7b56-59d8-4beb-90bd-f8f280d852a0" +currency: "JPY" +issue_date: "2025-03-15" +series: "INV" +code: "2025-001" +supplier: + tax_id: + country: "JP" + code: "9700150098417" + name: "Sakura Tech Co., Ltd." + addresses: + - street: "Marunouchi 1-9-2" + locality: "Chiyoda-ku" + region: "Tokyo" + code: "100-0005" + country: "JP" +customer: + tax_id: + country: "JP" + code: "5050005005266" + name: "Fuji Solutions K.K." + addresses: + - street: "Umeda 3-1-1" + locality: "Kita-ku" + region: "Osaka" + code: "530-0001" + country: "JP" +lines: + - quantity: 1 + item: + name: "Cloud hosting (March 2025)" + price: "500000" + taxes: + - cat: VAT + rate: standard + - quantity: 10 + item: + name: "Bento lunch boxes (catering)" + price: "1080" + taxes: + - cat: VAT + rate: reduced diff --git a/examples/jp/out/invoice-jp-credit-note.json b/examples/jp/out/invoice-jp-credit-note.json new file mode 100644 index 000000000..bc427b586 --- /dev/null +++ b/examples/jp/out/invoice-jp-credit-note.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "da1385def37750972a46a203da8385e292f511f5daebf231cae22c47f1120b49" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "JP", + "uuid": "4b128d92-7b1a-4e3c-a5f6-2d8a0e5c9b17", + "type": "credit-note", + "series": "CN", + "code": "2025-001", + "issue_date": "2025-03-20", + "currency": "JPY", + "preceding": [ + { + "type": "standard", + "issue_date": "2025-03-15", + "series": "INV", + "code": "2025-001", + "reason": "Returned defective items" + } + ], + "supplier": { + "name": "Sakura Tech Co., Ltd.", + "tax_id": { + "country": "JP", + "code": "9700150098417" + }, + "addresses": [ + { + "street": "Marunouchi 1-9-2", + "locality": "Chiyoda-ku", + "region": "Tokyo", + "code": "100-0005", + "country": "JP" + } + ] + }, + "customer": { + "name": "Fuji Solutions K.K.", + "tax_id": { + "country": "JP", + "code": "5050005005266" + }, + "addresses": [ + { + "street": "Umeda 3-1-1", + "locality": "Kita-ku", + "region": "Osaka", + "code": "530-0001", + "country": "JP" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "5", + "item": { + "name": "Bento lunch boxes (catering)", + "price": "1080" + }, + "sum": "5400", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "reduced", + "percent": "8%" + } + ], + "total": "5400" + } + ], + "totals": { + "sum": "5400", + "total": "5400", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "5400", + "percent": "8%", + "amount": "432" + } + ], + "amount": "432" + } + ], + "sum": "432" + }, + "tax": "432", + "total_with_tax": "5832", + "payable": "5832" + } + } +} \ No newline at end of file diff --git a/examples/jp/out/invoice-jp-reverse-charge.json b/examples/jp/out/invoice-jp-reverse-charge.json new file mode 100644 index 000000000..78732e780 --- /dev/null +++ b/examples/jp/out/invoice-jp-reverse-charge.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "cfe2c3032439ff3e49c1246cce7c0c84f704e6b2792b8e4d1669c7332ae029c3" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "JP", + "$tags": [ + "reverse-charge" + ], + "uuid": "7c94e8d1-3f52-4a6b-b9e0-1d7f3c8a2e45", + "type": "standard", + "series": "INV", + "code": "2025-002", + "issue_date": "2025-03-15", + "currency": "JPY", + "supplier": { + "name": "Sakura Tech Co., Ltd.", + "tax_id": { + "country": "JP", + "code": "9700150098417" + }, + "addresses": [ + { + "street": "Marunouchi 1-9-2", + "locality": "Chiyoda-ku", + "region": "Tokyo", + "code": "100-0005", + "country": "JP" + } + ] + }, + "customer": { + "name": "Acme Cloud Inc.", + "tax_id": { + "country": "US", + "code": "123456789" + }, + "addresses": [ + { + "street": "100 Market St", + "locality": "San Francisco", + "region": "CA", + "code": "94105", + "country": "US" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "IT consulting services", + "price": "300000" + }, + "sum": "300000", + "taxes": [ + { + "cat": "VAT", + "key": "reverse-charge" + } + ], + "total": "300000" + } + ], + "totals": { + "sum": "300000", + "total": "300000", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "reverse-charge", + "base": "300000", + "amount": "0" + } + ], + "amount": "0" + } + ], + "sum": "0" + }, + "tax": "0", + "total_with_tax": "300000", + "payable": "300000" + }, + "notes": [ + { + "key": "legal", + "src": "reverse-charge", + "text": "Reverse Charge" + } + ] + } +} \ No newline at end of file diff --git a/examples/jp/out/invoice-jp-standard.json b/examples/jp/out/invoice-jp-standard.json new file mode 100644 index 000000000..557ee8072 --- /dev/null +++ b/examples/jp/out/invoice-jp-standard.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "cafec2c3399b3f44223a8d0cd17a112dadf56060485be869cea37bd259a63f09" + } + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "$regime": "JP", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "INV", + "code": "2025-001", + "issue_date": "2025-03-15", + "currency": "JPY", + "supplier": { + "name": "Sakura Tech Co., Ltd.", + "tax_id": { + "country": "JP", + "code": "9700150098417" + }, + "addresses": [ + { + "street": "Marunouchi 1-9-2", + "locality": "Chiyoda-ku", + "region": "Tokyo", + "code": "100-0005", + "country": "JP" + } + ] + }, + "customer": { + "name": "Fuji Solutions K.K.", + "tax_id": { + "country": "JP", + "code": "5050005005266" + }, + "addresses": [ + { + "street": "Umeda 3-1-1", + "locality": "Kita-ku", + "region": "Osaka", + "code": "530-0001", + "country": "JP" + } + ] + }, + "lines": [ + { + "i": 1, + "quantity": "1", + "item": { + "name": "Cloud hosting (March 2025)", + "price": "500000" + }, + "sum": "500000", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "general", + "percent": "10%" + } + ], + "total": "500000" + }, + { + "i": 2, + "quantity": "10", + "item": { + "name": "Bento lunch boxes (catering)", + "price": "1080" + }, + "sum": "10800", + "taxes": [ + { + "cat": "VAT", + "key": "standard", + "rate": "reduced", + "percent": "8%" + } + ], + "total": "10800" + } + ], + "totals": { + "sum": "510800", + "total": "510800", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "500000", + "percent": "10%", + "amount": "50000" + }, + { + "key": "standard", + "base": "10800", + "percent": "8%", + "amount": "864" + } + ], + "amount": "50864" + } + ], + "sum": "50864" + }, + "tax": "50864", + "total_with_tax": "561664", + "payable": "561664" + } + } +} \ No newline at end of file diff --git a/regimes/jp/jp.go b/regimes/jp/jp.go new file mode 100644 index 000000000..3a27d63a6 --- /dev/null +++ b/regimes/jp/jp.go @@ -0,0 +1,82 @@ +// Package jp provides the tax regime definition for Japan. +package jp + +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/pkg/here" + "github.com/invopop/gobl/tax" +) + +func init() { + tax.RegisterRegimeDef(New()) +} + +// New provides the tax regime definition for Japan. +func New() *tax.RegimeDef { + return &tax.RegimeDef{ + Country: "JP", + Currency: currency.JPY, + TaxScheme: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "Japan", + i18n.JA: "日本", // Nihon + }, + Description: i18n.String{ + i18n.EN: here.Doc(` + Japan's tax system is administered by the National Tax Agency (NTA). + The Consumption Tax (消費税, Shōhizei) functions as a VAT and applies + at standard (10%) and reduced (8%) rates since October 2019. + + Businesses are identified by a 13-digit Corporate Number (法人番号, + Hōjin Bangō). The Qualified Invoice System (インボイス制度), effective + since October 2023, requires registered businesses to include a + Registration Number (T + Corporate Number) on invoices for buyers + to claim input tax credits. + + Credit notes are supported as Qualified Return Invoices (適格返還請求書) + for returns, discounts, and rebates. + `), + }, + Sources: []*cbc.Source{ + { + Title: i18n.NewString("National Tax Agency - Consumption Tax"), + URL: "https://www.nta.go.jp/english/taxes/consumption_tax/01.htm", + }, + }, + TimeZone: "Asia/Tokyo", + Scenarios: []*tax.ScenarioSet{ + invoiceScenarios, + }, + 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 *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: + normalizeTaxIdentity(obj) + } +} diff --git a/regimes/jp/scenarios.go b/regimes/jp/scenarios.go new file mode 100644 index 000000000..2dda3f13f --- /dev/null +++ b/regimes/jp/scenarios.go @@ -0,0 +1,23 @@ +package jp + +import ( + "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/tax" +) + +var invoiceScenarios = &tax.ScenarioSet{ + Schema: bill.ShortSchemaInvoice, + List: []*tax.Scenario{ + // Reverse Charges + { + Tags: []cbc.Key{tax.TagReverseCharge}, + Note: &tax.ScenarioNote{ + Key: org.NoteKeyLegal, + Src: tax.TagReverseCharge, + Text: "Reverse Charge", + }, + }, + }, +} diff --git a/regimes/jp/tax_categories.go b/regimes/jp/tax_categories.go new file mode 100644 index 000000000..095c12d02 --- /dev/null +++ b/regimes/jp/tax_categories.go @@ -0,0 +1,78 @@ +package jp + +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" +) + +var taxCategories = []*tax.CategoryDef{ + { + Code: tax.CategoryVAT, + Name: i18n.String{ + i18n.EN: "CT", + i18n.JA: "消費税", // Shōhi zei + }, + Title: i18n.String{ + i18n.EN: "Consumption Tax", + i18n.JA: "消費税", // Shōhi zei + }, + Sources: []*cbc.Source{ + { + Title: i18n.String{ + i18n.EN: "National Tax Agency - Consumption Tax", + i18n.JA: "国税庁 - 消費税", // Kokuzei chō - Shōhi zei + }, + URL: "https://www.nta.go.jp/english/taxes/consumption_tax/01.htm", + }, + }, + Retained: false, + Keys: tax.GlobalVATKeys(), + Rates: []*tax.RateDef{ + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateGeneral, + Name: i18n.String{ + i18n.EN: "General Rate", + i18n.JA: "標準税率", // Hyōjun zeiritsu + }, + Description: i18n.String{ + i18n.EN: "Applies to most goods and services unless specified otherwise.", + // Toku ni sadame ga nai kagiri, hotondo no shōhin sābisu ni tekiyō saremasu. + i18n.JA: "特に定めがない限り、ほとんどの商品・サービスに適用されます。", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2019, 10, 1), + Percent: num.MakePercentage(10, 2), + }, + { + Since: cal.NewDate(2014, 4, 1), + Percent: num.MakePercentage(8, 2), + }, + }, + }, + { + Keys: []cbc.Key{tax.KeyStandard}, + Rate: tax.RateReduced, + Name: i18n.String{ + i18n.EN: "Reduced Rate", + i18n.JA: "軽減税率", // Keigen zeiritsu + }, + Description: i18n.String{ + i18n.EN: "Applies to food and non-alcoholic beverages (excluding dining out) and newspaper subscriptions (2+ per week).", + // Inshoku ryōhin (gaishoku wo nozoku) oyobi teiki kōdoku no shinbun (shū ni kai ijō hakkō) ni tekiyō saremasu. + i18n.JA: "飲食料品(外食を除く)および定期購読の新聞(週2回以上発行)に適用されます。", + }, + Values: []*tax.RateValueDef{ + { + Since: cal.NewDate(2019, 10, 1), + Percent: num.MakePercentage(8, 2), + }, + }, + }, + }, + }, +} diff --git a/regimes/jp/tax_identity.go b/regimes/jp/tax_identity.go new file mode 100644 index 000000000..8e184fb04 --- /dev/null +++ b/regimes/jp/tax_identity.go @@ -0,0 +1,88 @@ +package jp + +import ( + "errors" + "regexp" + "strings" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/tax" + "github.com/invopop/validation" +) + +var ( + // Corporate Number: exactly 13 digits. + taxCodePattern = regexp.MustCompile(`^\d{13}$`) +) + +// normalizeTaxIdentity removes whitespace, hyphens, and the "T" prefix +// used in Registration Numbers (適格請求書発行事業者番号, Tekikaku Seikyūsho Hakkō Jigyōsha Bangō). +func normalizeTaxIdentity(tID *tax.Identity) { + if tID == nil { + return + } + tax.NormalizeIdentity(tID) + // Remove the "T" prefix used in Qualified Invoice Registration Numbers. + // The underlying number is still a Corporate Number. + code := tID.Code.String() + code = strings.TrimPrefix(code, "T") + tID.Code = cbc.Code(code) +} + +// validateTaxIdentity checks that the tax identity contains a valid +// 13-digit Japanese Corporate Number (法人番号, Hōjin Bangō) with correct checksum. +func validateTaxIdentity(tID *tax.Identity) error { + return validation.ValidateStruct(tID, + validation.Field(&tID.Code, validation.By(validateTaxCode)), + ) +} + +func validateTaxCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok || code == "" { + return nil + } + val := code.String() + + if !taxCodePattern.MatchString(val) { + return errors.New("must be a 13-digit number") + } + + if !validateCorporateNumberChecksum(val) { + return errors.New("invalid checksum") + } + + return nil +} + +// validateCorporateNumberChecksum verifies the check digit of a 13-digit +// Corporate Number using the official algorithm: +// +// check_digit = 9 - (SUM(Pn * Qn) for n=1..12) mod 9 +// +// where Pn is the n-th digit from the RIGHT of the 12 base digits (digits 2-13), +// and Qn is 1 if n is odd, 2 if n is even. +func validateCorporateNumberChecksum(code string) bool { + if len(code) != 13 { + return false + } + + // The first digit is the check digit; digits 2-13 are the base number. + checkDigit := int(code[0] - '0') + base := code[1:] // 12 digits + + sum := 0 + for n := 1; n <= 12; n++ { + // Pn: n-th digit from the right of the 12-digit base. + p := int(base[12-n] - '0') + // Qn: 1 if n is odd, 2 if n is even. + q := 1 + if n%2 == 0 { + q = 2 + } + sum += p * q + } + + expected := 9 - (sum % 9) + return checkDigit == expected +} diff --git a/regimes/jp/tax_identity_test.go b/regimes/jp/tax_identity_test.go new file mode 100644 index 000000000..ef8ec8d04 --- /dev/null +++ b/regimes/jp/tax_identity_test.go @@ -0,0 +1,74 @@ +package jp_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/regimes/jp" + "github.com/invopop/gobl/tax" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeTaxIdentity(t *testing.T) { + tests := []struct { + name string + code cbc.Code + expected cbc.Code + }{ + {name: "already clean", code: "9700150098417", expected: "9700150098417"}, + {name: "with spaces", code: "9700 1500 9841 7", expected: "9700150098417"}, + {name: "with hyphens", code: "9700-1500-9841-7", expected: "9700150098417"}, + {name: "with T prefix", code: "T9700150098417", expected: "9700150098417"}, + {name: "with T prefix and spaces", code: "T 9700 1500 9841 7", expected: "9700150098417"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "JP", Code: tt.code} + jp.Normalize(tID) + assert.Equal(t, tt.expected, tID.Code) + }) + } +} + +func TestNormalizeTaxIdentityNil(_ *testing.T) { + // Should not panic on nil. + jp.Normalize(nil) +} + +func TestValidateTaxIdentity(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + // Valid Corporate Numbers + {name: "valid checksum (1st digit) for real NTA example", code: "9700150098417"}, + {name: "valid checksum (1st digit)", code: "5050005005266"}, + {name: "empty code", code: ""}, + + // Invalid formats + {name: "too short", code: "970015009841", err: "must be a 13-digit number"}, + {name: "too long", code: "97001500984170", err: "must be a 13-digit number"}, + {name: "non-numeric", code: "970015009841A", err: "must be a 13-digit number"}, + {name: "with hyphens", code: "9700-150-09841", err: "must be a 13-digit number"}, + + // Invalid checksum + {name: "bad checksum (1st digit) for real NTA example", code: "1700150098417", err: "invalid checksum"}, + {name: "bad checksum (1st digit)", code: "9050005005266", err: "invalid checksum"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &tax.Identity{Country: "JP", Code: tt.code} + err := jp.Validate(tID) + if tt.err == "" { + assert.NoError(t, err) + } else { + if assert.Error(t, err) { + assert.Contains(t, err.Error(), tt.err) + } + } + }) + } +} diff --git a/regimes/regimes.go b/regimes/regimes.go index ff3cb73a8..aed92e692 100644 --- a/regimes/regimes.go +++ b/regimes/regimes.go @@ -22,6 +22,7 @@ import ( _ "github.com/invopop/gobl/regimes/ie" _ "github.com/invopop/gobl/regimes/in" _ "github.com/invopop/gobl/regimes/it" + _ "github.com/invopop/gobl/regimes/jp" _ "github.com/invopop/gobl/regimes/mx" _ "github.com/invopop/gobl/regimes/nl" _ "github.com/invopop/gobl/regimes/pl"