Skip to content

Commit 80b9e31

Browse files
pmenendzclaude
andcommitted
Require es-tbai-v1 addon and drop legacy tag fallback
Gate Convert on the addon being declared and remove the TagReverseCharge fallback in the breakdown, since the addon normalizer now guarantees es-tbai-exemption is set on every combo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1161c55 commit 80b9e31

3 files changed

Lines changed: 13 additions & 36 deletions

File tree

convert/breakdown.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ type DetalleNoSujeta struct {
8989

9090
type taxInfo struct {
9191
simplifiedRegime bool
92-
reverseCharge bool
9392
customerRates bool
9493
}
9594

@@ -145,7 +144,7 @@ func newDesgloseFactura(taxInfo taxInfo, rates []*tax.RateTotal) *DesgloseFactur
145144
})
146145
} else {
147146
dne := df.Sujeta.NoExenta.appendDetalle(&DetalleNoExenta{
148-
TipoNoExenta: taxInfo.nonExemptedType(rate),
147+
TipoNoExenta: nonExemptedType(rate),
149148
DesgloseIVA: &DesgloseIVA{},
150149
})
151150

@@ -219,7 +218,7 @@ func (di *DesgloseIVA) appendDetalle(d *DetalleIVA) *DetalleIVA {
219218
}
220219

221220
func newDetalleIVA(taxInfo taxInfo, rate *tax.RateTotal) *DetalleIVA {
222-
percent := num.PercentageZero // S2 reverse-charge rates have no percent
221+
percent := num.PercentageZero
223222
if rate.Percent != nil {
224223
percent = *rate.Percent
225224
}
@@ -253,28 +252,22 @@ func formatPercent(percent num.Percentage) string {
253252
func newTaxInfo(gobl *bill.Invoice) taxInfo {
254253
return taxInfo{
255254
simplifiedRegime: gobl.HasTags(es.TagSimplifiedScheme),
256-
reverseCharge: gobl.HasTags(tax.TagReverseCharge),
257255
customerRates: gobl.HasTags(tax.TagCustomerRates),
258256
}
259257
}
260258

261-
// notSubjectExemptionCodes are the es-tbai-exemption values that the
262-
// es-tbai-v1 addon assigns to "outside-scope" tax combos; they map to
263-
// DetalleNoSujeta/Causa in the TicketBAI XML.
259+
// notSubjectExemptionCodes lists the es-tbai-exemption codes that map to
260+
// DetalleNoSujeta/Causa.
264261
var notSubjectExemptionCodes = []cbc.Code{"OT", "RL", "VT", "IE"}
265262

266-
// reverseChargeExemptionCodes are the es-tbai-exemption values that map
267-
// to DetalleNoExenta/TipoNoExenta = S2 (subject to VAT with reverse
268-
// charge) in the TicketBAI XML.
263+
// reverseChargeExemptionCodes lists the es-tbai-exemption codes that map to
264+
// DetalleNoExenta/TipoNoExenta = S2.
269265
var reverseChargeExemptionCodes = []cbc.Code{"S2"}
270266

271-
// nonExemptedType returns the TBAI TipoNoExenta value for a "subject and
272-
// non-exempt" tax rate. The new path consults the es-tbai-exemption
273-
// extension set by the es-tbai-v1 addon normalizer; the legacy fallback
274-
// relies on the invoice-wide reverse-charge tag for callers that have
275-
// not been normalized through the addon.
276-
func (t taxInfo) nonExemptedType(r *tax.RateTotal) string {
277-
if t.reverseCharge || r.Ext.Get(tbai.ExtKeyExempt).In(reverseChargeExemptionCodes...) {
267+
// nonExemptedType returns the TBAI TipoNoExenta value for a subject,
268+
// non-exempt tax rate.
269+
func nonExemptedType(r *tax.RateTotal) string {
270+
if r.Ext.Get(tbai.ExtKeyExempt).In(reverseChargeExemptionCodes...) {
278271
return "S2"
279272
}
280273
return "S1"

convert/breakdown_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -449,23 +449,6 @@ func TestDesgloseConversion(t *testing.T) {
449449
assert.Equal(t, "S", desgloseIVA.DetalleIVA[0].OperacionEnRecargoDeEquivalenciaORegimenSimplificado)
450450
})
451451

452-
t.Run("should mark tax details if there is reverse charge", func(t *testing.T) {
453-
goblInvoice := invoiceFromCountry("ES")
454-
goblInvoice.SetTags(tax.TagReverseCharge)
455-
goblInvoice.Lines = []*bill.Line{{
456-
Index: 1,
457-
Quantity: num.MakeAmount(100, 0),
458-
Item: &org.Item{Name: "A", Price: num.NewAmount(10, 0)},
459-
Taxes: tax.Set{&tax.Combo{Category: tax.CategoryVAT, Rate: "standard"}},
460-
}}
461-
_ = goblInvoice.Calculate()
462-
463-
invoice, _ := convert.NewTicketBAI(goblInvoice, ts, role, convert.ZoneBI)
464-
465-
desglose := invoice.Factura.TipoDesglose.DesgloseFactura
466-
assert.Equal(t, "S2", desglose.Sujeta.NoExenta.DetalleNoExenta[0].TipoNoExenta)
467-
})
468-
469452
t.Run("should route es-tbai-exemption=S2 to Sujeta.NoExenta with TipoNoExenta=S2", func(t *testing.T) {
470453
goblInvoice := invoiceFromCountry("ES")
471454
goblInvoice.Lines = []*bill.Line{{
@@ -499,8 +482,6 @@ func TestDesgloseConversion(t *testing.T) {
499482
})
500483

501484
t.Run("should route es-tbai-exemption=S2 set via extension only to Sujeta.NoExenta", func(t *testing.T) {
502-
// Caller sets the exemption code directly via the extension;
503-
// the es-tbai-v1 addon normalizer maps it back to KeyReverseCharge.
504485
goblInvoice := invoiceFromCountry("ES")
505486
goblInvoice.Lines = []*bill.Line{{
506487
Index: 1,

document.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ func (c *Client) Convert(env *gobl.Envelope) (*convert.TicketBAI, error) {
2727
if inv.Supplier.TaxID.Country != l10n.ES.Tax() {
2828
return nil, ErrValidation.withMessage("only spanish invoices are supported")
2929
}
30+
if !tbai.V1.In(inv.GetAddons()...) {
31+
return nil, ErrValidation.withMessage("invoice must declare the %s addon", tbai.V1)
32+
}
3033
if inv.Totals == nil || inv.Totals.Taxes == nil {
3134
return nil, ErrValidation.withMessage("missing taxes")
3235
}

0 commit comments

Comments
 (0)