@itzenata/efact-node 1.0.1

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 (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/dist/_generated/api/invoices-api.d.ts +367 -0
  4. package/dist/_generated/api/invoices-api.d.ts.map +1 -0
  5. package/dist/_generated/api/invoices-api.js +485 -0
  6. package/dist/_generated/api/invoices-api.js.map +1 -0
  7. package/dist/_generated/api.d.ts +13 -0
  8. package/dist/_generated/api.d.ts.map +1 -0
  9. package/dist/_generated/api.js +16 -0
  10. package/dist/_generated/api.js.map +1 -0
  11. package/dist/_generated/base.d.ts +67 -0
  12. package/dist/_generated/base.d.ts.map +1 -0
  13. package/dist/_generated/base.js +65 -0
  14. package/dist/_generated/base.js.map +1 -0
  15. package/dist/_generated/common.d.ts +66 -0
  16. package/dist/_generated/common.d.ts.map +1 -0
  17. package/dist/_generated/common.js +135 -0
  18. package/dist/_generated/common.js.map +1 -0
  19. package/dist/_generated/configuration.d.ts +92 -0
  20. package/dist/_generated/configuration.d.ts.map +1 -0
  21. package/dist/_generated/configuration.js +97 -0
  22. package/dist/_generated/configuration.js.map +1 -0
  23. package/dist/_generated/index.d.ts +15 -0
  24. package/dist/_generated/index.d.ts.map +1 -0
  25. package/dist/_generated/index.js +18 -0
  26. package/dist/_generated/index.js.map +1 -0
  27. package/dist/_generated/models/api-response-invoice-response.d.ts +45 -0
  28. package/dist/_generated/models/api-response-invoice-response.d.ts.map +1 -0
  29. package/dist/_generated/models/api-response-invoice-response.js +16 -0
  30. package/dist/_generated/models/api-response-invoice-response.js.map +1 -0
  31. package/dist/_generated/models/api-response-invoice-status-response.d.ts +45 -0
  32. package/dist/_generated/models/api-response-invoice-status-response.d.ts.map +1 -0
  33. package/dist/_generated/models/api-response-invoice-status-response.js +16 -0
  34. package/dist/_generated/models/api-response-invoice-status-response.js.map +1 -0
  35. package/dist/_generated/models/api-response-page-response-invoice-response.d.ts +45 -0
  36. package/dist/_generated/models/api-response-page-response-invoice-response.d.ts.map +1 -0
  37. package/dist/_generated/models/api-response-page-response-invoice-response.js +16 -0
  38. package/dist/_generated/models/api-response-page-response-invoice-response.js.map +1 -0
  39. package/dist/_generated/models/create-invoice-line-request.d.ts +55 -0
  40. package/dist/_generated/models/create-invoice-line-request.d.ts.map +1 -0
  41. package/dist/_generated/models/create-invoice-line-request.js +16 -0
  42. package/dist/_generated/models/create-invoice-line-request.js.map +1 -0
  43. package/dist/_generated/models/create-invoice-request.d.ts +82 -0
  44. package/dist/_generated/models/create-invoice-request.d.ts.map +1 -0
  45. package/dist/_generated/models/create-invoice-request.js +22 -0
  46. package/dist/_generated/models/create-invoice-request.js.map +1 -0
  47. package/dist/_generated/models/field-error.d.ts +31 -0
  48. package/dist/_generated/models/field-error.d.ts.map +1 -0
  49. package/dist/_generated/models/field-error.js +16 -0
  50. package/dist/_generated/models/field-error.js.map +1 -0
  51. package/dist/_generated/models/index.d.ts +11 -0
  52. package/dist/_generated/models/index.d.ts.map +1 -0
  53. package/dist/_generated/models/index.js +12 -0
  54. package/dist/_generated/models/index.js.map +1 -0
  55. package/dist/_generated/models/invoice-response.d.ts +221 -0
  56. package/dist/_generated/models/invoice-response.d.ts.map +1 -0
  57. package/dist/_generated/models/invoice-response.js +24 -0
  58. package/dist/_generated/models/invoice-response.js.map +1 -0
  59. package/dist/_generated/models/invoice-status-response.d.ts +83 -0
  60. package/dist/_generated/models/invoice-status-response.d.ts.map +1 -0
  61. package/dist/_generated/models/invoice-status-response.js +24 -0
  62. package/dist/_generated/models/invoice-status-response.js.map +1 -0
  63. package/dist/_generated/models/page-response-invoice-response.d.ts +62 -0
  64. package/dist/_generated/models/page-response-invoice-response.d.ts.map +1 -0
  65. package/dist/_generated/models/page-response-invoice-response.js +16 -0
  66. package/dist/_generated/models/page-response-invoice-response.js.map +1 -0
  67. package/dist/_generated/models/update-invoice-request.d.ts +121 -0
  68. package/dist/_generated/models/update-invoice-request.d.ts.map +1 -0
  69. package/dist/_generated/models/update-invoice-request.js +31 -0
  70. package/dist/_generated/models/update-invoice-request.js.map +1 -0
  71. package/dist/client.d.ts +73 -0
  72. package/dist/client.d.ts.map +1 -0
  73. package/dist/client.js +155 -0
  74. package/dist/client.js.map +1 -0
  75. package/dist/errors.d.ts +100 -0
  76. package/dist/errors.d.ts.map +1 -0
  77. package/dist/errors.js +129 -0
  78. package/dist/errors.js.map +1 -0
  79. package/dist/index.d.ts +7 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +9 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/invoices.d.ts +36 -0
  84. package/dist/invoices.d.ts.map +1 -0
  85. package/dist/invoices.js +53 -0
  86. package/dist/invoices.js.map +1 -0
  87. package/dist/runtime.d.ts +34 -0
  88. package/dist/runtime.d.ts.map +1 -0
  89. package/dist/runtime.js +105 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/package.json +66 -0
  92. package/src/_generated/.openapi-generator/FILES +21 -0
  93. package/src/_generated/.openapi-generator/VERSION +1 -0
  94. package/src/_generated/.openapi-generator-ignore +23 -0
  95. package/src/_generated/api/invoices-api.ts +662 -0
  96. package/src/_generated/api.ts +19 -0
  97. package/src/_generated/base.ts +87 -0
  98. package/src/_generated/common.ts +151 -0
  99. package/src/_generated/configuration.ts +111 -0
  100. package/src/_generated/git_push.sh +57 -0
  101. package/src/_generated/index.ts +19 -0
  102. package/src/_generated/models/api-response-invoice-response.ts +55 -0
  103. package/src/_generated/models/api-response-invoice-status-response.ts +55 -0
  104. package/src/_generated/models/api-response-page-response-invoice-response.ts +55 -0
  105. package/src/_generated/models/create-invoice-line-request.ts +61 -0
  106. package/src/_generated/models/create-invoice-request.ts +93 -0
  107. package/src/_generated/models/field-error.ts +37 -0
  108. package/src/_generated/models/index.ts +11 -0
  109. package/src/_generated/models/invoice-response.ts +230 -0
  110. package/src/_generated/models/invoice-status-response.ts +92 -0
  111. package/src/_generated/models/page-response-invoice-response.ts +70 -0
  112. package/src/_generated/models/update-invoice-request.ts +131 -0
  113. package/src/client.ts +215 -0
  114. package/src/errors.ts +166 -0
  115. package/src/index.ts +35 -0
  116. package/src/invoices.ts +80 -0
  117. package/src/runtime.ts +179 -0
package/src/client.ts ADDED
@@ -0,0 +1,215 @@
1
+ import type { AxiosInstance } from "axios";
2
+ import { InvoicesApi } from "./_generated/api/invoices-api.js";
3
+ import { EFactConfigError } from "./errors.js";
4
+ import { createHttpInstance } from "./runtime.js";
5
+ import { InvoicesResource, type ListInvoicesParams } from "./invoices.js";
6
+ import type {
7
+ CreateInvoiceRequest,
8
+ InvoiceResponse,
9
+ InvoiceStatusResponse,
10
+ PageResponseInvoiceResponse,
11
+ UpdateInvoiceRequest,
12
+ } from "./_generated/index.js";
13
+
14
+ /**
15
+ * Environnements publics et leur URL de base (host uniquement — les chemins
16
+ * générés portent déjà le préfixe /v1). Seuls les environnements destinés aux
17
+ * clients sont nommés ; le dev local passe par l'option `baseUrl`
18
+ * (ex. baseUrl: "http://localhost:8080").
19
+ *
20
+ * NOTE : l'URL "production" pointe temporairement sur preview, qui fait office
21
+ * de prod tant que l'URL de prod définitive n'est pas déployée. La changer ici
22
+ * (et dans les serveurs OpenAPI) suffira — aucun changement côté clients.
23
+ */
24
+ export const ENVIRONMENTS = {
25
+ production: "https://preview.efact-api.itzenata.com",
26
+ } as const;
27
+
28
+ export type EFactEnvironment = keyof typeof ENVIRONMENTS;
29
+
30
+ const DEFAULT_ENVIRONMENT: EFactEnvironment = "production";
31
+ const DEFAULT_TIMEOUT_MS = 30_000;
32
+ const DEFAULT_MAX_RETRIES = 2;
33
+
34
+ export interface EFactOptions {
35
+ /** Environnement cible. Défaut "production". Ignoré si `baseUrl` est fourni. */
36
+ environment?: EFactEnvironment;
37
+ /** URL de base explicite (host), p.ex. "http://localhost:8080" en dev local. */
38
+ baseUrl?: string;
39
+ /** Délai max par requête, en ms (défaut 30000). */
40
+ timeout?: number;
41
+ /** Nombre max de nouveaux essais pour les requêtes sûres (défaut 2). */
42
+ maxRetries?: number;
43
+ }
44
+
45
+ /**
46
+ * Client EFact e-Facture.
47
+ *
48
+ * @example
49
+ * const efact = new EFact(process.env.EFACT_SECRET_KEY);
50
+ * const invoice = await efact.invoices.create({ ... });
51
+ */
52
+ export class EFact {
53
+ private readonly http: AxiosInstance;
54
+ private readonly baseUrl: string;
55
+ private readonly maskedKey: string;
56
+
57
+ /** Opérations sur les factures : efact.invoices.create/list/retrieve/... */
58
+ public readonly invoices: InvoicesResource;
59
+
60
+ /**
61
+ * @param secretKey Clé secrète au format efact_sk_... (jamais loggée en clair).
62
+ * @param options Options facultatives (environment, baseUrl, timeout, maxRetries).
63
+ */
64
+ constructor(secretKey: string, options: EFactOptions = {}) {
65
+ if (typeof secretKey !== "string" || secretKey.trim() === "") {
66
+ throw new EFactConfigError(
67
+ "[EFact] Erreur d'initialisation : la clé secrète est obligatoire.",
68
+ );
69
+ }
70
+ const key = secretKey.trim();
71
+
72
+ this.baseUrl = normalizeBaseUrl(resolveBaseUrl(options));
73
+ this.maskedKey = maskKey(key);
74
+ const timeoutMs = resolvePositiveInt(options.timeout, DEFAULT_TIMEOUT_MS, "timeout");
75
+ const maxRetries = resolveNonNegativeInt(options.maxRetries, DEFAULT_MAX_RETRIES, "maxRetries");
76
+
77
+ this.http = createHttpInstance(key, this.baseUrl, { timeoutMs, maxRetries });
78
+ // baseURL est porté par l'instance axios : InvoicesApi n'a besoin que d'elle.
79
+ this.invoices = new InvoicesResource(new InvoicesApi(undefined, undefined, this.http));
80
+ }
81
+
82
+ /** Configuration non sensible (la clé est toujours masquée). */
83
+ public getConfig(): { baseUrl: string; apiKeyMasked: string } {
84
+ return { baseUrl: this.baseUrl, apiKeyMasked: this.maskedKey };
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // Compatibilité : ancien client EFactClient({ apiKey, baseUrl }).
90
+ // ============================================================================
91
+
92
+ export interface EFactClientConfig {
93
+ apiKey: string;
94
+ baseUrl?: string;
95
+ timeout?: number;
96
+ maxRetries?: number;
97
+ }
98
+
99
+ /** @deprecated Utiliser `new EFact(secretKey, { baseUrl })`. */
100
+ export class EFactClient extends EFact {
101
+ constructor(config: EFactClientConfig) {
102
+ const opts: EFactOptions = {};
103
+ if (config.baseUrl !== undefined) opts.baseUrl = config.baseUrl;
104
+ if (config.timeout !== undefined) opts.timeout = config.timeout;
105
+ if (config.maxRetries !== undefined) opts.maxRetries = config.maxRetries;
106
+ super(config.apiKey, opts);
107
+ }
108
+
109
+ /** @deprecated Utiliser efact.invoices.create(). */
110
+ createInvoice(data: CreateInvoiceRequest): Promise<InvoiceResponse> {
111
+ return this.invoices.create(data);
112
+ }
113
+
114
+ /** @deprecated Utiliser efact.invoices.list(). */
115
+ listInvoices(params?: ListInvoicesParams): Promise<PageResponseInvoiceResponse> {
116
+ return this.invoices.list(params);
117
+ }
118
+
119
+ /** @deprecated Utiliser efact.invoices.retrieve(). */
120
+ getInvoice(invoiceId: string): Promise<InvoiceResponse> {
121
+ return this.invoices.retrieve(invoiceId);
122
+ }
123
+
124
+ /** @deprecated Utiliser efact.invoices.retrieveStatus(). */
125
+ getInvoiceStatus(invoiceId: string): Promise<InvoiceStatusResponse> {
126
+ return this.invoices.retrieveStatus(invoiceId);
127
+ }
128
+
129
+ /** @deprecated Utiliser efact.invoices.update(). */
130
+ updateInvoice(invoiceId: string, data: UpdateInvoiceRequest): Promise<InvoiceResponse> {
131
+ return this.invoices.update(invoiceId, data);
132
+ }
133
+
134
+ /** @deprecated Utiliser efact.invoices.cancel(). */
135
+ cancelInvoice(invoiceId: string): Promise<InvoiceResponse> {
136
+ return this.invoices.cancel(invoiceId);
137
+ }
138
+ }
139
+
140
+ // ============================================================================
141
+ // Validation des options de configuration.
142
+ // ============================================================================
143
+
144
+ function resolveBaseUrl(options: EFactOptions): string {
145
+ if (options.baseUrl !== undefined) {
146
+ return options.baseUrl;
147
+ }
148
+ const env = options.environment ?? DEFAULT_ENVIRONMENT;
149
+ const url = ENVIRONMENTS[env];
150
+ if (url === undefined) {
151
+ const known = Object.keys(ENVIRONMENTS).join(", ");
152
+ throw new EFactConfigError(
153
+ `[EFact] environment inconnu : "${env}". Valeurs valides : ${known}.`,
154
+ );
155
+ }
156
+ return url;
157
+ }
158
+
159
+ /** Valide la baseUrl (URL absolue http/https) et avertit si non-HTTPS hors localhost. */
160
+ function normalizeBaseUrl(raw: string): string {
161
+ let parsed: URL;
162
+ try {
163
+ parsed = new URL(raw);
164
+ } catch {
165
+ throw new EFactConfigError(
166
+ `[EFact] baseUrl invalide : "${raw}" n'est pas une URL absolue.`,
167
+ );
168
+ }
169
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
170
+ throw new EFactConfigError(
171
+ `[EFact] baseUrl invalide : protocole "${parsed.protocol}" non supporté (http/https requis).`,
172
+ );
173
+ }
174
+ const isLocal =
175
+ parsed.hostname === "localhost" ||
176
+ parsed.hostname === "127.0.0.1" ||
177
+ parsed.hostname === "[::1]" ||
178
+ parsed.hostname === "::1";
179
+ if (parsed.protocol === "http:" && !isLocal) {
180
+ console.warn(
181
+ `[EFact] Attention : baseUrl utilise HTTP non chiffré (${parsed.origin}). ` +
182
+ "La clé secrète transiterait en clair. Utilisez HTTPS en production.",
183
+ );
184
+ }
185
+ return raw;
186
+ }
187
+
188
+ /**
189
+ * Version masquée de la clé : au plus 4 caractères de tête/queue, jamais plus
190
+ * de la moitié d'une clé courte. Pour le diagnostic.
191
+ */
192
+ function maskKey(k: string): string {
193
+ if (k.length <= 8) return "****";
194
+ return `${k.slice(0, 4)}...${k.slice(-4)}`;
195
+ }
196
+
197
+ function resolvePositiveInt(value: number | undefined, fallback: number, name: string): number {
198
+ if (value === undefined) return fallback;
199
+ if (!Number.isFinite(value) || value <= 0) {
200
+ throw new EFactConfigError(
201
+ `[EFact] Option "${name}" invalide : un nombre positif est attendu.`,
202
+ );
203
+ }
204
+ return Math.floor(value);
205
+ }
206
+
207
+ function resolveNonNegativeInt(value: number | undefined, fallback: number, name: string): number {
208
+ if (value === undefined) return fallback;
209
+ if (!Number.isFinite(value) || value < 0) {
210
+ throw new EFactConfigError(
211
+ `[EFact] Option "${name}" invalide : un entier >= 0 est attendu.`,
212
+ );
213
+ }
214
+ return Math.floor(value);
215
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Détail d'erreur renvoyé dans le tableau "errors" de l'enveloppe backend
3
+ * (aligné sur le schéma généré `FieldError` : { field, message }).
4
+ * Forme permissive : le backend peut ajouter des clés.
5
+ */
6
+ export interface ApiErrorDetail {
7
+ code?: string;
8
+ message?: string;
9
+ field?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * Erreurs typées du SDK EFact.
15
+ *
16
+ * Toute réponse du backend est enveloppée ({ code, message, data, errors }).
17
+ * Le SDK transforme les réponses non-"OK" — et les statuts HTTP non-2xx — en
18
+ * erreurs typées portant le code applicatif, le statut HTTP et le détail des
19
+ * erreurs, pour que l'appelant puisse les distinguer proprement.
20
+ */
21
+
22
+ /** Erreur de base de toutes les erreurs renvoyées par le SDK. */
23
+ export class EFactError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = new.target.name;
27
+ // Maintient la chaîne de prototype correcte après transpilation.
28
+ Object.setPrototypeOf(this, new.target.prototype);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Erreur de configuration du SDK (clé manquante, baseUrl invalide, ...). Levée
34
+ * avant tout appel réseau.
35
+ */
36
+ export class EFactConfigError extends EFactError {}
37
+
38
+ /**
39
+ * Échec réseau : la requête n'a jamais abouti à une réponse HTTP (DNS, connexion
40
+ * refusée, TLS, coupure...). `cause` porte l'erreur sous-jacente.
41
+ */
42
+ export class EFactConnectionError extends EFactError {
43
+ readonly cause: unknown;
44
+ constructor(message: string, cause?: unknown) {
45
+ super(message);
46
+ this.cause = cause;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * La requête a dépassé le délai configuré et a été interrompue avant réponse.
52
+ */
53
+ export class EFactTimeoutError extends EFactError {
54
+ /** Délai dépassé, en millisecondes. */
55
+ readonly timeoutMs: number;
56
+ constructor(message: string, timeoutMs: number) {
57
+ super(message);
58
+ this.timeoutMs = timeoutMs;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Erreur renvoyée par l'API : statut HTTP non-2xx, ou enveloppe dont
64
+ * "code" !== "OK". Porte le code applicatif, le statut HTTP et les détails.
65
+ */
66
+ export class EFactApiError extends EFactError {
67
+ /** Code applicatif issu de l'enveloppe ("code"), si disponible. */
68
+ readonly code: string | undefined;
69
+ /** Statut HTTP de la réponse, si une réponse HTTP a été reçue. */
70
+ readonly httpStatus: number | undefined;
71
+ /** Détails d'erreur issus de l'enveloppe ("errors"), si présents. */
72
+ readonly errors: ApiErrorDetail[] | undefined;
73
+
74
+ constructor(
75
+ message: string,
76
+ options: {
77
+ code?: string | undefined;
78
+ httpStatus?: number | undefined;
79
+ errors?: ApiErrorDetail[] | undefined;
80
+ } = {},
81
+ ) {
82
+ super(message);
83
+ this.code = options.code;
84
+ this.httpStatus = options.httpStatus;
85
+ this.errors = options.errors;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Erreur d'authentification / autorisation. Levée sur HTTP 401/403 ou sur les
91
+ * codes d'auth connus (API_KEY_MISSING / API_KEY_INVALID).
92
+ */
93
+ export class EFactAuthError extends EFactApiError {}
94
+
95
+ /** Erreur de validation de la requête (HTTP 400/422 / codes de validation). */
96
+ export class EFactValidationError extends EFactApiError {}
97
+
98
+ /** Ressource introuvable (HTTP 404). Inclut le cas cross-tenant. */
99
+ export class EFactNotFoundError extends EFactApiError {}
100
+
101
+ /**
102
+ * Conflit d'état (HTTP 409), p.ex. tentative de modifier/annuler une facture
103
+ * qui n'est plus PENDING (code INVOICE_NOT_EDITABLE).
104
+ */
105
+ export class EFactConflictError extends EFactApiError {}
106
+
107
+ /**
108
+ * Codes d'erreur applicatifs traités comme des erreurs d'authentification.
109
+ * API_KEY_MISSING / API_KEY_INVALID sont confirmés par le contrat ; les autres
110
+ * sont conservés par prudence. Le mapping HTTP 401/403 fonctionne aussi seul.
111
+ */
112
+ export const AUTH_ERROR_CODES: ReadonlySet<string> = new Set([
113
+ "API_KEY_MISSING",
114
+ "API_KEY_INVALID",
115
+ "UNAUTHORIZED",
116
+ "UNAUTHENTICATED",
117
+ "INVALID_API_KEY",
118
+ "API_KEY_REQUIRED",
119
+ "FORBIDDEN",
120
+ ]);
121
+
122
+ /** Codes applicatifs traités comme des conflits d'état (409). */
123
+ export const CONFLICT_ERROR_CODES: ReadonlySet<string> = new Set([
124
+ "INVOICE_NOT_EDITABLE",
125
+ ]);
126
+
127
+ /** Codes applicatifs traités comme des erreurs de validation. */
128
+ export const VALIDATION_ERROR_CODES: ReadonlySet<string> = new Set([
129
+ "VALIDATION_ERROR",
130
+ "INVALID_REQUEST",
131
+ ]);
132
+
133
+ /**
134
+ * Fabrique l'erreur API appropriée à partir du statut HTTP et/ou du code
135
+ * applicatif. Point central de classification des erreurs du backend.
136
+ */
137
+ export function createApiError(
138
+ message: string,
139
+ options: {
140
+ code?: string | undefined;
141
+ httpStatus?: number | undefined;
142
+ errors?: ApiErrorDetail[] | undefined;
143
+ } = {},
144
+ ): EFactApiError {
145
+ const { code, httpStatus } = options;
146
+
147
+ if (httpStatus === 401 || httpStatus === 403 || (code && AUTH_ERROR_CODES.has(code))) {
148
+ return new EFactAuthError(message, options);
149
+ }
150
+ if (httpStatus === 404) {
151
+ return new EFactNotFoundError(message, options);
152
+ }
153
+ if (httpStatus === 409 || (code && CONFLICT_ERROR_CODES.has(code))) {
154
+ return new EFactConflictError(message, options);
155
+ }
156
+ // 400 (requête malformée) et 422 (échec de validation des champs côté
157
+ // backend) sont tous deux traités comme des erreurs de validation.
158
+ if (
159
+ httpStatus === 400 ||
160
+ httpStatus === 422 ||
161
+ (code && VALIDATION_ERROR_CODES.has(code))
162
+ ) {
163
+ return new EFactValidationError(message, options);
164
+ }
165
+ return new EFactApiError(message, options);
166
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // Surface publique du SDK.
2
+ export {
3
+ EFact,
4
+ ENVIRONMENTS,
5
+ type EFactEnvironment,
6
+ type EFactOptions,
7
+ EFactClient,
8
+ type EFactClientConfig,
9
+ } from "./client.js";
10
+ export { InvoicesResource, type ListInvoicesParams } from "./invoices.js";
11
+ export * from "./errors.js";
12
+
13
+ // Types du contrat, régénérés depuis l'OpenAPI (`src/_generated/`). Réexportés
14
+ // tels quels : un changement de champ côté API se propage ici automatiquement.
15
+ export {
16
+ type CreateInvoiceRequest,
17
+ type CreateInvoiceLineRequest,
18
+ type UpdateInvoiceRequest,
19
+ type InvoiceResponse,
20
+ type InvoiceStatusResponse,
21
+ type PageResponseInvoiceResponse,
22
+ type FieldError,
23
+ CreateInvoiceRequestPaymentMethodEnum,
24
+ InvoiceResponseStatusEnum,
25
+ } from "./_generated/index.js";
26
+ export { ListStatusEnum } from "./_generated/api/invoices-api.js";
27
+
28
+ // Alias de compatibilité (ancien nommage). Préférer les types générés.
29
+ export type {
30
+ InvoiceResponse as Invoice,
31
+ InvoiceStatusResponse as InvoiceStatusResult,
32
+ CreateInvoiceRequest as CreateInvoiceInput,
33
+ UpdateInvoiceRequest as UpdateInvoiceInput,
34
+ CreateInvoiceLineRequest as CreateInvoiceLineInput,
35
+ } from "./_generated/index.js";
@@ -0,0 +1,80 @@
1
+ import { InvoicesApi, type ListStatusEnum } from "./_generated/api/invoices-api.js";
2
+ import {
3
+ type CreateInvoiceRequest,
4
+ type InvoiceResponse,
5
+ type InvoiceStatusResponse,
6
+ type PageResponseInvoiceResponse,
7
+ type UpdateInvoiceRequest,
8
+ } from "./_generated/index.js";
9
+ import { unwrap, type Envelope } from "./runtime.js";
10
+ import type { AxiosResponse } from "axios";
11
+
12
+ /**
13
+ * Paramètres ergonomiques de liste. Aplatit le `Pageable` généré (page/size/sort)
14
+ * en options plates, façon Stripe.
15
+ */
16
+ export interface ListInvoicesParams {
17
+ page?: number;
18
+ size?: number;
19
+ /** ex. "createdAt,desc" */
20
+ sort?: string;
21
+ status?: ListStatusEnum;
22
+ }
23
+
24
+ /**
25
+ * Ressource "invoices" — surface publique STABLE (efact.invoices.*). Chaque
26
+ * méthode délègue à l'opération générée correspondante et déballe l'enveloppe.
27
+ * Aucune logique d'endpoint ici : ajouter/retirer un champ sur une facture se
28
+ * répercute via la régénération des types, sans toucher ce fichier.
29
+ */
30
+ export class InvoicesResource {
31
+ constructor(private readonly api: InvoicesApi) {}
32
+
33
+ /** POST /v1/invoices — crée une facture. */
34
+ create(input: CreateInvoiceRequest): Promise<InvoiceResponse> {
35
+ return unwrap(env(this.api.create({ createInvoiceRequest: input })));
36
+ }
37
+
38
+ /** GET /v1/invoices — liste paginée (filtre/pagination optionnels). */
39
+ list(params: ListInvoicesParams = {}): Promise<PageResponseInvoiceResponse> {
40
+ return unwrap(
41
+ env(
42
+ this.api.list({
43
+ ...(params.page !== undefined ? { page: params.page } : {}),
44
+ ...(params.size !== undefined ? { size: params.size } : {}),
45
+ ...(params.sort !== undefined ? { sort: [params.sort] } : {}),
46
+ ...(params.status !== undefined ? { status: params.status } : {}),
47
+ }),
48
+ ),
49
+ );
50
+ }
51
+
52
+ /** GET /v1/invoices/{id} — récupère une facture (404 si hors tenant). */
53
+ retrieve(invoiceId: string): Promise<InvoiceResponse> {
54
+ return unwrap(env(this.api.getById({ id: invoiceId })));
55
+ }
56
+
57
+ /** GET /v1/invoices/{id}/status — résultat de clairance DGI. */
58
+ retrieveStatus(invoiceId: string): Promise<InvoiceStatusResponse> {
59
+ return unwrap(env(this.api.getStatus({ id: invoiceId })));
60
+ }
61
+
62
+ /** PUT /v1/invoices/{id} — mise à jour partielle (PENDING uniquement). */
63
+ update(invoiceId: string, input: UpdateInvoiceRequest): Promise<InvoiceResponse> {
64
+ return unwrap(env(this.api.update({ id: invoiceId, updateInvoiceRequest: input })));
65
+ }
66
+
67
+ /** DELETE /v1/invoices/{id} — annule (PENDING uniquement). */
68
+ cancel(invoiceId: string): Promise<InvoiceResponse> {
69
+ return unwrap(env(this.api.cancel({ id: invoiceId })));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Recadre l'AxiosPromise générée (typée sur `ApiResponse*`) vers notre
75
+ * `Envelope<T>` unifié afin que `unwrap` en extraie `data`. Ne fait rien à
76
+ * l'exécution — simple pont de types.
77
+ */
78
+ function env<T>(p: Promise<AxiosResponse<unknown>>): Promise<AxiosResponse<Envelope<T>>> {
79
+ return p as Promise<AxiosResponse<Envelope<T>>>;
80
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,179 @@
1
+ import axios, {
2
+ type AxiosError,
3
+ type AxiosInstance,
4
+ type AxiosResponse,
5
+ type InternalAxiosRequestConfig,
6
+ } from "axios";
7
+ import {
8
+ createApiError,
9
+ EFactApiError,
10
+ EFactConnectionError,
11
+ EFactTimeoutError,
12
+ type ApiErrorDetail,
13
+ } from "./errors.js";
14
+
15
+ /**
16
+ * Couche transport STABLE du SDK. Elle configure une instance axios partagée et
17
+ * y branche les préoccupations transversales — auth, nouveaux essais, mapping
18
+ * d'erreurs typées, déballage d'enveloppe — qui ne dépendent JAMAIS d'un
19
+ * endpoint particulier. Regénérer `_generated/` n'oblige donc jamais à toucher
20
+ * ce fichier.
21
+ */
22
+
23
+ /** Enveloppe standard renvoyée par toutes les routes ({ code, data, ... }). */
24
+ export interface Envelope<T> {
25
+ code?: string;
26
+ message?: string | null;
27
+ data?: T;
28
+ errors?: ApiErrorDetail[] | null;
29
+ }
30
+
31
+ export interface RuntimeOptions {
32
+ timeoutMs: number;
33
+ maxRetries: number;
34
+ }
35
+
36
+ /** Méthodes HTTP rejouables sans effet de bord (sûres/idempotentes). */
37
+ const RETRYABLE_METHODS = new Set(["get", "head", "put", "delete"]);
38
+ /** Statuts HTTP qui méritent un nouvel essai (surcharge / erreurs serveur). */
39
+ const RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
40
+
41
+ /** Config axios enrichie d'un compteur d'essais interne au SDK. */
42
+ interface RetryConfig extends InternalAxiosRequestConfig {
43
+ __efactRetryCount?: number;
44
+ }
45
+
46
+ /**
47
+ * Construit l'instance axios utilisée par le client généré. C'est le SEUL
48
+ * endroit où l'on attache l'auth (Bearer), applique le timeout, rejoue les
49
+ * requêtes sûres et convertit les échecs en erreurs typées.
50
+ */
51
+ export function createHttpInstance(
52
+ secretKey: string,
53
+ baseUrl: string,
54
+ options: RuntimeOptions,
55
+ ): AxiosInstance {
56
+ const instance = axios.create({
57
+ baseURL: baseUrl.replace(/\/+$/, ""),
58
+ timeout: options.timeoutMs,
59
+ headers: {
60
+ // Seul endroit où la clé est attachée. Le backend attend un Bearer token.
61
+ Authorization: `Bearer ${secretKey}`,
62
+ Accept: "application/json",
63
+ },
64
+ // On lit nous-mêmes le corps d'erreur (enveloppe) pour typer l'erreur.
65
+ validateStatus: (status) => status >= 200 && status < 300,
66
+ });
67
+
68
+ instance.interceptors.response.use(
69
+ (response) => response,
70
+ async (error: AxiosError) => {
71
+ const cfg = error.config as RetryConfig | undefined;
72
+ const method = (cfg?.method ?? "get").toLowerCase();
73
+ const attempt = cfg?.__efactRetryCount ?? 0;
74
+
75
+ // 1. Réponse HTTP reçue mais non-2xx.
76
+ if (error.response) {
77
+ if (
78
+ canRetryMethod(method) &&
79
+ attempt < options.maxRetries &&
80
+ RETRYABLE_STATUSES.has(error.response.status)
81
+ ) {
82
+ return replay(instance, cfg!, attempt, backoffMs(attempt, error.response));
83
+ }
84
+ throw toApiError(error.response);
85
+ }
86
+
87
+ // 2. Pas de réponse : timeout ou coupure réseau.
88
+ const isTimeout = error.code === "ECONNABORTED" || error.code === "ETIMEDOUT";
89
+ if (canRetryMethod(method) && attempt < options.maxRetries) {
90
+ return replay(instance, cfg!, attempt, backoffMs(attempt));
91
+ }
92
+ if (isTimeout) {
93
+ throw new EFactTimeoutError(
94
+ `[EFact] La requête a dépassé le délai de ${options.timeoutMs} ms.`,
95
+ options.timeoutMs,
96
+ );
97
+ }
98
+ throw new EFactConnectionError(
99
+ "[EFact] Échec de connexion à l'API (réseau indisponible).",
100
+ error,
101
+ );
102
+ },
103
+ );
104
+
105
+ return instance;
106
+ }
107
+
108
+ /**
109
+ * Attend une réponse enveloppée, vérifie `code === "OK"` et renvoie `data`.
110
+ * Point unique de déballage : les méthodes de ressource restent des one-liners
111
+ * qui ne connaissent que le type généré, donc un changement de champ passe
112
+ * sans toucher au code.
113
+ */
114
+ export async function unwrap<T>(
115
+ call: Promise<AxiosResponse<Envelope<T>>>,
116
+ ): Promise<T> {
117
+ const response = await call;
118
+ const envelope = response.data;
119
+ if (!envelope || typeof envelope.code !== "string") {
120
+ throw createApiError(
121
+ `[EFact-API] Réponse inattendue (enveloppe absente) sur HTTP ${response.status}.`,
122
+ { httpStatus: response.status },
123
+ );
124
+ }
125
+ if (envelope.code !== "OK") {
126
+ throw createApiError(`[EFact-API] ${envelope.code}: ${envelope.message ?? "Erreur inconnue"}`, {
127
+ code: envelope.code,
128
+ httpStatus: response.status,
129
+ errors: envelope.errors ?? undefined,
130
+ });
131
+ }
132
+ return envelope.data as T;
133
+ }
134
+
135
+ function replay(
136
+ instance: AxiosInstance,
137
+ cfg: RetryConfig,
138
+ attempt: number,
139
+ waitMs: number,
140
+ ): Promise<AxiosResponse> {
141
+ cfg.__efactRetryCount = attempt + 1;
142
+ return delay(waitMs).then(() => instance.request(cfg));
143
+ }
144
+
145
+ function toApiError(response: AxiosResponse): EFactApiError {
146
+ const envelope = (response.data ?? {}) as Envelope<unknown>;
147
+ const code = envelope.code ?? `HTTP_${response.status}`;
148
+ const detail =
149
+ (typeof envelope.message === "string" && envelope.message) ||
150
+ response.statusText ||
151
+ "Erreur inconnue";
152
+ return createApiError(`[EFact-API] ${code}: ${detail}`, {
153
+ code: envelope.code ?? undefined,
154
+ httpStatus: response.status,
155
+ errors: envelope.errors ?? undefined,
156
+ });
157
+ }
158
+
159
+ function canRetryMethod(method: string): boolean {
160
+ return RETRYABLE_METHODS.has(method);
161
+ }
162
+
163
+ /** Back-off exponentiel plafonné, en respectant Retry-After si fourni. */
164
+ function backoffMs(attempt: number, response?: AxiosResponse): number {
165
+ const retryAfter = response?.headers?.["retry-after"];
166
+ if (retryAfter !== undefined) {
167
+ const seconds = Number(retryAfter);
168
+ if (Number.isFinite(seconds) && seconds >= 0) {
169
+ return Math.min(seconds * 1000, 20_000);
170
+ }
171
+ }
172
+ const base = Math.min(500 * 2 ** attempt, 8_000);
173
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
174
+ return Math.round(base + jitter);
175
+ }
176
+
177
+ function delay(ms: number): Promise<void> {
178
+ return new Promise((resolve) => setTimeout(resolve, ms));
179
+ }