@taxora/sdk 1.8.0 → 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,74 @@
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
+
5
73
  ## [1.8.0] — 2026-10-25
6
74
 
7
75
  ### Added
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 |
@@ -626,6 +631,7 @@ const tx = await client.eReporting.createTransaction({
626
631
  taxes: [{ name: 'TVA', percent: 20, category: 'S' }], // category E needs a VATEX `comment`
627
632
  },
628
633
  ],
634
+ internalReference: 'ORDER-4711', // optional, max 100: your own reference (order no., ERP id) — never sent to the buyer or the tax authority
629
635
  submitNow: false, // record now, submit later
630
636
  });
631
637
 
@@ -637,15 +643,34 @@ console.log(submitted.state, submitted.stateLabel); // e.g. "submitted"
637
643
  const transactions = await client.eReporting.listTransactions({
638
644
  dateFrom: '2026-07-01', // or a Date
639
645
  dateTo: '2026-07-31',
640
- state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error'
646
+ state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error' | 'not_required'
641
647
  transactionType: 'b2c_outbound',
642
648
  complianceEnrollmentId: enrollment.id, // optional: one enrollment only
643
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
644
653
  page: 1,
645
654
  perPage: 25,
646
655
  });
647
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
+ });
648
672
  await client.eReporting.updateTransaction(tx.id, { total: 130, taxAmount: 21.67 });
673
+ await client.eReporting.updateTransaction(tx.id, { internalReference: '' }); // empty string clears it
649
674
  await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
650
675
  ```
651
676
 
@@ -653,7 +678,8 @@ await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
653
678
 
654
679
  Bulk-import from a semicolon-separated CSV (one row per invoice line; rows sharing an
655
680
  `invoice_number` are grouped into one transaction). Idempotent: re-uploading skips rows whose
656
- invoice already exists.
681
+ invoice already exists. An optional `internal_reference` column sets your own reference per
682
+ invoice.
657
683
 
658
684
  ```ts
659
685
  import { readFileSync } from 'fs';
@@ -674,7 +700,7 @@ Every submitted transaction is tracked through the aggregated DGFiP ledger lifec
674
700
  (`new → sent → acknowledged → registered` or `refused`/`error`).
675
701
 
676
702
  ```ts
677
- 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
678
704
  for (const report of reports) {
679
705
  console.log(report.state, report.stateLabel, report.isTerminal, report.refusalReason);
680
706
  }
@@ -712,6 +738,7 @@ const stats = await client.eReporting.getRevenueStatistics({
712
738
  interval: 'month', // 'day' | 'week' | 'month' (default 'month')
713
739
  transactionType: 'b2c_outbound', // optional filter
714
740
  state: 'submitted', // optional filter
741
+ complianceEnrollmentId: enrollment.id, // optional: one enrollment = one country's figures
715
742
  });
716
743
 
717
744
  console.log(stats.primaryCurrency); // e.g. EUR
@@ -724,6 +751,23 @@ for (const row of stats.byCountry) {
724
751
  }
725
752
  ```
726
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
+
727
771
  ### 🇳🇴 E-Invoicing Norway (EHF / Peppol)
728
772
 
729
773
  Send EHF e-invoices and credit notes (Peppol BIS Billing 3.0) to Norwegian buyers. The
@@ -837,6 +881,99 @@ console.log(creditNote.isCreditNote); // true
837
881
  const noDocs = await client.eReporting.listTransactions({ country: 'NO' });
838
882
  ```
839
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
+
840
977
  ---
841
978
 
842
979
  ## 📜 Certificates
@@ -922,6 +1059,13 @@ error body when there is one, and falls back to the status line
922
1059
  (`Taxora API request failed (HTTP 504 Gateway Timeout).`) otherwise. The
923
1060
  untouched body stays available via `err.getResponseBody()`.
924
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
+
925
1069
  ### Automatic retries
926
1070
 
927
1071
  The SDK retries what never produced an answer from the API itself: **3 attempts,
@@ -1038,6 +1182,43 @@ Next.js App Router: `const rawBody = await request.text();` (not `request.json()
1038
1182
  `reason: 'invalid_body'`.
1039
1183
  - **Idempotency:** dedupe on `event.id` (= `Taxora-Webhook-Id`), which stays the same across retries.
1040
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
+
1041
1222
  ---
1042
1223
 
1043
1224
  ## 🔌 VatState Helpers