@taxora/sdk 1.7.1 → 1.9.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/CHANGELOG.md CHANGED
@@ -2,6 +2,90 @@
2
2
 
3
3
  All notable changes to `@taxora/sdk` are documented here. Versions follow [SemVer](https://semver.org/).
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.9.0] — 2026-10-27
8
+
9
+ ### Added
10
+
11
+ - **`ComplianceCoverageCountry.support`** — `'yes' | 'cross_border_only' | 'coming_soon'` from
12
+ `GET /compliance/offerings`: whether a country is fully served, reached only for invoices across the border, or not
13
+ live yet (can still be requested). Defaults to `'yes'` for older API versions.
14
+ - **`eReporting.getNotificationSettings()` / `eReporting.updateNotificationSettings(input)`** —
15
+ `GET` / `PUT /compliance/notification-settings`: the company's opt-in compliance emails as
16
+ `ComplianceNotificationSettings` (`{ recipients, language, notOnPeppol, digest, digestSentAt }`). `notOnPeppol` =
17
+ hourly email listing invoices held back because the buyer is not on Peppol; `digest` = `'off' | 'daily' | 'weekly'`
18
+ summary. Updates send only the provided fields (`UpdateComplianceNotificationSettingsInput`, at least one; max 5
19
+ recipients, validated before the request); 422 when an email is switched on without recipients, 403 `admin_only` /
20
+ `read_only_role` for non-admins.
21
+ - **`eReporting.exportTransactionsCsv(filters)`** — `GET /compliance/transactions/export.csv`: all transactions
22
+ matching the `listTransactions()` filters (without pagination, type `ComplianceTransactionExportFilters`) as a
23
+ semicolon-separated CSV for bookkeeping, incl. internal reference, delivery state, buyer rejection and ECB EUR
24
+ columns (max 50,000 rows). Returned verbatim as a string.
25
+ - **`ComplianceTransaction.eurEquivalent`** (`{ subtotal, taxAmount, total, rate, rateDate, source: 'ECB' }`): the
26
+ amounts of a non-EUR invoice in EUR at the ECB reference rate of the invoice date; null for EUR invoices or when no
27
+ rate is known.
28
+ - **E-invoicing status signals:** `compliance.transaction.state_changed` now fires for every real change (Taxora's
29
+ own state and the provider's delivery state: accepted, refused, paid, …; never twice, never for `sending`).
30
+ `ComplianceTransactionWebhookObject` gained the optional `internal_reference`, `previous_state`,
31
+ `previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check`, plus the
32
+ helper **`isTransactionBuyerRejected(object)`**.
33
+ - **`ComplianceTransaction.buyerRejected` / `.rejectionReason`** and the **`providerState`** filter on
34
+ `listTransactions()` (e.g. `'refused'` = rejected by the buyer).
35
+ - **`eReporting.getTransactionPdf(id)`** downloads the invoice as a readable PDF (the attached seller PDF or the
36
+ provider's rendering of the e-invoice) as `{ content, contentType, filename }`; 409 while no PDF is available.
37
+ **`ComplianceTransaction.pdfUrl`** is set once a PDF is available.
38
+ - **Internal reference:** `internalReference` on `createTransaction()` and `updateTransaction()` (max 100 chars;
39
+ an empty string clears it on update) and `ComplianceTransaction.internalReference`. Your own reference (order
40
+ number, ERP id) for searching — never sent to the buyer or the tax authority. Also an optional CSV import column
41
+ `internal_reference`.
42
+ - **`listTransactions()` filters `search`** (contains-match over invoice number, internal reference and buyer name)
43
+ **and `internalReference`** (exact match), both max 100 chars.
44
+ - **`eReporting.getTransactionDocument(id)`** downloads the legal e-invoice of a sent transaction
45
+ (`{ content: Uint8Array, contentType, filename }`); 409 while it is not available yet.
46
+ - **Invoice PDF attachment:** `invoicePdf` / `invoicePdfFilename` on `createTransaction()` and `updateTransaction()`
47
+ (e-invoicing only). Bytes (`Buffer` / `Uint8Array` / `ArrayBuffer`) are checked (PDF, max 10 MB) and base64-encoded;
48
+ strings are sent as-is (already base64).
49
+ - **`ComplianceTransaction.documentUrl`, `.deliveryCheck` and `.invoicePdf`** (`{ filename, size, attached }`).
50
+ - **`ComplianceTransactionState.NOT_REQUIRED`** (`not_required`): the buyer cannot receive e-invoices, so nothing was
51
+ sent or billed.
52
+ - **`eReporting.exportRevenueTaxRatesCsv(filters)`**: net and VAT per month and tax rate as CSV (BOM preserved).
53
+ - **Self-service plans:** `eReporting.getOfferings()` (`ComplianceOfferings`), `createCheckout(country, plan)`,
54
+ `confirmCheckout(sessionId)` and `createBillingPortalSession()`.
55
+ - **`client.billing`** (`BillingEndpoint`): `listUsageSummaries(page, perPage)` (`UsageSummaryPage`) and
56
+ `getUsageSummaries(period)` (`UsageSummary[]` with events).
57
+ - **Per-country filters:** `complianceEnrollmentId` on `getRevenueStatistics()` and `exportRevenueTaxRatesCsv()`;
58
+ `country` as 4th argument of `listTaxReports(page, perPage, state, country)`.
59
+ - README: compliance write requests by a read-only user return 403 with code `read_only_role` (HttpException).
60
+ - README: compliance webhook events (`compliance.transaction.state_changed`, `compliance.ledger.state_changed`) listed
61
+ with their payloads.
62
+
63
+ ### Fixed
64
+
65
+ - **`ComplianceOfferingPlan.requires`** is now typed `{ country: string; service: string } | null` and parsed from the
66
+ object the API sends (e.g. `{ country: 'FR', service: 'e_reporting' }`). It was typed `unknown[] | null` and always
67
+ came back `null`. Public type change.
68
+ - `createTransaction()` / `updateTransaction()` reject `invoicePdfFilename` without `invoicePdf`
69
+ (`invoicePdfFilename requires invoicePdf.`), like the PHP SDK.
70
+ - README: `admin_only` (403) listed with the compliance error codes; the webhook example no longer mentions a
71
+ `not_required` state event (it is never sent).
72
+
73
+ ## [1.8.0] — 2026-10-25
74
+
75
+ ### Added
76
+
77
+ - **`smartEnrichment.recheck(jobId, { rows?, dryRun? })`** searches again for rows without a usable answer
78
+ (`not_found`, or a number registered to another company). Every recheck counts as a check like a new lookup (Deep
79
+ Verify ×2), found or not; max 3 per row, 7 days apart; `no_vat_exists` and found rows are never rechecked.
80
+ `dryRun` reports rows, skipped rows and cost without starting anything. Returns `SmartEnrichmentRecheck`
81
+ (`rows`, `checks`, `skipped`, `dryRun`).
82
+ - **`SmartEnrichmentJob.recheck`** (`SmartEnrichmentRecheckState`: `eligible`, `runs`, `maxPerRow`, `cooldownDays`,
83
+ `isActive()`) and **`recheckCount` / `lastRecheckedAt`** on each result.
84
+
85
+ ### Changed
86
+
87
+ - Bundled OpenAPI specs (`resources/`) refreshed from the current API.
88
+
5
89
  ## [1.7.1] — 2026-10-24
6
90
 
7
91
  ### Fixed
package/README.md CHANGED
@@ -124,7 +124,10 @@ TaxoraClientFactory.create()
124
124
  └── TaxoraClient
125
125
  ├── auth → AuthEndpoint (login, loginWithClientId, refresh)
126
126
  ├── vat → VatEndpoint (validate, history, search, certificates)
127
- └── company → CompanyEndpoint (company info)
127
+ ├── company → CompanyEndpoint (company info)
128
+ ├── smartEnrichment → SmartEnrichmentEndpoint (reverse VAT lookup)
129
+ ├── eReporting → EReportingEndpoint (compliance: enrollments, transactions, plans)
130
+ └── billing → BillingEndpoint (usage summaries)
128
131
  ```
129
132
 
130
133
  Each endpoint handles:
@@ -153,6 +156,8 @@ Each endpoint handles:
153
156
  | `ProviderDocument` | Attached provider document metadata (type, date, MIME, hash, nested line item) |
154
157
  | `ComplianceEnrollment` | Per-country compliance enrollment: country, regime, `service`, provisioning status |
155
158
  | `ComplianceTransaction` | Recorded transaction / e-invoice: amounts, state, buyer fields, `documentType`, `providerState` |
159
+ | `ComplianceOfferings` | Bookable compliance plans per country, coverage, unlocked services |
160
+ | `UsageSummary` | Billed usage of one feature in one billing period (YYYY-MM), optionally with its events |
156
161
  | `ComplianceTaxReport` | DGFiP tax report lifecycle of a transaction (France) |
157
162
  | `SireneLookupResult` | Company data from the French SIRENE registry |
158
163
  | `RegistryCompany` | Company from a national register (Norway: Brønnøysund) — org number, VAT / register flags |
@@ -448,6 +453,26 @@ for (const row of history) {
448
453
  }
449
454
  ```
450
455
 
456
+ ### Check rows again
457
+
458
+ Search once more for rows without a usable answer (`not_found`, or a number registered to another company). Every
459
+ recheck counts as a check like a new lookup (Deep Verify ×2), whether a VAT number is found or not. A row can be
460
+ rechecked up to 3 times, 7 days apart; `no_vat_exists` and found rows are never rechecked.
461
+
462
+ ```ts
463
+ // What would run, and what it costs — nothing is started
464
+ const plan = await client.smartEnrichment.recheck(jobId, { dryRun: true });
465
+ console.log(plan.rows.length, 'rows,', plan.checks, 'checks, skipped:', plan.skipped);
466
+
467
+ await client.smartEnrichment.recheck(jobId); // every row that qualifies
468
+ await client.smartEnrichment.recheck(jobId, { rows: [3, 7] }); // or just these rows
469
+
470
+ // Improved rows arrive via the `enrichment.updated` webhook; or poll:
471
+ const job = await client.smartEnrichment.get(jobId);
472
+ console.log(job.recheck?.isActive() ? 'still rechecking' : 'done');
473
+ console.log(job.results[3]?.recheckCount, job.results[3]?.lastRecheckedAt);
474
+ ```
475
+
451
476
  ### Quota usage & billing history
452
477
 
453
478
  ```ts
@@ -606,6 +631,7 @@ const tx = await client.eReporting.createTransaction({
606
631
  taxes: [{ name: 'TVA', percent: 20, category: 'S' }], // category E needs a VATEX `comment`
607
632
  },
608
633
  ],
634
+ internalReference: 'ORDER-4711', // optional, max 100: your own reference (order no., ERP id) — never sent to the buyer or the tax authority
609
635
  submitNow: false, // record now, submit later
610
636
  });
611
637
 
@@ -617,15 +643,34 @@ console.log(submitted.state, submitted.stateLabel); // e.g. "submitted"
617
643
  const transactions = await client.eReporting.listTransactions({
618
644
  dateFrom: '2026-07-01', // or a Date
619
645
  dateTo: '2026-07-31',
620
- state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error'
646
+ state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error' | 'not_required'
621
647
  transactionType: 'b2c_outbound',
622
648
  complianceEnrollmentId: enrollment.id, // optional: one enrollment only
623
649
  country: 'FR', // optional: one country only
650
+ search: 'ACME', // optional: contains-match over invoice number, internal reference and buyer name
651
+ internalReference: 'ORDER-4711', // optional: exact match on your internal reference
652
+ providerState: 'refused', // optional: provider delivery state, 'refused' = rejected by the buyer
624
653
  page: 1,
625
654
  perPage: 25,
626
655
  });
627
656
  const one = await client.eReporting.getTransaction(tx.id);
657
+ // Non-EUR invoices carry the EUR amounts at the ECB reference rate of the invoice date
658
+ // (null for EUR invoices or when no rate is known):
659
+ if (one.eurEquivalent) {
660
+ const { total, rate, rateDate, source } = one.eurEquivalent; // decimal strings, e.g. '98.92', '11.6250'
661
+ console.log(`${total} EUR @ ${rate} (${source}, ${rateDate})`);
662
+ }
663
+
664
+ // The same filters (without page/perPage) as a semicolon-separated CSV for your bookkeeping —
665
+ // incl. internal reference, delivery state, buyer rejection and the eur_rate / eur_rate_date /
666
+ // eur_subtotal / eur_tax_amount / eur_total columns. Max 50,000 rows; returned verbatim.
667
+ const bookkeepingCsv = await client.eReporting.exportTransactionsCsv({
668
+ dateFrom: '2026-01-01',
669
+ dateTo: '2026-09-30',
670
+ country: 'NO',
671
+ });
628
672
  await client.eReporting.updateTransaction(tx.id, { total: 130, taxAmount: 21.67 });
673
+ await client.eReporting.updateTransaction(tx.id, { internalReference: '' }); // empty string clears it
629
674
  await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
630
675
  ```
631
676
 
@@ -633,7 +678,8 @@ await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
633
678
 
634
679
  Bulk-import from a semicolon-separated CSV (one row per invoice line; rows sharing an
635
680
  `invoice_number` are grouped into one transaction). Idempotent: re-uploading skips rows whose
636
- invoice already exists.
681
+ invoice already exists. An optional `internal_reference` column sets your own reference per
682
+ invoice.
637
683
 
638
684
  ```ts
639
685
  import { readFileSync } from 'fs';
@@ -654,7 +700,7 @@ Every submitted transaction is tracked through the aggregated DGFiP ledger lifec
654
700
  (`new → sent → acknowledged → registered` or `refused`/`error`).
655
701
 
656
702
  ```ts
657
- const reports = await client.eReporting.listTaxReports(1, 25, 'refused'); // state filter optional
703
+ const reports = await client.eReporting.listTaxReports(1, 25, 'refused', 'FR'); // state + country filters optional
658
704
  for (const report of reports) {
659
705
  console.log(report.state, report.stateLabel, report.isTerminal, report.refusalReason);
660
706
  }
@@ -692,6 +738,7 @@ const stats = await client.eReporting.getRevenueStatistics({
692
738
  interval: 'month', // 'day' | 'week' | 'month' (default 'month')
693
739
  transactionType: 'b2c_outbound', // optional filter
694
740
  state: 'submitted', // optional filter
741
+ complianceEnrollmentId: enrollment.id, // optional: one enrollment = one country's figures
695
742
  });
696
743
 
697
744
  console.log(stats.primaryCurrency); // e.g. EUR
@@ -704,6 +751,23 @@ for (const row of stats.byCountry) {
704
751
  }
705
752
  ```
706
753
 
754
+ ### Tax-rate CSV
755
+
756
+ Net and VAT per month, currency and tax rate (semicolon-separated, computed from the invoice lines) — for reconciling
757
+ the reported figures with your VAT return. Same filters as `getRevenueStatistics()` without `interval`. The body is
758
+ returned byte-for-byte, a UTF-8 BOM included.
759
+
760
+ ```ts
761
+ import { writeFileSync } from 'fs';
762
+
763
+ const csv = await client.eReporting.exportRevenueTaxRatesCsv({
764
+ dateFrom: '2026-01-01',
765
+ dateTo: '2026-12-31',
766
+ complianceEnrollmentId: enrollment.id, // optional: one country only
767
+ });
768
+ writeFileSync('tax-rates.csv', csv);
769
+ ```
770
+
707
771
  ### 🇳🇴 E-Invoicing Norway (EHF / Peppol)
708
772
 
709
773
  Send EHF e-invoices and credit notes (Peppol BIS Billing 3.0) to Norwegian buyers. The
@@ -817,6 +881,99 @@ console.log(creditNote.isCreditNote); // true
817
881
  const noDocs = await client.eReporting.listTransactions({ country: 'NO' });
818
882
  ```
819
883
 
884
+ **6. Attach your own invoice PDF** (e-invoicing only, max 10 MB) — pass the bytes (`Buffer` / `Uint8Array` /
885
+ `ArrayBuffer`); the SDK base64-encodes them. A string is sent as-is and must already be base64. The structured
886
+ e-invoice stays the legal document; whether the buyer's system shows the PDF depends on that system.
887
+
888
+ ```ts
889
+ import { readFileSync } from 'fs';
890
+
891
+ const withPdf = await client.eReporting.createTransaction({
892
+ ...invoiceInput,
893
+ invoicePdf: readFileSync('NO-2026-0002.pdf'),
894
+ invoicePdfFilename: 'NO-2026-0002.pdf', // optional, defaults to the invoice number
895
+ });
896
+ console.log(withPdf.invoicePdf); // { filename, size, attached } — attached once uploaded with the e-invoice
897
+
898
+ await client.eReporting.updateTransaction(withPdf.id, { invoicePdf: readFileSync('fixed.pdf') }); // replaces it
899
+ ```
900
+
901
+ **7. Buyer not on Peppol / download the legal document**
902
+
903
+ ```ts
904
+ const tx = await client.eReporting.getTransaction(invoice.id);
905
+
906
+ if (tx.state === 'not_required') {
907
+ // The buyer cannot receive e-invoices: nothing was sent or billed — send your regular invoice.
908
+ console.log(tx.deliveryCheck); // Peppol directory answer, e.g. { status: 'not_reachable', … }
909
+ }
910
+
911
+ if (tx.documentUrl !== null) {
912
+ const doc = await client.eReporting.getTransactionDocument(tx.id); // 409 while not available yet
913
+ writeFileSync(doc.filename ?? `${tx.invoiceNumber}.xml`, doc.content); // doc.contentType e.g. application/xml
914
+ }
915
+
916
+ // The invoice as a readable PDF (your attached PDF, or the provider's rendering of the e-invoice)
917
+ if (tx.pdfUrl !== null) {
918
+ const pdf = await client.eReporting.getTransactionPdf(tx.id); // 409 while no PDF is available
919
+ writeFileSync(pdf.filename ?? `${tx.invoiceNumber}.pdf`, pdf.content); // pdf.contentType: application/pdf
920
+ }
921
+ ```
922
+
923
+ ### Self-service plans (Stripe)
924
+
925
+ Book compliance plans without contacting sales. These routes work for companies **without** compliance access.
926
+
927
+ ```ts
928
+ const { offerings, coverage, access, accessByCountry } = await client.eReporting.getOfferings();
929
+ const plan = offerings.find((o) => o.country === 'NO')?.plans.find((p) => p.checkoutAvailable);
930
+
931
+ const session = await client.eReporting.createCheckout('NO', plan!.key); // 422 if contact-only / already active
932
+ // open session.url in the browser; Stripe returns the user to the Taxora portal
933
+ const state = await client.eReporting.confirmCheckout(session.id); // idempotent → { active, country, service, plan, status }
934
+
935
+ const portalUrl = await client.eReporting.createBillingPortalSession(); // invoices, payment method, cancel; 404 without a Stripe plan
936
+ ```
937
+
938
+ ### Notification emails
939
+
940
+ Opt-in e-reporting / e-invoicing emails for your company — readable by every member, changeable by company **admins**
941
+ only.
942
+
943
+ ```ts
944
+ const settings = await client.eReporting.getNotificationSettings();
945
+ // { recipients: ['accounting@example.com'], language: 'de' | 'en', notOnPeppol: boolean,
946
+ // digest: 'off' | 'daily' | 'weekly', digestSentAt: string | null }
947
+
948
+ // Only the provided fields change (at least one is required); recipients replaces the whole list (max 5):
949
+ await client.eReporting.updateNotificationSettings({
950
+ recipients: ['accounting@example.com', 'cfo@example.com'],
951
+ language: 'en',
952
+ notOnPeppol: true, // hourly email listing invoices held back because the buyer is not on Peppol
953
+ digest: 'weekly', // 'daily' = 08:00 for the day before, 'weekly' = Mondays for the last 7 days
954
+ });
955
+ ```
956
+
957
+ Invalid input (more than 5 or invalid addresses, unknown `language` / `digest`, no field) throws a `TaxoraException`
958
+ before any request. Switching an email on without any recipient answers `422` (`ValidationException`); a non-admin
959
+ gets `403` with body code `admin_only` (or `read_only_role`) as `HttpException`. `notOnPeppol` only reports invoices
960
+ held back after it was switched on.
961
+
962
+ ### Billing usage summaries
963
+
964
+ Read-only usage of your billable modules (E-Reporting / E-Invoicing, Smart Enrichment), per feature and billing
965
+ period.
966
+
967
+ ```ts
968
+ const page = await client.billing.listUsageSummaries(1, 25); // newest period first
969
+ for (const summary of page) {
970
+ console.log(summary.billingPeriod, summary.feature, summary.eventCount, summary.billableAmount, summary.state);
971
+ }
972
+
973
+ const september = await client.billing.getUsageSummaries('2026-09'); // with events; 404 for a period without usage
974
+ console.log(september[0]?.events.length);
975
+ ```
976
+
820
977
  ---
821
978
 
822
979
  ## 📜 Certificates
@@ -902,6 +1059,13 @@ error body when there is one, and falls back to the status line
902
1059
  (`Taxora API request failed (HTTP 504 Gateway Timeout).`) otherwise. The
903
1060
  untouched body stays available via `err.getResponseBody()`.
904
1061
 
1062
+ **Compliance 403 codes.** Compliance 403s are `HttpException`s; the reason is the `code` in the JSON body
1063
+ (`err.getResponseBody()`): `service_not_active` (the company has not booked the service/country),
1064
+ `read_only_role` (a write request — create, update, delete, submit, import — by a user whose only role is
1065
+ read-only) or `admin_only` (body `{ message, code: 'admin_only' }`: a developer-role user tried to create an
1066
+ enrollment, `createCheckout()`, `confirmCheckout()`, `createBillingPortalSession()` or
1067
+ `updateNotificationSettings()` — only admins can add countries, change plans or change the notification emails).
1068
+
905
1069
  ### Automatic retries
906
1070
 
907
1071
  The SDK retries what never produced an answer from the API itself: **3 attempts,
@@ -1018,6 +1182,43 @@ Next.js App Router: `const rawBody = await request.text();` (not `request.json()
1018
1182
  `reason: 'invalid_body'`.
1019
1183
  - **Idempotency:** dedupe on `event.id` (= `Taxora-Webhook-Id`), which stays the same across retries.
1020
1184
 
1185
+ ### Events
1186
+
1187
+ | Event | `data.object` |
1188
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
1189
+ | `vat.validated` / `vat.state_changed` | `VatWebhookObject` (`id`, `vat_uid`, `state`, `company_name`, …) |
1190
+ | `compliance.transaction.state_changed` | `ComplianceTransactionWebhookObject` (`id`, `state`, `previous_state`, `provider_state`, `buyer_rejected`, …) |
1191
+ | `compliance.ledger.state_changed` | `ComplianceLedgerWebhookObject` (`id`, `state`, `ledger_type`, `ledger_date`) |
1192
+ | `enrichment.completed` | `EnrichmentCompletedWebhookObject` (`jobId`, `status`, `results`, …) |
1193
+ | `enrichment.updated` | `EnrichmentUpdatedWebhookObject` (`jobId`, `rowIndex`, `result`, …) |
1194
+ | `webhook.test` | `TestWebhookObject` (`message`) |
1195
+
1196
+ `compliance.transaction.state_changed` fires for **every real change** of a transaction: Taxora's own state
1197
+ (`submitted`, `error`, `not_required`) and the provider's delivery state (`accepted`, `refused`, `paid`, …). It is
1198
+ never sent twice for the same state and never for the transient `sending`. Besides `id`, `invoice_number`,
1199
+ `country` (lowercase), `state` and `provider_state` the object carries `internal_reference`, `previous_state`,
1200
+ `previous_provider_state`, `buyer_rejected`, `rejection_reason`, `submission_error` and `delivery_check` — these are
1201
+ optional in the type because older deliveries lack them.
1202
+
1203
+ ```ts
1204
+ import { isTransactionBuyerRejected } from '@taxora/sdk';
1205
+
1206
+ if (event.event === 'compliance.transaction.state_changed') {
1207
+ const tx = event.data.object;
1208
+ if (tx.state === 'not_required') {
1209
+ // The buyer cannot receive e-invoices (tx.delivery_check e.g. 'not_reachable'):
1210
+ // nothing was sent or billed — send your PDF to the buyer by e-mail yourself.
1211
+ }
1212
+ if (isTransactionBuyerRejected(tx)) {
1213
+ // The buyer refused the e-invoice (tx.rejection_reason) — issue a credit note.
1214
+ }
1215
+ if (tx.state === 'error') console.warn(tx.submission_error);
1216
+ }
1217
+ ```
1218
+
1219
+ Transactions carry the same signal (`tx.buyerRejected`, `tx.rejectionReason`); find rejected invoices with
1220
+ `listTransactions({ providerState: 'refused' })`.
1221
+
1021
1222
  ---
1022
1223
 
1023
1224
  ## 🔌 VatState Helpers