@fin.cx/opendata 4.2.0 → 4.3.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.
@@ -0,0 +1,141 @@
1
+ import type { evatrStatusCodes, vatIdCountryCodes } from './constants.js';
2
+ /** A country code a VAT identification number starts with (`EL` for Greece, `XI` for Northern Ireland). */
3
+ export type TVatIdCountryCode = (typeof vatIdCountryCodes)[number];
4
+ /** A status code of the eVatR API. */
5
+ export type TEvatrStatusCode = (typeof evatrStatusCodes)[number];
6
+ /** A VAT identification number taken apart: its country code and the number after it. */
7
+ export interface IVatIdParts {
8
+ countryCode: TVatIdCountryCode;
9
+ number: string;
10
+ /** both together, without spaces, dots or hyphens, in capitals: `ATU12345678` */
11
+ vatId: string;
12
+ }
13
+ /**
14
+ * The fetch a client sends its requests with: the runtime's `fetch`, or one that decides which
15
+ * addresses may be reached (for example `createPublicOnlyFetch` of `@push.rocks/smartrequest`).
16
+ */
17
+ export type TVatIdFetch = (input: string, init: RequestInit) => Promise<Response>;
18
+ export interface IVatIdClientOptions {
19
+ /** default: the runtime's `fetch` */
20
+ fetch?: TVatIdFetch;
21
+ /** default: the official address (`evatrBaseUrl`, `viesBaseUrl`); a test's own server otherwise */
22
+ baseUrl?: string;
23
+ /** how long one request may take, the answer included; default `defaultVatIdTimeoutMs` */
24
+ timeoutMs?: number;
25
+ /** sent as `User-Agent` where given */
26
+ userAgent?: string;
27
+ }
28
+ /**
29
+ * A service's answer as it arrived: its HTTP status and its body, byte for byte as text. It is what the
30
+ * service said, whatever of it this client reads, so a caller can keep it as the evidence of a
31
+ * confirmation (§ 17d Abs. 1 UStDV asks the VAT ID of the buyer of an intra-Community supply to be
32
+ * shown in the books).
33
+ */
34
+ export interface IVatIdServiceAnswer {
35
+ httpStatus: number;
36
+ text: string;
37
+ }
38
+ /** The business data a qualified confirmation compares with what the member state registered. */
39
+ export interface IVatIdTraderData {
40
+ name: string;
41
+ street?: string;
42
+ postalCode?: string;
43
+ city: string;
44
+ }
45
+ /**
46
+ * What the member state says about one field of a qualified confirmation (eVatR):
47
+ * - `A`: it matches the registered data;
48
+ * - `B`: it does not match;
49
+ * - `C`: it was not asked about;
50
+ * - `D`: the member state did not give it.
51
+ */
52
+ export type TEvatrFieldResult = 'A' | 'B' | 'C' | 'D';
53
+ export interface IEvatrConfirmationInput {
54
+ /** the German VAT ID of the business that asks (`anfragendeUstid`); § 18e UStG confirms to a business */
55
+ requesterVatId: string;
56
+ /** the VAT ID asked about (`angefragteUstid`) */
57
+ vatId: string;
58
+ /** with the business data, a qualified confirmation; `name` and `city` are what makes it one */
59
+ trader?: IVatIdTraderData;
60
+ signal?: AbortSignal;
61
+ }
62
+ /**
63
+ * The BZSt's answer about one VAT ID. `valid` holds for `evatr-0000` and `evatr-2008` (valid, with a
64
+ * particularity of the qualified confirmation the BZSt explains on request); `evatr-2002` (not yet
65
+ * valid, from `validFrom`), `evatr-2006` (was valid from `validFrom` to `validTo`) and `evatr-2001`
66
+ * (not assigned) are answers that it is not.
67
+ */
68
+ export interface IEvatrConfirmation {
69
+ service: 'bzst-evatr';
70
+ /** the BZSt's id of the request (`id`), the reference of the confirmation */
71
+ requestId?: string;
72
+ /** when the BZSt answered, as it wrote it (`anfrageZeitpunkt`, ISO 8601 with its offset) */
73
+ requestedAt: string;
74
+ /** the same, as epoch milliseconds */
75
+ requestedAtMs: number;
76
+ status: 'evatr-0000' | 'evatr-2001' | 'evatr-2002' | 'evatr-2006' | 'evatr-2008';
77
+ valid: boolean;
78
+ /** `gueltigAb`, as the BZSt wrote it; its form is not documented */
79
+ validFrom?: string;
80
+ /** `gueltigBis`, as the BZSt wrote it */
81
+ validTo?: string;
82
+ /** what was asked, as it was sent */
83
+ requesterVatId: string;
84
+ vatId: string;
85
+ /**
86
+ * A qualified request (`trader` given) whose VAT ID is valid: what was compared and the member
87
+ * state's result for each field sent (`answered: true`), or, for `evatr-2008`, that the comparison
88
+ * has a particularity the BZSt explains on request and gives no result (`answered: false`). Absent
89
+ * for a simple request and for an answer that the VAT ID is not valid, which compares nothing.
90
+ */
91
+ qualified?: TEvatrQualifiedResult;
92
+ /** the answer as it arrived */
93
+ answer: IVatIdServiceAnswer;
94
+ }
95
+ export type TEvatrQualifiedResult = {
96
+ asked: IVatIdTraderData;
97
+ answered: true;
98
+ name: TEvatrFieldResult;
99
+ street?: TEvatrFieldResult;
100
+ postalCode?: TEvatrFieldResult;
101
+ city: TEvatrFieldResult;
102
+ } | {
103
+ asked: IVatIdTraderData;
104
+ answered: false;
105
+ };
106
+ /** What VIES says about one field of the trader data: matches, does not match, or not compared. */
107
+ export type TViesMatch = 'VALID' | 'INVALID' | 'NOT_PROCESSED';
108
+ export interface IViesCheckInput {
109
+ /** the VAT ID asked about, with its country code */
110
+ vatId: string;
111
+ /** the business that asks, with its own VAT ID; VIES gives a request identifier only with it */
112
+ requesterVatId?: string;
113
+ /** with the business data, VIES compares it ("approximate matching") */
114
+ trader?: IVatIdTraderData;
115
+ signal?: AbortSignal;
116
+ }
117
+ /** VIES's answer about one VAT ID. */
118
+ export interface IViesCheck {
119
+ service: 'vies';
120
+ valid: boolean;
121
+ /** when VIES answered (`requestDate`, ISO 8601) */
122
+ requestedAt: string;
123
+ requestedAtMs: number;
124
+ /** VIES's reference of the request (`requestIdentifier`); only where the requester was named */
125
+ requestId?: string;
126
+ vatId: string;
127
+ requesterVatId?: string;
128
+ /** the name and the address the member state registered, where it gives them */
129
+ name?: string;
130
+ address?: string;
131
+ /** the trader data asked about, and VIES's comparison of each field */
132
+ compared?: {
133
+ asked: IVatIdTraderData;
134
+ name?: TViesMatch;
135
+ street?: TViesMatch;
136
+ postalCode?: TViesMatch;
137
+ city?: TViesMatch;
138
+ };
139
+ /** the answer as it arrived, with everything VIES gave that is not read into the fields above */
140
+ answer: IVatIdServiceAnswer;
141
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3ZhdGlkL2ludGVyZmFjZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
@@ -0,0 +1,16 @@
1
+ import { type IVatIdErrorDetails } from './classes.vatiderror.js';
2
+ import type { IVatIdClientOptions, IVatIdServiceAnswer } from './interfaces.js';
3
+ /** An answer as it arrived (`answer`, its status and its text), and its body read as JSON, or `undefined` where it was none. */
4
+ export interface IJsonAnswer {
5
+ httpStatus: number;
6
+ body: unknown;
7
+ answer: IVatIdServiceAnswer;
8
+ }
9
+ /**
10
+ * Sends one JSON request and reads the answer whole, within the client's time. The caller's signal
11
+ * ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
12
+ * `unreachable`; nothing is retried. The timer ends with the request, answered or not.
13
+ */
14
+ export declare const postJson: (optionsArg: IVatIdClientOptions, serviceArg: IVatIdErrorDetails["service"], urlArg: string, bodyArg: unknown, signalArg: AbortSignal | undefined) => Promise<IJsonAnswer>;
15
+ /** A string field of an answer, where it is one and not empty. */
16
+ export declare const textField: (bodyArg: unknown, keyArg: string) => string | undefined;
@@ -0,0 +1,65 @@
1
+ import { VatIdError } from './classes.vatiderror.js';
2
+ import { defaultVatIdTimeoutMs } from './constants.js';
3
+ /**
4
+ * Sends one JSON request and reads the answer whole, within the client's time. The caller's signal
5
+ * ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
6
+ * `unreachable`; nothing is retried. The timer ends with the request, answered or not.
7
+ */
8
+ export const postJson = async (optionsArg, serviceArg, urlArg, bodyArg, signalArg) => {
9
+ const fetchFn = optionsArg.fetch ?? ((inputArg, initArg) => fetch(inputArg, initArg));
10
+ const timeout = new AbortController();
11
+ const timer = setTimeout(() => timeout.abort(new Error('the client\'s time ran out')), optionsArg.timeoutMs ?? defaultVatIdTimeoutMs);
12
+ const signal = signalArg ? AbortSignal.any([signalArg, timeout.signal]) : timeout.signal;
13
+ const failed = (errorArg) => {
14
+ if (signalArg?.aborted) {
15
+ return new VatIdError('aborted', 'the request was ended by its caller', { service: serviceArg }, { cause: errorArg });
16
+ }
17
+ if (timeout.signal.aborted) {
18
+ return new VatIdError('timeout', 'the service did not answer in time', { service: serviceArg }, { cause: errorArg });
19
+ }
20
+ return new VatIdError('unreachable', 'the service could not be reached', { service: serviceArg }, { cause: errorArg });
21
+ };
22
+ let response;
23
+ let text;
24
+ try {
25
+ if (signalArg?.aborted) {
26
+ throw failed(signalArg.reason);
27
+ }
28
+ try {
29
+ response = await fetchFn(urlArg, {
30
+ method: 'POST',
31
+ headers: {
32
+ 'content-type': 'application/json',
33
+ accept: 'application/json',
34
+ ...(optionsArg.userAgent ? { 'user-agent': optionsArg.userAgent } : {}),
35
+ },
36
+ body: JSON.stringify(bodyArg),
37
+ signal,
38
+ });
39
+ text = await response.text();
40
+ }
41
+ catch (error) {
42
+ throw failed(error);
43
+ }
44
+ }
45
+ finally {
46
+ clearTimeout(timer);
47
+ }
48
+ let body;
49
+ try {
50
+ body = text.trim() === '' ? undefined : JSON.parse(text);
51
+ }
52
+ catch {
53
+ body = undefined;
54
+ }
55
+ return { httpStatus: response.status, body, answer: { httpStatus: response.status, text } };
56
+ };
57
+ /** A string field of an answer, where it is one and not empty. */
58
+ export const textField = (bodyArg, keyArg) => {
59
+ if (!bodyArg || typeof bodyArg !== 'object') {
60
+ return undefined;
61
+ }
62
+ const value = Reflect.get(bodyArg, keyArg);
63
+ return typeof value === 'string' && value !== '' ? value : undefined;
64
+ };
65
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3ZhdGlkL3JlcXVlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBMkIsTUFBTSx5QkFBeUIsQ0FBQztBQUM5RSxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQVV2RDs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sUUFBUSxHQUFHLEtBQUssRUFDM0IsVUFBK0IsRUFDL0IsVUFBeUMsRUFDekMsTUFBYyxFQUNkLE9BQWdCLEVBQ2hCLFNBQWtDLEVBQ1osRUFBRTtJQUN4QixNQUFNLE9BQU8sR0FBZ0IsVUFBVSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQ25HLE1BQU0sT0FBTyxHQUFHLElBQUksZUFBZSxFQUFFLENBQUM7SUFDdEMsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLENBQUMsNEJBQTRCLENBQUMsQ0FBQyxFQUFFLFVBQVUsQ0FBQyxTQUFTLElBQUkscUJBQXFCLENBQUMsQ0FBQztJQUN0SSxNQUFNLE1BQU0sR0FBRyxTQUFTLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLEVBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUM7SUFDekYsTUFBTSxNQUFNLEdBQUcsQ0FBQyxRQUFpQixFQUFjLEVBQUU7UUFDL0MsSUFBSSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7WUFDdkIsT0FBTyxJQUFJLFVBQVUsQ0FBQyxTQUFTLEVBQUUscUNBQXFDLEVBQUUsRUFBRSxPQUFPLEVBQUUsVUFB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
@@ -0,0 +1,8 @@
1
+ import type { IVatIdParts } from './interfaces.js';
2
+ /**
3
+ * Takes a VAT identification number apart: its country code and the number after it, without the
4
+ * spaces, dots and hyphens it is often printed with, in capitals. `GR` is written `EL`, as VAT IDs of
5
+ * Greece are. Answers `undefined` for anything that does not start with a member state's code or has
6
+ * no number after it; whether the number has its member state's form is the service's to say.
7
+ */
8
+ export declare const splitVatId: (valueArg: string) => IVatIdParts | undefined;
@@ -0,0 +1,22 @@
1
+ import { vatIdCountryCodes } from './constants.js';
2
+ /** A VAT ID as it is printed: spaces, dots and hyphens between its characters are left out. */
3
+ const SEPARATORS = /[\s.\-]+/gu;
4
+ /** After the country code: letters and digits, 2 to 13 of them (the longest form, France and the Netherlands, has 12). */
5
+ const NUMBER_PART = /^[A-Z0-9+*]{2,13}$/u;
6
+ /**
7
+ * Takes a VAT identification number apart: its country code and the number after it, without the
8
+ * spaces, dots and hyphens it is often printed with, in capitals. `GR` is written `EL`, as VAT IDs of
9
+ * Greece are. Answers `undefined` for anything that does not start with a member state's code or has
10
+ * no number after it; whether the number has its member state's form is the service's to say.
11
+ */
12
+ export const splitVatId = (valueArg) => {
13
+ const compact = valueArg.replace(SEPARATORS, '').toUpperCase();
14
+ const prefix = compact.slice(0, 2);
15
+ const countryCode = prefix === 'GR' ? 'EL' : prefix;
16
+ const number = compact.slice(2);
17
+ if (!vatIdCountryCodes.includes(countryCode) || !NUMBER_PART.test(number)) {
18
+ return undefined;
19
+ }
20
+ return { countryCode: countryCode, number, vatId: `${countryCode}${number}` };
21
+ };
22
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmF0aWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy92YXRpZC92YXRpZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUduRCwrRkFBK0Y7QUFDL0YsTUFBTSxVQUFVLEdBQUcsWUFBWSxDQUFDO0FBRWhDLDBIQUEwSDtBQUMxSCxNQUFNLFdBQVcsR0FBRyxxQkFBcUIsQ0FBQztBQUUxQzs7Ozs7R0FLRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRyxDQUFDLFFBQWdCLEVBQTJCLEVBQUU7SUFDdEUsTUFBTSxPQUFPLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDL0QsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDbkMsTUFBTSxXQUFXLEdBQUcsTUFBTSxLQUFLLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDcEQsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNoQyxJQUFJLENBQUUsaUJBQXVDLENBQUMsUUFBUSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ2pHLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCxPQUFPLEVBQUUsV0FBVyxFQUFFLFdBQWdDLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxHQUFHLFdBQVcsR0FBRyxNQUFNLEVBQUUsRUFBRSxDQUFDO0FBQ3JHLENBQUMsQ0FBQyJ9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/opendata",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "private": false,
5
5
  "description": "Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, and stock and crypto prices and fundamentals from several providers.",
6
6
  "main": "dist_ts/index.js",
@@ -22,6 +22,10 @@
22
22
  "types": "./dist_ts/baserate/index.d.ts",
23
23
  "import": "./dist_ts/baserate/index.js"
24
24
  },
25
+ "./vatid": {
26
+ "types": "./dist_ts/vatid/index.d.ts",
27
+ "import": "./dist_ts/vatid/index.js"
28
+ },
25
29
  "./package.json": "./package.json"
26
30
  },
27
31
  "type": "module",
@@ -30,8 +34,8 @@
30
34
  "devDependencies": {
31
35
  "@git.zone/tsbuild": "^5.0.0",
32
36
  "@git.zone/tsbundle": "^2.15.0",
33
- "@git.zone/tsrun": "^3.0.0",
34
- "@git.zone/tstest": "^6.3.1",
37
+ "@git.zone/tsrun": "^3.0.1",
38
+ "@git.zone/tstest": "^6.3.2",
35
39
  "@types/node": "^26.6.2"
36
40
  },
37
41
  "dependencies": {
package/readme.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  `@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, the statutory public holidays of the German federal states, and the base rate of § 247 BGB. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
4
4
 
5
- It gives you five practical lanes in one package:
5
+ It gives you six practical lanes in one package:
6
6
 
7
7
  - `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
8
8
  - `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
9
9
  - `HandelsregisterClient`, from `@fin.cx/opendata/handelsregister`, for single lookups in the commercial, cooperative, partnership, association and civil-law partnership registers over plain HTTP, without any storage
10
10
  - the holiday calendar, from `@fin.cx/opendata/holidays`, for the statutory public holidays of each German federal state and the working days of § 193 BGB, computed per year without data files or requests
11
11
  - the base rate, from `@fin.cx/opendata/baserate`, for the Basiszinssatz of § 247 BGB per half-year as the Deutsche Bundesbank publishes it, for default interest under § 288 BGB
12
+ - VAT ID confirmation, from `@fin.cx/opendata/vatid`, over the BZSt's eVatR REST API (the confirmation of § 18e UStG, simple or qualified) and the European Commission's VIES REST API, without any storage
12
13
 
13
14
  ## Issue Reporting and Security
14
15
 
@@ -70,6 +71,40 @@ Everything here comes from `@fin.cx/opendata/baserate`. That entry point imports
70
71
  A span of days split where the rate changes, as interest under § 288 BGB is computed.
71
72
  - `baseRatePeriods`, `baseRateFirstDay`, `baseRatesReadOn`, `baseRateSource`
72
73
 
74
+ ### VAT ID Confirmation
75
+
76
+ Everything here comes from `@fin.cx/opendata/vatid`. That entry point imports nothing else of the package and has no dependencies. It keeps nothing: each answer goes to the caller, with the answer as it arrived (`answer`: the HTTP status and the body, byte for byte), so the caller can keep it as evidence where the law asks for it. For an intra-Community supply that is the books' record of the buyer's foreign VAT ID (§ 17d Abs. 1 UStDV); a confirmation also shows the care of a prudent businessman on which the exemption rests in good faith (§ 6a Abs. 4 UStG).
77
+
78
+ - `EvatrClient#confirm({ requesterVatId, vatId, trader? })`
79
+ The BZSt's confirmation of a foreign VAT ID (§ 18e Nr. 1 UStG). The requester is a business with a German VAT ID. With `trader` (name and city, optionally street and postal code) it is a qualified confirmation. Where the VAT ID is valid the answer carries the member state's result for each field (`qualified.answered: true`; `A` matches, `B` does not match, `C` not asked, `D` not given), or, for `evatr-2008`, that the comparison has a particularity the BZSt explains on request (`qualified.answered: false`). A valid answer to a qualified request without a documented result for each field sent is refused as `unexpected_answer`, never passed on as a simple confirmation.
80
+ - `ViesClient#check({ vatId, requesterVatId?, trader? })`
81
+ The European Commission's VIES check, with the registered name and address where the member state gives them, a comparison of the trader data (`VALID`, `INVALID`, `NOT_PROCESSED`), and VIES's request identifier where the requester is named. The name is VIES's `name`, or, where it gives none, its `traderName`: the contract lists `traderName` in the answer but does not say whether it is the registered name or the one asked about; the test service answers a comparison with the registered name there. Everything else VIES answers is in `answer`.
82
+ - `splitVatId(value)`, `vatIdCountryCodes`, `evatrStatusCodes`
83
+ - `VatIdError` with a code for every reason no answer about validity arrived, and the answer as it arrived where one did (`answer`)
84
+
85
+ Both clients take a `fetch` (for example `createPublicOnlyFetch` of `@push.rocks/smartrequest`), a `baseUrl` for a test server, a `timeoutMs` (30 seconds by default; its timer ends with the request) and a caller's `signal`. Nothing is retried.
86
+
87
+ ```typescript
88
+ import { EvatrClient, ViesClient, VatIdError } from '@fin.cx/opendata/vatid';
89
+
90
+ const evatr = new EvatrClient();
91
+ const confirmation = await evatr.confirm({
92
+ requesterVatId: 'DE123456789',
93
+ vatId: 'ATU12345678',
94
+ trader: { name: 'Musterhaus GmbH & Co KG', street: 'Musterstrasse 22', postalCode: '12345', city: 'musterort' },
95
+ });
96
+ // { service: 'bzst-evatr', requestId, requestedAt, status: 'evatr-0000', valid: true,
97
+ // qualified: { answered: true, name: 'A', city: 'A', ... }, answer: { httpStatus: 200, text: '{"id":…}' } }
98
+
99
+ try {
100
+ await new ViesClient().check({ vatId: 'ATU12345678', requesterVatId: 'DE123456789' });
101
+ } catch (error) {
102
+ if (error instanceof VatIdError) {
103
+ console.log(error.code, error.serviceCode); // for example 'member_state_unavailable', 'MS_UNAVAILABLE'
104
+ }
105
+ }
106
+ ```
107
+
73
108
  ## Before You Start
74
109
 
75
110
  - No stock or fundamentals providers are pre-registered. You must register them before calling the services.
@@ -461,6 +496,12 @@ Four holidays apply only in part of a state:
461
496
  - `holidayCalendarFirstYear`, `holidayLawsReadOn`
462
497
  - `HolidayCalendarError` with the codes `invalid_date`, `unsupported_year`, `unknown_state` and `invalid_options`
463
498
 
499
+ ### VAT ID (`@fin.cx/opendata/vatid`)
500
+
501
+ - `EvatrClient` with `confirm(input)`; `ViesClient` with `check(input)`
502
+ - `splitVatId(value)`, `vatIdCountryCodes`, `evatrStatusCodes`, `evatrBaseUrl`, `viesBaseUrl`, `defaultVatIdTimeoutMs`
503
+ - `VatIdError` with the codes `invalid_input`, `vat_id_malformed`, `requester_invalid`, `not_permitted`, `qualified_limit_reached`, `unavailable`, `member_state_unavailable`, `busy`, `blocked`, `unreachable`, `timeout`, `aborted` and `unexpected_answer`
504
+
464
505
  ### Base rate (`@fin.cx/opendata/baserate`)
465
506
 
466
507
  - `getBaseRate(date)`
@@ -517,6 +558,12 @@ The holiday calendar is derived from the holiday laws of the federal states, rea
517
558
 
518
559
  The base rate of `@fin.cx/opendata/baserate` (`ts/baserate/data.baserate.ts`) is the table "Basiszinssatz nach § 247 BGB" of the Deutsche Bundesbank (https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820), which the Bundesbank publishes under § 247 Abs. 2 BGB, read on 2026-09-27 and reproduced unaltered: every half-year from 1 July 2002, the first row it lists, to the half-year from 1 July 2026 (1,52 %). The rate changes on 1 January and 1 July (§ 247 Abs. 1 Satz 2 BGB); a day after 31 December 2026 answers `not_published` until a release carries the next row.
519
560
 
561
+ The VAT ID clients of `@fin.cx/opendata/vatid` ask two official services at run time, one request at a time, read on 2026-09-27:
562
+
563
+ - The BZSt's eVatR REST API (`https://api.evatr.vies.bzst.de/app`, OpenAPI at `https://api.evatr.vies.bzst.de/api-docs`, version "v:1.2.3.19.886"): `POST /v1/abfrage` with `anfragendeUstid`, `angefragteUstid` and, for a qualified confirmation, `firmenname` and `ort` (and optionally `strasse`, `plz`). The status codes are those of `/v1/info/statusmeldungen`, and the country codes those of `/v1/info/eu_mitgliedstaaten`. The BZSt's page on foreign VAT IDs names the online form (`https://www.bzst.de/evatr`) and the REST API, and says the former XML-RPC interface is obsolete from 30 November 2025.
564
+ - The European Commission's VIES REST API (`https://ec.europa.eu/taxation_customs/vies/rest-api`, contract `swagger_publicVAT.yaml`): `POST /check-vat-number`. Its errors arrive with HTTP 200 and `actionSucceed: false`, as its test service (`/check-vat-test-service`) shows.
565
+ - Not verified: the terms of use of either service, whether eVatR needs a registration (its documentation names none), how many qualified confirmations the BZSt allows per session (`evatr-0008`), and the form of `gueltigAb`/`gueltigBis`, which are passed on as the BZSt writes them. The OpenAPI document lists `evatr-0008` under HTTP 400 and the status list under 403, so the client decides by the code.
566
+
520
567
  ## Behavior And Caveats
521
568
 
522
569
  - Historical market data is cached aggressively because it is treated as immutable.
@@ -532,7 +579,7 @@ The base rate of `@fin.cx/opendata/baserate` (`ts/baserate/data.baserate.ts`) is
532
579
  pnpm test
533
580
  ```
534
581
 
535
- Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal.
582
+ Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal. The VAT ID clients are tested against the answers the two services gave on 2026-09-27 to their documented test values (`test/fixtures/vatid/`); the tests never contact them.
536
583
 
537
584
  ## License and Legal Information
538
585
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '4.2.0',
6
+ version: '4.3.0',
7
7
  description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, the base rate of § 247 BGB, and stock and crypto prices and fundamentals from several providers.'
8
8
  }
@@ -0,0 +1,182 @@
1
+ import { VatIdError, type TVatIdErrorCode } from './classes.vatiderror.js';
2
+ import { evatrBaseUrl, evatrConfirmationPath, evatrStatusCodes } from './constants.js';
3
+ import type {
4
+ IEvatrConfirmation,
5
+ IEvatrConfirmationInput,
6
+ IVatIdClientOptions,
7
+ IVatIdTraderData,
8
+ TEvatrFieldResult,
9
+ TEvatrQualifiedResult,
10
+ TEvatrStatusCode,
11
+ } from './interfaces.js';
12
+ import { postJson, textField } from './request.js';
13
+ import { splitVatId } from './vatid.js';
14
+
15
+ /** The status codes that are an answer about the VAT ID asked about, and whether they say it is valid. */
16
+ const ANSWERS: Readonly<Partial<Record<TEvatrStatusCode, boolean>>> = {
17
+ 'evatr-0000': true,
18
+ 'evatr-2008': true,
19
+ 'evatr-2001': false,
20
+ 'evatr-2002': false,
21
+ 'evatr-2006': false,
22
+ };
23
+
24
+ /** What every other status code means for the caller (`VatIdError`). */
25
+ const REFUSALS: Readonly<Partial<Record<TEvatrStatusCode, TVatIdErrorCode>>> = {
26
+ 'evatr-0001': 'invalid_input',
27
+ 'evatr-0002': 'invalid_input',
28
+ 'evatr-0003': 'invalid_input',
29
+ 'evatr-0004': 'requester_invalid',
30
+ 'evatr-0005': 'vat_id_malformed',
31
+ 'evatr-0006': 'not_permitted',
32
+ 'evatr-0007': 'invalid_input',
33
+ 'evatr-0008': 'qualified_limit_reached',
34
+ 'evatr-0011': 'unavailable',
35
+ 'evatr-0012': 'vat_id_malformed',
36
+ 'evatr-0013': 'unavailable',
37
+ 'evatr-1001': 'unavailable',
38
+ 'evatr-1002': 'unavailable',
39
+ 'evatr-1003': 'unavailable',
40
+ 'evatr-1004': 'unavailable',
41
+ 'evatr-2003': 'vat_id_malformed',
42
+ 'evatr-2004': 'unavailable',
43
+ 'evatr-2005': 'requester_invalid',
44
+ 'evatr-2007': 'member_state_unavailable',
45
+ 'evatr-2011': 'unavailable',
46
+ 'evatr-3011': 'unavailable',
47
+ };
48
+
49
+ const FIELD_RESULTS: readonly TEvatrFieldResult[] = ['A', 'B', 'C', 'D'];
50
+
51
+ const fieldResult = (bodyArg: unknown, keyArg: string): TEvatrFieldResult | undefined => {
52
+ const value = textField(bodyArg, keyArg);
53
+ return value !== undefined && (FIELD_RESULTS as readonly string[]).includes(value) ? (value as TEvatrFieldResult) : undefined;
54
+ };
55
+
56
+ /**
57
+ * The BZSt's confirmation of foreign VAT IDs (§ 18e Nr. 1 UStG) over its eVatR REST API
58
+ * (`https://api.evatr.vies.bzst.de/api-docs`). A simple confirmation says whether a VAT ID is valid
59
+ * when asked; a qualified one also compares the business's name, city, and where given street and
60
+ * postal code with what its member state registered. The BZSt confirms to a business with a German
61
+ * VAT ID of its own, which every request names. Nothing is kept: each answer goes to the caller.
62
+ */
63
+ export class EvatrClient {
64
+ constructor(private readonly options: IVatIdClientOptions = {}) {}
65
+
66
+ public async confirm(inputArg: IEvatrConfirmationInput): Promise<IEvatrConfirmation> {
67
+ const requester = splitVatId(inputArg.requesterVatId);
68
+ if (!requester || requester.countryCode !== 'DE') {
69
+ throw new VatIdError('invalid_input', 'the requester is named by its German VAT ID', { service: 'bzst-evatr', field: 'requesterVatId' });
70
+ }
71
+ const asked = splitVatId(inputArg.vatId);
72
+ if (!asked) {
73
+ throw new VatIdError('invalid_input', 'the VAT ID asked about starts with no member state\'s code', { service: 'bzst-evatr', field: 'vatId' });
74
+ }
75
+ const trader = inputArg.trader ? traderOf(inputArg.trader) : undefined;
76
+ if (inputArg.trader && !trader) {
77
+ throw new VatIdError('invalid_input', 'a qualified confirmation needs the name and the city', { service: 'bzst-evatr', field: 'trader' });
78
+ }
79
+ const answer = await postJson(this.options, 'bzst-evatr', `${this.options.baseUrl ?? evatrBaseUrl}${evatrConfirmationPath}`, {
80
+ anfragendeUstid: requester.vatId,
81
+ angefragteUstid: asked.vatId,
82
+ ...(trader
83
+ ? {
84
+ firmenname: trader.name,
85
+ ort: trader.city,
86
+ ...(trader.street ? { strasse: trader.street } : {}),
87
+ ...(trader.postalCode ? { plz: trader.postalCode } : {}),
88
+ }
89
+ : {}),
90
+ }, inputArg.signal);
91
+ const status = textField(answer.body, 'status');
92
+ if (!status || !(evatrStatusCodes as readonly string[]).includes(status)) {
93
+ throw new VatIdError('unexpected_answer', `the BZSt answered HTTP ${answer.httpStatus} without a status it documents`, {
94
+ service: 'bzst-evatr',
95
+ httpStatus: answer.httpStatus,
96
+ ...(status ? { serviceCode: status } : {}),
97
+ answer: answer.answer,
98
+ });
99
+ }
100
+ const code = status as TEvatrStatusCode;
101
+ const valid = ANSWERS[code];
102
+ if (valid === undefined) {
103
+ throw new VatIdError(REFUSALS[code] ?? 'unexpected_answer', `the BZSt answered ${code}`, {
104
+ service: 'bzst-evatr',
105
+ serviceCode: code,
106
+ httpStatus: answer.httpStatus,
107
+ answer: answer.answer,
108
+ });
109
+ }
110
+ const requestedAt = textField(answer.body, 'anfrageZeitpunkt');
111
+ const requestedAtMs = requestedAt ? Date.parse(requestedAt) : Number.NaN;
112
+ if (!requestedAt || Number.isNaN(requestedAtMs)) {
113
+ throw new VatIdError('unexpected_answer', 'the BZSt answered without the moment of the request', {
114
+ service: 'bzst-evatr',
115
+ serviceCode: code,
116
+ httpStatus: answer.httpStatus,
117
+ answer: answer.answer,
118
+ });
119
+ }
120
+ const requestId = textField(answer.body, 'id');
121
+ const validFrom = textField(answer.body, 'gueltigAb');
122
+ const validTo = textField(answer.body, 'gueltigBis');
123
+ const qualified = trader && valid ? qualifiedResultOf(trader, code, answer.body) : undefined;
124
+ if (qualified === null) {
125
+ // a qualified request answered valid without a result for each field sent would read as a simple
126
+ // confirmation; it is refused instead of being passed on as one
127
+ throw new VatIdError('unexpected_answer', `the BZSt answered ${code} to a qualified request without a documented result for each field sent`, {
128
+ service: 'bzst-evatr',
129
+ serviceCode: code,
130
+ httpStatus: answer.httpStatus,
131
+ answer: answer.answer,
132
+ });
133
+ }
134
+ return {
135
+ service: 'bzst-evatr',
136
+ ...(requestId ? { requestId } : {}),
137
+ requestedAt,
138
+ requestedAtMs,
139
+ status: code as IEvatrConfirmation['status'],
140
+ valid,
141
+ ...(validFrom ? { validFrom } : {}),
142
+ ...(validTo ? { validTo } : {}),
143
+ requesterVatId: requester.vatId,
144
+ vatId: asked.vatId,
145
+ ...(qualified ? { qualified } : {}),
146
+ answer: answer.answer,
147
+ };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * The comparison of a qualified request whose VAT ID is valid: `evatr-2008` says it has a
153
+ * particularity and gives no result (`answered: false`); otherwise every field sent has a documented
154
+ * result (`A`…`D`), or the answer is not one this client reads (`null`). A result the BZSt gives for a
155
+ * field not sent is kept as it is.
156
+ */
157
+ const qualifiedResultOf = (traderArg: IVatIdTraderData, codeArg: TEvatrStatusCode, bodyArg: unknown): TEvatrQualifiedResult | null => {
158
+ if (codeArg === 'evatr-2008') {
159
+ return { asked: traderArg, answered: false };
160
+ }
161
+ const name = fieldResult(bodyArg, 'ergFirmenname');
162
+ const city = fieldResult(bodyArg, 'ergOrt');
163
+ // a field not sent may still carry its result (`C`, not asked); one sent must
164
+ const street = fieldResult(bodyArg, 'ergStrasse');
165
+ const postalCode = fieldResult(bodyArg, 'ergPlz');
166
+ if (!name || !city || (traderArg.street && !street) || (traderArg.postalCode && !postalCode)) {
167
+ return null;
168
+ }
169
+ return { asked: traderArg, answered: true, name, city, ...(street ? { street } : {}), ...(postalCode ? { postalCode } : {}) };
170
+ };
171
+
172
+ /** The business data as a qualified confirmation sends it: trimmed, with the name and the city required. */
173
+ export const traderOf = (traderArg: IVatIdTraderData): IVatIdTraderData | undefined => {
174
+ const name = traderArg.name.trim();
175
+ const city = traderArg.city.trim();
176
+ if (!name || !city) {
177
+ return undefined;
178
+ }
179
+ const street = traderArg.street?.trim();
180
+ const postalCode = traderArg.postalCode?.trim();
181
+ return { name, city, ...(street ? { street } : {}), ...(postalCode ? { postalCode } : {}) };
182
+ };