@facturino/node 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,47 +13,528 @@ npm install @facturino/node
13
13
 
14
14
  ## Usage
15
15
 
16
+ The recommended path is decision-first: identity → final tax decision →
17
+ create the decision-backed draft immediately → your chosen collection flow.
18
+ Facturino imposes no payment service provider and no payment method: an
19
+ immediate capture, a bank transfer, a direct debit or payment on agreed terms
20
+ all fit the same contract.
21
+
16
22
  ```typescript
17
23
  import Facturino from '@facturino/node'
18
24
 
19
25
  const facturino = new Facturino('fac_test_xxx')
20
26
 
27
+ // 1. Decide before the final amount is presented, the invoice is issued,
28
+ // or collection starts.
29
+ const decision = await facturino.taxDecisions.create({
30
+ taxSource: 'facturino', // or 'integration' to supply your own VAT
31
+ customerId: 'cus_8f2k4m9n',
32
+ effectiveAt: '2026-09-15',
33
+ currency: 'eur',
34
+ priceMode: 'tax_exclusive',
35
+ lines: [{
36
+ reference: 'abo-pro',
37
+ description: 'Abonnement Pro',
38
+ category: 'electronically_supplied_services',
39
+ rateCategory: 'standard',
40
+ unitAmount: 2900, // integer cents
41
+ quantity: '1', // decimal STRING, never a float
42
+ }],
43
+ }, { idempotencyKey: `order-${orderId}` })
44
+
45
+ // 2. Act only on a final decision. `pending_verification` does not mean
46
+ // "nothing to charge": totals and amountToCharge are null, not 0.
47
+ if (decision.status !== 'final') {
48
+ return askForMissingEvidence(decision.issues)
49
+ }
50
+
51
+ // 3. Create the decision-backed draft immediately: no VAT is restated,
52
+ // the amounts are the decision's.
21
53
  const invoice = await facturino.invoices.create({
54
+ customerId: decision.customerId,
55
+ taxDecisionId: decision.id,
56
+ decisionLines: [{ taxLineRef: 'abo-pro', unit: 'month' }],
57
+ buyer: buyerSnapshot,
58
+ dates: { issued: '2026-09-15', due: '2026-10-15' },
59
+ payment: paymentTerms,
60
+ })
61
+
62
+ // 4. Choose your collection flow — see the two variants below.
63
+ ```
64
+
65
+ **Immediate collection** — capture the decided amount, verify, then finalize:
66
+
67
+ ```typescript
68
+ // Capture exactly amountToCharge through your payment provider, payment
69
+ // processor, bank transfer or external collection flow. Carry `decision.id`
70
+ // in the provider metadata, order reference or custom reference. The
71
+ // settlement keeps its OWN financial reference (charge id, transfer
72
+ // wording…): the two identifiers are different things and must stay distinct.
73
+ const settlement = await yourCollectionProcess.capture({
74
+ amount: decision.amountToCharge!, // never a locally computed total
75
+ currency: decision.currency,
76
+ metadata: { taxDecisionId: decision.id },
77
+ })
78
+
79
+ // Re-read the decision by its own id and verify what was actually captured.
80
+ const source = await facturino.taxDecisions.retrieve(decision.id)
81
+ if (settlement.amount !== source.amountToCharge) throw new Error('amount mismatch')
82
+ if (settlement.currency !== source.currency) throw new Error('currency mismatch')
83
+
84
+ await facturino.invoices.finalize(invoice.id)
85
+
86
+ // Record the REAL payment — its real date, method and the settlement's
87
+ // financial reference (never the decision id).
88
+ await facturino.payments.create(invoice.id, {
89
+ amount: settlement.amount,
90
+ // transfer, card, check, cash, direct_debit, sepa, paypal or other
91
+ method: settlement.method,
92
+ reference: settlement.reference,
93
+ paidAt: settlement.paidAt,
94
+ })
95
+
96
+ // Send to the platform only on the channel the FROZEN decision states.
97
+ if (source.invoiceChannel === 'einvoicing') {
98
+ await facturino.invoices.send(invoice.id)
99
+ }
100
+ ```
101
+
102
+ **Payment on terms** — finalize and deliver now, collect later:
103
+
104
+ ```typescript
105
+ await facturino.invoices.finalize(invoice.id)
106
+ if (decision.invoiceChannel === 'einvoicing') {
107
+ await facturino.invoices.send(invoice.id)
108
+ }
109
+
110
+ // …once the transfer arrives, record the REAL collection date.
111
+ await facturino.payments.create(invoice.id, {
112
+ amount: decision.amountToCharge!,
113
+ method: 'transfer',
114
+ reference: 'VIR-2026-000871',
115
+ paidAt: '2026-10-12',
116
+ })
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ ```typescript
122
+ const facturino = new Facturino('fac_test_xxx', {
123
+ maxRetries: 3, // retries on 429/5xx
124
+ timeout: 30000, // ms
125
+ apiVersion: '2026-09-01',
126
+ })
127
+ ```
128
+
129
+ ## Tax decisions
130
+
131
+ The full walkthrough lives in [Usage](#usage). A decision is immutable: it
132
+ fixes the VAT, the exact `amountToCharge` and the reporting obligations of one
133
+ commercial operation, then never changes. Only a `final` decision carries
134
+ amounts, and the amount always comes from the decision — never from a locally
135
+ computed total.
136
+
137
+ ### Optional: carrying the decision id through a PSP
138
+
139
+ These are examples, not requirements. If you collect through a PSP, keep the
140
+ decision id on the payment so step 4 can verify what was actually captured.
141
+
142
+ Stripe — any field that survives the round trip works; `metadata` is the usual one:
143
+
144
+ ```typescript
145
+ const intent = await stripe.paymentIntents.create({
146
+ amount: decision.amountToCharge!,
147
+ currency: decision.currency,
148
+ metadata: { facturino_tax_decision_id: decision.id },
149
+ })
150
+ ```
151
+
152
+ PayPal has no `metadata`; carry the decision id in `custom_id`, and convert the
153
+ cents to decimal units for the order amount:
154
+
155
+ ```typescript
156
+ custom_id: decision.id,
157
+ amount: {
158
+ currency_code: decision.currency.toUpperCase(),
159
+ value: (decision.amountToCharge! / 100).toFixed(2),
160
+ },
161
+ ```
162
+
163
+ ### What a decision states
164
+
165
+ | Field | Meaning |
166
+ |---|---|
167
+ | `status` | `final`, `pending_verification` or `unsupported`. Only `final` carries amounts. |
168
+ | `amountToCharge` | Exact amount to debit, integer cents. `null` unless `final`. |
169
+ | `totals` | `totalHT` / `totalVAT` / `totalTTC`, integer cents. `null` unless `final`. |
170
+ | `invoiceChannel` | `einvoicing` or `none` — whether the invoice travels the network. |
171
+ | `transactionReporting` | `ereporting`, `none` or `outside_scope`. |
172
+ | `paymentReporting` | `fr212`, `ereporting` or `none`. |
173
+ | `settledObligations` | The axes French law settles DESPITE a non-final decision. `null` when final — the three axes above are then the settled ones. Each axis inside is `null` when it depends on the treatment that could not be concluded. It authorises nothing. |
174
+ | `foreignTaxReviewRequired` | A foreign tax may apply; review it outside Facturino. |
175
+ | `vies` | VIES status only (`valid`, `invalid`, `unavailable`, `invalid_format`). |
176
+ | `issues` | What is missing, when the decision is not final. |
177
+ | `obligationReasons` | Why each axis carries the obligation it does. |
178
+ | `expiresAt` / `expired` | Past this instant the decision no longer opens a payment. |
179
+
180
+ `create()` requires an `Idempotency-Key` (255 characters at most; the SDK checks
181
+ it before sending). The API answers `201` on creation and `200` when the same
182
+ key already produced that decision — both return the decision, so your code
183
+ reads one shape either way. Reusing the same key with a different body answers
184
+ `409` and raises `ConflictError`.
185
+
186
+ Facturino decides **French VAT and the matching French obligations**. It does
187
+ not provide worldwide tax compliance: when a foreign tax may apply, the decision
188
+ says so through `foreignTaxReviewRequired`. An operation whose `invoiceChannel`
189
+ is `none` is not deposited on a certified platform — its obligation, if any,
190
+ goes through e-reporting.
191
+
192
+ ### Missing evidence, then a retry
193
+
194
+ A decision that lacks a location or business-status proof comes back
195
+ `pending_verification`. Supply the evidence and retry the SAME operation:
196
+
197
+ ```typescript
198
+ const retried = await facturino.taxDecisions.create({
199
+ ...sameOperation,
200
+ retryOfTaxDecisionId: pending.id,
201
+ locationEvidence: [{
202
+ kind: 'billing_address',
203
+ country: 'FR',
204
+ postalCode: '75002',
205
+ thirdParty: false,
206
+ source: 'declared',
207
+ collectedAt: '2026-09-15',
208
+ }],
209
+ }, { idempotencyKey: `order-${orderId}-retry-${pending.id}` })
210
+ ```
211
+
212
+ Send the territorial **signal**, never the raw one: a country and, where the
213
+ territory needs it, a postal code — not an IP address, a PSP payload or bank
214
+ account details. `reference` is a bounded opaque identifier such as a charge id.
215
+
216
+ ## Three status axes
217
+
218
+ A document has three states that do not follow from one another. The historical
219
+ `status` field stays populated as their projection.
220
+
221
+ ```typescript
222
+ invoice.documentStatus // draft | finalized | cancelled
223
+ invoice.transmissionStatus // not_applicable | pending | sending | deposited | transmitted | approved | rejected
224
+ invoice.transmissionDetail // available | received | suspended | refused | null
225
+ invoice.paymentStatus // unpaid | partially_paid | paid | partially_refunded | refunded
226
+ ```
227
+
228
+ Recording a payment never moves the transmission axis, and a refund does not
229
+ erase the collection that happened.
230
+
231
+ ## Supplying your own VAT (`taxSource: 'integration'`)
232
+
233
+ If your own tax engine concludes the VAT, create the decision under the
234
+ `integration` source: the same commercial and territorial data, plus the VAT
235
+ per line (`vatRate`, `vatCode`, and `vatexCode` for exempt categories).
236
+ Facturino validates the coherence of the supplied values and refuses any
237
+ detectable contradiction (`integration_vat_incoherent`) — it never silently
238
+ corrects a rate. The amounts, the legal mentions and the three reporting axes
239
+ are still decided server-side, by the same engines. The invoice is then created
240
+ exactly like a facturino-sourced one, and carries `taxSource: 'integration'`.
241
+
242
+ ```typescript
243
+ const decision = await facturino.taxDecisions.create({
244
+ taxSource: 'integration',
22
245
  customerId: 'cus_xxx',
23
- buyer: {
24
- companyName: 'Acme SAS',
25
- siret: '55208131766522',
26
- address: { line1: '10 rue de la Paix', postalCode: '75002', city: 'Paris', country: 'FR' },
27
- },
246
+ effectiveAt: '2026-09-15',
247
+ currency: 'eur',
248
+ priceMode: 'tax_exclusive',
28
249
  lines: [{
250
+ reference: 'consulting',
29
251
  description: 'Consulting',
252
+ category: 'services',
253
+ unitAmount: 10000, // 100.00 EUR (integer cents)
30
254
  quantity: '1', // decimal string
31
- unit: 'flat_rate',
32
- unitPrice: 10000, // 100.00 EUR (centimes)
33
- vatRate: 2000, // 20.00% (centièmes de pourcent)
255
+ vatRate: 2000, // 20.00% — supplied, never corrected
34
256
  vatCode: 'S',
35
257
  }],
36
- dates: { issued: '2026-07-01', due: '2026-07-31' },
258
+ }, { idempotencyKey: `order-${orderId}` })
259
+
260
+ const invoice = await facturino.invoices.create({
261
+ customerId: 'cus_xxx',
262
+ taxDecisionId: decision.id,
263
+ decisionLines: [{ taxLineRef: 'consulting', unit: 'flat_rate' }],
264
+ buyer: buyerSnapshot,
265
+ dates: { issued: '2026-09-15', due: '2026-10-15' },
37
266
  payment: { terms: 'Paiement à 30 jours', termsDays: 30, method: 'transfer', latePaymentRate: '10.00', collectionFee: '40.00' },
38
267
  })
39
268
 
40
269
  const finalized = await facturino.invoices.finalize(invoice.id)
41
270
 
42
271
  // One-shot: pass `autoFinalize: true` (and optionally
43
- // `autoSend: { email: true, pa: true }`) to `invoices.create(...)` to
44
- // finalize — and deliver by email and/or to the PA — in a single call.
272
+ // `autoSend: { email: true }`) to finalize — and deliver by email — in a
273
+ // single call.
45
274
  ```
46
275
 
47
- ## Configuration
276
+ ### What this source is not
277
+
278
+ `vatRate`, `vatCode` and `vatexCode` describe **French VAT**. The contract has no
279
+ local-tax jurisdiction, no local tax scheme and no withholding, so this source is
280
+ not a way to pass one through.
281
+
282
+ Where a local tax of a French overseas collectivity or the TAAF (Saint-Pierre-et-
283
+ Miquelon, Saint-Barthélemy, Saint-Martin, French Polynesia, New Caledonia,
284
+ Wallis-and-Futuna, TAAF) can change what you invoice — or what you actually
285
+ collect — the decision is **not final under either source**, with the same issue
286
+ code and no amount:
287
+
288
+ | Issue code | When |
289
+ | --- | --- |
290
+ | `com_taaf_local_tax_not_determined` | Non-taxable buyer, operation located in the collectivity (electronically supplied service, CGI art. 259 D). |
291
+ | `com_taaf_local_regime_not_sourced` | Taxable buyer, but no official act of the collectivity states who bears its tax. |
292
+ | `com_taaf_payment_withholding_not_modelled` | French Polynesia: the client withholds part of the payment at source. |
293
+ | `seller_com_taaf_local_tax_not_determined` | The seller itself is established in one of the seven. |
294
+
295
+ The one sourced exception stays final under both sources: a B2B service located
296
+ in **New Caledonia** supplied by a seller **not established in New Caledonia**,
297
+ where art. Lp. 507-1 makes the taxable customer account for the taxe générale sur
298
+ la consommation itself. That article reaches only a supplier established outside
299
+ the territory. A seller established in New Caledonia is the ordinary collector of
300
+ the taxe générale sur la consommation on its own sales, at a rate this contract
301
+ does not carry, so its decision is not final either
302
+ (`seller_com_taaf_local_tax_not_determined`).
303
+
304
+ Because of this, `placeOfSupply` is **required** on every line as soon as the
305
+ buyer is established in one of the seven — the place of the operation is what
306
+ says whether the local tax is at stake, and it is never assumed
307
+ (`422 integration_vat_incoherent` otherwise). A place located in France (goods
308
+ that never leave the territory, a general B2C service) keeps the decision final
309
+ as anywhere else.
310
+
311
+ ## B2C sales to consumers in other member states
312
+
313
+ An **electronically supplied service** to a consumer established in another
314
+ member state (Directive 2006/112/EC art. 58) and an **intra-EU distance sale** of
315
+ goods (art. 33(a)) follow one common regime. A **general** B2C service does not:
316
+ it stays taxed where the supplier is established (art. 45), and nothing below
317
+ concerns it.
318
+
319
+ Four questions are answered separately, in this order. Collapsing any two of them
320
+ produces a wrong rate:
321
+
322
+ 1. is the operation covered by a destination rule;
323
+ 2. does the common **EUR 10,000** threshold still allow taxation at origin
324
+ (art. 59c(1));
325
+ 3. did the seller **opt** for taxation at destination (art. 59c(3));
326
+ 4. how is the tax due at destination **declared** — Union one-stop shop, or a
327
+ local VAT registration in that member state?
328
+
329
+ The one-stop shop answers the **last** of the four: it is a way of declaring and
330
+ paying a tax, not a rule of place. Not registering never restores the seller's
331
+ own national VAT — it leaves the decision without an amount.
332
+
333
+ That does not make the questions watertight in fact. For a **French** seller,
334
+ registering for the Union scheme is how the option of art. 59c(3) is exercised:
335
+ an **active** registration therefore settles the place at destination on its own,
336
+ and the threshold has nothing left to decide (`basis: "oss_union_registration"`,
337
+ `threshold: null`). The registration is dated — one opened in October decides
338
+ nothing for a September sale, and one that ended decides nothing any more. It is
339
+ sourced for France only: the way the option is exercised is fixed by the member
340
+ state where it is exercised, so a seller established elsewhere keeps the ordinary
341
+ threshold path and states its option explicitly.
342
+
343
+ The threshold is an **inclusive** cap of `1000000` centimes excluding VAT, open
344
+ only to a seller established in a **single** member state; the operation that
345
+ carries the running total past it is itself taxed at destination.
346
+
347
+ That running total lives in an **annual ledger** — `/v1/eu-threshold-ledgers`,
348
+ one per company, per mode and per calendar year — and NOT on the fiscal profile.
349
+ A profile revision is an immutable rule that decisions freeze; a turnover total
350
+ moves with every sale and gets corrected, so the two are kept apart. Facturino
351
+ keeps the register of the operations it receives; the sales made on your other
352
+ channels must be brought in by an adjustment, or by the opening declaration:
353
+
354
+ - **opening a year** declares four figures — the previous year's total and the
355
+ total already made this year, each with its *services* part (see the two
356
+ counters below) — plus the coverage mode: `facturino_only` (every covered sale
357
+ goes through Facturino) or `mixed_channels`;
358
+ - **an adjustment** adds the turnover of another channel and moves forward the
359
+ day those channels are declared complete through. Under `mixed_channels` a
360
+ decision is served only up to that day.
361
+
362
+ **Two counters, strictly apart — and independent.** The same movements feed two
363
+ thresholds that do not measure the same thing: the common EUR 10,000 threshold
364
+ above, and the EUR 100,000 threshold of Reg. 282/2011 art. 24b that governs how
365
+ many items of location evidence are required. The second counts only telecom,
366
+ broadcasting and electronically supplied services, **domestic ones included**;
367
+ the first counts only **cross-border** supplies. A distance sale of goods raises
368
+ the first and never the second — and a domestic electronic service raises the
369
+ second and never the first.
370
+
371
+ Neither bounds the other, in either direction: a publisher selling mostly at home
372
+ legitimately declares far more on the evidence counter than on the common one.
373
+ That is why every figure comes in a pair (`amount` / `evidenceAmount`,
374
+ `acquiredMin` / `acquiredEvidenceMin`, …) rather than as a total and a share of
375
+ it, and why the single-evidence relaxation is **computed by the engine** on that
376
+ second counter rather than declared by the seller.
377
+
378
+ **Acquired and reserved are published apart, and never summed.** `acquiredMin`
379
+ is what the year has certainly made; `reservedMin` is the slices held right now
380
+ by operations still being decided. A held slice may still disappear, and one
381
+ combined "total" would hide exactly that.
382
+
383
+ Nothing is assumed: no year starts at zero on its own, and no sale made elsewhere
384
+ is presumed absent. A decision reserves its slice of the total in a transaction
385
+ and consumes it with the decision itself, so two concurrent operations never read
386
+ the same figure as certain and a replay counts nothing twice. A verdict is frozen
387
+ only when it holds at **both** bounds — with every concurrent slice counted and
388
+ with none of them — which is what makes an abandoned operation simply disappear
389
+ instead of staying in the total as turnover that never existed.
390
+
391
+ **Giving an amount back is a qualified correction, never a negative
392
+ adjustment.** Directive 2006/112/EC art. 90(1) reduces the taxable amount of a
393
+ supply on cancellation, refusal or a price reduction after the supply, and the
394
+ thresholds count the VALUE of the supplies — so a correction names the movement
395
+ it corrects, its qualification, the resource it rests on and its evidence.
396
+
397
+ A movement gives back what it brought in **once**, whatever the number of
398
+ corrections: the ledger keeps each movement's balance inside the same
399
+ transaction, and every entry publishes its `remainingMin`. `correctsEntryId` is
400
+ restricted to the ids the ledger itself mints — it reaches a document path, and
401
+ free text must not.
402
+
403
+ What cannot be qualified that way is not subtracted at all: the ledger goes
404
+ **under review** and stops deciding, rather than freeze verdicts on a total
405
+ nobody stands behind. A review is settled by **reconciliation**, never by a
406
+ comment: you state the version you checked and the two acquired totals you
407
+ verified, and only an exact agreement reopens the ledger.
408
+
409
+ **A decision is never frozen without its slice.** If the slice it held has
410
+ disappeared when the decision is about to be written, the transaction is
411
+ abandoned — no decision, no audit entry, no settled claim
412
+ (`409 eu_threshold_reservation_lost`) — and the ledger goes under review on a
413
+ path of its own, so the review survives that abandonment. Freezing a decision the
414
+ running total does not carry would leave the sale inside a decision and outside
415
+ the year the next operation reads.
416
+
417
+ Movements are paginated with a cursor (`limit`, `starting_after`): the ledger
418
+ keeps them all, a page shows some.
419
+
420
+ | Issue code | When |
421
+ | --- | --- |
422
+ | `eu_threshold_state_missing` | No ledger is open for the year of the operation. Open it: nothing is assumed to be zero. |
423
+ | `eu_threshold_external_coverage_incomplete` | Other channels exist and are not declared complete through the operation date. Record an adjustment — even a zero one, which simply states that nothing happened. |
424
+ | `eu_threshold_backdated_operation` | The operation predates one already counted, and decisions were frozen on that running total. It is refused rather than silently recomputed. |
425
+ | `eu_threshold_concurrent_decision_pending` | Other operations of the company are being decided at this very moment, and the cap falls between "all of them confirmed" and "all abandoned". Nothing is frozen on that: decide again once they conclude. |
426
+ | `eu_threshold_review_required` | The ledger is under review — its running total is known to be wrong, and no verdict rests on it until the review is settled. |
427
+ | `location_evidence_relief_undetermined` | One third-party item of evidence, and the art. 24b relaxation could not be established: open the year's ledger and declare the services figures, or supply a second item. |
428
+ | `eu_threshold_reservation_lost` | The slice this decision held is gone. The decision is refused rather than frozen without it, and the ledger goes under review. |
429
+ | `destination_threshold_operation_value_missing` | A line value cannot be sized, so no slice of the total can be taken for it. |
430
+ | `destination_threshold_price_mode_ambiguous` | Tax-inclusive price: the VAT-exclusive value depends on the rate the threshold has to decide, and the bounds fall on both sides of the cap. |
431
+ | `destination_option_period_invalid` | The option is declared over less than its minimum binding period. |
432
+ | `destination_option_scope_not_sourced` | Seller established outside France: the binding period is fixed by the member state where the option is exercised, and only the French one is sourced here. |
433
+ | `destination_establishment_in_member_state` | The seller declares an establishment in the destination member state: which establishment supplies then decides both the place and the mechanism, and no fact of the contract names it. A local VAT registration alone is not an establishment. |
434
+ | `destination_regional_scope_undetermined` | The member state publishes regional standard rates and the address places none of them — state the customer's postal code. |
435
+ | `destination_mechanism_missing` | Destination taxation is due with neither the Union scheme nor a local registration valid for that state on that date. |
436
+ | `franchise_destination_taxation_not_modelled` | Seller under the French small-enterprise exemption whose operation is taxed by another member state. |
437
+
438
+ Rates come from a **local, dated, versioned registry**: no network call during a
439
+ decision, and a decision replayed years later reproduces the same rate. A rate
440
+ change is a new period, never a rewrite. Only the **standard** rate of the 27
441
+ member states is tabulated — the scope of the reduced rates follows a national
442
+ classification this contract does not hold, and an ordinary electronically
443
+ supplied service is never granted the rate an electronic publication may benefit
444
+ from (`destination_rate_band_not_available`). An effect date before the registry
445
+ answers `destination_rate_not_sourced_for_date`.
446
+
447
+ A **region publishing its own standard rate** never blocks a whole member state.
448
+ What governs the region decides the answer:
449
+
450
+ - **the rate follows the place of the operation.** The region is then a territory
451
+ of its own and the address decides: Portugal mainland 23%, Madeira 22%, Azores
452
+ 16% (CIVA art. 18, CTT postal ranges). Only an address placing no region at all
453
+ is refused — `destination_regional_scope_undetermined`, naming the missing fact
454
+ rather than falling back on the mainland rate;
455
+ - **the rate is reserved to operations carried out in the zone by a supplier
456
+ established there.** A supplier at distance does not acquire it from the
457
+ consumer's address, so the national rate is the final answer — and no postal
458
+ cartography of the zone is needed to say so: Austrian Jungholz and
459
+ Kleinwalsertal (§ 10(4) UStG — 20%, not 19%), and the Greek island regime FOR
460
+ SERVICES, which AADE reserves to a supplier established on the island for an
461
+ operation performed there. An electronically supplied service from France is
462
+ therefore taxed at 24% in Greece, in Athens and in Kalymnos alike;
463
+ - **the rate follows the destination and the zone is not cartographied here.**
464
+ Neither the regional rate nor the national one can be asserted, so the
465
+ operation is refused: `destination_regional_regime_not_sourced`, non-final and
466
+ without an amount. This is Greece FOR GOODS: since 2026-01-01 the islands of
467
+ fewer than 20,000 inhabitants apply a reduced standard rate to the goods
468
+ delivered there, intra-community acquisitions included, and the list of those
469
+ islands is not cartographied by postal code here. An intra-EU distance sale of
470
+ goods to Greece is therefore refused — in Athens as in Kalymnos — and never
471
+ taxed at 24% by default. The answer is given per FAMILY of operation: the same
472
+ member state can be settled for services and left open for goods.
473
+
474
+ ### The annual ledger
48
475
 
49
476
  ```typescript
50
- const facturino = new Facturino('fac_test_xxx', {
51
- maxRetries: 3, // retries on 429/5xx
52
- timeout: 30000, // ms
53
- apiVersion: '2026-03-01',
477
+ // Open the year nothing starts at zero on its own.
478
+ await facturino.euThresholdLedgers.open({
479
+ year: '2026',
480
+ previousYearAmount: 250000, // cents, VAT excluded
481
+ currentYearOpening: 100000,
482
+ coverageMode: 'mixed_channels', // other channels exist
483
+ externalCompleteThroughDate: '2026-01-01',
484
+ })
485
+
486
+ // Bring in what was sold elsewhere. Append-only, idempotent on `reference`.
487
+ await facturino.euThresholdLedgers.adjust('2026', {
488
+ reference: 'marketplace-2026-08',
489
+ amount: 40000,
490
+ externalCompleteThroughDate: '2026-09-15',
491
+ reason: 'Marketplace sales, August',
54
492
  })
493
+
494
+ const ledger = await facturino.euThresholdLedgers.retrieve('2026')
495
+ ledger.cumulativeMin // total already acquired, in cents
496
+ ledger.remainingMin // what is left before the cap
55
497
  ```
56
498
 
499
+ ### The same rule under `taxSource: 'integration'`
500
+
501
+ An integration that concludes its own VAT does not get a different territoriality.
502
+ The `integration` source traverses the same coverage, the same threshold, the same
503
+ option, the same evidence and the same declarative mechanism; what differs is the
504
+ outcome. Where `facturino` **produces** the rate, `integration` **compares** the
505
+ one you supply to the legal result:
506
+
507
+ - equal — decision `final`;
508
+ - a category no B2C supply taxed at destination can carry (`AE`, `K`, `G`, `O`),
509
+ or a `placeOfSupply` the rule contradicts — `422 integration_vat_incoherent`;
510
+ - a rate neither the destination **standard** rate nor the published bands of the
511
+ seller's own territory confirm — non-final, with
512
+ `eu_b2c_rate_supplied_mismatch`. Facturino holds only the standard rate of
513
+ another member state, and only the bands it publishes for a French territory,
514
+ so it can neither confirm a reduced rate nor correct yours. At origin that
515
+ refusal asserts NO foreign tax: the operation is taxed in France;
516
+ - a rule that could not conclude — blocked exactly as under `facturino`, with
517
+ the same meaning of `pending_verification` and `unsupported`.
518
+
519
+ The confrontation reaches BOTH places the rule can settle, and the territorial
520
+ frontier is shared too: a seller established outside the French VAT territory, or
521
+ a buyer sitting in a territory excluded from the EU VAT territory, raises under
522
+ `integration` exactly the obstacle it raises under `facturino`.
523
+
524
+ Because of this, `goodsMovement` is **required** on a goods line as soon as the
525
+ buyer is a consumer of another member state: that movement decides whether the
526
+ distance-sale rule applies, and it is never assumed.
527
+
528
+ ### What the decision freezes
529
+
530
+ `TaxDecision.euB2cDestination` carries what the rule concluded, as data rather
531
+ than as a sentence — the verdict and its basis, the threshold figures it was
532
+ decided on, the declarative mechanism, and the rate entry with its registry
533
+ version, its source, its verification date, its period and its region. It is
534
+ present as soon as the rule covers a line — including on a decision that is NOT
535
+ final, where it states exactly what is missing — and `null` on every operation
536
+ the rule does not reach.
537
+
57
538
  ## Amounts
58
539
 
59
540
  Monetary values are integers in **centimes** (10000 = 100.00 EUR).
@@ -74,8 +555,15 @@ const page = await facturino.invoices.list({ status: 'draft' })
74
555
  ## Resources
75
556
 
76
557
  ```typescript
558
+ // Tax decisions (immutable — no update, no delete)
559
+ facturino.taxDecisions.create(params, { idempotencyKey })
560
+ facturino.taxDecisions.retrieve('taxdec_xxx')
561
+
77
562
  // Invoices
78
- facturino.invoices.create(params)
563
+ facturino.invoices.create({ taxDecisionId, decisionLines, ... }) // always backed by a FINAL decision
564
+ // `deposits` and `schedule` are settled server-side against the decided
565
+ // amount; the decided total never changes. A create without a decision is
566
+ // rejected locally, before any HTTP call.
79
567
  facturino.invoices.get('inv_xxx')
80
568
  facturino.invoices.get('inv_xxx', { expand: ['customer', 'credit_notes'] })
81
569
  facturino.invoices.update('inv_xxx', params)
@@ -100,19 +588,32 @@ facturino.customers.lookup({ siret: '73282932000074' })
100
588
  // Products
101
589
  facturino.products.list({ q: 'consult', category: 'services', active: true })
102
590
 
103
- // Quotes
591
+ // Quotes — convert, decide, bind, finalize: ONE invoice throughout.
104
592
  facturino.quotes.create(params)
105
593
  facturino.quotes.send('quo_xxx')
106
594
  facturino.quotes.accept('quo_xxx')
107
- facturino.quotes.convert('quo_xxx') // -> draft invoice
108
595
  facturino.quotes.clone('quo_xxx') // -> duplicated draft quote
109
596
 
597
+ // A converted quote yields a COMMERCIAL draft: it states the operation and no
598
+ // VAT (`taxSource: null`). Bind a final decision to that same invoice, then
599
+ // finalize it — never create a second one.
600
+ const { invoiceId } = await facturino.quotes.convert('quo_xxx')
601
+ const decision = await facturino.taxDecisions.create(decisionInput, { idempotencyKey })
602
+ await facturino.invoices.bindTaxDecision(invoiceId, {
603
+ taxDecisionId: decision.id,
604
+ decisionLines: [{ taxLineRef: 'l1', unit: 'unit' }],
605
+ })
606
+ await facturino.invoices.finalize(invoiceId)
607
+
110
608
  // Credit Notes
111
- facturino.creditNotes.create(params)
609
+ facturino.creditNotes.create({ relatedInvoiceId, creditNoteType, reasonCode, creditedLines })
610
+ // A credit note inherits the fiscal position of the invoice it corrects —
611
+ // source, snapshot and lines. It never restates any VAT.
112
612
  facturino.creditNotes.finalize('crn_xxx')
113
613
 
114
614
  // Recurring Invoices
115
- facturino.recurringInvoices.create(params)
615
+ facturino.recurringInvoices.create({ ..., taxInputs }) // each occurrence decided on its own date,
616
+ // under the recurrence's single fiscal source
116
617
  facturino.recurringInvoices.pause('rec_xxx')
117
618
  facturino.recurringInvoices.resume('rec_xxx')
118
619
 
@@ -163,8 +664,39 @@ app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
163
664
 
164
665
  ## Idempotency
165
666
 
667
+ An `Idempotency-Key` protects the **replay of one request**. It is not a
668
+ deduplicator: the API never decides on its own that two requests "mean the same
669
+ thing".
670
+
671
+ - **Same key + same canonical body** — the first 2xx response is replayed
672
+ verbatim, and the operation is not executed a second time.
673
+ - **Same key + different body** — `409 idempotency_error`. A key belongs to a
674
+ request, not to an endpoint.
675
+ - **Different keys** — two distinct operations, even with byte-identical bodies.
676
+ Two requests describing the same operation are **not** deduplicated
677
+ automatically; the key, and only the key, declares that two sends are the same
678
+ attempt.
679
+ - **Canonical body** — JSON object keys are compared in a stable order, so
680
+ reordering them does not change the request. Changing a value, adding or
681
+ removing a field does. Array order is significant: two lines swapped are two
682
+ different documents.
683
+ - **Failure before execution** (validation, read-only field, sanitisation)
684
+ releases the key, so a corrected retry with the same key runs.
685
+ - **Business refusal during execution** is stored and replayed; the operation is
686
+ not re-executed.
687
+ - **Scope** — 24 hours, per API key. `POST /v1/tax-decisions` additionally
688
+ carries a durable business idempotency that never expires.
689
+
166
690
  ```typescript
167
- await facturino.invoices.create(params, { idempotencyKey: 'unique-id' })
691
+ // Same key + same body -> the first response, replayed.
692
+ await facturino.invoices.create(params, { idempotencyKey: 'order-4821' })
693
+
694
+ // Retry with new evidence is NOT idempotency. It takes a NEW decision on the
695
+ // same commercial operation: use a NEW key and link the previous decision.
696
+ await facturino.taxDecisions.create(
697
+ { ...operation, retryOfTaxDecisionId: suspended.id },
698
+ { idempotencyKey: 'order-4821-retry-1' },
699
+ )
168
700
  ```
169
701
 
170
702
  ## Errors
@@ -1,5 +1,5 @@
1
1
  import type { FacturinoConfig, RequestOptions } from './types.js';
2
- export declare const VERSION = "1.2.0";
2
+ export declare const VERSION = "2.1.0";
3
3
  /** HTTP client with retries, exponential backoff, and structured errors. */
4
4
  export declare class HttpClient {
5
5
  private readonly apiKey;
@@ -5,11 +5,11 @@ const errors_js_1 = require("./errors.js");
5
5
  const DEFAULT_BASE_URL = 'https://facturino.com/api';
6
6
  const DEFAULT_MAX_RETRIES = 3;
7
7
  const DEFAULT_TIMEOUT = 30000;
8
- const DEFAULT_API_VERSION = '2026-03-01';
8
+ const DEFAULT_API_VERSION = '2026-09-01';
9
9
  const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503]);
10
10
  const INITIAL_RETRY_DELAY_MS = 500;
11
11
  const MAX_RETRY_DELAY_MS = 30000;
12
- exports.VERSION = '1.2.0';
12
+ exports.VERSION = '2.1.0';
13
13
  /** HTTP client with retries, exponential backoff, and structured errors. */
14
14
  class HttpClient {
15
15
  constructor(apiKey, config = {}) {