Summary
In TicketBaiFactory.GetFacturaDetails, the foreign-customer branch filters charge items into two buckets — entregaChargeItems and prestacionServiciosChargeItems. ChargeItemCaseTypeOfService.OtherService is present in both filter lists, so any charge item whose TypeOfService() is OtherService is reported twice in the emitted XML: once under <Entrega> and once under <PrestacionServicios>. The same <BaseImponible> (or VAT base, or <Importe>) ends up appearing in both branches, which both inflates the totals AEAT cross-checks and produces an XML the recipient sees as two distinct operations.
Reproduction
Build a ProcessRequest with:
- A foreign
cbCustomer (e.g. CustomerCountry = \"FR\"),
- A single
ChargeItem with ftChargeItemCase carrying TypeOfService = OtherService (low byte 0x28) and any NatureOfVat,
and run TicketBaiFactory.ConvertTo. The produced <TipoDesglose><DesgloseTipoOperacion> has both an <Entrega> and a <PrestacionServicios> block, each containing the full charge-item amount.
Location
scu-es/src/fiskaltrust.Middleware.SCU.ES.TicketBAI.Common/TicketBaiFactory.cs, around the foreign-customer branch of GetFacturaDetails:
```csharp
var entregaChargeItems = new List {
ChargeItemCaseTypeOfService.UnknownService,
ChargeItemCaseTypeOfService.Delivery,
ChargeItemCaseTypeOfService.Voucher,
ChargeItemCaseTypeOfService.CatalogService,
ChargeItemCaseTypeOfService.NotOwnSales,
ChargeItemCaseTypeOfService.OtherService // ← also in prestacion list
};
var prestacionServiciosChargeItems = new List {
ChargeItemCaseTypeOfService.OtherService, // ← also in entrega list
ChargeItemCaseTypeOfService.Tip,
ChargeItemCaseTypeOfService.Grant,
ChargeItemCaseTypeOfService.Receivable,
ChargeItemCaseTypeOfService.CashTransfer
};
```
Pre-existing
This was introduced when the foreign-customer branch was first added in a1c14a66 "Support for foreign customers" and was preserved through the recent exempt-reasons refactor in #669 — the refactor passes the same filtered lists into the new BuildDesgloseParts helper.
Suggested fix
Decide which branch OtherService belongs in. The semantics in fiskaltrust.ifPOS.v2 are "a service of an unspecified kind", which strongly suggests PrestacionServicios (provision of services) — i.e. remove it from the entregaChargeItems list. If there are real use cases where an OtherService charge item should land in Entrega, the input model needs a more specific TypeOfService rather than overloaded routing.
Impact
Limited today because the TicketBaiFactory is itself only reachable for receipts that pass through the TicketBAI SCU (Araba / Bizkaia / Gipuzkoa), and the foreign-customer path is hit only when cbCustomer.CustomerCountry != \"ES\". But any foreign-customer invoice that uses OtherService charge items today produces a malformed AEAT submission, and the refactor in #669 makes the same shape produced for exempt cases too, so the doubling now also duplicates <CausaExencion> and <Causa> declarations — not just <DesgloseIVA>. Worth fixing before #669 ships.
Related
🤖 Filed via Claude Code on behalf of @StefanKert
Summary
In
TicketBaiFactory.GetFacturaDetails, the foreign-customer branch filters charge items into two buckets —entregaChargeItemsandprestacionServiciosChargeItems.ChargeItemCaseTypeOfService.OtherServiceis present in both filter lists, so any charge item whoseTypeOfService()isOtherServiceis reported twice in the emitted XML: once under<Entrega>and once under<PrestacionServicios>. The same<BaseImponible>(or VAT base, or<Importe>) ends up appearing in both branches, which both inflates the totals AEAT cross-checks and produces an XML the recipient sees as two distinct operations.Reproduction
Build a
ProcessRequestwith:cbCustomer(e.g.CustomerCountry = \"FR\"),ChargeItemwithftChargeItemCasecarryingTypeOfService = OtherService(low byte0x28) and anyNatureOfVat,and run
TicketBaiFactory.ConvertTo. The produced<TipoDesglose><DesgloseTipoOperacion>has both an<Entrega>and a<PrestacionServicios>block, each containing the full charge-item amount.Location
scu-es/src/fiskaltrust.Middleware.SCU.ES.TicketBAI.Common/TicketBaiFactory.cs, around the foreign-customer branch ofGetFacturaDetails:```csharp
var entregaChargeItems = new List {
ChargeItemCaseTypeOfService.UnknownService,
ChargeItemCaseTypeOfService.Delivery,
ChargeItemCaseTypeOfService.Voucher,
ChargeItemCaseTypeOfService.CatalogService,
ChargeItemCaseTypeOfService.NotOwnSales,
ChargeItemCaseTypeOfService.OtherService // ← also in prestacion list
};
var prestacionServiciosChargeItems = new List {
ChargeItemCaseTypeOfService.OtherService, // ← also in entrega list
ChargeItemCaseTypeOfService.Tip,
ChargeItemCaseTypeOfService.Grant,
ChargeItemCaseTypeOfService.Receivable,
ChargeItemCaseTypeOfService.CashTransfer
};
```
Pre-existing
This was introduced when the foreign-customer branch was first added in a1c14a66 "Support for foreign customers" and was preserved through the recent exempt-reasons refactor in #669 — the refactor passes the same filtered lists into the new
BuildDesglosePartshelper.Suggested fix
Decide which branch
OtherServicebelongs in. The semantics infiskaltrust.ifPOS.v2are "a service of an unspecified kind", which strongly suggestsPrestacionServicios(provision of services) — i.e. remove it from theentregaChargeItemslist. If there are real use cases where anOtherServicecharge item should land inEntrega, the input model needs a more specificTypeOfServicerather than overloaded routing.Impact
Limited today because the
TicketBaiFactoryis itself only reachable for receipts that pass through the TicketBAI SCU (Araba / Bizkaia / Gipuzkoa), and the foreign-customer path is hit only whencbCustomer.CustomerCountry != \"ES\". But any foreign-customer invoice that usesOtherServicecharge items today produces a malformed AEAT submission, and the refactor in #669 makes the same shape produced for exempt cases too, so the doubling now also duplicates<CausaExencion>and<Causa>declarations — not just<DesgloseIVA>. Worth fixing before #669 ships.Related
Delivery/Tipto demonstrate the intended split; comment in that test cross-references this issue.🤖 Filed via Claude Code on behalf of @StefanKert