VAT
Every line, allowance and charge has a VAT category (UNCL5305), and the invoice sums them in a VAT breakdown (BG-23). The builder computes the breakdown; you add what it cannot know, such as why a category is exempt. The snippets change the one line of the base invoice, which is standard rated (S, 25 %).
The snippets share one helper: inCategory() returns the base invoice with its line moved to another category, plus whatever else that category needs, and buyerWithVat adds the buyer's VAT identifier.
// The base invoice with its one line moved to another VAT category, plus whatever else the
// category needs (`rest`).
const [baseLine] = baseInvoice.invoiceLine;
if (baseLine === undefined) {
throw new Error('baseInvoice has a line');
}
const line: InvoiceLineInput = baseLine;
function inCategory(
classifiedTaxCategory: InvoiceLineInput['item']['classifiedTaxCategory'],
rest: Partial<InvoiceInput> = {},
) {
return createInvoice({
...baseInvoice,
...rest,
invoiceLine: [{ ...line, item: { ...line.item, classifiedTaxCategory } }],
});
}
// Reverse charge and intra-community supplies also need the buyer's VAT identifier.
const buyerWithVat: InvoiceInput['accountingCustomerParty'] = {
party: {
...baseInvoice.accountingCustomerParty.party,
partyTaxScheme: { companyId: 'SE4598375937' },
},
};Zero rated and exempt (Z, E)
A zero rated line has rate 0. An exempt one also needs the exemption reason (BT-120 or BT-121), which lives on the VAT breakdown line: state that one taxSubtotal without amounts.
const zeroRated = inCategory({ id: 'Z', percent: 0 });
const exempt = inCategory(
{ id: 'E', percent: 0 },
{
// Say why in the VAT breakdown line; the builder still computes its amounts.
taxTotal: [
{
taxSubtotal: [
{
taxCategory: {
id: 'E',
percent: 0,
taxExemptionReasonCode: 'VATEX-EU-132',
taxExemptionReason: 'Exempt: medical care',
},
},
],
},
],
},
);Computed: taxableAmount and taxAmount of the stated breakdown line, and a breakdown line for any other category the invoice uses. Exemption reason codes come from the VATEX list.
Reverse charge (AE)
Reverse charge needs the VAT identifiers of both the seller and the buyer, and a reason.
const reverseCharge = inCategory(
{ id: 'AE', percent: 0 },
{
accountingCustomerParty: buyerWithVat, // both VAT ids are required
taxTotal: [
{
taxSubtotal: [
{
taxCategory: {
id: 'AE',
percent: 0,
taxExemptionReasonCode: 'VATEX-EU-AE',
taxExemptionReason: 'Reverse charge',
},
},
],
},
],
},
);Intra-community supply (K)
An intra-community supply also needs the delivery date (BT-72) or an invoicing period, and the country it is delivered to (BT-80).
const intraCommunity = inCategory(
{ id: 'K', percent: 0 },
{
accountingCustomerParty: buyerWithVat,
delivery: {
actualDeliveryDate: '2017-11-01',
deliveryLocation: {
address: { cityName: 'Stockholm', country: { identificationCode: 'SE' } },
},
},
taxTotal: [
{
taxSubtotal: [
{
taxCategory: {
id: 'K',
percent: 0,
taxExemptionReasonCode: 'VATEX-EU-IC',
taxExemptionReason: 'Intra-community supply',
},
},
],
},
],
},
);Export outside the EU (G)
An export outside the EU has rate 0. It needs the seller's VAT identifier and an exemption reason, such as VATEX-EU-G.
const exported = inCategory(
{ id: 'G', percent: 0 },
{
taxTotal: [
{
taxSubtotal: [
{
taxCategory: {
id: 'G',
percent: 0,
taxExemptionReasonCode: 'VATEX-EU-G',
taxExemptionReason: 'Export outside the EU',
},
},
],
},
],
},
);Computed: the breakdown line's amounts, as for the other exempt categories.
Not subject to VAT (O)
A line outside the scope of VAT has no rate, the seller and buyer state no VAT identifier, and an invoice with category O has no other category. Identify the seller by another identifier instead.
const outsideScope = inCategory(
{ id: 'O' }, // no rate
{
// A seller outside the scope of VAT states no VAT identifier, so identify it by its registration.
accountingSupplierParty: {
party: {
endpointId: { schemeId: '0088', value: '7300010000001' },
postalAddress: { cityName: 'London', country: { identificationCode: 'GB' } },
partyLegalEntity: { registrationName: 'SupplierOfficialName Ltd', companyId: 'GB983294' },
},
},
taxTotal: [
{
taxSubtotal: [
{
taxCategory: {
id: 'O',
taxExemptionReasonCode: 'VATEX-EU-O',
taxExemptionReason: 'Not subject to VAT',
},
},
],
},
],
},
);Canary Islands and Ceuta and Melilla (L, M)
IGIC (L) and IPSI (M) work like standard rated VAT, with their own rates.
const canaryIslands = inCategory({ id: 'L', percent: 7 }); // IGIC
const ceutaMelilla = inCategory({ id: 'M', percent: 4 }); // IPSIYour own VAT breakdown
State the amounts yourself when your system has already computed them. The builder keeps them as written and computes only the document totals.
const statedBreakdown = createInvoice({
...baseInvoice,
taxTotal: [
{
taxAmount: 700,
taxSubtotal: [{ taxableAmount: 2800, taxAmount: 700, taxCategory: { id: 'S', percent: 25 } }],
},
],
});VAT in another currency
When VAT is accounted in a currency other than the invoice currency, set taxCurrencyCode (BT-6) and add a second tax total with the VAT amount in that currency (BT-111).
const withTaxCurrency = createInvoice({
...baseInvoice,
taxCurrencyCode: 'SEK',
taxTotal: [
{}, // computed, in EUR
{ taxAmount: { value: 7350, currencyId: 'SEK' } }, // BT-111: stated, in SEK
],
});Computed: only the first tax total. The builder has no exchange rate, so it throws a TypeError when the second one has no taxAmount.