diff --git a/bill/invoice_correct.go b/bill/invoice_correct.go index f0178d88..5e5fc0ef 100644 --- a/bill/invoice_correct.go +++ b/bill/invoice_correct.go @@ -21,18 +21,14 @@ import ( type CorrectionOptions struct { head.CorrectionOptions + // The type of corrective invoice to produce. + Type cbc.Key `json:"type" jsonschema:"title=Type"` // When the new corrective invoice's issue date should be set to. IssueDate *cal.Date `json:"issue_date,omitempty" jsonschema:"title=Issue Date"` // Stamps of the previous document to include in the preceding data. Stamps []*head.Stamp `json:"stamps,omitempty" jsonschema:"title=Stamps"` - // Credit when true indicates that the corrective document should cancel the previous document. - Credit bool `json:"credit,omitempty" jsonschema:"title=Credit"` - // Debit when true indicates that the corrective document should add new items to the previous document. - Debit bool `json:"debit,omitempty" jsonschema:"title=Debit"` // Human readable reason for the corrective operation. Reason string `json:"reason,omitempty" jsonschema:"title=Reason"` - // Correction method as defined by the tax regime. - Method cbc.Key `json:"method,omitempty" jsonschema:"title=Method"` // Changes keys that describe the specific changes according to the tax regime. Changes []cbc.Key `json:"changes,omitempty" jsonschema:"title=Changes"` @@ -78,14 +74,6 @@ func WithReason(reason string) schema.Option { } } -// WithMethod defines the method used to correct the previous invoice. -func WithMethod(method cbc.Key) schema.Option { - return func(o interface{}) { - opts := o.(*CorrectionOptions) - opts.Method = method - } -} - // WithChanges adds the set of change keys to the invoice's preceding data, // can be called multiple times. func WithChanges(changes ...cbc.Key) schema.Option { @@ -104,18 +92,25 @@ func WithIssueDate(date cal.Date) schema.Option { } } +// Corrective is used for creating corrective or rectified invoices +// that completely replace a previous document. +var Corrective schema.Option = func(o interface{}) { + opts := o.(*CorrectionOptions) + opts.Type = InvoiceTypeCorrective +} + // Credit indicates that the corrective operation requires a credit note // or equivalent. var Credit schema.Option = func(o interface{}) { opts := o.(*CorrectionOptions) - opts.Credit = true + opts.Type = InvoiceTypeCreditNote } // Debit indicates that the corrective operation is to append // new items to the previous invoice, usually as a debit note. var Debit schema.Option = func(o interface{}) { opts := o.(*CorrectionOptions) - opts.Debit = true + opts.Type = InvoiceTypeDebitNote } // CorrectionOptionsSchema provides a dynamic JSON schema of the options @@ -147,29 +142,18 @@ func (inv *Invoice) CorrectionOptionsSchema() (interface{}, error) { cos := schema.Definitions["CorrectionOptions"] - // Improve the quality of the schema - cos.Required = append(cos.Required, "credit") - cd := r.CorrectionDefinitionFor(ShortSchemaInvoice) if cd == nil { return schema, nil } - if cd.ReasonRequired { - cos.Required = append(cos.Required, "reason") - } - - if len(cd.Methods) > 0 { - cos.Required = append(cos.Required, "method") - if ps, ok := cos.Properties.Get("method"); ok { - ps.OneOf = make([]*jsonschema.Schema, len(cd.Methods)) - for i, v := range cd.Methods { + if len(cd.Types) > 0 { + if ps, ok := cos.Properties.Get("type"); ok { + ps.OneOf = make([]*jsonschema.Schema, len(cd.Types)) + for i, v := range cd.Types { ps.OneOf[i] = &jsonschema.Schema{ - Const: v.Key.String(), - Title: v.Name.String(), - } - if !v.Desc.IsEmpty() { - ps.OneOf[i].Description = v.Desc.String() + Const: v.String(), + Title: v.String(), } } } @@ -192,6 +176,10 @@ func (inv *Invoice) CorrectionOptionsSchema() (interface{}, error) { } } + if cd.ReasonRequired { + cos.Required = append(cos.Required, "reason") + } + return schema, nil } @@ -216,15 +204,16 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { // Copy and prepare the basic fields pre := &Preceding{ - UUID: inv.UUID, - Series: inv.Series, - Code: inv.Code, - IssueDate: inv.IssueDate.Clone(), - Reason: o.Reason, - CorrectionMethod: o.Method, - Changes: o.Changes, + UUID: inv.UUID, + Type: inv.Type, + Series: inv.Series, + Code: inv.Code, + IssueDate: inv.IssueDate.Clone(), + Reason: o.Reason, + Changes: o.Changes, } inv.UUID = nil + inv.Type = o.Type inv.Series = "" inv.Code = "" if o.IssueDate != nil { @@ -235,10 +224,6 @@ func (inv *Invoice) Correct(opts ...schema.Option) error { cd := r.CorrectionDefinitionFor(ShortSchemaInvoice) - if err := inv.prepareCorrectionType(o, cd); err != nil { - return err - } - if err := inv.validatePrecedingData(o, cd, pre); err != nil { return err } @@ -269,42 +254,10 @@ func prepareCorrectionOptions(o *CorrectionOptions, opts ...schema.Option) error } } - if o.Credit && o.Debit { - return errors.New("cannot use both credit and debit options") + if o.Type == cbc.KeyEmpty { + return errors.New("missing correction type") } - return nil -} -func (inv *Invoice) prepareCorrectionType(o *CorrectionOptions, cd *tax.CorrectionDefinition) error { - // Take the regime def to figure out what needs to be copied - if o.Credit { - if cd.HasType(InvoiceTypeCreditNote) { - // regular credit note - inv.Type = InvoiceTypeCreditNote - } else if cd.HasType(InvoiceTypeCorrective) { - // corrective invoice with negative values - inv.Type = InvoiceTypeCorrective - inv.Invert() - } else { - return errors.New("credit note not supported by regime") - } - inv.Payment.ResetAdvances() - } else if o.Debit { - if cd.HasType(InvoiceTypeDebitNote) { - // regular debit note, implies no rows as new ones - // will be added - inv.Type = InvoiceTypeDebitNote - inv.Empty() - } else { - return errors.New("debit note not supported by regime") - } - } else { - if cd.HasType(InvoiceTypeCorrective) { - inv.Type = InvoiceTypeCorrective - } else { - return fmt.Errorf("corrective invoice type not supported by regime, try credit or debit") - } - } return nil } @@ -326,22 +279,17 @@ func (inv *Invoice) validatePrecedingData(o *CorrectionOptions, cd *tax.Correcti pre.Stamps = append(pre.Stamps, s) } - if len(cd.Methods) > 0 { - if pre.CorrectionMethod == cbc.KeyEmpty { - return errors.New("missing correction method") - } - if !cd.HasMethod(pre.CorrectionMethod) { - return fmt.Errorf("invalid correction method: %v", pre.CorrectionMethod) - } + if !o.Type.In(cd.Types...) { + return fmt.Errorf("invalid correction type: %v", o.Type.String()) } if len(cd.Changes) > 0 { if len(pre.Changes) == 0 { - return errors.New("missing changes") + return errors.New("missing correction changes") } for _, k := range pre.Changes { if !cd.HasChange(k) { - return fmt.Errorf("invalid change key: '%v'", k) + return fmt.Errorf("invalid correction change key: '%v'", k) } } } diff --git a/bill/invoice_correct_test.go b/bill/invoice_correct_test.go index 3203de5f..60a99cdb 100644 --- a/bill/invoice_correct_test.go +++ b/bill/invoice_correct_test.go @@ -21,41 +21,47 @@ import ( func TestInvoiceCorrect(t *testing.T) { // Spanish Case (only corrective) + + // debit note not supported in Spain i := testInvoiceESForCorrection(t) - err := i.Correct(bill.Credit, bill.Debit) + err := i.Correct(bill.Debit) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot use both credit and debit options") + assert.Contains(t, err.Error(), "invalid correction type: debit-note") i = testInvoiceESForCorrection(t) err = i.Correct(bill.Credit, bill.WithReason("test refund"), - bill.WithMethod(es.CorrectionMethodKeyComplete), bill.WithChanges(es.CorrectionKeyLine), ) require.NoError(t, err) - assert.Equal(t, bill.InvoiceTypeCorrective, i.Type) - assert.Equal(t, i.Lines[0].Quantity.String(), "-10") + assert.Equal(t, bill.InvoiceTypeCreditNote, i.Type) + assert.Equal(t, i.Lines[0].Quantity.String(), "10") assert.Equal(t, i.IssueDate, cal.Today()) pre := i.Preceding[0] assert.Equal(t, pre.Series, "TEST") assert.Equal(t, pre.Code, "123") assert.Equal(t, pre.IssueDate, cal.NewDate(2022, 6, 13)) assert.Equal(t, pre.Reason, "test refund") - assert.Equal(t, i.Totals.Payable.String(), "-900.00") + assert.Equal(t, i.Totals.Payable.String(), "900.00") // can't run twice - err = i.Correct() + err = i.Correct(bill.Corrective) require.Error(t, err) assert.Contains(t, err.Error(), "cannot correct an invoice without a code") + i = testInvoiceESForCorrection(t) + err = i.Correct() + require.Error(t, err) + assert.Contains(t, err.Error(), "missing correction type") + i = testInvoiceESForCorrection(t) err = i.Correct(bill.Debit, bill.WithReason("should fail")) require.Error(t, err) - assert.Contains(t, err.Error(), "debit note not supported by regime") + assert.Contains(t, err.Error(), "invalid correction type: debit-note") i = testInvoiceESForCorrection(t) err = i.Correct( - bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.Corrective, bill.WithChanges(es.CorrectionKeyLine), ) require.NoError(t, err) @@ -65,8 +71,8 @@ func TestInvoiceCorrect(t *testing.T) { i = testInvoiceESForCorrection(t) d := cal.MakeDate(2023, 6, 13) err = i.Correct( + bill.Credit, bill.WithIssueDate(d), - bill.WithMethod(es.CorrectionMethodKeyComplete), bill.WithChanges(es.CorrectionKeyLine), ) require.NoError(t, err) @@ -74,7 +80,7 @@ func TestInvoiceCorrect(t *testing.T) { // France case (both corrective and credit note) i = testInvoiceFRForCorrection(t) - err = i.Correct() + err = i.Correct(bill.Corrective) require.NoError(t, err) assert.Equal(t, i.Type, bill.InvoiceTypeCorrective) @@ -102,44 +108,43 @@ func TestInvoiceCorrect(t *testing.T) { } i = testInvoiceCOForCorrection(t) - err = i.Correct(bill.WithStamps(stamps)) + err = i.Correct(bill.Corrective, bill.WithStamps(stamps)) require.Error(t, err) - assert.Contains(t, err.Error(), "corrective invoice type not supported by regime, try credit or debit") + assert.Contains(t, err.Error(), "invalid correction type: corrective") i = testInvoiceCOForCorrection(t) err = i.Correct( bill.Credit, bill.WithStamps(stamps), - bill.WithMethod(co.CorrectionMethodKeyRevoked), bill.WithReason("test refund"), + bill.WithChanges(co.CorrectionKeyRevoked), ) require.NoError(t, err) assert.Equal(t, i.Type, bill.InvoiceTypeCreditNote) pre = i.Preceding[0] require.Len(t, pre.Stamps, 1) assert.Equal(t, pre.Stamps[0].Provider, co.StampProviderDIANCUDE) - assert.Equal(t, pre.CorrectionMethod, co.CorrectionMethodKeyRevoked) + // assert.Equal(t, pre.CorrectionMethod, co.CorrectionMethodKeyRevoked) } func TestCorrectWithOptions(t *testing.T) { i := testInvoiceESForCorrection(t) opts := &bill.CorrectionOptions{ - Credit: true, + Type: bill.InvoiceTypeCreditNote, Reason: "test refund", - Method: es.CorrectionMethodKeyComplete, Changes: []cbc.Key{es.CorrectionKeyLine}, } err := i.Correct(bill.WithOptions(opts)) require.NoError(t, err) - assert.Equal(t, bill.InvoiceTypeCorrective, i.Type) - assert.Equal(t, i.Lines[0].Quantity.String(), "-10") + assert.Equal(t, bill.InvoiceTypeCreditNote, i.Type) + assert.Equal(t, i.Lines[0].Quantity.String(), "10") assert.Equal(t, i.IssueDate, cal.Today()) pre := i.Preceding[0] assert.Equal(t, pre.Series, "TEST") assert.Equal(t, pre.Code, "123") assert.Equal(t, pre.IssueDate, cal.NewDate(2022, 6, 13)) assert.Equal(t, pre.Reason, "test refund") - assert.Equal(t, i.Totals.Payable.String(), "-900.00") + assert.Equal(t, i.Totals.Payable.String(), "900.00") } func TestCorrectionOptionsSchema(t *testing.T) { @@ -151,13 +156,13 @@ func TestCorrectionOptionsSchema(t *testing.T) { require.True(t, ok) cos := schema.Definitions["CorrectionOptions"] - assert.Equal(t, cos.Properties.Len(), 7) + assert.Equal(t, cos.Properties.Len(), 5) - pm, ok := cos.Properties.Get("method") + pm, ok := cos.Properties.Get("changes") require.True(t, ok) - assert.Len(t, pm.OneOf, 4) + assert.Len(t, pm.Items.OneOf, 22) - exp := `{"$ref":"https://gobl.org/draft-0/cbc/key","title":"Method","description":"Correction method as defined by the tax regime.","oneOf":[{"const":"complete","title":"Complete"},{"const":"partial","title":"Corrected items only"},{"const":"discount","title":"Bulk deal in a given period"},{"const":"authorized","title":"Authorized by the Tax Agency"}]}` + exp := `{"items":{"$ref":"https://gobl.org/draft-0/cbc/key","oneOf":[{"const":"code","title":"Invoice code"},{"const":"series","title":"Invoice series"},{"const":"issue-date","title":"Issue date"},{"const":"supplier-name","title":"Name and surnames/Corporate name - Issuer (Sender)"},{"const":"customer-name","title":"Name and surnames/Corporate name - Receiver"},{"const":"supplier-tax-id","title":"Issuer's Tax Identification Number"},{"const":"customer-tax-id","title":"Receiver's Tax Identification Number"},{"const":"supplier-addr","title":"Issuer's address"},{"const":"customer-addr","title":"Receiver's address"},{"const":"line","title":"Item line"},{"const":"tax-rate","title":"Applicable Tax Rate"},{"const":"tax-amount","title":"Applicable Tax Amount"},{"const":"period","title":"Applicable Date/Period"},{"const":"type","title":"Invoice Class"},{"const":"legal-details","title":"Legal literals"},{"const":"tax-base","title":"Taxable Base"},{"const":"tax","title":"Calculation of tax outputs"},{"const":"tax-retained","title":"Calculation of tax inputs"},{"const":"refund","title":"Taxable Base modified due to return of packages and packaging materials"},{"const":"discount","title":"Taxable Base modified due to discounts and rebates"},{"const":"judicial","title":"Taxable Base modified due to firm court ruling or administrative decision"},{"const":"insolvency","title":"Taxable Base modified due to unpaid outputs where there is a judgement opening insolvency proceedings"}]},"type":"array","title":"Changes","description":"Changes keys that describe the specific changes according to the tax regime."}` data, err := json.Marshal(pm) require.NoError(t, err) if !assert.JSONEq(t, exp, string(data)) { @@ -171,15 +176,15 @@ func TestCorrectionOptionsSchema(t *testing.T) { func TestCorrectWithData(t *testing.T) { i := testInvoiceESForCorrection(t) - data := []byte(`{"credit":true,"reason":"test refund"}`) + data := []byte(`{"type":"credit-note","reason":"test refund"}`) err := i.Correct( bill.WithData(data), - bill.WithMethod(es.CorrectionMethodKeyComplete), bill.WithChanges(es.CorrectionKeyLine), ) assert.NoError(t, err) - assert.Equal(t, i.Lines[0].Quantity.String(), "-10") // implies credit was made + assert.Equal(t, i.Type, bill.InvoiceTypeCreditNote) + assert.Equal(t, i.Lines[0].Quantity.String(), "10") // implies credit was made data = []byte(`{"credit": true`) // invalid json err = i.Correct(bill.WithData(data)) diff --git a/bill/invoice_type_test.go b/bill/invoice_type_test.go index 3769c9aa..1c19c654 100644 --- a/bill/invoice_type_test.go +++ b/bill/invoice_type_test.go @@ -26,5 +26,11 @@ func TestInvoiceType(t *testing.T) { assert.True(t, c.In("bar", "foo")) assert.False(t, c.In("bar", "dom")) +} +func TestInvoiceUNTDID1001(t *testing.T) { + inv := testInvoiceESForCorrection(t) + assert.Equal(t, cbc.CodeEmpty, inv.UNTDID1001()) + inv.Type = bill.InvoiceTypeStandard + assert.Equal(t, cbc.Code("380"), inv.UNTDID1001()) } diff --git a/bill/preceding.go b/bill/preceding.go index 93a9bde4..fda501cc 100644 --- a/bill/preceding.go +++ b/bill/preceding.go @@ -16,6 +16,8 @@ import ( type Preceding struct { // Preceding document's UUID if available can be useful for tracing. UUID *uuid.UUID `json:"uuid,omitempty" jsonschema:"title=UUID"` + // Type of the preceding document + Type cbc.Key `json:"type,omitempty" jsonschema:"title=Type"` // Series identification code Series string `json:"series,omitempty" jsonschema:"title=Series"` // Code of the previous document. @@ -26,8 +28,6 @@ type Preceding struct { Reason string `json:"reason,omitempty" jsonschema:"title=Reason"` // Seals of approval from other organisations that may need to be listed. Stamps []*head.Stamp `json:"stamps,omitempty" jsonschema:"title=Stamps"` - // Tax regime specific key reflecting the method used to correct the preceding invoice. - CorrectionMethod cbc.Key `json:"correction_method,omitempty" jsonschema:"title=Correction Method"` // Tax regime specific keys reflecting what has been changed from the previous invoice. Changes []cbc.Key `json:"changes,omitempty" jsonschema:"title=Changes"` // Tax period in which the previous invoice had an effect required by some tax regimes and formats. @@ -58,11 +58,11 @@ func (p *Preceding) UnmarshalJSON(data []byte) error { func (p *Preceding) Validate() error { return validation.ValidateStruct(p, validation.Field(&p.UUID), + validation.Field(&p.Type), validation.Field(&p.Series), validation.Field(&p.Code, validation.Required), validation.Field(&p.IssueDate, cal.DateNotZero()), validation.Field(&p.Stamps), - validation.Field(&p.CorrectionMethod), validation.Field(&p.Changes), validation.Field(&p.Period), validation.Field(&p.Meta), diff --git a/bill/preceding_test.go b/bill/preceding_test.go index 1317da93..730257b2 100644 --- a/bill/preceding_test.go +++ b/bill/preceding_test.go @@ -18,10 +18,9 @@ func TestPrecedingValidation(t *testing.T) { } func TestPrecedingJSONMigration(t *testing.T) { - data := []byte(`{"correction_method":"foo","corrections":["bar"]}`) + data := []byte(`{"corrections":["bar"]}`) p := new(bill.Preceding) err := p.UnmarshalJSON(data) assert.NoError(t, err) - assert.Equal(t, "foo", p.CorrectionMethod.String()) assert.Equal(t, "bar", p.Changes[0].String()) } diff --git a/data/schemas/bill/correction-options.json b/data/schemas/bill/correction-options.json index 0ec33727..1d09a717 100644 --- a/data/schemas/bill/correction-options.json +++ b/data/schemas/bill/correction-options.json @@ -5,6 +5,11 @@ "$defs": { "CorrectionOptions": { "properties": { + "type": { + "$ref": "https://gobl.org/draft-0/cbc/key", + "title": "Type", + "description": "The type of corrective invoice to produce." + }, "issue_date": { "$ref": "https://gobl.org/draft-0/cal/date", "title": "Issue Date", @@ -18,26 +23,11 @@ "title": "Stamps", "description": "Stamps of the previous document to include in the preceding data." }, - "credit": { - "type": "boolean", - "title": "Credit", - "description": "Credit when true indicates that the corrective document should cancel the previous document." - }, - "debit": { - "type": "boolean", - "title": "Debit", - "description": "Debit when true indicates that the corrective document should add new items to the previous document." - }, "reason": { "type": "string", "title": "Reason", "description": "Human readable reason for the corrective operation." }, - "method": { - "$ref": "https://gobl.org/draft-0/cbc/key", - "title": "Method", - "description": "Correction method as defined by the tax regime." - }, "changes": { "items": { "$ref": "https://gobl.org/draft-0/cbc/key" @@ -48,6 +38,9 @@ } }, "type": "object", + "required": [ + "type" + ], "description": "CorrectionOptions defines a structure used to pass configuration options to correct a previous invoice." } }, diff --git a/data/schemas/bill/invoice.json b/data/schemas/bill/invoice.json index afc77752..ca5b83d9 100644 --- a/data/schemas/bill/invoice.json +++ b/data/schemas/bill/invoice.json @@ -674,6 +674,11 @@ "title": "UUID", "description": "Preceding document's UUID if available can be useful for tracing." }, + "type": { + "$ref": "https://gobl.org/draft-0/cbc/key", + "title": "Type", + "description": "Type of the preceding document" + }, "series": { "type": "string", "title": "Series", @@ -702,11 +707,6 @@ "title": "Stamps", "description": "Seals of approval from other organisations that may need to be listed." }, - "correction_method": { - "$ref": "https://gobl.org/draft-0/cbc/key", - "title": "Correction Method", - "description": "Tax regime specific key reflecting the method used to correct the preceding invoice." - }, "changes": { "items": { "$ref": "https://gobl.org/draft-0/cbc/key" diff --git a/data/schemas/pay/advance.json b/data/schemas/pay/advance.json index a4d2ec9a..6c202900 100644 --- a/data/schemas/pay/advance.json +++ b/data/schemas/pay/advance.json @@ -115,6 +115,21 @@ "$ref": "https://gobl.org/draft-0/currency/code", "title": "Currency", "description": "If different from the parent document's base currency." + }, + "card": { + "$ref": "#/$defs/Card", + "title": "Card", + "description": "Details of the payment that was made via a credit or debit card." + }, + "credit_transfer": { + "$ref": "#/$defs/CreditTransfer", + "title": "Credit Transfer", + "description": "Details about how the payment was made by credit (bank) transfer." + }, + "meta": { + "$ref": "https://gobl.org/draft-0/cbc/meta", + "title": "Meta", + "description": "Additional details useful for the parties involved." } }, "type": "object", @@ -123,6 +138,57 @@ "amount" ], "description": "Advance represents a single payment that has been made already, such as a deposit on an intent to purchase, or as credit from a previous invoice which was later corrected or cancelled." + }, + "Card": { + "properties": { + "last4": { + "type": "string", + "title": "Last 4", + "description": "Last 4 digits of the card's Primary Account Number (PAN)." + }, + "holder": { + "type": "string", + "title": "Holder Name", + "description": "Name of the person whom the card belongs to." + } + }, + "type": "object", + "required": [ + "last4", + "holder" + ], + "description": "Card contains simplified card holder data as a reference for the customer." + }, + "CreditTransfer": { + "properties": { + "iban": { + "type": "string", + "title": "IBAN", + "description": "International Bank Account Number" + }, + "bic": { + "type": "string", + "title": "BIC", + "description": "Bank Identifier Code used for international transfers." + }, + "number": { + "type": "string", + "title": "Number", + "description": "Account number, if IBAN not available." + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name of the bank." + }, + "branch": { + "$ref": "https://gobl.org/draft-0/org/address", + "title": "Branch", + "description": "Bank office branch address, not normally required." + } + }, + "type": "object", + "description": "CreditTransfer contains fields that can be used for making payments via a bank transfer or wire." } }, "$comment": "Generated with GOBL v0.65.0" diff --git a/data/schemas/tax/regime.json b/data/schemas/tax/regime.json index 9d95821e..81241fb9 100644 --- a/data/schemas/tax/regime.json +++ b/data/schemas/tax/regime.json @@ -116,14 +116,6 @@ "title": "Types", "description": "The types of sub-documents supported by the regime" }, - "methods": { - "items": { - "$ref": "#/$defs/KeyDefinition" - }, - "type": "array", - "title": "Methods", - "description": "Methods describe the methods used to correct an invoice." - }, "changes": { "items": { "$ref": "#/$defs/KeyDefinition" diff --git a/envelope.go b/envelope.go index 5313e5ac..fa07bf1c 100644 --- a/envelope.go +++ b/envelope.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "strconv" "github.com/invopop/validation" @@ -141,7 +140,6 @@ func (e *Envelope) ValidateWithContext(ctx context.Context) error { ), ) if err != nil { - fmt.Printf("TYPE: %T\n", err) return wrapError(err) } return wrapError(e.verifyDigest()) diff --git a/envelope_test.go b/envelope_test.go index 5da978ee..b68c26ed 100644 --- a/envelope_test.go +++ b/envelope_test.go @@ -300,16 +300,16 @@ func TestEnvelopeCorrect(t *testing.T) { require.NoError(t, env.Calculate()) _, err = env.Correct( + bill.Corrective, bill.WithChanges(es.CorrectionKeyLine), - bill.WithMethod(es.CorrectionMethodKeyComplete), ) require.NoError(t, err) doc := env.Extract().(*bill.Invoice) - assert.Equal(t, doc.Type, bill.InvoiceTypeStandard, "no change") + assert.Equal(t, doc.Type, bill.InvoiceTypeStandard, "should not update in place") e2, err := env.Correct( - bill.WithMethod(es.CorrectionMethodKeyComplete), + bill.Corrective, bill.WithChanges(es.CorrectionKeyLine), ) require.NoError(t, err) diff --git a/regimes/co/co.go b/regimes/co/co.go index 5f31871e..a81956f4 100644 --- a/regimes/co/co.go +++ b/regimes/co/co.go @@ -60,7 +60,7 @@ func New() *tax.Regime { Stamps: []cbc.Key{ StampProviderDIANCUDE, }, - Methods: correctionMethodList, + Changes: correctionList, }, }, Categories: taxCategories, diff --git a/regimes/co/corrections.go b/regimes/co/corrections.go index 960cc10a..a383f46d 100644 --- a/regimes/co/corrections.go +++ b/regimes/co/corrections.go @@ -9,16 +9,16 @@ import ( // Preceding document correction method constants. const ( - CorrectionMethodKeyPartial cbc.Key = "partial" - CorrectionMethodKeyRevoked cbc.Key = "revoked" - CorrectionMethodKeyDiscount cbc.Key = "discount" - CorrectionMethodKeyPriceAdjustment cbc.Key = "price-adjustment" - CorrectionMethodKeyOther cbc.Key = "other" + CorrectionKeyPartial cbc.Key = "partial" + CorrectionKeyRevoked cbc.Key = "revoked" + CorrectionKeyDiscount cbc.Key = "discount" + CorrectionKeyPriceAdjustment cbc.Key = "price-adjustment" + CorrectionKeyOther cbc.Key = "other" ) -var correctionMethodList = []*tax.KeyDefinition{ +var correctionList = []*tax.KeyDefinition{ { - Key: CorrectionMethodKeyPartial, + Key: CorrectionKeyPartial, Name: i18n.String{ i18n.EN: "Partial refund", i18n.ES: "Devolución parcial", @@ -32,7 +32,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, }, { - Key: CorrectionMethodKeyRevoked, + Key: CorrectionKeyRevoked, Name: i18n.String{ i18n.EN: "Revoked", i18n.ES: "Anulación", @@ -46,7 +46,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, }, { - Key: CorrectionMethodKeyDiscount, + Key: CorrectionKeyDiscount, Name: i18n.String{ i18n.EN: "Discount", i18n.ES: "Descuento", @@ -60,7 +60,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, }, { - Key: CorrectionMethodKeyPriceAdjustment, + Key: CorrectionKeyPriceAdjustment, Name: i18n.String{ i18n.EN: "Adjustment", i18n.ES: "Ajuste", @@ -74,7 +74,7 @@ var correctionMethodList = []*tax.KeyDefinition{ }, }, { - Key: CorrectionMethodKeyOther, + Key: CorrectionKeyOther, Name: i18n.String{ i18n.EN: "Other", i18n.ES: "Otros", @@ -85,12 +85,12 @@ var correctionMethodList = []*tax.KeyDefinition{ }, } -func correctionMethodKeys() []interface{} { - keys := make([]interface{}, len(correctionMethodList)) - for i, v := range correctionMethodList { +func correctionKeys() []interface{} { + keys := make([]interface{}, len(correctionList)) + for i, v := range correctionList { keys[i] = v.Key } return keys } -var isValidCorrectionMethodKey = validation.In(correctionMethodKeys()...) +var isValidCorrectionKey = validation.In(correctionKeys()...) diff --git a/regimes/co/invoices.go b/regimes/co/invoices.go index 3622ff29..cf5645a8 100644 --- a/regimes/co/invoices.go +++ b/regimes/co/invoices.go @@ -103,7 +103,11 @@ func (v *invoiceValidator) preceding(value interface{}) error { return nil } return validation.ValidateStruct(obj, - validation.Field(&obj.CorrectionMethod, validation.Required, isValidCorrectionMethodKey), + validation.Field(&obj.Changes, + validation.Required, + validation.Length(1, 1), // only one change expected in Colombia + validation.Each(isValidCorrectionKey), + ), validation.Field(&obj.Reason, validation.Required), ) } diff --git a/regimes/co/invoices_test.go b/regimes/co/invoices_test.go index 8d50bad5..afd543d2 100644 --- a/regimes/co/invoices_test.go +++ b/regimes/co/invoices_test.go @@ -6,6 +6,7 @@ import ( _ "github.com/invopop/gobl" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/cal" + "github.com/invopop/gobl/cbc" "github.com/invopop/gobl/currency" "github.com/invopop/gobl/l10n" "github.com/invopop/gobl/num" @@ -69,9 +70,9 @@ func creditNote() *bill.Invoice { IssueDate: cal.MakeDate(2022, 12, 29), Preceding: []*bill.Preceding{ { - Code: "TEST", - IssueDate: cal.NewDate(2022, 12, 27), - CorrectionMethod: co.CorrectionMethodKeyRevoked, + Code: "TEST", + IssueDate: cal.NewDate(2022, 12, 27), + Changes: []cbc.Key{co.CorrectionKeyRevoked}, }, }, Supplier: &org.Party{ @@ -156,12 +157,18 @@ func TestBasicCreditNoteValidation(t *testing.T) { require.NoError(t, err) err = inv.Validate() assert.NoError(t, err) - assert.Equal(t, inv.Preceding[0].CorrectionMethod, co.CorrectionMethodKeyRevoked) + assert.Contains(t, inv.Preceding[0].Changes, co.CorrectionKeyRevoked) - inv.Preceding[0].CorrectionMethod = "fooo" + inv.Preceding[0].Changes = []cbc.Key{co.CorrectionKeyDiscount, co.CorrectionKeyOther} err = inv.Validate() if assert.Error(t, err) { - assert.Contains(t, err.Error(), "method: must be a valid value") + assert.Contains(t, err.Error(), "changes: the length must be exactly 1.") + } + + inv.Preceding[0].Changes = []cbc.Key{"fooo"} + err = inv.Validate() + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "changes: (0: must be a valid value.)") } } diff --git a/regimes/es/corrections.go b/regimes/es/corrections.go index c8fff7ad..0e1222d1 100644 --- a/regimes/es/corrections.go +++ b/regimes/es/corrections.go @@ -35,14 +35,6 @@ const ( CorrectionKeyInsolvency cbc.Key = "insolvency" // the customer is insolvent and cannot pay ) -// List of correction methods derived from the Spanish FacturaE format. -const ( - CorrectionMethodKeyComplete cbc.Key = "complete" // everything has changed - CorrectionMethodKeyPartial cbc.Key = "partial" // only differences corrected - CorrectionMethodKeyDiscount cbc.Key = "discount" // deducted from future invoices - CorrectionMethodKeyAuthorized cbc.Key = "authorized" // Permitted by tax agency -) - // correctionList contains an array of Key Definitions describing each of the acceptable // correction keys, descriptions, and their "code" as determined by the FacturaE specifications. var correctionChangesList = []*tax.KeyDefinition{ @@ -224,41 +216,6 @@ var correctionChangesList = []*tax.KeyDefinition{ }, } -var correctionMethodList = []*tax.KeyDefinition{ - { - Key: CorrectionMethodKeyComplete, - Name: i18n.String{ - i18n.EN: "Complete", - i18n.ES: "Rectificaticón íntegra", - }, - Map: cbc.CodeMap{KeyFacturaE: "01"}, - }, - { - Key: CorrectionMethodKeyPartial, - Name: i18n.String{ - i18n.EN: "Corrected items only", - i18n.ES: "Rectificación por diferencias", - }, - Map: cbc.CodeMap{KeyFacturaE: "02"}, - }, - { - Key: CorrectionMethodKeyDiscount, - Name: i18n.String{ - i18n.EN: "Bulk deal in a given period", - i18n.ES: "Rectificación por descuento por volumen de operaciones durante un periodo", - }, - Map: cbc.CodeMap{KeyFacturaE: "03"}, - }, - { - Key: CorrectionMethodKeyAuthorized, - Name: i18n.String{ - i18n.EN: "Authorized by the Tax Agency", - i18n.ES: "Autorizadas por la Agencia Tributaria", - }, - Map: cbc.CodeMap{KeyFacturaE: "04"}, - }, -} - func correctionChangeKeys() []interface{} { keys := make([]interface{}, len(correctionChangesList)) i := 0 @@ -269,16 +226,4 @@ func correctionChangeKeys() []interface{} { return keys } -func correctionMethodKeys() []interface{} { - keys := make([]interface{}, len(correctionMethodList)) - i := 0 - for _, v := range correctionMethodList { - keys[i] = v.Key - i++ - } - return keys -} - var isValidCorrectionChangeKey = validation.In(correctionChangeKeys()...) - -var isValidCorrectionMethodKey = validation.In(correctionMethodKeys()...) diff --git a/regimes/es/es.go b/regimes/es/es.go index 118579af..2b27585a 100644 --- a/regimes/es/es.go +++ b/regimes/es/es.go @@ -89,8 +89,8 @@ func New() *tax.Regime { Schema: bill.ShortSchemaInvoice, Types: []cbc.Key{ bill.InvoiceTypeCorrective, + bill.InvoiceTypeCreditNote, }, - Methods: correctionMethodList, Changes: correctionChangesList, }, }, diff --git a/regimes/es/invoices.go b/regimes/es/invoices.go index 7008b259..6a41472d 100644 --- a/regimes/es/invoices.go +++ b/regimes/es/invoices.go @@ -97,10 +97,6 @@ func (v *invoiceValidator) preceding(value interface{}) error { validation.Required, validation.Each(isValidCorrectionChangeKey), ), - validation.Field(&obj.CorrectionMethod, - validation.Required, - isValidCorrectionMethodKey, - ), ) } diff --git a/tax/regime.go b/tax/regime.go index 2ebfa317..c5a3db0f 100644 --- a/tax/regime.go +++ b/tax/regime.go @@ -198,8 +198,6 @@ type CorrectionDefinition struct { Schema string `json:"schema" jsonschema:"title=Schema"` // The types of sub-documents supported by the regime Types []cbc.Key `json:"types,omitempty" jsonschema:"title=Types"` - // Methods describe the methods used to correct an invoice. - Methods []*KeyDefinition `json:"methods,omitempty" jsonschema:"title=Methods"` // List of change keys that can be used to describe what has been corrected. Changes []*KeyDefinition `json:"changes,omitempty" jsonschema:"title=Changes"` // ReasonRequired when true implies that a reason must be provided @@ -618,26 +616,12 @@ func (cd *CorrectionDefinition) HasChange(key cbc.Key) bool { return false } -// HasMethod returns true if the correction definition has the method provided. -func (cd *CorrectionDefinition) HasMethod(key cbc.Key) bool { - if cd == nil { - return false // no correction definitions - } - for _, kd := range cd.Methods { - if kd.Key == key { - return true - } - } - return false -} - // Validate ensures the key definition looks correct in the context of the regime. func (cd *CorrectionDefinition) Validate() error { err := validation.ValidateStruct(cd, validation.Field(&cd.Schema, validation.Required), validation.Field(&cd.Types), validation.Field(&cd.Stamps), - validation.Field(&cd.Methods), validation.Field(&cd.Changes), ) return err