diff --git a/regimes/hu/extensions.go b/addons/hu/osa/extensions.go similarity index 85% rename from regimes/hu/extensions.go rename to addons/hu/osa/extensions.go index 70022aa8..55586b83 100644 --- a/regimes/hu/extensions.go +++ b/addons/hu/osa/extensions.go @@ -1,102 +1,102 @@ -package hu +package osa import ( "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" ) -// Special codes to be used inside rates +// Special Values to be used inside rates const ( - ExtKeyExemptionCode = "hu-exemption-code" + ExtKeyExemptionValue = "hu-exemption-Value" ) var extensionKeys = []*cbc.KeyDefinition{ { - Key: ExtKeyExemptionCode, + Key: ExtKeyExemptionValue, Name: i18n.String{ - i18n.EN: "Tax exemption reason code", + i18n.EN: "Tax exemption reason Value", i18n.HU: "Adómentesség okának kódja", }, - Codes: []*cbc.CodeDefinition{ + Values: []*cbc.ValueDefinition{ { - Code: "AAM", + Value: "AAM", Name: i18n.String{ i18n.EN: "Personal tax exemption: Chapter XIII of the VAT Act", i18n.HU: "Személyi adómentesség: ÁFA törvény XIII. fejezete", }, }, { - Code: "TAM", + Value: "TAM", Name: i18n.String{ i18n.EN: "Public interest: Section 85 and 86 of the VAT Act", i18n.HU: "Közérdekűség: ÁFA törvény 85. és 86. §", }, }, { - Code: "KBAET", + Value: "KBAET", Name: i18n.String{ i18n.EN: "Intra-Community supply (no new means of transport): Section 89 of the VAT Act", i18n.HU: "Közösségi beszerzés (nem új közlekedési eszköz): ÁFA törvény 89. §", }, }, { - Code: "KBAUK", + Value: "KBAUK", Name: i18n.String{ i18n.EN: "Intra-Community supply (new means of transport): Section 89 of the VAT Act", i18n.HU: "Közösségi beszerzés (új közlekedési eszköz): ÁFA törvény 89. §", }, }, { - Code: "EAM", + Value: "EAM", Name: i18n.String{ i18n.EN: "Export to non-EU countries: Sections 98 to 109 of the VAT Act", i18n.HU: "Export az EU-n kívüli országokba: ÁFA törvény 98-109. §", }, }, { - Code: "NAM", + Value: "NAM", Name: i18n.String{ i18n.EN: "Other international transaction: Sections 110 to 118 of the VAT Act", i18n.HU: "Egyéb nemzetközi ügylet: ÁFA törvény 110-118. §", }, }, { - Code: "ATK", + Value: "ATK", Name: i18n.String{ i18n.EN: "Outside Scope of VAT act: Sections 2 and 3 of the VAT Act", i18n.HU: "ÁFA törvény hatálya alól mentesített: ÁFA törvény 2. és 3. §", }, }, { - Code: "EUFAD37", + Value: "EUFAD37", Name: i18n.String{ i18n.EN: "Reverse charge in another member state: Section 37 of the VAT Act", i18n.HU: "Fordított adózás más tagállamban: ÁFA törvény 37. §", }, }, { - Code: "EUFADE", + Value: "EUFADE", Name: i18n.String{ i18n.EN: "Reverse charge in another member state: Not subject to section 37 of the VAT Act", i18n.HU: "Fordított adózás más tagállamban: Nem tartozik az ÁFA törvény 37. § hatálya alá", }, }, { - Code: "EUE", + Value: "EUE", Name: i18n.String{ i18n.EN: "Non-reverse charge in another member state", i18n.HU: "Nem fordított adózás más tagállamban", }, }, { - Code: "HO", + Value: "HO", Name: i18n.String{ i18n.EN: "Transaction in a 3rd country", i18n.HU: "Ügylet harmadik országban", }, }, { - Code: "UNKNOWN", + Value: "UNKNOWN", Name: i18n.String{ i18n.EN: "It can be used for modifying or cancelling invoices or if unknown", i18n.HU: "Számla módosítására, törlésére vagy ismeretlen esetén használható", diff --git a/regimes/hu/hu.go b/regimes/hu/hu.go index a659d1cf..13436b35 100644 --- a/regimes/hu/hu.go +++ b/regimes/hu/hu.go @@ -5,6 +5,8 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -22,12 +24,14 @@ func New() *tax.RegimeDef { i18n.HU: "Magyarország", }, TimeZone: "Europe/Budapest", - Extensions: extensionKeys, Categories: taxCategories, - Tags: invoiceTags, - Validator: Validate, - Normalizer: Normalize, - Scenarios: scenarios, + Tags: []*tax.TagSet{ + common.InvoiceTags().Merge(invoiceTags), + }, + Validator: Validate, + Normalizer: Normalize, + Scenarios: scenarios, + IdentityKeys: identityKeyDefinitions, } } @@ -38,6 +42,8 @@ func Validate(doc interface{}) error { return validateInvoice(obj) case *tax.Identity: return validateTaxIdentity(obj) + case *org.Identity: + return validateIdentity(obj) } return nil } diff --git a/regimes/hu/identities.go b/regimes/hu/identities.go new file mode 100644 index 00000000..422a8983 --- /dev/null +++ b/regimes/hu/identities.go @@ -0,0 +1,86 @@ +package hu + +import ( + "errors" + "fmt" + "strconv" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/i18n" + "github.com/invopop/gobl/org" + "github.com/invopop/validation" +) + +const ( + // When an individual belongs to a group, the user group tax number must be included in a separate field. + IdentityKeyGroupNumber cbc.Key = "hu-group-number" +) + +var identityKeyDefinitions = []*cbc.KeyDefinition{ + { + Key: IdentityKeyGroupNumber, + Name: i18n.String{ + i18n.EN: "Group Member Number", + i18n.HU: "Csoport tag adószáma", + }, + }, +} + +func validateIdentity(id *org.Identity) error { + if id == nil || id.Key != IdentityKeyGroupNumber { + return nil + } + switch id.Key { + case IdentityKeyGroupNumber: + return validation.ValidateStruct(id, + validation.Field(&id.Code, validation.By(validateGroupCode)), + ) + default: + return nil + } +} + +func validateGroupCode(value interface{}) error { + code, ok := value.(cbc.Code) + if !ok { + return nil + } + if code == "" { + return nil + } + + for _, v := range code { + x := v - 48 + if x < 0 || x > 9 { + return errors.New("contains invalid characters") + } + } + + if len(code) != 11 { + return errors.New("invalid length") + } + + str := code.String() + + // Calculate check-digit + result := 9*int(str[0]-'0') + 7*int(str[1]-'0') + 3*int(str[2]-'0') + int(str[3]-'0') + 9*int(str[4]-'0') + 7*int(str[5]-'0') + 3*int(str[6]-'0') + checkDigit := (10 - result%10) % 10 + + compare, err := strconv.Atoi(string(code[7])) + if err != nil { + return fmt.Errorf("invalid check digit: %w", err) + } + if compare != checkDigit { + return errors.New("checksum mismatch") + } + + if len(code) == 11 { + if !validAreaCodes[code[9:11]] { + return errors.New("invalid area code") + } + if code[8:9] != "4" { + return errors.New("invalid VAT code") + } + } + return nil +} diff --git a/regimes/hu/identities_test.go b/regimes/hu/identities_test.go new file mode 100644 index 00000000..317b6d4a --- /dev/null +++ b/regimes/hu/identities_test.go @@ -0,0 +1,40 @@ +package hu_test + +import ( + "testing" + + "github.com/invopop/gobl/cbc" + "github.com/invopop/gobl/org" + "github.com/invopop/gobl/regimes/hu" + "github.com/stretchr/testify/assert" +) + +func TestValidateGroupId(t *testing.T) { + tests := []struct { + name string + code cbc.Code + err string + }{ + {"Empty code", "", ""}, + {"Invalid length (5)", "12345", "invalid length"}, + {"Invalid length (10)", "1234567890", "invalid length"}, + {"Invalid check digit", "12345678", "checksum mismatch"}, + {"Invalid VAT code", "21114445123", "invalid VAT code"}, + {"Invalid area code", "82713452101", "invalid area code"}, + {"Valid code (8 chars)", "98109858", ""}, + {"Valid code (11 chars)", "88212131403", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tID := &org.Identity{Country: "HU", Code: tt.code, Key: hu.IdentityKeyGroupNumber} + err := hu.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/hu/invoices_test.go b/regimes/hu/invoices_test.go index 3e9fc776..4141bf5d 100644 --- a/regimes/hu/invoices_test.go +++ b/regimes/hu/invoices_test.go @@ -162,7 +162,7 @@ func TestInvoiceValidation(t *testing.T) { t.Run("Valid Credit Note", func(t *testing.T) { inv := baseInvoice() inv.Type = bill.InvoiceTypeCreditNote - inv.Preceding = []*bill.Preceding{ + inv.Preceding = []*org.DocumentRef{ { Code: "TEST-001", }, diff --git a/regimes/hu/scenarios.go b/regimes/hu/scenarios.go index 5f0cbf32..fed4651a 100644 --- a/regimes/hu/scenarios.go +++ b/regimes/hu/scenarios.go @@ -4,7 +4,6 @@ import ( "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/i18n" - "github.com/invopop/gobl/regimes/common" "github.com/invopop/gobl/tax" ) @@ -41,40 +40,43 @@ var invoiceScenarios = &tax.ScenarioSet{ }, } -var invoiceTags = common.InvoiceTagsWith([]*cbc.KeyDefinition{ - { - Key: TagDomesticReverseCharge, - Name: i18n.String{ - i18n.EN: "Domestic Reverse Charge", - i18n.HU: "Belföldi fordított adózás", +var invoiceTags = &tax.TagSet{ + Schema: bill.ShortSchemaInvoice, + List: []*cbc.KeyDefinition{ + { + Key: TagDomesticReverseCharge, + Name: i18n.String{ + i18n.EN: "Domestic Reverse Charge", + i18n.HU: "Belföldi fordított adózás", + }, }, - }, - { - Key: TagTravelAgency, - Name: i18n.String{ - i18n.EN: "Travel Agency", - i18n.HU: "Utazási iroda", + { + Key: TagTravelAgency, + Name: i18n.String{ + i18n.EN: "Travel Agency", + i18n.HU: "Utazási iroda", + }, }, - }, - { - Key: TagSecondHand, - Name: i18n.String{ - i18n.EN: "Second Hand", - i18n.HU: "Használt cikk", + { + Key: TagSecondHand, + Name: i18n.String{ + i18n.EN: "Second Hand", + i18n.HU: "Használt cikk", + }, }, - }, - { - Key: TagArt, - Name: i18n.String{ - i18n.EN: "Art", - i18n.HU: "Műalkotás", + { + Key: TagArt, + Name: i18n.String{ + i18n.EN: "Art", + i18n.HU: "Műalkotás", + }, }, - }, - { - Key: TagAntiques, - Name: i18n.String{ - i18n.EN: "Antiques", - i18n.HU: "Antikvitás", + { + Key: TagAntiques, + Name: i18n.String{ + i18n.EN: "Antiques", + i18n.HU: "Antikvitás", + }, }, }, -}) +} diff --git a/regimes/hu/tax_categories.go b/regimes/hu/tax_categories.go index a66ebfb5..d19edf46 100644 --- a/regimes/hu/tax_categories.go +++ b/regimes/hu/tax_categories.go @@ -2,7 +2,6 @@ package hu 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" @@ -30,9 +29,6 @@ var taxCategories = []*tax.CategoryDef{ }, }, Retained: false, - Extensions: []cbc.Key{ - ExtKeyExemptionCode, - }, Rates: []*tax.RateDef{ { Key: tax.RateZero, diff --git a/regimes/hu/tax_identity.go b/regimes/hu/tax_identity.go index 20494c85..9ac9069a 100644 --- a/regimes/hu/tax_identity.go +++ b/regimes/hu/tax_identity.go @@ -15,7 +15,7 @@ import ( // Number 4 is only valid for the group tax subject to VAT (second tax id) var ( validVatCodes = map[cbc.Code]bool{ - "1": true, "2": true, "3": true, "4": true, "5": true, + "1": true, "2": true, "3": true, "5": true, } validAreaCodes = map[cbc.Code]bool{