@taxora/sdk 1.5.0 → 1.7.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,40 @@
2
2
 
3
3
  All notable changes to `@taxora/sdk` are documented here. Versions follow [SemVer](https://semver.org/).
4
4
 
5
+ ## [1.7.0] — 2026-10-24
6
+
7
+ ### Added
8
+
9
+ - **Smart Enrichment: `taxNumberType`, `taxNumberLabel`, `taxNumberSource`** next to `taxNumber`. `taxNumber` is now
10
+ the company's local tax / business number: on `found` it is read off the confirmed VAT ID where that country's
11
+ VAT ID contains it (`taxNumberSource` = `'vat_id'`: FR SIREN, IT Partita IVA, ES NIF, PL NIP, BE enterprise
12
+ number, DK CVR, PT, RO, CZ IČO, SK DIČ, SE/NO orgnr, FI Y-tunnus, SI, HR OIB, BG EIK, GR AFM), otherwise a
13
+ domestic number the company published (`'company_website'`, e.g. a DE/AT Steuernummer — not a VAT ID).
14
+ `taxNumberType` is the code (`pl_nip`, `fr_siren`, `de_steuernummer` …), `taxNumberLabel` its national name.
15
+ New type `SmartEnrichmentTaxNumberSource`.
16
+ - **Local tax numbers report** on `client.vat`: `localTaxNumbersCsv()` (raw CSV, UTF-8 with BOM),
17
+ `localTaxNumbers()` (parsed `LocalTaxNumberRow[]` — BOM dropped, the Excel TAB guard before digit-only tax numbers
18
+ trimmed, empty cells `null`) and `localTaxNumbersSummary()` (`{ companies, withTaxNumber }`). Filters: `dateFrom`,
19
+ `dateTo`, `countries`, `onlyWithTaxNumber`. `parseLocalTaxNumbersCsv()` parses a saved file.
20
+
21
+ ### Changed
22
+
23
+ - Bundled OpenAPI specs (`resources/`) refreshed from the current API.
24
+
25
+ ## [1.6.0] — 2026-10-22
26
+
27
+ ### Added
28
+
29
+ - **Smart Enrichment: `taxNumber`** — a domestic tax number (e.g. a German Steuernummer) a company's page shows
30
+ instead of a VAT ID. It is not a VAT ID; the lookup stays `not_found`.
31
+ - **Smart Enrichment: the official VAT check behind a found number** — `vatValid`, `aiScore`, `nameMatch`
32
+ (`'ok' | 'mismatch'`, plus `nameMismatch()`), `officialName` and `officialAddress`
33
+ (`SmartEnrichmentOfficialAddress`). Lets you write the register's name and address back into your records.
34
+
35
+ ### Changed
36
+
37
+ - Bundled OpenAPI specs (`resources/`) refreshed from the current API.
38
+
5
39
  ## [1.5.0] — 2026-10-22
6
40
 
7
41
  ### Added
package/README.md CHANGED
@@ -307,6 +307,39 @@ const results = await client.vat.search('Alpha Handels', 25 /* perPage */);
307
307
 
308
308
  ---
309
309
 
310
+ ## 🧾 Local tax numbers report
311
+
312
+ Every company you checked — single and bulk VAT checks and Smart Enrichment lookups — once, with its **local tax
313
+ number** (NIP, SIREN, Partita IVA, Steuernummer …), the number's national name and where it comes from (`vat_id` =
314
+ read off the confirmed VAT ID, `company_website` = published by the company). All filters are optional.
315
+
316
+ ```typescript
317
+ import { parseLocalTaxNumbersCsv } from '@taxora/sdk';
318
+ import { writeFileSync } from 'node:fs';
319
+
320
+ // Parsed rows (BOM dropped, cells trimmed, empty cells null)
321
+ const rows = await client.vat.localTaxNumbers({
322
+ dateFrom: '2026-10-01', // optional (YYYY-MM-DD or Date)
323
+ dateTo: '2026-10-31', // optional
324
+ countries: ['PL', 'IT'], // optional (ISO-2)
325
+ onlyWithTaxNumber: true, // optional: skip companies without one
326
+ });
327
+ for (const row of rows) {
328
+ // e.g. Orlen S.A. PL 5260250995 (NIP, vat_id)
329
+ console.log(`${row.companyName} ${row.country} ${row.taxNumber} (${row.taxNumberName}, ${row.taxNumberSource})`);
330
+ }
331
+ // row.vatStatus: valid | invalid | unconfirmed | wrong_company | no_vat_id | not_found
332
+ // row.source: vat_check | smart_enrichment; row.checkId: the VAT check uuid or the lookup job id
333
+
334
+ // The raw CSV (UTF-8 with BOM) for a spreadsheet. Digit-only tax numbers carry a leading TAB so
335
+ // Excel keeps leading zeros; parseLocalTaxNumbersCsv() turns a saved file into rows.
336
+ writeFileSync('local-tax-numbers.csv', await client.vat.localTaxNumbersCsv({ countries: ['PL'] }));
337
+
338
+ // Counts for the same filters
339
+ const summary = await client.vat.localTaxNumbersSummary({ dateFrom: '2026-10-01' });
340
+ console.log(`${summary.withTaxNumber} of ${summary.companies} companies have a local tax number`);
341
+ ```
342
+
310
343
  ## 🔎 Smart Enrichment (reverse VAT lookup)
311
344
 
312
345
  Resolve a company **name + address + country → VAT number + confidence**. A confident match
@@ -370,6 +403,23 @@ console.log(result?.sourceUrl ?? '—'); // e.g. 'https://example.de/impressum'
370
403
  console.log(result?.sourceUrls.join(', ')); // every page it was found on, main one first
371
404
  console.log(result?.registeredOwner ?? '—'); // e.g. 'Kevin Seibel'
372
405
 
406
+ // The official VAT check behind a found number. nameMatch 'mismatch' means the register has the
407
+ // number under a different name — another company, unless `registeredOwner` is set (an e.K. filed
408
+ // under its owner): then write officialName / officialAddress into your own records.
409
+ if (result?.nameMismatch()) {
410
+ console.log(result.officialName, result.officialAddress);
411
+ }
412
+
413
+ // `taxNumber` is the company's LOCAL tax / business number — what domestic e-invoicing asks for.
414
+ // On 'found' it is read off the confirmed VAT ID where that country's VAT ID contains it
415
+ // (taxNumberSource 'vat_id': FR SIREN, IT Partita IVA, ES NIF, PL NIP, BE, DK CVR, CZ IČO, …).
416
+ // Otherwise it is a number the company published itself ('company_website', e.g. a German
417
+ // Steuernummer shown under the VAT ID heading) — that one is not a VAT ID.
418
+ console.log(result?.taxNumber ?? '—'); // e.g. '5260250995'
419
+ console.log(result?.taxNumberType ?? '—'); // e.g. 'pl_nip', 'fr_siren', 'de_steuernummer'
420
+ console.log(result?.taxNumberLabel ?? '—'); // e.g. 'NIP'
421
+ console.log(result?.taxNumberSource ?? '—'); // 'vat_id' | 'company_website'
422
+
373
423
  // 'not_found' doesn't always mean nothing turned up. When the research found a VAT number but
374
424
  // the official check threw it out (e.g. the tax authority reports it as invalid), the number
375
425
  // and the reason are in `rejectedCandidates` — no need to re-check it yourself.
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  ImportResult: () => ImportResult,
47
47
  InMemoryTokenStorage: () => InMemoryTokenStorage,
48
48
  Language: () => Language,
49
+ LocalTaxNumberRow: () => LocalTaxNumberRow,
49
50
  LoginIdentifier: () => LoginIdentifier,
50
51
  NorwegianOrgNumber: () => NorwegianOrgNumber,
51
52
  PeppolLookupResult: () => PeppolLookupResult,
@@ -98,6 +99,7 @@ __export(index_exports, {
98
99
  generateWebhookSignatureHeader: () => generateWebhookSignatureHeader,
99
100
  getBaseUrl: () => getBaseUrl,
100
101
  getFailedVatStates: () => getFailedVatStates,
102
+ parseLocalTaxNumbersCsv: () => parseLocalTaxNumbersCsv,
101
103
  parseWebhookSignatureHeader: () => parseWebhookSignatureHeader,
102
104
  toComplianceDocumentType: () => toComplianceDocumentType,
103
105
  toComplianceEnrollmentStatus: () => toComplianceEnrollmentStatus,
@@ -2430,8 +2432,22 @@ function toRejectedCandidates(value) {
2430
2432
  }
2431
2433
  return candidates;
2432
2434
  }
2435
+ function toOfficialAddress(value) {
2436
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2437
+ return void 0;
2438
+ }
2439
+ const record = value;
2440
+ const address = {};
2441
+ for (const key of ["street", "postal_code", "city", "country"]) {
2442
+ const part = typeof record[key] === "string" ? record[key].trim() : "";
2443
+ if (part !== "") {
2444
+ address[key] = part;
2445
+ }
2446
+ }
2447
+ return Object.keys(address).length > 0 ? address : void 0;
2448
+ }
2433
2449
  var SmartEnrichmentResource = class _SmartEnrichmentResource {
2434
- constructor(status, vatNumber, vatType, confidence, matchedCompanyName, matchedAddress, country, source, mode = void 0, providerVerdicts = [], addressQuality = void 0, rejectedCandidates = [], sourceUrl = void 0, registeredOwner = void 0, sourceUrls = []) {
2450
+ constructor(status, vatNumber, vatType, confidence, matchedCompanyName, matchedAddress, country, source, mode = void 0, providerVerdicts = [], addressQuality = void 0, rejectedCandidates = [], sourceUrl = void 0, registeredOwner = void 0, sourceUrls = [], taxNumber = void 0, vatValid = void 0, aiScore = void 0, nameMatch = void 0, officialName = void 0, officialAddress = void 0, taxNumberType = void 0, taxNumberLabel = void 0, taxNumberSource = void 0) {
2435
2451
  this.status = status;
2436
2452
  this.vatNumber = vatNumber;
2437
2453
  this.vatType = vatType;
@@ -2447,6 +2463,15 @@ var SmartEnrichmentResource = class _SmartEnrichmentResource {
2447
2463
  this.sourceUrl = sourceUrl;
2448
2464
  this.registeredOwner = registeredOwner;
2449
2465
  this.sourceUrls = sourceUrls;
2466
+ this.taxNumber = taxNumber;
2467
+ this.vatValid = vatValid;
2468
+ this.aiScore = aiScore;
2469
+ this.nameMatch = nameMatch;
2470
+ this.officialName = officialName;
2471
+ this.officialAddress = officialAddress;
2472
+ this.taxNumberType = taxNumberType;
2473
+ this.taxNumberLabel = taxNumberLabel;
2474
+ this.taxNumberSource = taxNumberSource;
2450
2475
  }
2451
2476
  status;
2452
2477
  vatNumber;
@@ -2463,6 +2488,19 @@ var SmartEnrichmentResource = class _SmartEnrichmentResource {
2463
2488
  sourceUrl;
2464
2489
  registeredOwner;
2465
2490
  sourceUrls;
2491
+ taxNumber;
2492
+ vatValid;
2493
+ aiScore;
2494
+ nameMatch;
2495
+ officialName;
2496
+ officialAddress;
2497
+ taxNumberType;
2498
+ taxNumberLabel;
2499
+ taxNumberSource;
2500
+ /** True when the register knows the number under a different name than the one requested. */
2501
+ nameMismatch() {
2502
+ return this.nameMatch === "mismatch";
2503
+ }
2466
2504
  /** True when a VAT number was found but rejected by the official check. */
2467
2505
  hasRejectedCandidates() {
2468
2506
  return this.rejectedCandidates.length > 0;
@@ -2500,7 +2538,16 @@ var SmartEnrichmentResource = class _SmartEnrichmentResource {
2500
2538
  toRejectedCandidates(data["rejectedCandidates"]),
2501
2539
  url && isWebUrl(url) ? url : void 0,
2502
2540
  text("registeredOwner"),
2503
- Array.isArray(urls) ? [...new Set(urls.filter(isWebUrl).map((u) => u.trim()))] : []
2541
+ Array.isArray(urls) ? [...new Set(urls.filter(isWebUrl).map((u) => u.trim()))] : [],
2542
+ text("taxNumber"),
2543
+ typeof data["vatValid"] === "boolean" ? data["vatValid"] : void 0,
2544
+ num("aiScore"),
2545
+ data["nameMatch"] === "ok" || data["nameMatch"] === "mismatch" ? data["nameMatch"] : void 0,
2546
+ text("officialName"),
2547
+ toOfficialAddress(data["officialAddress"]),
2548
+ text("taxNumberType"),
2549
+ text("taxNumberLabel"),
2550
+ data["taxNumberSource"] === "vat_id" || data["taxNumberSource"] === "company_website" ? data["taxNumberSource"] : void 0
2504
2551
  );
2505
2552
  }
2506
2553
  toArray() {
@@ -2519,7 +2566,16 @@ var SmartEnrichmentResource = class _SmartEnrichmentResource {
2519
2566
  rejectedCandidates: this.rejectedCandidates.map((c) => ({ vatNumber: c.vatNumber, reason: c.reason })),
2520
2567
  sourceUrl: this.sourceUrl ?? null,
2521
2568
  registeredOwner: this.registeredOwner ?? null,
2522
- sourceUrls: this.sourceUrls
2569
+ sourceUrls: this.sourceUrls,
2570
+ taxNumber: this.taxNumber ?? null,
2571
+ vatValid: this.vatValid ?? null,
2572
+ aiScore: this.aiScore ?? null,
2573
+ nameMatch: this.nameMatch ?? null,
2574
+ officialName: this.officialName ?? null,
2575
+ officialAddress: this.officialAddress ?? null,
2576
+ taxNumberType: this.taxNumberType ?? null,
2577
+ taxNumberLabel: this.taxNumberLabel ?? null,
2578
+ taxNumberSource: this.taxNumberSource ?? null
2523
2579
  };
2524
2580
  }
2525
2581
  };
@@ -3527,6 +3583,123 @@ var VatValidationAddressInput = class _VatValidationAddressInput {
3527
3583
  }
3528
3584
  };
3529
3585
 
3586
+ // src/dto/LocalTaxNumberReport.ts
3587
+ var LocalTaxNumberRow = class _LocalTaxNumberRow {
3588
+ constructor(companyName, requestedName, country, vatNumber, vatStatus, taxNumber, taxNumberName, taxNumberSource, source, checkedAt, checkId) {
3589
+ this.companyName = companyName;
3590
+ this.requestedName = requestedName;
3591
+ this.country = country;
3592
+ this.vatNumber = vatNumber;
3593
+ this.vatStatus = vatStatus;
3594
+ this.taxNumber = taxNumber;
3595
+ this.taxNumberName = taxNumberName;
3596
+ this.taxNumberSource = taxNumberSource;
3597
+ this.source = source;
3598
+ this.checkedAt = checkedAt;
3599
+ this.checkId = checkId;
3600
+ }
3601
+ companyName;
3602
+ requestedName;
3603
+ country;
3604
+ vatNumber;
3605
+ vatStatus;
3606
+ taxNumber;
3607
+ taxNumberName;
3608
+ taxNumberSource;
3609
+ source;
3610
+ checkedAt;
3611
+ checkId;
3612
+ /** One CSV row keyed by column name. */
3613
+ static fromArray(data) {
3614
+ const cell = (key) => {
3615
+ const value = data[key];
3616
+ const trimmed = typeof value === "string" ? value.trim() : "";
3617
+ return trimmed === "" ? null : trimmed;
3618
+ };
3619
+ return new _LocalTaxNumberRow(
3620
+ cell("companyName"),
3621
+ cell("requestedName"),
3622
+ cell("country"),
3623
+ cell("vatNumber"),
3624
+ cell("vatStatus"),
3625
+ cell("taxNumber"),
3626
+ cell("taxNumberName"),
3627
+ cell("taxNumberSource"),
3628
+ cell("source"),
3629
+ cell("checkedAt"),
3630
+ cell("checkId")
3631
+ );
3632
+ }
3633
+ toArray() {
3634
+ return {
3635
+ companyName: this.companyName,
3636
+ requestedName: this.requestedName,
3637
+ country: this.country,
3638
+ vatNumber: this.vatNumber,
3639
+ vatStatus: this.vatStatus,
3640
+ taxNumber: this.taxNumber,
3641
+ taxNumberName: this.taxNumberName,
3642
+ taxNumberSource: this.taxNumberSource,
3643
+ source: this.source,
3644
+ checkedAt: this.checkedAt,
3645
+ checkId: this.checkId
3646
+ };
3647
+ }
3648
+ };
3649
+ function parseCsvRecords(csv) {
3650
+ const records = [];
3651
+ let record = [];
3652
+ let cell = "";
3653
+ let quoted = false;
3654
+ for (let i = 0; i < csv.length; i++) {
3655
+ const ch = csv[i];
3656
+ if (quoted) {
3657
+ if (ch === '"') {
3658
+ if (csv[i + 1] === '"') {
3659
+ cell += '"';
3660
+ i++;
3661
+ } else {
3662
+ quoted = false;
3663
+ }
3664
+ } else {
3665
+ cell += ch;
3666
+ }
3667
+ } else if (ch === '"') {
3668
+ quoted = true;
3669
+ } else if (ch === ",") {
3670
+ record.push(cell);
3671
+ cell = "";
3672
+ } else if (ch === "\n" || ch === "\r") {
3673
+ if (ch === "\r" && csv[i + 1] === "\n") i++;
3674
+ record.push(cell);
3675
+ records.push(record);
3676
+ record = [];
3677
+ cell = "";
3678
+ } else {
3679
+ cell += ch;
3680
+ }
3681
+ }
3682
+ if (cell !== "" || record.length > 0) {
3683
+ record.push(cell);
3684
+ records.push(record);
3685
+ }
3686
+ return records.filter((r) => !(r.length === 1 && r[0] === ""));
3687
+ }
3688
+ function parseLocalTaxNumbersCsv(csv) {
3689
+ const [header, ...records] = parseCsvRecords(csv.replace(/^\uFEFF/, ""));
3690
+ if (!header) {
3691
+ return [];
3692
+ }
3693
+ const names = header.map((name) => name.trim());
3694
+ return records.map((cells) => {
3695
+ const row = {};
3696
+ names.forEach((name, i) => {
3697
+ row[name] = cells[i];
3698
+ });
3699
+ return LocalTaxNumberRow.fromArray(row);
3700
+ });
3701
+ }
3702
+
3530
3703
  // src/endpoints/VatEndpoint.ts
3531
3704
  var DATE_REGEX2 = /^\d{4}-\d{2}-\d{2}$/;
3532
3705
  var VatEndpoint = class {
@@ -3637,6 +3810,69 @@ var VatEndpoint = class {
3637
3810
  return new Uint8Array(buffer);
3638
3811
  });
3639
3812
  }
3813
+ /**
3814
+ * Local tax numbers report as raw CSV (UTF-8 with BOM, comma-separated): every company checked —
3815
+ * single and bulk VAT checks and Smart Enrichment lookups — once, with its local tax number
3816
+ * (taxNumber, taxNumberName, taxNumberSource). Digit-only taxNumber cells carry a leading TAB so
3817
+ * Excel keeps them as text; `localTaxNumbers()` strips it for you.
3818
+ */
3819
+ async localTaxNumbersCsv(options = {}) {
3820
+ const params = this.localTaxNumbersParams(options);
3821
+ if (options.onlyWithTaxNumber !== void 0) {
3822
+ params.set("only_with_tax_number", options.onlyWithTaxNumber ? "1" : "0");
3823
+ }
3824
+ const url = this.withQuery(`${this.baseUrl}/reports/local-tax-numbers`, params);
3825
+ return withRetries(this.retryPolicy, "Local tax numbers report", async () => {
3826
+ const headers = this.buildHeaders();
3827
+ delete headers["Content-Type"];
3828
+ headers["Accept"] = "text/csv, application/json";
3829
+ const response = await this.httpClient.request("GET", url, { headers });
3830
+ if (response.status === 422) {
3831
+ await this.parseJsonResponse(response);
3832
+ }
3833
+ const text = new TextDecoder("utf-8", { ignoreBOM: true }).decode(await response.arrayBuffer());
3834
+ if (!response.ok) {
3835
+ throw new HttpException(
3836
+ describeApiError(text, response.status),
3837
+ response.status,
3838
+ text,
3839
+ {},
3840
+ response.headers.get("retry-after")
3841
+ );
3842
+ }
3843
+ return text;
3844
+ });
3845
+ }
3846
+ /** The local tax numbers report, parsed: one row per company (same filters as `localTaxNumbersCsv()`). */
3847
+ async localTaxNumbers(options = {}) {
3848
+ return parseLocalTaxNumbersCsv(await this.localTaxNumbersCsv(options));
3849
+ }
3850
+ /** How many companies the report would list for the filters, and how many have a tax number. */
3851
+ async localTaxNumbersSummary(options = {}) {
3852
+ const url = this.withQuery(
3853
+ `${this.baseUrl}/reports/local-tax-numbers/summary`,
3854
+ this.localTaxNumbersParams(options)
3855
+ );
3856
+ return withRetries(this.retryPolicy, "Local tax numbers summary", async () => {
3857
+ const response = await this.sendRequest("GET", url);
3858
+ const data = await this.parseJsonResponse(response);
3859
+ const count = (key) => typeof data[key] === "number" ? data[key] : 0;
3860
+ return { companies: count("companies"), withTaxNumber: count("withTaxNumber") };
3861
+ });
3862
+ }
3863
+ localTaxNumbersParams(options) {
3864
+ const params = new URLSearchParams();
3865
+ if (options.dateFrom !== void 0) params.set("date_from", this.formatDate(options.dateFrom));
3866
+ if (options.dateTo !== void 0) params.set("date_to", this.formatDate(options.dateTo));
3867
+ for (const country of options.countries ?? []) {
3868
+ params.append("countries[]", country.trim().toUpperCase());
3869
+ }
3870
+ return params;
3871
+ }
3872
+ withQuery(url, params) {
3873
+ const queryString = params.toString();
3874
+ return queryString ? `${url}?${queryString}` : url;
3875
+ }
3640
3876
  buildHeaders() {
3641
3877
  const headers = {
3642
3878
  "Content-Type": "application/json",
@@ -3797,7 +4033,7 @@ var AuthRetryHttpClient = class {
3797
4033
  };
3798
4034
 
3799
4035
  // src/version.ts
3800
- var SDK_VERSION = "1.5.0";
4036
+ var SDK_VERSION = "1.7.0";
3801
4037
 
3802
4038
  // src/http/SdkVersionHttpClient.ts
3803
4039
  var SDK_VERSION_HEADER = "X-Taxora-SDK-Version";
@@ -4011,6 +4247,7 @@ function toBytes(rawBody) {
4011
4247
  ImportResult,
4012
4248
  InMemoryTokenStorage,
4013
4249
  Language,
4250
+ LocalTaxNumberRow,
4014
4251
  LoginIdentifier,
4015
4252
  NorwegianOrgNumber,
4016
4253
  PeppolLookupResult,
@@ -4063,6 +4300,7 @@ function toBytes(rawBody) {
4063
4300
  generateWebhookSignatureHeader,
4064
4301
  getBaseUrl,
4065
4302
  getFailedVatStates,
4303
+ parseLocalTaxNumbersCsv,
4066
4304
  parseWebhookSignatureHeader,
4067
4305
  toComplianceDocumentType,
4068
4306
  toComplianceEnrollmentStatus,