@factpulse/sdk 3.0.37 → 4.0.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.
Files changed (110) hide show
  1. package/.openapi-generator/FILES +6 -6
  2. package/CHANGELOG.md +10 -13
  3. package/README.md +135 -148
  4. package/api/invoice-processing-api.ts +2 -2
  5. package/dist/esm/models/chorus-pro-credentials.d.ts +19 -7
  6. package/dist/esm/models/chorus-pro-destination.d.ts +2 -2
  7. package/dist/esm/models/{facture-electronique-rest-api-schemas-chorus-pro-chorus-pro-credentials.d.ts → facture-electronique-models-invoice-type-code.d.ts} +63 -18
  8. package/dist/{models/facture-electronique-rest-api-schemas-ereporting-invoice-type-code.js → esm/models/facture-electronique-models-invoice-type-code.js} +66 -10
  9. package/dist/esm/models/{facture-electronique-rest-api-schemas-validation-validation-error-response.d.ts → facture-electronique-rest-api-schemas-cdar-validation-error-response.d.ts} +13 -4
  10. package/dist/esm/models/{facture-electronique-rest-api-schemas-ereporting-invoice-type-code.d.ts → facture-electronique-rest-api-schemas-processing-chorus-pro-credentials.d.ts} +11 -9
  11. package/dist/esm/models/get-chorus-pro-id-request.d.ts +2 -2
  12. package/dist/esm/models/get-invoice-request.d.ts +2 -2
  13. package/dist/esm/models/get-structure-request.d.ts +2 -2
  14. package/dist/esm/models/index.d.ts +3 -3
  15. package/dist/esm/models/index.js +3 -3
  16. package/dist/esm/models/invoice-input.d.ts +2 -2
  17. package/dist/esm/models/invoice-type-code.d.ts +6 -65
  18. package/dist/esm/models/invoice-type-code.js +6 -65
  19. package/dist/esm/models/recipient.d.ts +1 -1
  20. package/dist/esm/models/scheme-id.d.ts +7 -7
  21. package/dist/esm/models/scheme-id.js +7 -7
  22. package/dist/esm/models/search-structure-request.d.ts +2 -2
  23. package/dist/esm/models/simplified-invoice-data.d.ts +2 -2
  24. package/dist/esm/models/submit-complete-invoice-response.d.ts +1 -1
  25. package/dist/esm/models/submit-invoice-request.d.ts +2 -2
  26. package/dist/esm/models/supplier.d.ts +1 -1
  27. package/dist/esm/models/validate-cdarresponse.d.ts +3 -3
  28. package/dist/esm/models/validation-error-response.d.ts +3 -12
  29. package/dist/esm/src/helpers/client.d.ts +43 -265
  30. package/dist/esm/src/helpers/client.js +196 -779
  31. package/dist/esm/src/helpers/index.d.ts +1 -2
  32. package/dist/esm/src/helpers/index.js +1 -3
  33. package/dist/models/chorus-pro-credentials.d.ts +19 -7
  34. package/dist/models/chorus-pro-destination.d.ts +2 -2
  35. package/dist/models/{facture-electronique-rest-api-schemas-chorus-pro-chorus-pro-credentials.d.ts → facture-electronique-models-invoice-type-code.d.ts} +63 -18
  36. package/dist/models/facture-electronique-models-invoice-type-code.js +85 -0
  37. package/dist/models/{facture-electronique-rest-api-schemas-validation-validation-error-response.d.ts → facture-electronique-rest-api-schemas-cdar-validation-error-response.d.ts} +13 -4
  38. package/dist/{esm/models/facture-electronique-rest-api-schemas-ereporting-invoice-type-code.js → models/facture-electronique-rest-api-schemas-processing-chorus-pro-credentials.d.ts} +11 -10
  39. package/dist/models/get-chorus-pro-id-request.d.ts +2 -2
  40. package/dist/models/get-invoice-request.d.ts +2 -2
  41. package/dist/models/get-structure-request.d.ts +2 -2
  42. package/dist/models/index.d.ts +3 -3
  43. package/dist/models/index.js +3 -3
  44. package/dist/models/invoice-input.d.ts +2 -2
  45. package/dist/models/invoice-type-code.d.ts +6 -65
  46. package/dist/models/invoice-type-code.js +6 -65
  47. package/dist/models/recipient.d.ts +1 -1
  48. package/dist/models/scheme-id.d.ts +7 -7
  49. package/dist/models/scheme-id.js +7 -7
  50. package/dist/models/search-structure-request.d.ts +2 -2
  51. package/dist/models/simplified-invoice-data.d.ts +2 -2
  52. package/dist/models/submit-complete-invoice-response.d.ts +1 -1
  53. package/dist/models/submit-invoice-request.d.ts +2 -2
  54. package/dist/models/supplier.d.ts +1 -1
  55. package/dist/models/validate-cdarresponse.d.ts +3 -3
  56. package/dist/models/validation-error-response.d.ts +3 -12
  57. package/dist/src/helpers/client.d.ts +43 -265
  58. package/dist/src/helpers/client.js +199 -823
  59. package/dist/src/helpers/index.d.ts +1 -2
  60. package/dist/src/helpers/index.js +2 -12
  61. package/docs/ChorusProCredentials.md +9 -9
  62. package/docs/ChorusProDestination.md +1 -1
  63. package/docs/FactureElectroniqueModelsInvoiceTypeCode.md +39 -0
  64. package/docs/FactureElectroniqueRestApiSchemasCdarValidationErrorResponse.md +27 -0
  65. package/docs/FactureElectroniqueRestApiSchemasProcessingChorusProCredentials.md +29 -0
  66. package/docs/GetChorusProIdRequest.md +1 -1
  67. package/docs/GetInvoiceRequest.md +1 -1
  68. package/docs/GetStructureRequest.md +1 -1
  69. package/docs/InvoiceInput.md +1 -1
  70. package/docs/InvoiceTypeCode.md +6 -28
  71. package/docs/Recipient.md +1 -1
  72. package/docs/SchemeID.md +4 -4
  73. package/docs/SearchStructureRequest.md +1 -1
  74. package/docs/SimplifiedInvoiceData.md +1 -1
  75. package/docs/SubmitCompleteInvoiceResponse.md +2 -2
  76. package/docs/SubmitInvoiceRequest.md +1 -1
  77. package/docs/Supplier.md +1 -1
  78. package/docs/ValidateCDARResponse.md +2 -2
  79. package/docs/ValidationErrorResponse.md +3 -9
  80. package/models/chorus-pro-credentials.ts +19 -7
  81. package/models/chorus-pro-destination.ts +2 -2
  82. package/models/{facture-electronique-rest-api-schemas-chorus-pro-chorus-pro-credentials.ts → facture-electronique-models-invoice-type-code.ts} +67 -18
  83. package/models/{facture-electronique-rest-api-schemas-validation-validation-error-response.ts → facture-electronique-rest-api-schemas-cdar-validation-error-response.ts} +13 -4
  84. package/models/{facture-electronique-rest-api-schemas-ereporting-invoice-type-code.ts → facture-electronique-rest-api-schemas-processing-chorus-pro-credentials.ts} +11 -13
  85. package/models/get-chorus-pro-id-request.ts +2 -2
  86. package/models/get-invoice-request.ts +2 -2
  87. package/models/get-structure-request.ts +2 -2
  88. package/models/index.ts +3 -3
  89. package/models/invoice-input.ts +2 -2
  90. package/models/invoice-type-code.ts +6 -65
  91. package/models/recipient.ts +1 -1
  92. package/models/scheme-id.ts +7 -7
  93. package/models/search-structure-request.ts +2 -2
  94. package/models/simplified-invoice-data.ts +2 -2
  95. package/models/submit-complete-invoice-response.ts +1 -1
  96. package/models/submit-invoice-request.ts +2 -2
  97. package/models/supplier.ts +1 -1
  98. package/models/validate-cdarresponse.ts +3 -3
  99. package/models/validation-error-response.ts +3 -12
  100. package/package.json +1 -1
  101. package/src/helpers/client.ts +211 -834
  102. package/src/helpers/index.ts +1 -3
  103. package/dist/models/facture-electronique-rest-api-schemas-ereporting-invoice-type-code.d.ts +0 -22
  104. package/docs/FactureElectroniqueRestApiSchemasChorusProChorusProCredentials.md +0 -29
  105. package/docs/FactureElectroniqueRestApiSchemasEreportingInvoiceTypeCode.md +0 -17
  106. package/docs/FactureElectroniqueRestApiSchemasValidationValidationErrorResponse.md +0 -21
  107. /package/dist/esm/models/{facture-electronique-rest-api-schemas-chorus-pro-chorus-pro-credentials.js → facture-electronique-rest-api-schemas-cdar-validation-error-response.js} +0 -0
  108. /package/dist/esm/models/{facture-electronique-rest-api-schemas-validation-validation-error-response.js → facture-electronique-rest-api-schemas-processing-chorus-pro-credentials.js} +0 -0
  109. /package/dist/models/{facture-electronique-rest-api-schemas-chorus-pro-chorus-pro-credentials.js → facture-electronique-rest-api-schemas-cdar-validation-error-response.js} +0 -0
  110. /package/dist/models/{facture-electronique-rest-api-schemas-validation-validation-error-response.js → facture-electronique-rest-api-schemas-processing-chorus-pro-credentials.js} +0 -0
@@ -1,914 +1,291 @@
1
+ /**
2
+ * FactPulse SDK - Thin HTTP wrapper with auto-polling.
3
+ *
4
+ * Usage:
5
+ * const client = new FactPulseClient({ email: 'user@example.com', password: 'secret', clientUid: 'xxx' });
6
+ *
7
+ * // POST /api/v1/processing/invoices/submit-complete-async
8
+ * const result = await client.post('processing/invoices/submit-complete-async', {
9
+ * invoiceData: {...},
10
+ * sourcePdf: Buffer.from(pdf).toString('base64'),
11
+ * destination: { type: 'afnor' }
12
+ * });
13
+ * const pdfBytes = result.content; // auto-decoded, auto-polled
14
+ *
15
+ * // GET /api/v1/chorus-pro/structures/123
16
+ * const structure = await client.get('chorus-pro/structures/123');
17
+ */
1
18
  import axios, { AxiosInstance, AxiosError } from 'axios';
2
- import FormData from 'form-data';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import { FactPulseAuthError, FactPulsePollingTimeout, FactPulseValidationError, ValidationErrorDetail } from './exceptions';
6
-
7
- // =============================================================================
8
- // Credentials interfaces - for simplified configuration
9
- // =============================================================================
10
-
11
- /** Chorus Pro credentials for Zero-Trust mode. */
12
- export interface ChorusProCredentials {
13
- pisteClientId: string;
14
- pisteClientSecret: string;
15
- chorusProLogin: string;
16
- chorusProPassword: string;
17
- sandbox?: boolean;
18
- }
19
19
 
20
- /** AFNOR PDP credentials for Zero-Trust mode. The FactPulse API uses these credentials to authenticate with the AFNOR PDP. */
21
- export interface AFNORCredentials {
22
- flowServiceUrl: string;
23
- tokenUrl: string;
24
- clientId: string;
25
- clientSecret: string;
26
- directoryServiceUrl?: string;
20
+ export class FactPulseError extends Error {
21
+ statusCode?: number;
22
+ details?: unknown[];
23
+ constructor(message: string, statusCode?: number, details?: unknown[]) {
24
+ super(message);
25
+ this.name = 'FactPulseError';
26
+ this.statusCode = statusCode;
27
+ this.details = details ?? [];
28
+ }
27
29
  }
28
30
 
29
31
  export interface FactPulseClientConfig {
30
32
  email: string;
31
33
  password: string;
34
+ clientUid: string;
32
35
  apiUrl?: string;
33
- clientUid?: string;
34
- chorusCredentials?: ChorusProCredentials;
35
- afnorCredentials?: AFNORCredentials;
36
- pollingInterval?: number;
36
+ timeout?: number;
37
37
  pollingTimeout?: number;
38
38
  maxRetries?: number;
39
39
  }
40
40
 
41
- // =============================================================================
42
- // Helpers for anyOf types - avoids verbosity of generated wrappers
43
- // =============================================================================
44
-
45
- type AmountValue = string | number | null | undefined;
46
-
47
- /** Converts a value to an amount string for the API. */
48
- export function amount(value: AmountValue): string {
49
- if (value === null || value === undefined) return '0.00';
50
- if (typeof value === 'number') return value.toFixed(2);
51
- if (typeof value === 'string') return value;
52
- return '0.00';
53
- }
54
-
55
- /** Creates a simplified InvoiceTotals object. */
56
- export function invoiceTotals(
57
- exclTax: AmountValue, vat: AmountValue, inclTax: AmountValue, amountDue: AmountValue,
58
- options?: { globalAllowanceAmount?: AmountValue; globalAllowanceReason?: string; prepayment?: AmountValue }
59
- ): Record<string, unknown> {
60
- const result: Record<string, unknown> = {
61
- totalNetAmount: amount(exclTax), vatAmount: amount(vat),
62
- totalGrossAmount: amount(inclTax), amountDue: amount(amountDue),
63
- };
64
- if (options?.globalAllowanceAmount !== undefined) result.globalAllowanceAmount = amount(options.globalAllowanceAmount);
65
- if (options?.globalAllowanceReason !== undefined) result.globalAllowanceReason = options.globalAllowanceReason;
66
- if (options?.prepayment !== undefined) result.prepayment = amount(options.prepayment);
67
- return result;
68
- }
69
-
70
- /** Creates an invoice line (aligned with InvoiceLine in models.py).
71
- * For VAT rate: either vatRate (code e.g.: "VAT20") or manualVatRate (value e.g.: 20.00) */
72
- export function invoiceLine(
73
- lineNumber: number, itemName: string, quantity: AmountValue, unitNetPrice: AmountValue, lineNetAmount: AmountValue,
74
- options?: { vatRate?: string; manualVatRate?: AmountValue; vatCategory?: string; unit?: string; reference?: string;
75
- lineAllowanceAmount?: AmountValue; allowanceReasonCode?: string; allowanceReason?: string;
76
- periodStartDate?: string; periodEndDate?: string }
77
- ): Record<string, unknown> {
78
- const result: Record<string, unknown> = {
79
- lineNumber, itemName, quantity: amount(quantity), unitNetPrice: amount(unitNetPrice),
80
- lineNetAmount: amount(lineNetAmount),
81
- vatCategory: options?.vatCategory ?? 'S', unit: options?.unit ?? 'LUMP_SUM',
82
- };
83
- // Either vatRate (code) or manualVatRate (value)
84
- if (options?.vatRate) result.vatRate = options.vatRate;
85
- else result.manualVatRate = amount(options?.manualVatRate ?? '20.00');
86
- if (options?.reference) result.reference = options.reference;
87
- if (options?.lineAllowanceAmount !== undefined) result.lineAllowanceAmount = amount(options.lineAllowanceAmount);
88
- if (options?.allowanceReasonCode) result.allowanceReasonCode = options.allowanceReasonCode;
89
- if (options?.allowanceReason) result.allowanceReason = options.allowanceReason;
90
- if (options?.periodStartDate) result.periodStartDate = options.periodStartDate;
91
- if (options?.periodEndDate) result.periodEndDate = options.periodEndDate;
92
- return result;
93
- }
94
-
95
- /** Creates a VAT line (aligned with VATLine in models.py).
96
- * For rate: either rate (code e.g.: "VAT20") or manualRate (value e.g.: 20.00) */
97
- export function vatLine(
98
- taxableAmount: AmountValue, vatAmount: AmountValue,
99
- options?: { rate?: string; manualRate?: AmountValue; category?: string }
100
- ): Record<string, unknown> {
101
- const result: Record<string, unknown> = {
102
- taxableAmount: amount(taxableAmount), vatAmount: amount(vatAmount), category: options?.category ?? 'S',
103
- };
104
- // Either rate (code) or manualRate (value)
105
- if (options?.rate) result.rate = options.rate;
106
- else result.manualRate = amount(options?.manualRate ?? '20.00');
107
- return result;
108
- }
109
-
110
- /** Creates a postal address for the FactPulse API. */
111
- export function postalAddress(
112
- lineOne: string, postalCode: string, city: string,
113
- options?: { countryCode?: string; lineTwo?: string; lineThree?: string }
114
- ): Record<string, unknown> {
115
- const result: Record<string, unknown> = { lineOne, postalCode, city, countryCode: options?.countryCode ?? 'FR' };
116
- if (options?.lineTwo) result.lineTwo = options.lineTwo;
117
- if (options?.lineThree) result.lineThree = options.lineThree;
118
- return result;
119
- }
120
-
121
- /** Creates an electronic address for the FactPulse API. schemeId: "0009"=SIREN, "0225"=SIRET */
122
- export function electronicAddress(identifier: string, schemeId = '0009'): Record<string, unknown> {
123
- return { identifier, schemeId };
124
- }
125
-
126
- /** Computes the French intra-community VAT number from a SIREN. */
127
- function computeVatIntra(siren: string): string | null {
128
- if (siren.length !== 9 || !/^\d+$/.test(siren)) return null;
129
- const cle = (12 + 3 * (parseInt(siren, 10) % 97)) % 97;
130
- return `FR${cle.toString().padStart(2, '0')}${siren}`;
131
- }
132
-
133
- /** Creates a supplier (issuer) with auto-computed SIREN, intra-EU VAT number and addresses. */
134
- export function supplier(
135
- name: string, siret: string, addressLine1: string, postalCode: string, city: string,
136
- options?: { supplierId?: number; siren?: string; vatNumber?: string; iban?: string; countryCode?: string; addressLine2?: string; supplierServiceId?: number; supplierBankDetailsCode?: number }
137
- ): Record<string, unknown> {
138
- const opts = options ?? {};
139
- const siren = opts.siren ?? (siret.length === 14 ? siret.slice(0, 9) : undefined);
140
- const vatNumber = opts.vatNumber ?? (siren ? computeVatIntra(siren) : null);
141
- const result: Record<string, unknown> = {
142
- name, supplierId: opts.supplierId ?? 0, siret,
143
- electronicAddress: electronicAddress(siret, '0225'),
144
- postalAddress: postalAddress(addressLine1, postalCode, city, { countryCode: opts.countryCode, lineTwo: opts.addressLine2 }),
145
- };
146
- if (siren) result.siren = siren;
147
- if (vatNumber) result.vatNumber = vatNumber;
148
- if (opts.iban) result.iban = opts.iban;
149
- if (opts.supplierServiceId) result.supplierServiceId = opts.supplierServiceId;
150
- if (opts.supplierBankDetailsCode) result.supplierBankDetailsCode = opts.supplierBankDetailsCode;
151
- return result;
152
- }
153
-
154
- /** Creates a recipient (customer) with auto-computed SIREN and addresses. */
155
- export function recipient(
156
- name: string, siret: string, addressLine1: string, postalCode: string, city: string,
157
- options?: { siren?: string; countryCode?: string; addressLine2?: string; executingServiceCode?: string }
158
- ): Record<string, unknown> {
159
- const opts = options ?? {};
160
- const siren = opts.siren ?? (siret.length === 14 ? siret.slice(0, 9) : undefined);
161
- const result: Record<string, unknown> = {
162
- name, siret,
163
- electronicAddress: electronicAddress(siret, '0225'),
164
- postalAddress: postalAddress(addressLine1, postalCode, city, { countryCode: opts.countryCode, lineTwo: opts.addressLine2 }),
165
- };
166
- if (siren) result.siren = siren;
167
- if (opts.executingServiceCode) result.executingServiceCode = opts.executingServiceCode;
168
- return result;
169
- }
170
-
171
- /**
172
- * Creates a beneficiary (factor) for factoring.
173
- *
174
- * The beneficiary (BG-10 / PayeeTradeParty) is used when payment
175
- * must be made to a third party different from the supplier, typically
176
- * a factor (factoring company).
177
- *
178
- * For factored invoices, you also need to:
179
- * - Use a factored document type (393, 396, 501, 502, 472, 473)
180
- * - Add an ACC note with the subrogation mention
181
- * - The beneficiary's IBAN will be used for payment
182
- *
183
- * @param name Factor's business name (BT-59)
184
- * @param options Options: siret (BT-60), siren (BT-61), iban, bic
185
- * @returns Dict ready to be used in a factored invoice
186
- *
187
- * @example
188
- * const factor = beneficiary('FACTOR SAS', {
189
- * siret: '30000000700033',
190
- * iban: 'FR76 3000 4000 0500 0012 3456 789',
191
- * });
192
- */
193
- export function beneficiary(
194
- name: string,
195
- options?: { siret?: string; siren?: string; iban?: string; bic?: string }
196
- ): Record<string, unknown> {
197
- const opts = options ?? {};
198
- // Auto-compute SIREN from SIRET
199
- const siren = opts.siren ?? (opts.siret && opts.siret.length === 14 ? opts.siret.slice(0, 9) : undefined);
200
-
201
- const result: Record<string, unknown> = { name };
202
- if (opts.siret) result.siret = opts.siret;
203
- if (siren) result.siren = siren;
204
- if (opts.iban) result.iban = opts.iban;
205
- if (opts.bic) result.bic = opts.bic;
206
- return result;
207
- }
208
-
209
- // =============================================================================
210
- // Main client
211
- // =============================================================================
212
-
213
41
  const DEFAULT_API_URL = 'https://factpulse.fr';
214
- const DEFAULT_POLLING_INTERVAL = 2000;
215
- const DEFAULT_POLLING_TIMEOUT = 120000;
216
- const DEFAULT_MAX_RETRIES = 1;
217
-
218
- interface InternalConfig {
219
- email: string; password: string; apiUrl: string; clientUid: string;
220
- chorusCredentials?: ChorusProCredentials; afnorCredentials?: AFNORCredentials;
221
- pollingInterval: number; pollingTimeout: number; maxRetries: number;
222
- }
223
42
 
224
43
  export class FactPulseClient {
225
- private config: InternalConfig;
226
- private httpClient: AxiosInstance;
227
- private accessToken: string | null = null;
228
- private refreshToken: string | null = null;
229
- private tokenExpiresAt: number | null = null;
230
-
231
- public readonly chorusCredentials?: ChorusProCredentials;
232
- public readonly afnorCredentials?: AFNORCredentials;
44
+ private readonly apiUrl: string;
45
+ private readonly email: string;
46
+ private readonly password: string;
47
+ private readonly clientUid: string;
48
+ private readonly timeout: number;
49
+ private readonly pollingTimeout: number;
50
+ private readonly httpClient: AxiosInstance;
51
+ private token: string | null = null;
52
+ private tokenExpiresAt = 0;
233
53
 
234
54
  constructor(config: FactPulseClientConfig) {
235
- this.config = {
236
- email: config.email, password: config.password,
237
- apiUrl: (config.apiUrl || DEFAULT_API_URL).replace(/\/$/, ''),
238
- clientUid: config.clientUid || '',
239
- chorusCredentials: config.chorusCredentials,
240
- afnorCredentials: config.afnorCredentials,
241
- pollingInterval: config.pollingInterval || DEFAULT_POLLING_INTERVAL,
242
- pollingTimeout: config.pollingTimeout || DEFAULT_POLLING_TIMEOUT,
243
- maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
244
- };
245
- this.chorusCredentials = config.chorusCredentials;
246
- this.afnorCredentials = config.afnorCredentials;
247
- this.httpClient = axios.create({ timeout: 30000, headers: { 'Content-Type': 'application/json' } });
55
+ this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, '');
56
+ this.email = config.email;
57
+ this.password = config.password;
58
+ this.clientUid = config.clientUid;
59
+ this.timeout = config.timeout ?? 60000;
60
+ this.pollingTimeout = config.pollingTimeout ?? 120000;
61
+ this.httpClient = axios.create({
62
+ timeout: this.timeout,
63
+ validateStatus: () => true, // Handle all status codes manually
64
+ });
248
65
  }
249
66
 
250
- getChorusCredentialsForApi(): Record<string, unknown> | undefined {
251
- if (!this.chorusCredentials) return undefined;
252
- return {
253
- piste_client_id: this.chorusCredentials.pisteClientId,
254
- piste_client_secret: this.chorusCredentials.pisteClientSecret,
255
- chorus_pro_login: this.chorusCredentials.chorusProLogin,
256
- chorus_pro_password: this.chorusCredentials.chorusProPassword,
257
- sandbox: this.chorusCredentials.sandbox ?? true,
258
- };
67
+ /** POST request to /api/v1/{path} */
68
+ async post(path: string, data?: Record<string, unknown>): Promise<unknown> {
69
+ return this._doRequest('POST', path, data, true);
259
70
  }
260
71
 
261
- getAfnorCredentialsForApi(): Record<string, unknown> | undefined {
262
- if (!this.afnorCredentials) return undefined;
263
- return {
264
- client_id: this.afnorCredentials.clientId,
265
- client_secret: this.afnorCredentials.clientSecret,
266
- flow_service_url: this.afnorCredentials.flowServiceUrl,
267
- };
72
+ /** GET request to /api/v1/{path} */
73
+ async get(path: string, params?: Record<string, unknown>): Promise<unknown> {
74
+ return this._doRequest('GET', path, params, true);
268
75
  }
269
76
 
270
- // Shorter aliases
271
- getChorusProCredentials(): Record<string, unknown> | undefined { return this.getChorusCredentialsForApi(); }
272
- getAfnorCredentials(): Record<string, unknown> | undefined { return this.getAfnorCredentialsForApi(); }
77
+ // Dynamic endpoint builder (alternative syntax)
78
+ get processing() { return this._endpoint('processing'); }
79
+ get chorus_pro() { return this._endpoint('chorus-pro'); }
80
+ get afnor() { return this._endpoint('afnor'); }
273
81
 
274
- private async obtainToken(): Promise<{ access: string; refresh: string }> {
275
- const payload: Record<string, string> = { username: this.config.email, password: this.config.password };
276
- if (this.config.clientUid) payload.client_uid = this.config.clientUid;
277
- try {
278
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/token/`, payload);
279
- return response.data;
280
- } catch (error) {
281
- const axiosError = error as AxiosError<{ detail?: string }>;
282
- throw new FactPulseAuthError(`Unable to obtain JWT token: ${axiosError.response?.data?.detail || axiosError.message}`);
283
- }
82
+ private _endpoint(path: string): Endpoint {
83
+ return new Endpoint(this, path);
284
84
  }
285
85
 
286
- async ensureAuthenticated(forceRefresh: boolean = false): Promise<void> {
287
- const now = Date.now();
288
- if (forceRefresh || !this.accessToken || (this.tokenExpiresAt && now >= this.tokenExpiresAt)) {
289
- const tokens = await this.obtainToken();
290
- this.accessToken = tokens.access; this.refreshToken = tokens.refresh;
291
- this.tokenExpiresAt = now + 28 * 60 * 1000;
292
- }
86
+ async _request(method: 'GET' | 'POST', path: string, data?: Record<string, unknown>): Promise<unknown> {
87
+ return this._doRequest(method, path, data, true);
293
88
  }
294
89
 
295
- resetAuth(): void { this.accessToken = this.refreshToken = null; this.tokenExpiresAt = null; }
90
+ private async _doRequest(method: 'GET' | 'POST', path: string, data: Record<string, unknown> | undefined, retryAuth: boolean): Promise<unknown> {
91
+ await this._ensureAuth();
92
+ const url = `${this.apiUrl}/api/v1/${path}`;
93
+ const headers = { Authorization: `Bearer ${this.token}` };
296
94
 
297
- async pollTask(taskId: string, timeout?: number, interval?: number): Promise<Record<string, unknown>> {
298
- const timeoutMs = timeout ?? this.config.pollingTimeout;
299
- const intervalMs = interval ?? this.config.pollingInterval;
300
- const startTime = Date.now();
301
- let currentInterval = intervalMs;
95
+ let response;
96
+ try {
97
+ response = method === 'GET'
98
+ ? await this.httpClient.get(url, { headers, params: data })
99
+ : await this.httpClient.post(url, data, { headers });
100
+ } catch (e) {
101
+ throw new FactPulseError(`Network error: ${(e as Error).message}`);
102
+ }
302
103
 
303
- while (true) {
304
- if (Date.now() - startTime > timeoutMs) throw new FactPulsePollingTimeout(taskId, timeoutMs);
305
- await this.ensureAuthenticated();
306
- try {
307
- const response = await this.httpClient.get(`${this.config.apiUrl}/api/v1/processing/tasks/${taskId}/status`, {
308
- headers: { Authorization: `Bearer ${this.accessToken}` },
309
- });
310
- const { status, result } = response.data;
311
- if (status === 'SUCCESS') return (result as Record<string, unknown>) || {};
312
- if (status === 'FAILURE') {
313
- // Format AFNOR: errorMessage, details
314
- const failureResult = result as Record<string, unknown> | undefined;
315
- const errors: ValidationErrorDetail[] = Array.isArray(result?.details) ? result.details.filter((e: unknown): e is ValidationErrorDetail => typeof e === 'object' && e !== null) : [];
316
- throw new FactPulseValidationError(`Task ${taskId} failed: ${result?.errorMessage || 'Unknown error'}`, errors);
317
- }
318
- await new Promise(resolve => setTimeout(resolve, currentInterval));
319
- currentInterval = Math.min(currentInterval * 1.5, 10000);
320
- } catch (error) {
321
- if (error instanceof FactPulseValidationError || error instanceof FactPulsePollingTimeout) throw error;
322
- const axiosError = error as AxiosError;
323
- if (axiosError.response?.status === 401) { this.resetAuth(); continue; }
324
- throw new FactPulseValidationError(`API Error: ${axiosError.message}`);
325
- }
104
+ if (response.status === 401 && retryAuth) {
105
+ this._invalidateToken();
106
+ return this._doRequest(method, path, data, false);
326
107
  }
327
- }
328
108
 
329
- async generateFacturx(invoiceData: Record<string, unknown> | string, pdfPath: string, profile = 'EN16931', outputFormat = 'pdf', sync = true, timeout?: number): Promise<Buffer | string> {
330
- const jsonData = typeof invoiceData === 'string' ? invoiceData : JSON.stringify(invoiceData);
331
- let taskId: string | null = null;
332
-
333
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
334
- await this.ensureAuthenticated();
335
- const form = new FormData();
336
- form.append('invoice_data', Buffer.from(jsonData, 'utf-8'), { contentType: 'application/json' });
337
- form.append('profile', profile);
338
- form.append('output_format', outputFormat);
339
- form.append('source_pdf', fs.createReadStream(pdfPath), { filename: path.basename(pdfPath), contentType: 'application/pdf' });
340
- try {
341
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/generate-invoice`, form, {
342
- headers: { ...form.getHeaders(), Authorization: `Bearer ${this.accessToken}` }, timeout: 60000,
343
- });
344
- taskId = response.data.taskId; break;
345
- } catch (error) {
346
- const axiosError = error as AxiosError<{ detail?: unknown; errorMessage?: string }>;
347
- if (axiosError.response?.status === 401 && attempt < this.config.maxRetries) { this.resetAuth(); continue; }
348
-
349
- // Extract error details from response body
350
- const responseData = axiosError.response?.data;
351
- let errorMsg = `API Error (${axiosError.response?.status || 'unknown'}): ${axiosError.message}`;
352
- const errors: ValidationErrorDetail[] = [];
353
-
354
- if (responseData) {
355
- // Format FastAPI/Pydantic: {"detail": [{"loc": [...], "msg": "...", "type": "..."}]}
356
- if (Array.isArray(responseData.detail)) {
357
- errorMsg = 'Validation error';
358
- for (const err of responseData.detail) {
359
- if (typeof err === 'object' && err !== null) {
360
- const loc = (err as { loc?: unknown[] }).loc || [];
361
- errors.push({
362
- level: 'ERROR',
363
- item: loc.map(String).join(' -> '),
364
- reason: (err as { msg?: string }).msg || String(err),
365
- source: 'validation',
366
- code: (err as { type?: string }).type,
367
- });
368
- }
369
- }
370
- } else if (typeof responseData.detail === 'string') {
371
- errorMsg = responseData.detail;
372
- } else if (responseData.errorMessage) {
373
- errorMsg = responseData.errorMessage;
374
- }
375
- }
109
+ const result = this._parseResponse(response.status, response.data);
376
110
 
377
- console.error(`API Error ${axiosError.response?.status}:`, responseData);
378
- throw new FactPulseValidationError(errorMsg, errors);
111
+ // Auto-poll: support both taskId (camelCase) and task_id (snake_case)
112
+ if (result && typeof result === 'object') {
113
+ const r = result as Record<string, unknown>;
114
+ const taskId = r.taskId ?? r.task_id;
115
+ if (taskId) {
116
+ return this._poll(taskId as string);
379
117
  }
380
118
  }
381
- if (!taskId) throw new FactPulseValidationError("No task ID");
382
- if (!sync) return taskId;
383
- const result = await this.pollTask(taskId, timeout);
384
-
385
- // Check for business error (task succeeded but business result is ERROR)
386
- if (result.status === 'ERROR') {
387
- const errorMsg = (result.errorMessage as string) || 'Business error';
388
- const errors: ValidationErrorDetail[] = Array.isArray(result.details)
389
- ? result.details.filter((e: unknown): e is ValidationErrorDetail => typeof e === 'object' && e !== null)
390
- : [];
391
- throw new FactPulseValidationError(errorMsg, errors);
392
- }
393
119
 
394
- if (result.content_b64) return Buffer.from(result.content_b64 as string, 'base64');
395
- throw new FactPulseValidationError('No content');
396
- }
120
+ // Auto-decode: support both content_b64 and contentB64
121
+ if (result && typeof result === 'object') {
122
+ const r = result as Record<string, unknown>;
123
+ const b64Content = r.content_b64 ?? r.contentB64;
124
+ if (b64Content) {
125
+ r.content = Buffer.from(b64Content as string, 'base64');
126
+ delete r.content_b64;
127
+ delete r.contentB64;
128
+ }
129
+ }
397
130
 
398
- static formatAmount(m: unknown): string {
399
- if (m === null || m === undefined) return '0.00';
400
- if (typeof m === 'number') return m.toFixed(2);
401
- if (typeof m === 'string') return m;
402
- return '0.00';
131
+ return result;
403
132
  }
404
133
 
405
- // =========================================================================
406
- // AFNOR - Authentication and internal helpers
407
- // =========================================================================
408
-
409
- /**
410
- * Retrieves AFNOR credentials (stored or zero-trust mode).
411
- * Zero-trust mode: Returns the afnorCredentials provided to the constructor.
412
- * Stored mode: Retrieves credentials via GET /api/v1/afnor/credentials.
413
- */
414
- private async getAfnorCredentialsInternal(): Promise<AFNORCredentials> {
415
- // Zero-trust mode: credentials provided to the constructor
416
- if (this.afnorCredentials) {
417
- return this.afnorCredentials;
418
- }
134
+ private _parseResponse(status: number, data: unknown): unknown {
135
+ if (status >= 200 && status < 300) return data;
419
136
 
420
- // Stored mode: retrieve credentials via the API
421
- await this.ensureAuthenticated();
137
+ let msg = `HTTP ${status}`;
138
+ const details: unknown[] = [];
422
139
 
423
- try {
424
- const response = await this.httpClient.get(`${this.config.apiUrl}/api/v1/afnor/credentials`, {
425
- headers: { Authorization: `Bearer ${this.accessToken}` },
426
- });
427
-
428
- const creds = response.data;
429
- return {
430
- flowServiceUrl: creds.flow_service_url,
431
- tokenUrl: creds.token_url,
432
- clientId: creds.client_id,
433
- clientSecret: creds.client_secret,
434
- directoryServiceUrl: creds.directory_service_url,
435
- };
436
- } catch (error) {
437
- const axiosError = error as AxiosError<{ detail?: { error?: string; message?: string } }>;
438
- if (axiosError.response?.status === 400) {
439
- const detail = axiosError.response.data?.detail;
440
- if (typeof detail === 'object' && detail?.error === 'NO_CLIENT_UID') {
441
- throw new FactPulseAuthError(
442
- "No client_uid in JWT. To use AFNOR endpoints, either:\n" +
443
- "1. Generate a token with a client_uid (stored mode)\n" +
444
- "2. Provide AFNORCredentials to the client constructor (zero-trust mode)"
445
- );
446
- }
140
+ if (data && typeof data === 'object') {
141
+ const d = data as Record<string, unknown>;
142
+ if (Array.isArray(d.detail)) {
143
+ msg = 'Validation error: ' + d.detail.map((e: unknown) => {
144
+ if (typeof e === 'object' && e !== null) {
145
+ const err = e as Record<string, unknown>;
146
+ details.push(e);
147
+ return `${(err.loc as unknown[])?.slice(-1)[0] ?? '?'}: ${err.msg ?? '?'}`;
148
+ }
149
+ return String(e);
150
+ }).join('; ');
151
+ } else if (typeof d.detail === 'string') {
152
+ msg = d.detail;
153
+ } else if (typeof d.errorMessage === 'string') {
154
+ msg = d.errorMessage;
447
155
  }
448
- throw new FactPulseAuthError(`Failed to retrieve AFNOR credentials: ${axiosError.message}`);
449
156
  }
450
- }
451
157
 
452
- /**
453
- * Obtains the AFNOR OAuth2 token and the PDP URL.
454
- * This method:
455
- * 1. Retrieves AFNOR credentials (stored or zero-trust mode)
456
- * 2. Performs AFNOR OAuth to obtain a token
457
- * 3. Returns the token and the PDP URL
458
- */
459
- private async getAfnorTokenAndUrl(): Promise<{ token: string; pdpBaseUrl: string }> {
460
- // Step 1: Get AFNOR credentials
461
- const credentials = await this.getAfnorCredentialsInternal();
462
-
463
- // Step 2: Perform AFNOR OAuth via the FactPulse proxy
464
- const oauthData = new URLSearchParams({
465
- grant_type: 'client_credentials',
466
- client_id: credentials.clientId,
467
- client_secret: credentials.clientSecret,
468
- });
158
+ throw new FactPulseError(msg, status, details);
159
+ }
469
160
 
470
- try {
471
- const response = await this.httpClient.post(
472
- `${this.config.apiUrl}/api/v1/afnor/oauth/token`,
473
- oauthData.toString(),
474
- {
475
- headers: {
476
- 'Content-Type': 'application/x-www-form-urlencoded',
477
- 'X-PDP-Token-URL': credentials.tokenUrl,
478
- },
479
- }
480
- );
161
+ private async _poll(taskId: string): Promise<unknown> {
162
+ const start = Date.now();
163
+ let interval = 1000;
481
164
 
482
- const tokenData = response.data;
483
- if (!tokenData.access_token) {
484
- throw new FactPulseAuthError('Invalid AFNOR OAuth2 response: access_token missing');
165
+ while (true) {
166
+ const elapsed = Date.now() - start;
167
+ if (elapsed >= this.pollingTimeout) {
168
+ throw new FactPulseError(`Polling timeout after ${this.pollingTimeout}ms for task ${taskId}`);
485
169
  }
486
170
 
487
- return {
488
- token: tokenData.access_token,
489
- pdpBaseUrl: credentials.flowServiceUrl,
490
- };
491
- } catch (error) {
492
- const axiosError = error as AxiosError;
493
- throw new FactPulseAuthError(`AFNOR OAuth2 failed: ${axiosError.message}`);
494
- }
495
- }
496
-
497
- /**
498
- * Performs a request to the AFNOR API with auth and error handling.
499
- * IMPORTANT: This method uses the AFNOR OAuth token, NOT the FactPulse JWT!
500
- */
501
- private async makeAfnorRequest<T = unknown>(
502
- method: 'GET' | 'POST',
503
- endpoint: string,
504
- options?: { data?: unknown; files?: FormData; params?: Record<string, string> }
505
- ): Promise<T> {
506
- // Obtenir le token AFNOR et l'URL de la PDP
507
- const { token: afnorToken, pdpBaseUrl } = await this.getAfnorTokenAndUrl();
508
-
509
- const url = `${this.config.apiUrl}/api/v1/afnor${endpoint}`;
510
-
511
- // ALWAYS use the AFNOR token + X-PDP-Base-URL header
512
- const headers: Record<string, string> = {
513
- 'Authorization': `Bearer ${afnorToken}`,
514
- 'X-PDP-Base-URL': pdpBaseUrl,
515
- };
516
-
517
- try {
171
+ await this._ensureAuth();
518
172
  let response;
519
- if (options?.files) {
520
- response = await this.httpClient.request({
521
- method,
522
- url,
523
- data: options.files,
524
- headers: { ...options.files.getHeaders(), ...headers },
525
- params: options?.params,
526
- timeout: 60000,
527
- });
528
- } else {
529
- response = await this.httpClient.request({
530
- method,
531
- url,
532
- data: options?.data,
533
- headers: { ...headers, 'Content-Type': 'application/json' },
534
- params: options?.params,
535
- timeout: 30000,
536
- });
173
+ try {
174
+ response = await this.httpClient.get(
175
+ `${this.apiUrl}/api/v1/processing/tasks/${taskId}/status`,
176
+ { headers: { Authorization: `Bearer ${this.token}` } }
177
+ );
178
+ } catch (e) {
179
+ throw new FactPulseError(`Network error while polling: ${(e as Error).message}`);
537
180
  }
538
- return response.data;
539
- } catch (error) {
540
- const axiosError = error as AxiosError<{ errorMessage?: string; detail?: string }>;
541
- const errorMsg = axiosError.response?.data?.errorMessage ||
542
- axiosError.response?.data?.detail ||
543
- axiosError.message;
544
- throw new FactPulseValidationError(`AFNOR Error: ${errorMsg}`);
545
- }
546
- }
547
-
548
- // ==================== AFNOR Directory ====================
549
-
550
- /** Gets a facility by SIRET in the AFNOR directory. */
551
- async getSiretAfnor(siret: string): Promise<Record<string, unknown>> {
552
- return this.makeAfnorRequest('GET', `/directory/v1/siret/code-insee:${siret}`);
553
- }
554
-
555
- /** Gets a legal unit by SIREN in the AFNOR directory. */
556
- async getSirenAfnor(siren: string): Promise<Record<string, unknown>> {
557
- return this.makeAfnorRequest('GET', `/directory/v1/siren/code-insee:${siren}`);
558
- }
559
-
560
- /** Searches for legal units (SIREN) in the AFNOR directory. */
561
- async searchSirenAfnor(
562
- options: { filters?: Record<string, unknown>; limit?: number } = {}
563
- ): Promise<Record<string, unknown>> {
564
- const { filters = {}, limit = 25 } = options;
565
- return this.makeAfnorRequest('POST', '/directory/v1/siren/search', { data: { filters, limit } });
566
- }
567
-
568
- /** Searches for routing codes in the AFNOR directory. */
569
- async searchRoutingCodesAfnor(
570
- options: { filters?: Record<string, unknown>; limit?: number } = {}
571
- ): Promise<Record<string, unknown>> {
572
- const { filters = {}, limit = 25 } = options;
573
- return this.makeAfnorRequest('POST', '/directory/v1/routing-code/search', { data: { filters, limit } });
574
- }
575
181
 
576
- /** Gets a routing code by SIRET and routing identifier. */
577
- async getRoutingCodeAfnor(siret: string, routingIdentifier: string): Promise<Record<string, unknown>> {
578
- return this.makeAfnorRequest('GET', `/directory/v1/routing-code/siret:${siret}/code:${routingIdentifier}`);
579
- }
182
+ if (response.status === 401) {
183
+ this._invalidateToken();
184
+ continue;
185
+ }
580
186
 
581
- // ==================== AFNOR Flow ====================
582
-
583
- /**
584
- * Submits an invoice to a PDP via the AFNOR API.
585
- * Authentication uses the AFNOR OAuth token (obtained automatically),
586
- * either via stored credentials (stored mode), or via the afnorCredentials
587
- * provided to the constructor (zero-trust mode).
588
- *
589
- * @param pdfBuffer Buffer of the Factur-X PDF to submit
590
- * @param flowName Flow name (e.g.: "Invoice INV-2025-001")
591
- * @param options Options: trackingId, flowSyntax (CII/UBL), flowProfile
592
- */
593
- async submitInvoiceAfnor(
594
- pdfBuffer: Buffer,
595
- flowName: string,
596
- options: { trackingId?: string; flowSyntax?: string; flowProfile?: string } = {}
597
- ): Promise<Record<string, unknown>> {
598
- const { trackingId, flowSyntax = 'CII', flowProfile = 'EN16931' } = options;
599
-
600
- // Compute SHA-256
601
- const crypto = require('crypto');
602
- const sha256 = crypto.createHash('sha256').update(pdfBuffer).digest('hex');
603
-
604
- // Prepare flowInfo
605
- const flowInfo: Record<string, unknown> = { name: flowName, flowSyntax, flowProfile, sha256 };
606
- if (trackingId) flowInfo.trackingId = trackingId;
607
-
608
- const form = new FormData();
609
- form.append('file', pdfBuffer, { filename: 'facture.pdf', contentType: 'application/pdf' });
610
- form.append('flowInfo', JSON.stringify(flowInfo), { contentType: 'application/json' });
611
-
612
- return this.makeAfnorRequest('POST', '/flow/v1/flows', { files: form });
613
- }
187
+ const data = response.data as Record<string, unknown>;
188
+ const status = data.status;
614
189
 
615
- async searchFlowsAfnor(criteria: { trackingId?: string; status?: string; offset?: number; limit?: number } = {}): Promise<Record<string, unknown>> {
616
- const searchBody: Record<string, unknown> = {
617
- offset: criteria.offset ?? 0,
618
- limit: criteria.limit ?? 25,
619
- where: {},
620
- };
621
- if (criteria.trackingId) (searchBody.where as Record<string, unknown>).trackingId = criteria.trackingId;
622
- if (criteria.status) (searchBody.where as Record<string, unknown>).status = criteria.status;
190
+ if (status === 'SUCCESS') {
191
+ const result = (data.result ?? {}) as Record<string, unknown>;
192
+ if (result.content_b64) {
193
+ result.content = Buffer.from(result.content_b64 as string, 'base64');
194
+ delete result.content_b64;
195
+ }
196
+ return result;
197
+ }
623
198
 
624
- return this.makeAfnorRequest('POST', '/flow/v1/flows/search', { data: searchBody });
625
- }
199
+ if (status === 'FAILURE') {
200
+ const result = (data.result ?? {}) as Record<string, unknown>;
201
+ throw new FactPulseError(
202
+ (result.errorMessage as string) ?? 'Task failed',
203
+ undefined,
204
+ result.details as unknown[]
205
+ );
206
+ }
626
207
 
627
- async downloadFlowAfnor(flowId: string): Promise<Buffer> {
628
- // For downloading, we need to handle the response type differently
629
- const { token: afnorToken, pdpBaseUrl } = await this.getAfnorTokenAndUrl();
630
-
631
- const url = `${this.config.apiUrl}/api/v1/afnor/flow/v1/flows/${flowId}`;
632
- const response = await this.httpClient.get(url, {
633
- headers: {
634
- 'Authorization': `Bearer ${afnorToken}`,
635
- 'X-PDP-Base-URL': pdpBaseUrl,
636
- },
637
- responseType: 'arraybuffer',
638
- });
639
- return Buffer.from(response.data);
208
+ await new Promise(r => setTimeout(r, Math.min(interval, this.pollingTimeout - elapsed)));
209
+ interval = Math.min(interval * 1.5, 10000);
210
+ }
640
211
  }
641
212
 
642
- /**
643
- * Retrieves JSON metadata of an incoming flow (supplier invoice).
644
- * Downloads an incoming flow from the AFNOR PDP and extracts invoice
645
- * metadata into a unified JSON format. Supports Factur-X, CII and UBL.
646
- *
647
- * Note: This endpoint uses FactPulse JWT authentication (not AFNOR OAuth).
648
- * The FactPulse server handles calling the PDP with stored credentials.
649
- *
650
- * @param flowId Flow identifier (UUID)
651
- * @param includeDocument If true, includes the original document encoded in base64
652
- * @returns Invoice metadata (supplier, amounts, dates, etc.)
653
- *
654
- * @example
655
- * const invoice = await client.getIncomingInvoiceAfnor("550e8400-...");
656
- * console.log(`Supplier: ${invoice.supplier.name}`);
657
- * console.log(`Total incl. tax: ${invoice.total_incl_tax} ${invoice.currency}`);
658
- */
659
- async getIncomingInvoiceAfnor(
660
- flowId: string,
661
- includeDocument: boolean = false
662
- ): Promise<Record<string, unknown>> {
663
- await this.ensureAuthenticated();
664
-
665
- const url = `${this.config.apiUrl}/api/v1/afnor/incoming-flows/${flowId}`;
666
- const params: Record<string, string> = {};
667
- if (includeDocument) {
668
- params.include_document = 'true';
213
+ private async _ensureAuth(): Promise<void> {
214
+ if (Date.now() >= this.tokenExpiresAt) {
215
+ await this._refreshToken();
669
216
  }
217
+ }
670
218
 
219
+ private async _refreshToken(): Promise<void> {
220
+ let response;
671
221
  try {
672
- const response = await this.httpClient.get(url, {
673
- headers: { 'Authorization': `Bearer ${this.accessToken}` },
674
- params: Object.keys(params).length > 0 ? params : undefined,
675
- timeout: 60000,
222
+ response = await this.httpClient.post(`${this.apiUrl}/api/token/`, {
223
+ username: this.email,
224
+ password: this.password,
225
+ client_uid: this.clientUid,
676
226
  });
677
- return response.data;
678
- } catch (error) {
679
- const axiosError = error as AxiosError<{ detail?: string }>;
680
- throw new FactPulseValidationError(
681
- `Incoming flow error: ${axiosError.response?.data?.detail || axiosError.message}`
682
- );
227
+ } catch (e) {
228
+ throw new FactPulseError(`Auth network error: ${(e as Error).message}`);
683
229
  }
684
- }
685
230
 
686
- async healthcheckAfnor(): Promise<Record<string, unknown>> {
687
- return this.makeAfnorRequest('GET', '/flow/v1/healthcheck');
688
- }
689
-
690
- // ==================== Chorus Pro ====================
691
-
692
- async rechercherStructureChorus(criteria: Record<string, unknown>): Promise<unknown[]> {
693
- await this.ensureAuthenticated();
694
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/chorus-pro/structures/rechercher`, criteria, {
695
- headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
696
- });
697
- return response.data;
698
- }
231
+ if (response.status !== 200) {
232
+ throw new FactPulseError(`Authentication failed: HTTP ${response.status}`, response.status);
233
+ }
699
234
 
700
- async consulterStructureChorus(idStructureCpp: number): Promise<Record<string, unknown>> {
701
- await this.ensureAuthenticated();
702
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/chorus-pro/structures/consulter`, { id_structure_cpp: idStructureCpp }, {
703
- headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
704
- });
705
- return response.data;
706
- }
235
+ const data = response.data as { access?: string };
236
+ if (!data.access) {
237
+ throw new FactPulseError('Invalid auth response');
238
+ }
707
239
 
708
- /**
709
- * Lists the services of a Chorus Pro structure.
710
- * @param idStructureCpp Chorus Pro ID of the structure
711
- * @returns Object with listeServices, total, codeRetour, libelle
712
- */
713
- async listerServicesStructureChorus(idStructureCpp: number): Promise<Record<string, unknown>> {
714
- await this.ensureAuthenticated();
715
- const response = await this.httpClient.get(`${this.config.apiUrl}/api/v1/chorus-pro/structures/${idStructureCpp}/services`, {
716
- headers: { Authorization: `Bearer ${this.accessToken}` },
717
- });
718
- return response.data;
240
+ this.token = data.access;
241
+ this.tokenExpiresAt = Date.now() + 28 * 60 * 1000;
719
242
  }
720
243
 
721
- async obtenirIdChorusDepuisSiret(siret: string): Promise<number | null> {
722
- const results = await this.rechercherStructureChorus({ identifiant_structure: siret, type_identifiant: 'SIRET' }) as Record<string, unknown>[];
723
- if (results.length > 0 && results[0].id_structure_cpp) return results[0].id_structure_cpp as number;
724
- return null;
244
+ private _invalidateToken(): void {
245
+ this.tokenExpiresAt = 0;
725
246
  }
247
+ }
726
248
 
727
- async submitInvoiceChorus(invoiceData: Record<string, unknown>): Promise<Record<string, unknown>> {
728
- await this.ensureAuthenticated();
729
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/chorus/factures/soumettre`, invoiceData, {
730
- headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
731
- });
732
- return response.data;
733
- }
249
+ class Endpoint {
250
+ constructor(private client: FactPulseClient, private path: string) {}
734
251
 
735
- async lookupInvoiceChorus(invoiceIdCpp: number): Promise<Record<string, unknown>> {
736
- await this.ensureAuthenticated();
737
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/chorus/factures/consulter`, { identifiant_facture_cpp: invoiceIdCpp }, {
738
- headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
739
- });
740
- return response.data;
252
+ // Chain path segments: client.processing.invoices processing/invoices
253
+ private _child(segment: string): Endpoint {
254
+ return new Endpoint(this.client, `${this.path}/${segment.replace(/_/g, '-')}`);
741
255
  }
742
256
 
743
- // ==================== Validation ====================
744
-
745
- /**
746
- * Validates a Factur-X PDF.
747
- * @param pdfBuffer - PDF content as Buffer
748
- * @param options - Validation options
749
- * @param options.profile - Factur-X profile (MINIMUM, BASIC, EN16931, EXTENDED). If not specified, auto-detected.
750
- * @param options.useVerapdf - Enable strict PDF/A validation with VeraPDF (default: false)
751
- */
752
- async validateFacturxPdf(
753
- pdfBuffer: Buffer,
754
- options: { profile?: string; useVerapdf?: boolean } = {}
755
- ): Promise<Record<string, unknown>> {
756
- await this.ensureAuthenticated();
757
- const form = new FormData();
758
- form.append('pdf_file', pdfBuffer, { filename: 'facture.pdf', contentType: 'application/pdf' });
759
- if (options.profile) {
760
- form.append('profile', options.profile);
761
- }
762
- form.append('use_verapdf', String(options.useVerapdf ?? false));
763
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/validate-facturx-pdf`, form, {
764
- headers: { ...form.getHeaders(), Authorization: `Bearer ${this.accessToken}` },
765
- });
766
- return response.data;
767
- }
257
+ // Dynamic property access for path building
258
+ get invoices() { return this._child('invoices'); }
259
+ get structures() { return this._child('structures'); }
260
+ get tasks() { return this._child('tasks'); }
261
+ get validate_facturx_pdf() { return this._child('validate-facturx-pdf'); }
262
+ get submit_complete_async() { return this._child('submit-complete-async'); }
768
263
 
769
- async validateFacturxXml(xmlContent: string, profile = 'EN16931'): Promise<Record<string, unknown>> {
770
- await this.ensureAuthenticated();
771
- const form = new FormData();
772
- form.append('xml_file', Buffer.from(xmlContent, 'utf-8'), { filename: 'facture.xml', contentType: 'application/xml' });
773
- form.append('profile', profile);
774
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/validate-xml`, form, {
775
- headers: { ...form.getHeaders(), Authorization: `Bearer ${this.accessToken}` },
776
- });
777
- return response.data;
778
- }
264
+ // For dynamic segments like client.structures['123']
265
+ [key: string]: unknown;
779
266
 
780
- async validatePdfSignature(pdfBuffer: Buffer): Promise<Record<string, unknown>> {
781
- await this.ensureAuthenticated();
782
- const form = new FormData();
783
- form.append('pdf_file', pdfBuffer, { filename: 'document.pdf', contentType: 'application/pdf' });
784
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/validate-pdf-signature`, form, {
785
- headers: { ...form.getHeaders(), Authorization: `Bearer ${this.accessToken}` },
786
- });
787
- return response.data;
267
+ // POST request
268
+ async call(data?: Record<string, unknown>): Promise<unknown> {
269
+ return this.client._request('POST', this.path, data);
788
270
  }
789
271
 
790
- // ==================== Signature ====================
791
-
792
- /**
793
- * Signs a PDF with the server-configured certificate (via JWT client_uid).
794
- * The certificate must be previously configured in Django Admin.
795
- */
796
- async signPdf(
797
- pdfBuffer: Buffer,
798
- options: { reason?: string; location?: string; contact?: string; usePadesLt?: boolean; useTimestamp?: boolean } = {}
799
- ): Promise<Buffer> {
800
- await this.ensureAuthenticated();
801
- const form = new FormData();
802
- form.append('pdf_file', pdfBuffer, { filename: 'document.pdf', contentType: 'application/pdf' });
803
- if (options.reason) form.append('reason', options.reason);
804
- if (options.location) form.append('location', options.location);
805
- if (options.contact) form.append('contact', options.contact);
806
- if (options.usePadesLt !== undefined) form.append('use_pades_lt', String(options.usePadesLt));
807
- if (options.useTimestamp !== undefined) form.append('use_timestamp', String(options.useTimestamp));
808
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/sign-pdf`, form, {
809
- headers: { ...form.getHeaders(), Authorization: `Bearer ${this.accessToken}` },
810
- });
811
- // The API returns JSON with pdf_signe_base64
812
- const data = response.data as { pdf_signe_base64?: string };
813
- if (data.pdf_signe_base64) {
814
- return Buffer.from(data.pdf_signe_base64, 'base64');
815
- }
816
- throw new Error('Invalid signature response');
817
- }
818
-
819
- /**
820
- * Generates a test certificate (NOT FOR PRODUCTION).
821
- * The certificate must then be configured in Django Admin.
822
- */
823
- async generateTestCertificate(
824
- options: { cn?: string; organisation?: string; email?: string; validityDays?: number; keySize?: number } = {}
825
- ): Promise<Record<string, unknown>> {
826
- await this.ensureAuthenticated();
827
- const response = await this.httpClient.post(`${this.config.apiUrl}/api/v1/processing/generate-test-certificate`, {
828
- cn: options.cn || 'Test Organisation',
829
- organisation: options.organisation || 'Test Organisation',
830
- email: options.email || 'test@example.com',
831
- validity_days: options.validityDays || 365,
832
- key_size: options.keySize || 2048,
833
- }, {
834
- headers: { Authorization: `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' },
835
- });
836
- return response.data;
272
+ // GET request
273
+ async get(params?: Record<string, unknown>): Promise<unknown> {
274
+ return this.client._request('GET', this.path, params);
837
275
  }
276
+ }
838
277
 
839
- // ==================== Workflow complet ====================
840
-
841
- /**
842
- * Complete workflow: generation + validation + signature + AFNOR submission.
843
- * Note: Signature uses the server-configured certificate (via JWT client_uid).
844
- * @param invoiceData Invoice data
845
- * @param pdfPath Path to the source PDF
846
- * @param options Workflow options
847
- * @returns Result with pdfBytes, validation, signature and afnor
848
- */
849
- async generateCompleteFacturx(
850
- invoiceData: Record<string, unknown>,
851
- pdfPath: string,
852
- options: {
853
- profile?: string;
854
- validate?: boolean;
855
- sign?: boolean;
856
- submitAfnor?: boolean;
857
- afnorFlowName?: string;
858
- afnorTrackingId?: string;
859
- timeout?: number;
860
- } = {}
861
- ): Promise<{
862
- pdfBytes: Buffer;
863
- validation?: Record<string, unknown>;
864
- signature?: { signed: boolean };
865
- afnor?: Record<string, unknown>;
866
- }> {
867
- const {
868
- profile = 'EN16931',
869
- validate = true,
870
- sign = false,
871
- submitAfnor = false,
872
- afnorFlowName,
873
- afnorTrackingId,
874
- timeout,
875
- } = options;
876
- const result: {
877
- pdfBytes: Buffer;
878
- validation?: Record<string, unknown>;
879
- signature?: { signed: boolean };
880
- afnor?: Record<string, unknown>;
881
- } = { pdfBytes: Buffer.alloc(0) };
882
-
883
- // 1. Generation
884
- const pdfBytes = await this.generateFacturx(invoiceData, pdfPath, profile, 'pdf', true, timeout) as Buffer;
885
- result.pdfBytes = pdfBytes;
886
-
887
- // 2. Validation
888
- if (validate) {
889
- const validation = await this.validateFacturxPdf(pdfBytes, { profile });
890
- result.validation = validation;
891
- if (!(validation as { isCompliant?: boolean }).isCompliant) {
892
- return result;
278
+ // Proxy to allow dynamic path segments
279
+ const EndpointProxy = new Proxy(Endpoint, {
280
+ construct(target, args) {
281
+ const instance = new target(args[0], args[1]);
282
+ return new Proxy(instance, {
283
+ get(obj, prop) {
284
+ if (typeof prop === 'string' && !prop.startsWith('_') && !(prop in obj)) {
285
+ return new EndpointProxy(obj['client'], `${obj['path']}/${prop.replace(/_/g, '-')}`);
286
+ }
287
+ return (obj as unknown as Record<string | symbol, unknown>)[prop];
893
288
  }
894
- }
895
-
896
- // 3. Signature (uses the server-configured certificate)
897
- if (sign) {
898
- const signedPdf = await this.signPdf(result.pdfBytes);
899
- result.pdfBytes = signedPdf;
900
- result.signature = { signed: true };
901
- }
902
-
903
- // 4. AFNOR submission
904
- if (submitAfnor) {
905
- const invoiceNumber = (invoiceData.invoiceNumber || invoiceData.invoice_number || 'INVOICE') as string;
906
- const flowName = afnorFlowName || `Invoice ${invoiceNumber}`;
907
- const trackingId = afnorTrackingId || invoiceNumber;
908
- const afnorResult = await this.submitInvoiceAfnor(result.pdfBytes, flowName, { trackingId });
909
- result.afnor = afnorResult;
910
- }
911
-
912
- return result;
289
+ });
913
290
  }
914
- }
291
+ });