@taxora/sdk 1.0.7 → 1.2.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
@@ -301,6 +301,331 @@ const results = await client.vat.search('Alpha Handels', 25 /* perPage */);
301
301
 
302
302
  ---
303
303
 
304
+ ## 🔎 Smart Enrichment (reverse VAT lookup)
305
+
306
+ Resolve a company **name + address + country → VAT number + confidence**. A confident match
307
+ returns synchronously; harder cases (and all bulk batches) are processed asynchronously and
308
+ delivered via the `enrichment.completed` webhook — poll with `get(jobId)` in the meantime.
309
+ Requires the Smart Enrichment add-on to be active on your account.
310
+
311
+ ```ts
312
+ // Single lookup
313
+ let job = await client.smartEnrichment.lookup({
314
+ companyName: 'Example Company GmbH',
315
+ country: 'AT',
316
+ street: 'Main Street 10',
317
+ postalCode: '1010',
318
+ city: 'Vienna',
319
+ });
320
+
321
+ if (job.isProcessing()) {
322
+ // Resolved asynchronously — poll until done (or wait for the webhook)
323
+ job = await client.smartEnrichment.waitForCompletion(job.jobId, {
324
+ pollIntervalMs: 2000, // optional, default 2000
325
+ timeoutMs: 120_000, // optional, default 120000 — throws TaxoraException on timeout
326
+ });
327
+ }
328
+
329
+ const result = job.result();
330
+ console.log(result?.status); // 'found' | 'no_vat_exists' | 'not_found'
331
+ console.log(result?.vatNumber); // e.g. 'ATU12345678'
332
+ console.log(result?.confidence); // 0–100
333
+
334
+ // Search mode: 'default' searches with one AI provider and only consults a second one when
335
+ // the first finds nothing. 'complex' has two independent providers search from the start and
336
+ // puts every number either of them proposes through the tax authority's checks. Complex finds
337
+ // companies a single search misses — typically ones whose VAT only appears in a website
338
+ // Impressum — and costs more per lookup, so it is opt-in.
339
+ const deep = await client.smartEnrichment.lookup({
340
+ companyName: 'Vodafone D2 GmbH',
341
+ country: 'DE',
342
+ postalCode: '40547',
343
+ mode: SmartEnrichmentMode.COMPLEX,
344
+ });
345
+
346
+ // When more than one provider searched, you can see what each one concluded on its own.
347
+ // Two of them reporting the same number is the strongest confirmation this layer produces.
348
+ for (const verdict of deep.result()?.providerVerdicts ?? []) {
349
+ console.log(`${verdict.provider}: ${verdict.vatNumber ?? 'nothing'}`);
350
+ }
351
+
352
+ // The API also grades the address you sent. Worth writing back into your own records: a
353
+ // 'postal_code_city_mismatch' / 'postal_code_unassigned' warning means the postal code in
354
+ // your source data is wrong, and `derivedPlace` is the area it actually points at.
355
+ const quality = deep.result()?.addressQuality;
356
+ if (quality && quality.warnings.length > 0) {
357
+ console.log(quality.warnings.join(', '), '→', quality.derivedPlace);
358
+ }
359
+
360
+ // Bulk lookup (always async → webhook + polling)
361
+ // The batch-level mode applies to every row; a row carrying its own `mode` overrides it,
362
+ // so you can pay for 'complex' on just the rows that need it.
363
+ const bulk = await client.smartEnrichment.bulkLookup([
364
+ { companyName: 'A GmbH', country: 'DE', city: 'Berlin' },
365
+ { companyName: 'B SARL', country: 'FR' },
366
+ ]);
367
+ const done = await client.smartEnrichment.waitForCompletion(bulk.jobId);
368
+ for (const r of done.results) {
369
+ console.log(r.status, r.vatNumber ?? '—', r.matchedAddress ?? '');
370
+ }
371
+
372
+ // Lookup history (paginated, newest first; perPage is capped at 100 server-side)
373
+ const history = await client.smartEnrichment.history(1, 25, 'Example GmbH' /* optional free-text search */);
374
+ console.log(`${history.total} lookups`);
375
+ console.log(history.stats?.total, history.stats?.found); // account-wide tiles (independent of the search filter)
376
+ for (const row of history) {
377
+ console.log(row.queryCompanyName, '→', row.result?.vatNumber ?? row.status);
378
+ }
379
+ ```
380
+
381
+ ### Quota usage & billing history
382
+
383
+ ```ts
384
+ const usage = await client.smartEnrichment.usage();
385
+ console.log(usage.period, `${usage.used}/${usage.included}`, 'remaining:', usage.remaining);
386
+ console.log(usage.overageCount, '×', usage.overagePrice, '=', usage.overageAmount);
387
+ for (const entry of usage.history) {
388
+ console.log(entry.period, entry.matches, entry.amount, entry.state); // 'billed' | 'pending'
389
+ }
390
+ ```
391
+
392
+ ### CSV export
393
+
394
+ Download your lookups as CSV in the bulk-input shape plus the resolved VAT columns (bulk jobs
395
+ are flattened to one line per input row). All filters are optional.
396
+
397
+ ```ts
398
+ import { SmartEnrichmentStatus } from '@taxora/sdk';
399
+ import { writeFileSync } from 'fs';
400
+
401
+ const csv = await client.smartEnrichment.export({
402
+ dateFrom: '2026-01-01', // job created_at lower bound (YYYY-MM-DD)
403
+ dateTo: '2026-06-30', // job created_at upper bound (YYYY-MM-DD)
404
+ minConfidence: 80, // 0–100
405
+ status: [SmartEnrichmentStatus.FOUND, SmartEnrichmentStatus.NO_VAT_EXISTS],
406
+ onlyFound: true, // only rows that resolved to a VAT number
407
+ });
408
+ writeFileSync('smart-enrichment.csv', csv);
409
+ ```
410
+
411
+ ### Statistics
412
+
413
+ Aggregated lookup statistics: headline totals, a time series and breakdowns by source, outcome
414
+ and confidence band. Defaults to the last 12 months and a monthly interval (server-side).
415
+
416
+ ```ts
417
+ const stats = await client.smartEnrichment.statistics({
418
+ dateFrom: '2026-01-01', // optional (YYYY-MM-DD)
419
+ dateTo: '2026-06-30', // optional (YYYY-MM-DD)
420
+ interval: 'month', // 'day' | 'week' | 'month'
421
+ });
422
+
423
+ console.log(stats.totals.items, 'items,', stats.totals.found, `found (${stats.totals.foundRate}%)`);
424
+ console.log('avg confidence:', stats.totals.avgConfidence);
425
+ for (const bucket of stats.timeSeries) {
426
+ console.log(`${bucket.bucket}: ${bucket.found}/${bucket.items}`);
427
+ }
428
+ for (const band of stats.confidenceBuckets) {
429
+ console.log(band.bucket, band.count); // high / medium / low
430
+ }
431
+ ```
432
+
433
+ > Note: `no_vat_exists` means the company was identified but legitimately has **no** VAT/UID
434
+ > number (common for purely-domestic German firms). It is a definitive answer — not a failure —
435
+ > and is not billed.
436
+
437
+ ---
438
+
439
+ ## 🇫🇷 E-Reporting / Compliance (DGFiP Flux 10)
440
+
441
+ Full French e-reporting integration: enroll a company with the compliance provider, record and
442
+ submit transactions (individually or via CSV import), and track the resulting DGFiP tax reports.
443
+ All `compliance` routes require an active E-Reporting subscription / feature grant on your
444
+ account — except `requestEReportingAccess()`, which is how you ask for one.
445
+
446
+ ### Request feature activation
447
+
448
+ Not enabled yet? This route is deliberately **not** gated — it requests activation of the
449
+ E-Reporting feature for your account (the authenticated user's name/email take precedence over
450
+ the form values):
451
+
452
+ ```ts
453
+ await client.eReporting.requestEReportingAccess({
454
+ company: 'Example GmbH',
455
+ phone: '+43 660 1234567',
456
+ message: 'Please activate e-reporting for our account.',
457
+ language: 'de', // locale of the confirmation mail
458
+ });
459
+ ```
460
+
461
+ ### 1. SIRENE lookup (prefill enrollment data)
462
+
463
+ Resolve company data by SIRET, SIREN or French VAT number via the French SIRENE registry.
464
+ **Rate-limited to 3 requests/minute per user** (protects the external French government API).
465
+
466
+ ```ts
467
+ const company = await client.eReporting.sireneLookup('FR32123456789'); // or a SIRET/SIREN
468
+
469
+ console.log(company.companyName, company.siret);
470
+ console.log(company.nafCode, company.enterpriseSize); // e.g. "47", "pme"
471
+ ```
472
+
473
+ ### 2. Create an enrollment
474
+
475
+ Creates the local enrollment, provisions the provider account and (by default) activates the
476
+ DGFiP regime. Either `siret` or `siren` is required. On provider failure the API responds 502
477
+ (an `HttpException`) and the enrollment is persisted in an error state for retries.
478
+
479
+ ```ts
480
+ const enrollment = await client.eReporting.createEnrollment({
481
+ siret: company.siret!, // or siren: company.siren
482
+ companyName: company.companyName,
483
+ address: company.address ?? undefined,
484
+ city: company.city ?? undefined,
485
+ postalcode: company.postalcode ?? undefined,
486
+ email: 'tax@acme.fr',
487
+ nafCode: company.nafCode!, // 2-digit NAF division, e.g. "47"
488
+ enterpriseSize: 'pme', // 'micro' | 'pme' | 'eti' | 'ge'
489
+ typeOperation: 'mixed', // 'services' | 'goods' | 'mixed'
490
+ reportingStartDate: '2026-09-01', // or a Date
491
+ autoActivate: true, // optional — false creates the account without regime activation
492
+ });
493
+
494
+ console.log(enrollment.id, enrollment.status, enrollment.statusLabel);
495
+
496
+ const page = await client.eReporting.listEnrollments(1, 25); // paginated
497
+ const single = await client.eReporting.getEnrollment(enrollment.id);
498
+ ```
499
+
500
+ ### 3. Record & submit transactions
501
+
502
+ Records the transaction and (by default) submits it right away; pass `submitNow: false` to defer.
503
+ Creation is **idempotent** on the invoice's natural key (enrollment, type, invoice number,
504
+ invoice date, counterparty VAT) — a client retry returns the existing transaction instead of
505
+ creating a duplicate.
506
+
507
+ > **Note:** Immediate per-invoice submission (and `submitTransaction()`) requires the
508
+ > **e-invoicing** feature on your account. In pure e-reporting mode transactions are only
509
+ > recorded here and reported automatically via the aggregated daily ledgers —
510
+ > `submitTransaction()` then returns a 422 `ValidationException` explaining this.
511
+
512
+ ```ts
513
+ const tx = await client.eReporting.createTransaction({
514
+ complianceEnrollmentId: enrollment.id,
515
+ transactionType: 'b2c_outbound', // | 'b2b_domestic_outbound' | 'b2b_domestic_inbound'
516
+ // | 'crossborder_outbound' | 'crossborder_inbound'
517
+ invoiceNumber: 'TKT-2026-00001',
518
+ invoiceDate: '2026-07-01', // or a Date
519
+ currency: 'EUR', // optional, default EUR
520
+ subtotal: 100,
521
+ taxAmount: 20,
522
+ total: 120,
523
+ invoiceLines: [
524
+ {
525
+ description: 'Ticket',
526
+ quantity: 1,
527
+ price: 100,
528
+ taxes: [{ name: 'TVA', percent: 20, category: 'S' }], // category E needs a VATEX `comment`
529
+ },
530
+ ],
531
+ submitNow: false, // record now, submit later
532
+ });
533
+
534
+ // Submit (or retry) a pending/error transaction later — replays the stored invoice lines
535
+ const submitted = await client.eReporting.submitTransaction(tx.id);
536
+ console.log(submitted.state, submitted.stateLabel); // e.g. "submitted"
537
+
538
+ // Manage recorded transactions (only `pending` ones can be edited/deleted)
539
+ const transactions = await client.eReporting.listTransactions({
540
+ dateFrom: '2026-07-01', // or a Date
541
+ dateTo: '2026-07-31',
542
+ state: 'pending', // 'pending' | 'sending' | 'submitted' | 'error'
543
+ transactionType: 'b2c_outbound',
544
+ page: 1,
545
+ perPage: 25,
546
+ });
547
+ const one = await client.eReporting.getTransaction(tx.id);
548
+ await client.eReporting.updateTransaction(tx.id, { total: 130, taxAmount: 21.67 });
549
+ await client.eReporting.deleteTransaction(tx.id); // resolves void on 204
550
+ ```
551
+
552
+ ### CSV import
553
+
554
+ Bulk-import from a semicolon-separated CSV (one row per invoice line; rows sharing an
555
+ `invoice_number` are grouped into one transaction). Idempotent: re-uploading skips rows whose
556
+ invoice already exists.
557
+
558
+ ```ts
559
+ import { readFileSync } from 'fs';
560
+
561
+ const result = await client.eReporting.importTransactions(
562
+ enrollment.id,
563
+ readFileSync('transactions.csv', 'utf8'),
564
+ 'transactions.csv', // optional filename
565
+ );
566
+
567
+ console.log(result.created, result.skippedDuplicates);
568
+ for (const error of result.errors) console.warn(error); // row-level errors
569
+ ```
570
+
571
+ ### 4. Track tax reports
572
+
573
+ Every submitted transaction is tracked through the aggregated DGFiP ledger lifecycle
574
+ (`new → sent → acknowledged → registered` or `refused`/`error`).
575
+
576
+ ```ts
577
+ const reports = await client.eReporting.listTaxReports(1, 25, 'refused'); // state filter optional
578
+ for (const report of reports) {
579
+ console.log(report.state, report.stateLabel, report.isTerminal, report.refusalReason);
580
+ }
581
+
582
+ const report = await client.eReporting.getTaxReport(reports.rows[0]!.id);
583
+ console.log(report.baseAmount, report.taxAmount, report.totalAmount); // precision-safe strings
584
+ ```
585
+
586
+ ### VAT rates & DGFiP tax categories
587
+
588
+ The canonical rates for a reporting country (the seller's reporting-country rates). Use these to
589
+ build the `taxes` entries of your invoice lines.
590
+
591
+ ```ts
592
+ const rates = await client.eReporting.getVatRates('FR'); // defaults to FR when omitted
593
+
594
+ for (const rate of rates.rates) {
595
+ console.log(rate.percent, rate.category, rate.taxName, rate.requiresVatex);
596
+ // 20 'S' 'TVA' false … 0 'E' 'TVA' true (VATEX code required in the line's comment)
597
+ }
598
+ ```
599
+
600
+ ### Revenue statistics
601
+
602
+ Read-only turnover aggregation over your e-reporting transactions — totals, a time series and
603
+ breakdowns by transaction type, counterparty country and state. All headline figures are
604
+ expressed in the most frequent currency in the range (`primaryCurrency`); when more than one
605
+ currency is present, `isMultiCurrency` is `true` and `byCurrency` lists each one. Monetary values
606
+ are returned as 4-decimal strings to preserve precision.
607
+
608
+ ```typescript
609
+ const stats = await client.eReporting.getRevenueStatistics({
610
+ dateFrom: '2026-01-01', // optional — defaults to 12 months ago (server-side)
611
+ dateTo: new Date(), // optional — defaults to today
612
+ interval: 'month', // 'day' | 'week' | 'month' (default 'month')
613
+ transactionType: 'b2c_outbound', // optional filter
614
+ state: 'submitted', // optional filter
615
+ });
616
+
617
+ console.log(stats.primaryCurrency); // e.g. EUR
618
+ console.log(stats.totals.total); // e.g. 120000.0000
619
+ for (const bucket of stats.timeSeries) {
620
+ console.log(`${bucket.bucket}: ${bucket.total}`); // 2026-01: 10800.0000
621
+ }
622
+ for (const row of stats.byCountry) {
623
+ console.log(`${row.country} → ${row.total}`); // 'unknown' groups NULL partners
624
+ }
625
+ ```
626
+
627
+ ---
628
+
304
629
  ## 📜 Certificates
305
630
 
306
631
  ### Single Certificate (PDF)
@@ -371,11 +696,73 @@ try {
371
696
  console.error('Validation errors:', err.getErrors()); // Record<string, string[]>
372
697
  } else if (err instanceof HttpException) {
373
698
  // Any other HTTP error
374
- console.error(`HTTP ${err.getStatusCode()}:`, err.getResponseBody());
699
+ console.error(`HTTP ${err.getStatusCode()}:`, err.message);
375
700
  }
376
701
  }
377
702
  ```
378
703
 
704
+ **Exception messages are always short and safe to log.** A response body is
705
+ never used as the message: gateways in front of the API answer `502`/`503`/`504`
706
+ with a full HTML error page, and that page would otherwise land in every log
707
+ line of your application. The SDK uses the `message` / `error` field of a JSON
708
+ error body when there is one, and falls back to the status line
709
+ (`Taxora API request failed (HTTP 504 Gateway Timeout).`) otherwise. The
710
+ untouched body stays available via `err.getResponseBody()`.
711
+
712
+ ### Automatic retries
713
+
714
+ The SDK retries what never produced an answer from the API itself: **3 attempts,
715
+ with 500 ms and 1000 ms of backoff in between**. That covers
716
+
717
+ - **gateway failures (`502`, `503`, `504`)** — the infrastructure in front of the
718
+ API, where a second attempt a moment later usually succeeds;
719
+ - **connection-level failures** — reset connections, client-side timeouts, DNS
720
+ hiccups (`fetch failed`, `ECONNRESET`, `UND_ERR_CONNECT_TIMEOUT`, …). Once the
721
+ attempts are used up, the original error is rethrown unchanged;
722
+ - **`429 Too Many Requests`, but only with a `Retry-After`** — then the SDK waits
723
+ exactly as long as the API asked instead of using its own backoff. Without that
724
+ header a retry would only hammer a rate limit that is already tripped, so the
725
+ error is surfaced immediately. A `Retry-After` longer than `maxRetryAfterMs`
726
+ (10 s by default) also fails immediately rather than blocking your worker — the
727
+ raw header stays readable via `err.getRetryAfter()`.
728
+
729
+ Everything else fails on the first attempt, including any `5xx` the API produced
730
+ itself (a `500` is a bug, not a hiccup — repeating it just costs time).
731
+
732
+ Retries apply to read-only calls only — VAT validation, lookups, state, history,
733
+ search, certificate downloads, company and e-reporting reads, login and token
734
+ refresh. Calls that change state or cost quota (`certificatesBulkExport()`,
735
+ `smartEnrichment.lookup()`, e-reporting enrollment/transaction writes) are
736
+ **never** retried automatically, because a gateway timeout does not tell us
737
+ whether the API already processed the request.
738
+
739
+ Once the attempts are used up you get one clean error:
740
+
741
+ ```
742
+ Taxora VAT validation failed after 3 attempts (HTTP 504 Gateway Timeout).
743
+ ```
744
+
745
+ Tune or disable it via `RetryPolicy`:
746
+
747
+ ```ts
748
+ import { RetryPolicy, TaxoraClientFactory } from '@taxora/sdk';
749
+
750
+ const client = TaxoraClientFactory.create({
751
+ apiKey: 'YOUR_X_API_KEY',
752
+ retryPolicy: new RetryPolicy({
753
+ maxAttempts: 5,
754
+ initialDelayMs: 250,
755
+ retryableStatusCodes: [502, 503, 504], // add or drop status codes
756
+ respectRetryAfter: true, // false = ignore the header entirely
757
+ maxRetryAfterMs: 10_000, // longest wait accepted from Retry-After
758
+ retryOnNetworkErrors: true, // false = surface transport errors at once
759
+ }),
760
+ });
761
+
762
+ // or, for callers that do their own retrying (queue workers, cron jobs):
763
+ const noRetries = TaxoraClientFactory.create({ apiKey: '…', retryPolicy: RetryPolicy.disabled() });
764
+ ```
765
+
379
766
  ---
380
767
 
381
768
  ## 🔌 VatState Helpers