@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/vatid/classes.evatrclient.d.ts +15 -0
- package/dist_ts/vatid/classes.evatrclient.js +169 -0
- package/dist_ts/vatid/classes.vatiderror.d.ts +51 -0
- package/dist_ts/vatid/classes.vatiderror.js +29 -0
- package/dist_ts/vatid/classes.viesclient.d.ts +12 -0
- package/dist_ts/vatid/classes.viesclient.js +118 -0
- package/dist_ts/vatid/constants.d.ts +28 -0
- package/dist_ts/vatid/constants.js +37 -0
- package/dist_ts/vatid/index.d.ts +6 -0
- package/dist_ts/vatid/index.js +10 -0
- package/dist_ts/vatid/interfaces.d.ts +141 -0
- package/dist_ts/vatid/interfaces.js +2 -0
- package/dist_ts/vatid/request.d.ts +16 -0
- package/dist_ts/vatid/request.js +65 -0
- package/dist_ts/vatid/vatid.d.ts +8 -0
- package/dist_ts/vatid/vatid.js +22 -0
- package/package.json +7 -3
- package/readme.md +49 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/vatid/classes.evatrclient.ts +182 -0
- package/ts/vatid/classes.vatiderror.ts +83 -0
- package/ts/vatid/classes.viesclient.ts +123 -0
- package/ts/vatid/constants.ts +40 -0
- package/ts/vatid/index.ts +17 -0
- package/ts/vatid/interfaces.ts +154 -0
- package/ts/vatid/request.ts +77 -0
- package/ts/vatid/vatid.ts +25 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IVatIdServiceAnswer } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Why a VAT ID could not be confirmed. Every code means that the service gave no answer about the
|
|
5
|
+
* validity of the VAT ID asked about:
|
|
6
|
+
* - `invalid_input`: what was asked is incomplete or not a VAT ID of a member state (checked before
|
|
7
|
+
* sending, or refused by the service: eVatR `evatr-0002`, `evatr-0003`, `evatr-0007`, VIES
|
|
8
|
+
* `INVALID_INPUT`);
|
|
9
|
+
* - `vat_id_malformed`: the service says the VAT ID asked about does not have the form its member
|
|
10
|
+
* state gives (eVatR `evatr-0005`, `evatr-0012`, `evatr-2003`);
|
|
11
|
+
* - `requester_invalid`: the requester's own VAT ID is malformed or not valid (eVatR `evatr-0004`,
|
|
12
|
+
* `evatr-2005`, VIES `INVALID_REQUESTER_INFO`);
|
|
13
|
+
* - `not_permitted`: a German VAT ID asked about by a German one (eVatR `evatr-0006`);
|
|
14
|
+
* - `qualified_limit_reached`: the qualified confirmations of this session are used up (eVatR
|
|
15
|
+
* `evatr-0008`); a simple one is still possible;
|
|
16
|
+
* - `unavailable`: the service cannot answer now (eVatR `evatr-0011`, `-0013`, `-1001`…`-1004`,
|
|
17
|
+
* `-2004`, `-2011`, `-3011`; VIES `SERVICE_UNAVAILABLE`);
|
|
18
|
+
* - `member_state_unavailable`: the member state's system did not answer or failed (eVatR
|
|
19
|
+
* `evatr-2007`; VIES `MS_UNAVAILABLE`, `TIMEOUT`);
|
|
20
|
+
* - `busy`: too many requests at once (VIES `GLOBAL_MAX_CONCURRENT_REQ`, `MS_MAX_CONCURRENT_REQ`
|
|
21
|
+
* and their `_TIME` forms);
|
|
22
|
+
* - `blocked`: the service blocks this address or this VAT ID (VIES `IP_BLOCKED`, `VAT_BLOCKED`);
|
|
23
|
+
* - `unreachable`: no answer arrived — the name, the connection or TLS failed;
|
|
24
|
+
* - `timeout`: the answer did not arrive within the client's time;
|
|
25
|
+
* - `aborted`: the caller's signal ended the request;
|
|
26
|
+
* - `unexpected_answer`: an answer this client cannot read, or a status it does not know.
|
|
27
|
+
*/
|
|
28
|
+
export type TVatIdErrorCode =
|
|
29
|
+
| 'invalid_input'
|
|
30
|
+
| 'vat_id_malformed'
|
|
31
|
+
| 'requester_invalid'
|
|
32
|
+
| 'not_permitted'
|
|
33
|
+
| 'qualified_limit_reached'
|
|
34
|
+
| 'unavailable'
|
|
35
|
+
| 'member_state_unavailable'
|
|
36
|
+
| 'busy'
|
|
37
|
+
| 'blocked'
|
|
38
|
+
| 'unreachable'
|
|
39
|
+
| 'timeout'
|
|
40
|
+
| 'aborted'
|
|
41
|
+
| 'unexpected_answer';
|
|
42
|
+
|
|
43
|
+
export interface IVatIdErrorDetails {
|
|
44
|
+
service: 'bzst-evatr' | 'vies';
|
|
45
|
+
/** the service's own code: `evatr-0008`, `MS_UNAVAILABLE` */
|
|
46
|
+
serviceCode?: string;
|
|
47
|
+
/** the HTTP status of the answer, where one arrived */
|
|
48
|
+
httpStatus?: number;
|
|
49
|
+
/** which input a local check refused */
|
|
50
|
+
field?: 'requesterVatId' | 'vatId' | 'trader';
|
|
51
|
+
/** the answer as it arrived, where one did */
|
|
52
|
+
answer?: IVatIdServiceAnswer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The typed error of the VAT ID clients. */
|
|
56
|
+
export class VatIdError extends Error {
|
|
57
|
+
public readonly code: TVatIdErrorCode;
|
|
58
|
+
public readonly service: IVatIdErrorDetails['service'];
|
|
59
|
+
public readonly serviceCode?: string;
|
|
60
|
+
public readonly httpStatus?: number;
|
|
61
|
+
public readonly field?: IVatIdErrorDetails['field'];
|
|
62
|
+
/** the answer as it arrived, where one did: a refusal, or an answer this client cannot read */
|
|
63
|
+
public readonly answer?: IVatIdServiceAnswer;
|
|
64
|
+
|
|
65
|
+
constructor(code: TVatIdErrorCode, message: string, detailsArg: IVatIdErrorDetails, options?: { cause?: unknown }) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = 'VatIdError';
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.service = detailsArg.service;
|
|
70
|
+
if (detailsArg.serviceCode !== undefined) {
|
|
71
|
+
this.serviceCode = detailsArg.serviceCode;
|
|
72
|
+
}
|
|
73
|
+
if (detailsArg.httpStatus !== undefined) {
|
|
74
|
+
this.httpStatus = detailsArg.httpStatus;
|
|
75
|
+
}
|
|
76
|
+
if (detailsArg.field !== undefined) {
|
|
77
|
+
this.field = detailsArg.field;
|
|
78
|
+
}
|
|
79
|
+
if (detailsArg.answer !== undefined) {
|
|
80
|
+
this.answer = detailsArg.answer;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { VatIdError, type TVatIdErrorCode } from './classes.vatiderror.js';
|
|
2
|
+
import { traderOf } from './classes.evatrclient.js';
|
|
3
|
+
import { viesBaseUrl, viesCheckPath } from './constants.js';
|
|
4
|
+
import type { IViesCheck, IViesCheckInput, IVatIdClientOptions, TViesMatch } from './interfaces.js';
|
|
5
|
+
import { postJson, textField } from './request.js';
|
|
6
|
+
import { splitVatId } from './vatid.js';
|
|
7
|
+
|
|
8
|
+
/** The error codes of VIES's `errorWrappers`, as its test service answers them (read on 2026-09-27). */
|
|
9
|
+
const REFUSALS: Readonly<Record<string, TVatIdErrorCode>> = {
|
|
10
|
+
INVALID_INPUT: 'invalid_input',
|
|
11
|
+
INVALID_REQUESTER_INFO: 'requester_invalid',
|
|
12
|
+
SERVICE_UNAVAILABLE: 'unavailable',
|
|
13
|
+
MS_UNAVAILABLE: 'member_state_unavailable',
|
|
14
|
+
TIMEOUT: 'member_state_unavailable',
|
|
15
|
+
VAT_BLOCKED: 'blocked',
|
|
16
|
+
IP_BLOCKED: 'blocked',
|
|
17
|
+
GLOBAL_MAX_CONCURRENT_REQ: 'busy',
|
|
18
|
+
GLOBAL_MAX_CONCURRENT_REQ_TIME: 'busy',
|
|
19
|
+
MS_MAX_CONCURRENT_REQ: 'busy',
|
|
20
|
+
MS_MAX_CONCURRENT_REQ_TIME: 'busy',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MATCHES: readonly TViesMatch[] = ['VALID', 'INVALID', 'NOT_PROCESSED'];
|
|
24
|
+
|
|
25
|
+
const matchOf = (bodyArg: unknown, keyArg: string): TViesMatch | undefined => {
|
|
26
|
+
const value = textField(bodyArg, keyArg);
|
|
27
|
+
return value !== undefined && (MATCHES as readonly string[]).includes(value) ? (value as TViesMatch) : undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** VIES writes `---` where a member state gives no name or address. */
|
|
31
|
+
const givenText = (bodyArg: unknown, keyArg: string): string | undefined => {
|
|
32
|
+
const value = textField(bodyArg, keyArg)?.trim();
|
|
33
|
+
return value && value !== '---' ? value : undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The European Commission's VIES over its REST API ("Vies on-the-Web", `swagger_publicVAT.yaml`):
|
|
38
|
+
* whether a VAT ID is valid, and with the business data a comparison of each field. A request that
|
|
39
|
+
* names the requester's own VAT ID gets VIES's request identifier. Errors arrive with HTTP 200 and
|
|
40
|
+
* `actionSucceed: false`; they are thrown as `VatIdError`. Nothing is kept.
|
|
41
|
+
*/
|
|
42
|
+
export class ViesClient {
|
|
43
|
+
constructor(private readonly options: IVatIdClientOptions = {}) {}
|
|
44
|
+
|
|
45
|
+
public async check(inputArg: IViesCheckInput): Promise<IViesCheck> {
|
|
46
|
+
const asked = splitVatId(inputArg.vatId);
|
|
47
|
+
if (!asked) {
|
|
48
|
+
throw new VatIdError('invalid_input', 'the VAT ID asked about starts with no member state\'s code', { service: 'vies', field: 'vatId' });
|
|
49
|
+
}
|
|
50
|
+
const requester = inputArg.requesterVatId === undefined ? undefined : splitVatId(inputArg.requesterVatId);
|
|
51
|
+
if (inputArg.requesterVatId !== undefined && !requester) {
|
|
52
|
+
throw new VatIdError('invalid_input', 'the requester\'s VAT ID starts with no member state\'s code', { service: 'vies', field: 'requesterVatId' });
|
|
53
|
+
}
|
|
54
|
+
const trader = inputArg.trader ? traderOf(inputArg.trader) : undefined;
|
|
55
|
+
if (inputArg.trader && !trader) {
|
|
56
|
+
throw new VatIdError('invalid_input', 'a comparison needs the name and the city', { service: 'vies', field: 'trader' });
|
|
57
|
+
}
|
|
58
|
+
const answer = await postJson(this.options, 'vies', `${this.options.baseUrl ?? viesBaseUrl}${viesCheckPath}`, {
|
|
59
|
+
countryCode: asked.countryCode,
|
|
60
|
+
vatNumber: asked.number,
|
|
61
|
+
...(requester ? { requesterMemberStateCode: requester.countryCode, requesterNumber: requester.number } : {}),
|
|
62
|
+
...(trader
|
|
63
|
+
? {
|
|
64
|
+
traderName: trader.name,
|
|
65
|
+
traderCity: trader.city,
|
|
66
|
+
...(trader.street ? { traderStreet: trader.street } : {}),
|
|
67
|
+
...(trader.postalCode ? { traderPostalCode: trader.postalCode } : {}),
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
}, inputArg.signal);
|
|
71
|
+
const body = answer.body;
|
|
72
|
+
const succeeded: unknown = body && typeof body === 'object' ? Reflect.get(body, 'actionSucceed') : undefined;
|
|
73
|
+
if (succeeded === false) {
|
|
74
|
+
const wrappers: unknown = Reflect.get(body as object, 'errorWrappers');
|
|
75
|
+
const first: unknown = Array.isArray(wrappers) ? wrappers[0] : undefined;
|
|
76
|
+
const serviceCode = textField(first, 'error');
|
|
77
|
+
throw new VatIdError(serviceCode ? (REFUSALS[serviceCode] ?? 'unexpected_answer') : 'unexpected_answer', `VIES answered ${serviceCode ?? 'an error without a code'}`, {
|
|
78
|
+
service: 'vies',
|
|
79
|
+
httpStatus: answer.httpStatus,
|
|
80
|
+
...(serviceCode ? { serviceCode } : {}),
|
|
81
|
+
answer: answer.answer,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const valid: unknown = body && typeof body === 'object' ? Reflect.get(body, 'valid') : undefined;
|
|
85
|
+
const requestedAt = textField(body, 'requestDate');
|
|
86
|
+
const requestedAtMs = requestedAt ? Date.parse(requestedAt) : Number.NaN;
|
|
87
|
+
if (answer.httpStatus !== 200 || typeof valid !== 'boolean' || !requestedAt || Number.isNaN(requestedAtMs)) {
|
|
88
|
+
throw new VatIdError('unexpected_answer', `VIES answered HTTP ${answer.httpStatus} in a form this client cannot read`, {
|
|
89
|
+
service: 'vies',
|
|
90
|
+
httpStatus: answer.httpStatus,
|
|
91
|
+
answer: answer.answer,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const requestId = textField(body, 'requestIdentifier');
|
|
95
|
+
const name = givenText(body, 'name') ?? givenText(body, 'traderName');
|
|
96
|
+
const address = givenText(body, 'address');
|
|
97
|
+
const compared = trader
|
|
98
|
+
? {
|
|
99
|
+
asked: trader,
|
|
100
|
+
...optionalMatch('name', matchOf(body, 'traderNameMatch')),
|
|
101
|
+
...optionalMatch('street', matchOf(body, 'traderStreetMatch')),
|
|
102
|
+
...optionalMatch('postalCode', matchOf(body, 'traderPostalCodeMatch')),
|
|
103
|
+
...optionalMatch('city', matchOf(body, 'traderCityMatch')),
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
106
|
+
return {
|
|
107
|
+
service: 'vies',
|
|
108
|
+
valid,
|
|
109
|
+
requestedAt,
|
|
110
|
+
requestedAtMs,
|
|
111
|
+
...(requestId ? { requestId } : {}),
|
|
112
|
+
vatId: asked.vatId,
|
|
113
|
+
...(requester ? { requesterVatId: requester.vatId } : {}),
|
|
114
|
+
...(name ? { name } : {}),
|
|
115
|
+
...(address ? { address } : {}),
|
|
116
|
+
...(compared ? { compared } : {}),
|
|
117
|
+
answer: answer.answer,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const optionalMatch = <TKey extends string>(keyArg: TKey, matchArg: TViesMatch | undefined): Partial<Record<TKey, TViesMatch>> =>
|
|
123
|
+
(matchArg ? { [keyArg]: matchArg } : {}) as Partial<Record<TKey, TViesMatch>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BZSt's eVatR REST API (Bestätigungsverfahren of § 18e UStG), as its OpenAPI document names it
|
|
3
|
+
* (`https://api.evatr.vies.bzst.de/api-docs`, read on 2026-09-27): the server, and the path of a
|
|
4
|
+
* confirmation. The XML-RPC interface the BZSt offered before is obsolete from 30 November 2025.
|
|
5
|
+
*/
|
|
6
|
+
export const evatrBaseUrl = 'https://api.evatr.vies.bzst.de/app';
|
|
7
|
+
export const evatrConfirmationPath = '/v1/abfrage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The European Commission's "Vies on-the-Web" REST API: the base under which its contract's paths
|
|
11
|
+
* (`swagger_publicVAT.yaml`) answer, read on 2026-09-27 (`/check-status` answered there, and so did
|
|
12
|
+
* `/check-vat-test-service`). The contract itself names no host.
|
|
13
|
+
*/
|
|
14
|
+
export const viesBaseUrl = 'https://ec.europa.eu/taxation_customs/vies/rest-api';
|
|
15
|
+
export const viesCheckPath = '/check-vat-number';
|
|
16
|
+
|
|
17
|
+
/** How long one request may take by default, the answer included. */
|
|
18
|
+
export const defaultVatIdTimeoutMs = 30_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The country codes a VAT identification number starts with, as the eVatR API's list of member
|
|
22
|
+
* states gives them (`/v1/info/eu_mitgliedstaaten`, read on 2026-09-27): the 27 member states,
|
|
23
|
+
* Greece as `EL`, and `XI` for Northern Ireland.
|
|
24
|
+
*/
|
|
25
|
+
export const vatIdCountryCodes = [
|
|
26
|
+
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'HR', 'HU',
|
|
27
|
+
'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 'XI',
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every status code of the eVatR API with the HTTP status its list of messages gives it
|
|
32
|
+
* (`/v1/info/statusmeldungen`, read on 2026-09-27). The OpenAPI document lists `evatr-0008` under
|
|
33
|
+
* 400 and the list under 403, so the code decides, never the HTTP status.
|
|
34
|
+
*/
|
|
35
|
+
export const evatrStatusCodes = [
|
|
36
|
+
'evatr-0000', 'evatr-0001', 'evatr-0002', 'evatr-0003', 'evatr-0004', 'evatr-0005', 'evatr-0006',
|
|
37
|
+
'evatr-0007', 'evatr-0008', 'evatr-0011', 'evatr-0012', 'evatr-0013', 'evatr-1001', 'evatr-1002',
|
|
38
|
+
'evatr-1003', 'evatr-1004', 'evatr-2001', 'evatr-2002', 'evatr-2003', 'evatr-2004', 'evatr-2005',
|
|
39
|
+
'evatr-2006', 'evatr-2007', 'evatr-2008', 'evatr-2011', 'evatr-3011',
|
|
40
|
+
] as const;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// This entry point stands alone: it imports nothing from the rest of the package and has no
|
|
2
|
+
// dependencies. It keeps nothing: every answer goes to the caller, who keeps it as evidence where the
|
|
3
|
+
// law asks for it.
|
|
4
|
+
export * from './interfaces.js';
|
|
5
|
+
export { VatIdError, type IVatIdErrorDetails, type TVatIdErrorCode } from './classes.vatiderror.js';
|
|
6
|
+
export {
|
|
7
|
+
defaultVatIdTimeoutMs,
|
|
8
|
+
evatrBaseUrl,
|
|
9
|
+
evatrConfirmationPath,
|
|
10
|
+
evatrStatusCodes,
|
|
11
|
+
vatIdCountryCodes,
|
|
12
|
+
viesBaseUrl,
|
|
13
|
+
viesCheckPath,
|
|
14
|
+
} from './constants.js';
|
|
15
|
+
export { splitVatId } from './vatid.js';
|
|
16
|
+
export { EvatrClient } from './classes.evatrclient.js';
|
|
17
|
+
export { ViesClient } from './classes.viesclient.js';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { evatrStatusCodes, vatIdCountryCodes } from './constants.js';
|
|
2
|
+
|
|
3
|
+
/** A country code a VAT identification number starts with (`EL` for Greece, `XI` for Northern Ireland). */
|
|
4
|
+
export type TVatIdCountryCode = (typeof vatIdCountryCodes)[number];
|
|
5
|
+
|
|
6
|
+
/** A status code of the eVatR API. */
|
|
7
|
+
export type TEvatrStatusCode = (typeof evatrStatusCodes)[number];
|
|
8
|
+
|
|
9
|
+
/** A VAT identification number taken apart: its country code and the number after it. */
|
|
10
|
+
export interface IVatIdParts {
|
|
11
|
+
countryCode: TVatIdCountryCode;
|
|
12
|
+
number: string;
|
|
13
|
+
/** both together, without spaces, dots or hyphens, in capitals: `ATU12345678` */
|
|
14
|
+
vatId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The fetch a client sends its requests with: the runtime's `fetch`, or one that decides which
|
|
19
|
+
* addresses may be reached (for example `createPublicOnlyFetch` of `@push.rocks/smartrequest`).
|
|
20
|
+
*/
|
|
21
|
+
export type TVatIdFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
22
|
+
|
|
23
|
+
export interface IVatIdClientOptions {
|
|
24
|
+
/** default: the runtime's `fetch` */
|
|
25
|
+
fetch?: TVatIdFetch;
|
|
26
|
+
/** default: the official address (`evatrBaseUrl`, `viesBaseUrl`); a test's own server otherwise */
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
/** how long one request may take, the answer included; default `defaultVatIdTimeoutMs` */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** sent as `User-Agent` where given */
|
|
31
|
+
userAgent?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A service's answer as it arrived: its HTTP status and its body, byte for byte as text. It is what the
|
|
36
|
+
* service said, whatever of it this client reads, so a caller can keep it as the evidence of a
|
|
37
|
+
* confirmation (§ 17d Abs. 1 UStDV asks the VAT ID of the buyer of an intra-Community supply to be
|
|
38
|
+
* shown in the books).
|
|
39
|
+
*/
|
|
40
|
+
export interface IVatIdServiceAnswer {
|
|
41
|
+
httpStatus: number;
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The business data a qualified confirmation compares with what the member state registered. */
|
|
46
|
+
export interface IVatIdTraderData {
|
|
47
|
+
name: string;
|
|
48
|
+
street?: string;
|
|
49
|
+
postalCode?: string;
|
|
50
|
+
city: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What the member state says about one field of a qualified confirmation (eVatR):
|
|
55
|
+
* - `A`: it matches the registered data;
|
|
56
|
+
* - `B`: it does not match;
|
|
57
|
+
* - `C`: it was not asked about;
|
|
58
|
+
* - `D`: the member state did not give it.
|
|
59
|
+
*/
|
|
60
|
+
export type TEvatrFieldResult = 'A' | 'B' | 'C' | 'D';
|
|
61
|
+
|
|
62
|
+
export interface IEvatrConfirmationInput {
|
|
63
|
+
/** the German VAT ID of the business that asks (`anfragendeUstid`); § 18e UStG confirms to a business */
|
|
64
|
+
requesterVatId: string;
|
|
65
|
+
/** the VAT ID asked about (`angefragteUstid`) */
|
|
66
|
+
vatId: string;
|
|
67
|
+
/** with the business data, a qualified confirmation; `name` and `city` are what makes it one */
|
|
68
|
+
trader?: IVatIdTraderData;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The BZSt's answer about one VAT ID. `valid` holds for `evatr-0000` and `evatr-2008` (valid, with a
|
|
74
|
+
* particularity of the qualified confirmation the BZSt explains on request); `evatr-2002` (not yet
|
|
75
|
+
* valid, from `validFrom`), `evatr-2006` (was valid from `validFrom` to `validTo`) and `evatr-2001`
|
|
76
|
+
* (not assigned) are answers that it is not.
|
|
77
|
+
*/
|
|
78
|
+
export interface IEvatrConfirmation {
|
|
79
|
+
service: 'bzst-evatr';
|
|
80
|
+
/** the BZSt's id of the request (`id`), the reference of the confirmation */
|
|
81
|
+
requestId?: string;
|
|
82
|
+
/** when the BZSt answered, as it wrote it (`anfrageZeitpunkt`, ISO 8601 with its offset) */
|
|
83
|
+
requestedAt: string;
|
|
84
|
+
/** the same, as epoch milliseconds */
|
|
85
|
+
requestedAtMs: number;
|
|
86
|
+
status: 'evatr-0000' | 'evatr-2001' | 'evatr-2002' | 'evatr-2006' | 'evatr-2008';
|
|
87
|
+
valid: boolean;
|
|
88
|
+
/** `gueltigAb`, as the BZSt wrote it; its form is not documented */
|
|
89
|
+
validFrom?: string;
|
|
90
|
+
/** `gueltigBis`, as the BZSt wrote it */
|
|
91
|
+
validTo?: string;
|
|
92
|
+
/** what was asked, as it was sent */
|
|
93
|
+
requesterVatId: string;
|
|
94
|
+
vatId: string;
|
|
95
|
+
/**
|
|
96
|
+
* A qualified request (`trader` given) whose VAT ID is valid: what was compared and the member
|
|
97
|
+
* state's result for each field sent (`answered: true`), or, for `evatr-2008`, that the comparison
|
|
98
|
+
* has a particularity the BZSt explains on request and gives no result (`answered: false`). Absent
|
|
99
|
+
* for a simple request and for an answer that the VAT ID is not valid, which compares nothing.
|
|
100
|
+
*/
|
|
101
|
+
qualified?: TEvatrQualifiedResult;
|
|
102
|
+
/** the answer as it arrived */
|
|
103
|
+
answer: IVatIdServiceAnswer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type TEvatrQualifiedResult =
|
|
107
|
+
| {
|
|
108
|
+
asked: IVatIdTraderData;
|
|
109
|
+
answered: true;
|
|
110
|
+
name: TEvatrFieldResult;
|
|
111
|
+
street?: TEvatrFieldResult;
|
|
112
|
+
postalCode?: TEvatrFieldResult;
|
|
113
|
+
city: TEvatrFieldResult;
|
|
114
|
+
}
|
|
115
|
+
| { asked: IVatIdTraderData; answered: false };
|
|
116
|
+
|
|
117
|
+
/** What VIES says about one field of the trader data: matches, does not match, or not compared. */
|
|
118
|
+
export type TViesMatch = 'VALID' | 'INVALID' | 'NOT_PROCESSED';
|
|
119
|
+
|
|
120
|
+
export interface IViesCheckInput {
|
|
121
|
+
/** the VAT ID asked about, with its country code */
|
|
122
|
+
vatId: string;
|
|
123
|
+
/** the business that asks, with its own VAT ID; VIES gives a request identifier only with it */
|
|
124
|
+
requesterVatId?: string;
|
|
125
|
+
/** with the business data, VIES compares it ("approximate matching") */
|
|
126
|
+
trader?: IVatIdTraderData;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** VIES's answer about one VAT ID. */
|
|
131
|
+
export interface IViesCheck {
|
|
132
|
+
service: 'vies';
|
|
133
|
+
valid: boolean;
|
|
134
|
+
/** when VIES answered (`requestDate`, ISO 8601) */
|
|
135
|
+
requestedAt: string;
|
|
136
|
+
requestedAtMs: number;
|
|
137
|
+
/** VIES's reference of the request (`requestIdentifier`); only where the requester was named */
|
|
138
|
+
requestId?: string;
|
|
139
|
+
vatId: string;
|
|
140
|
+
requesterVatId?: string;
|
|
141
|
+
/** the name and the address the member state registered, where it gives them */
|
|
142
|
+
name?: string;
|
|
143
|
+
address?: string;
|
|
144
|
+
/** the trader data asked about, and VIES's comparison of each field */
|
|
145
|
+
compared?: {
|
|
146
|
+
asked: IVatIdTraderData;
|
|
147
|
+
name?: TViesMatch;
|
|
148
|
+
street?: TViesMatch;
|
|
149
|
+
postalCode?: TViesMatch;
|
|
150
|
+
city?: TViesMatch;
|
|
151
|
+
};
|
|
152
|
+
/** the answer as it arrived, with everything VIES gave that is not read into the fields above */
|
|
153
|
+
answer: IVatIdServiceAnswer;
|
|
154
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { VatIdError, type IVatIdErrorDetails } from './classes.vatiderror.js';
|
|
2
|
+
import { defaultVatIdTimeoutMs } from './constants.js';
|
|
3
|
+
import type { IVatIdClientOptions, IVatIdServiceAnswer, TVatIdFetch } from './interfaces.js';
|
|
4
|
+
|
|
5
|
+
/** An answer as it arrived (`answer`, its status and its text), and its body read as JSON, or `undefined` where it was none. */
|
|
6
|
+
export interface IJsonAnswer {
|
|
7
|
+
httpStatus: number;
|
|
8
|
+
body: unknown;
|
|
9
|
+
answer: IVatIdServiceAnswer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sends one JSON request and reads the answer whole, within the client's time. The caller's signal
|
|
14
|
+
* ends it as `aborted`, the time as `timeout`, and a request that could not be sent or answered as
|
|
15
|
+
* `unreachable`; nothing is retried. The timer ends with the request, answered or not.
|
|
16
|
+
*/
|
|
17
|
+
export const postJson = async (
|
|
18
|
+
optionsArg: IVatIdClientOptions,
|
|
19
|
+
serviceArg: IVatIdErrorDetails['service'],
|
|
20
|
+
urlArg: string,
|
|
21
|
+
bodyArg: unknown,
|
|
22
|
+
signalArg: AbortSignal | undefined,
|
|
23
|
+
): Promise<IJsonAnswer> => {
|
|
24
|
+
const fetchFn: TVatIdFetch = optionsArg.fetch ?? ((inputArg, initArg) => fetch(inputArg, initArg));
|
|
25
|
+
const timeout = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => timeout.abort(new Error('the client\'s time ran out')), optionsArg.timeoutMs ?? defaultVatIdTimeoutMs);
|
|
27
|
+
const signal = signalArg ? AbortSignal.any([signalArg, timeout.signal]) : timeout.signal;
|
|
28
|
+
const failed = (errorArg: unknown): VatIdError => {
|
|
29
|
+
if (signalArg?.aborted) {
|
|
30
|
+
return new VatIdError('aborted', 'the request was ended by its caller', { service: serviceArg }, { cause: errorArg });
|
|
31
|
+
}
|
|
32
|
+
if (timeout.signal.aborted) {
|
|
33
|
+
return new VatIdError('timeout', 'the service did not answer in time', { service: serviceArg }, { cause: errorArg });
|
|
34
|
+
}
|
|
35
|
+
return new VatIdError('unreachable', 'the service could not be reached', { service: serviceArg }, { cause: errorArg });
|
|
36
|
+
};
|
|
37
|
+
let response: Response;
|
|
38
|
+
let text: string;
|
|
39
|
+
try {
|
|
40
|
+
if (signalArg?.aborted) {
|
|
41
|
+
throw failed(signalArg.reason);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
response = await fetchFn(urlArg, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'content-type': 'application/json',
|
|
48
|
+
accept: 'application/json',
|
|
49
|
+
...(optionsArg.userAgent ? { 'user-agent': optionsArg.userAgent } : {}),
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(bodyArg),
|
|
52
|
+
signal,
|
|
53
|
+
});
|
|
54
|
+
text = await response.text();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw failed(error);
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
let body: unknown;
|
|
62
|
+
try {
|
|
63
|
+
body = text.trim() === '' ? undefined : JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
body = undefined;
|
|
66
|
+
}
|
|
67
|
+
return { httpStatus: response.status, body, answer: { httpStatus: response.status, text } };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** A string field of an answer, where it is one and not empty. */
|
|
71
|
+
export const textField = (bodyArg: unknown, keyArg: string): string | undefined => {
|
|
72
|
+
if (!bodyArg || typeof bodyArg !== 'object') {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const value: unknown = Reflect.get(bodyArg, keyArg);
|
|
76
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
77
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { vatIdCountryCodes } from './constants.js';
|
|
2
|
+
import type { IVatIdParts, TVatIdCountryCode } from './interfaces.js';
|
|
3
|
+
|
|
4
|
+
/** A VAT ID as it is printed: spaces, dots and hyphens between its characters are left out. */
|
|
5
|
+
const SEPARATORS = /[\s.\-]+/gu;
|
|
6
|
+
|
|
7
|
+
/** After the country code: letters and digits, 2 to 13 of them (the longest form, France and the Netherlands, has 12). */
|
|
8
|
+
const NUMBER_PART = /^[A-Z0-9+*]{2,13}$/u;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Takes a VAT identification number apart: its country code and the number after it, without the
|
|
12
|
+
* spaces, dots and hyphens it is often printed with, in capitals. `GR` is written `EL`, as VAT IDs of
|
|
13
|
+
* Greece are. Answers `undefined` for anything that does not start with a member state's code or has
|
|
14
|
+
* no number after it; whether the number has its member state's form is the service's to say.
|
|
15
|
+
*/
|
|
16
|
+
export const splitVatId = (valueArg: string): IVatIdParts | undefined => {
|
|
17
|
+
const compact = valueArg.replace(SEPARATORS, '').toUpperCase();
|
|
18
|
+
const prefix = compact.slice(0, 2);
|
|
19
|
+
const countryCode = prefix === 'GR' ? 'EL' : prefix;
|
|
20
|
+
const number = compact.slice(2);
|
|
21
|
+
if (!(vatIdCountryCodes as readonly string[]).includes(countryCode) || !NUMBER_PART.test(number)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return { countryCode: countryCode as TVatIdCountryCode, number, vatId: `${countryCode}${number}` };
|
|
25
|
+
};
|