@scell/sdk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -294,4 +294,80 @@ export class HttpClient {
294
294
  ...options,
295
295
  });
296
296
  }
297
+
298
+ /**
299
+ * GET request that returns raw binary data as ArrayBuffer
300
+ *
301
+ * Use this for downloading files (PDF, XML, etc.)
302
+ *
303
+ * @param path - API endpoint path
304
+ * @param query - Query parameters
305
+ * @param options - Request options
306
+ * @returns ArrayBuffer containing the file content
307
+ */
308
+ async getRaw(
309
+ path: string,
310
+ query?: Record<string, string | number | boolean | undefined>,
311
+ options?: RequestOptions
312
+ ): Promise<ArrayBuffer> {
313
+ const url = this.buildUrl(path, query);
314
+ const requestHeaders = this.buildHeaders(options?.headers);
315
+ const requestTimeout = options?.timeout ?? this.timeout;
316
+
317
+ // Remove JSON content-type for raw requests
318
+ delete requestHeaders['Content-Type'];
319
+ requestHeaders['Accept'] = '*/*';
320
+
321
+ const controller = new AbortController();
322
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
323
+
324
+ if (options?.signal) {
325
+ options.signal.addEventListener('abort', () => controller.abort());
326
+ }
327
+
328
+ try {
329
+ const response = await this.fetchFn(url, {
330
+ method: 'GET',
331
+ headers: requestHeaders,
332
+ signal: controller.signal,
333
+ });
334
+
335
+ clearTimeout(timeoutId);
336
+
337
+ if (!response.ok) {
338
+ // Try to parse error as JSON
339
+ const contentType = response.headers.get('Content-Type') ?? '';
340
+ let responseBody: unknown;
341
+
342
+ if (contentType.includes('application/json')) {
343
+ responseBody = await response.json();
344
+ } else {
345
+ responseBody = await response.text();
346
+ }
347
+
348
+ parseApiError(response.status, responseBody, response.headers);
349
+ }
350
+
351
+ return response.arrayBuffer();
352
+ } catch (error) {
353
+ clearTimeout(timeoutId);
354
+
355
+ if (error instanceof Error) {
356
+ if (error.name === 'AbortError') {
357
+ throw new ScellTimeoutError(
358
+ `Request timed out after ${requestTimeout}ms`
359
+ );
360
+ }
361
+
362
+ if (
363
+ error.name === 'TypeError' &&
364
+ error.message.includes('fetch')
365
+ ) {
366
+ throw new ScellNetworkError('Network request failed', error);
367
+ }
368
+ }
369
+
370
+ throw error;
371
+ }
372
+ }
297
373
  }
@@ -11,13 +11,19 @@ import type {
11
11
  SingleResponse,
12
12
  } from '../types/common.js';
13
13
  import type {
14
+ AcceptInvoiceInput,
14
15
  AuditTrailResponse,
15
16
  ConvertInvoiceInput,
16
17
  CreateInvoiceInput,
18
+ DisputeInvoiceInput,
19
+ IncomingInvoiceParams,
17
20
  Invoice,
18
21
  InvoiceDownloadResponse,
19
22
  InvoiceDownloadType,
23
+ InvoiceFileFormat,
20
24
  InvoiceListOptions,
25
+ MarkPaidInput,
26
+ RejectInvoiceInput,
21
27
  } from '../types/invoices.js';
22
28
 
23
29
  /**
@@ -243,4 +249,215 @@ export class InvoicesResource {
243
249
  target_format: string;
244
250
  }>('/invoices/convert', input, requestOptions);
245
251
  }
252
+
253
+ /**
254
+ * List incoming invoices (from suppliers)
255
+ *
256
+ * Returns invoices where your company is the buyer.
257
+ *
258
+ * @param params - Filter and pagination options
259
+ * @param requestOptions - Request options
260
+ * @returns Paginated list of incoming invoices
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // List all incoming invoices
265
+ * const { data, meta } = await client.invoices.incoming({
266
+ * status: 'pending',
267
+ * per_page: 50
268
+ * });
269
+ * console.log(`Found ${meta.total} incoming invoices`);
270
+ *
271
+ * // Filter by seller
272
+ * const fromSupplier = await client.invoices.incoming({
273
+ * seller_siret: '12345678901234'
274
+ * });
275
+ * ```
276
+ */
277
+ async incoming(
278
+ params: IncomingInvoiceParams = {},
279
+ requestOptions?: RequestOptions
280
+ ): Promise<PaginatedResponse<Invoice>> {
281
+ return this.http.get<PaginatedResponse<Invoice>>(
282
+ '/invoices/incoming',
283
+ params as Record<string, string | number | boolean | undefined>,
284
+ requestOptions
285
+ );
286
+ }
287
+
288
+ /**
289
+ * Accept an incoming invoice
290
+ *
291
+ * Mark an incoming invoice as accepted, optionally specifying a payment date.
292
+ *
293
+ * @param id - Invoice UUID
294
+ * @param data - Optional acceptance data
295
+ * @param requestOptions - Request options
296
+ * @returns Updated invoice
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * // Accept with payment date
301
+ * const { data: invoice } = await client.invoices.accept('invoice-uuid', {
302
+ * payment_date: '2024-02-15',
303
+ * note: 'Approved by accounting'
304
+ * });
305
+ *
306
+ * // Simple acceptance
307
+ * await client.invoices.accept('invoice-uuid');
308
+ * ```
309
+ */
310
+ async accept(
311
+ id: string,
312
+ data?: AcceptInvoiceInput,
313
+ requestOptions?: RequestOptions
314
+ ): Promise<SingleResponse<Invoice>> {
315
+ return this.http.post<SingleResponse<Invoice>>(
316
+ `/invoices/${id}/accept`,
317
+ data,
318
+ requestOptions
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Reject an incoming invoice
324
+ *
325
+ * Mark an incoming invoice as rejected with a reason.
326
+ *
327
+ * @param id - Invoice UUID
328
+ * @param data - Rejection details
329
+ * @param requestOptions - Request options
330
+ * @returns Updated invoice
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const { data: invoice } = await client.invoices.reject('invoice-uuid', {
335
+ * reason: 'Invoice amount does not match purchase order',
336
+ * reason_code: 'incorrect_amount'
337
+ * });
338
+ * ```
339
+ */
340
+ async reject(
341
+ id: string,
342
+ data: RejectInvoiceInput,
343
+ requestOptions?: RequestOptions
344
+ ): Promise<SingleResponse<Invoice>> {
345
+ return this.http.post<SingleResponse<Invoice>>(
346
+ `/invoices/${id}/reject`,
347
+ data,
348
+ requestOptions
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Dispute an incoming invoice
354
+ *
355
+ * Open a dispute on an incoming invoice for resolution.
356
+ *
357
+ * @param id - Invoice UUID
358
+ * @param data - Dispute details
359
+ * @param requestOptions - Request options
360
+ * @returns Updated invoice
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * const { data: invoice } = await client.invoices.dispute('invoice-uuid', {
365
+ * reason: 'Billed amount exceeds agreed price',
366
+ * dispute_type: 'amount_dispute',
367
+ * expected_amount: 950.00
368
+ * });
369
+ * ```
370
+ */
371
+ async dispute(
372
+ id: string,
373
+ data: DisputeInvoiceInput,
374
+ requestOptions?: RequestOptions
375
+ ): Promise<SingleResponse<Invoice>> {
376
+ return this.http.post<SingleResponse<Invoice>>(
377
+ `/invoices/${id}/dispute`,
378
+ data,
379
+ requestOptions
380
+ );
381
+ }
382
+
383
+ /**
384
+ * Mark an incoming invoice as paid
385
+ *
386
+ * This is a mandatory step in the French e-invoicing lifecycle for incoming invoices.
387
+ * Once marked as paid, the invoice status changes to 'paid' and payment details are recorded.
388
+ *
389
+ * @param id - Invoice UUID
390
+ * @param data - Optional payment details (reference, date, note)
391
+ * @param requestOptions - Request options
392
+ * @returns Updated invoice with payment information
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * // Mark as paid with payment details
397
+ * const { data: invoice } = await client.invoices.markPaid('invoice-uuid', {
398
+ * payment_reference: 'VIR-2026-0124',
399
+ * paid_at: '2026-01-24T10:30:00Z',
400
+ * note: 'Payment received via bank transfer'
401
+ * });
402
+ *
403
+ * // Simple mark as paid (uses current date/time)
404
+ * await client.invoices.markPaid('invoice-uuid');
405
+ * ```
406
+ */
407
+ async markPaid(
408
+ id: string,
409
+ data?: MarkPaidInput,
410
+ requestOptions?: RequestOptions
411
+ ): Promise<SingleResponse<Invoice>> {
412
+ return this.http.post<SingleResponse<Invoice>>(
413
+ `/invoices/${id}/mark-paid`,
414
+ data,
415
+ requestOptions
416
+ );
417
+ }
418
+
419
+ /**
420
+ * Download invoice source file as binary content
421
+ *
422
+ * Downloads the original invoice file (PDF with embedded XML for Factur-X,
423
+ * or standalone XML for UBL/CII formats).
424
+ *
425
+ * @param id - Invoice UUID
426
+ * @param format - File format to download: 'pdf' (default) or 'xml'
427
+ * @param requestOptions - Request options
428
+ * @returns ArrayBuffer containing the file content
429
+ *
430
+ * @example
431
+ * ```typescript
432
+ * // Download invoice as PDF (Factur-X)
433
+ * const pdfBuffer = await client.invoices.downloadFile('invoice-uuid');
434
+ *
435
+ * // In Node.js, save to file:
436
+ * import { writeFileSync } from 'fs';
437
+ * writeFileSync('invoice.pdf', Buffer.from(pdfBuffer));
438
+ *
439
+ * // Download XML version (UBL/CII)
440
+ * const xmlBuffer = await client.invoices.downloadFile('invoice-uuid', 'xml');
441
+ * writeFileSync('invoice.xml', Buffer.from(xmlBuffer));
442
+ *
443
+ * // In browser, trigger download:
444
+ * const blob = new Blob([pdfBuffer], { type: 'application/pdf' });
445
+ * const url = URL.createObjectURL(blob);
446
+ * const a = document.createElement('a');
447
+ * a.href = url;
448
+ * a.download = 'invoice.pdf';
449
+ * a.click();
450
+ * ```
451
+ */
452
+ async downloadFile(
453
+ id: string,
454
+ format: InvoiceFileFormat = 'pdf',
455
+ requestOptions?: RequestOptions
456
+ ): Promise<ArrayBuffer> {
457
+ return this.http.getRaw(
458
+ `/invoices/${id}/download`,
459
+ { format },
460
+ requestOptions
461
+ );
462
+ }
246
463
  }
@@ -26,20 +26,28 @@ export type {
26
26
 
27
27
  // Invoice types
28
28
  export type {
29
+ AcceptInvoiceInput,
29
30
  AuditTrailEntry,
30
31
  AuditTrailResponse,
31
32
  ConvertInvoiceInput,
32
33
  CreateInvoiceInput,
34
+ DisputeInvoiceInput,
35
+ DisputeType,
36
+ IncomingInvoiceParams,
33
37
  Invoice,
34
38
  InvoiceDirection,
35
39
  InvoiceDownloadResponse,
36
40
  InvoiceDownloadType,
41
+ InvoiceFileFormat,
37
42
  InvoiceFormat,
38
43
  InvoiceLine,
39
44
  InvoiceLineInput,
40
45
  InvoiceListOptions,
41
46
  InvoiceParty,
42
47
  InvoiceStatus,
48
+ MarkPaidInput,
49
+ RejectInvoiceInput,
50
+ RejectionCode,
43
51
  } from './invoices.js';
44
52
 
45
53
  // Signature types
@@ -85,6 +93,7 @@ export type {
85
93
  export type {
86
94
  BalanceWebhookData,
87
95
  CreateWebhookInput,
96
+ InvoiceIncomingPaidPayload,
88
97
  InvoiceWebhookData,
89
98
  SignatureWebhookData,
90
99
  UpdateWebhookInput,
@@ -29,6 +29,9 @@ export type InvoiceStatus =
29
29
  | 'transmitted'
30
30
  | 'accepted'
31
31
  | 'rejected'
32
+ | 'paid'
33
+ | 'disputed'
34
+ | 'cancelled'
32
35
  | 'error';
33
36
 
34
37
  /**
@@ -86,6 +89,12 @@ export interface Invoice {
86
89
  validated_at: DateTimeString | null;
87
90
  transmitted_at: DateTimeString | null;
88
91
  completed_at: DateTimeString | null;
92
+ /** Date when the invoice was marked as paid (ISO 8601) */
93
+ paid_at: DateTimeString | null;
94
+ /** Payment reference (bank transfer ID, check number, etc.) */
95
+ payment_reference: string | null;
96
+ /** Optional note about the payment */
97
+ payment_note: string | null;
89
98
  }
90
99
 
91
100
  /**
@@ -189,3 +198,85 @@ export interface AuditTrailResponse {
189
198
  data: AuditTrailEntry[];
190
199
  integrity_valid: boolean;
191
200
  }
201
+
202
+ /**
203
+ * Incoming invoice list filter options
204
+ */
205
+ export interface IncomingInvoiceParams {
206
+ status?: InvoiceStatus | undefined;
207
+ seller_siret?: Siret | undefined;
208
+ from?: DateString | undefined;
209
+ to?: DateString | undefined;
210
+ min_amount?: number | undefined;
211
+ max_amount?: number | undefined;
212
+ page?: number | undefined;
213
+ per_page?: number | undefined;
214
+ }
215
+
216
+ /**
217
+ * Rejection reason code for incoming invoices
218
+ */
219
+ export type RejectionCode =
220
+ | 'incorrect_amount'
221
+ | 'duplicate'
222
+ | 'unknown_order'
223
+ | 'incorrect_vat'
224
+ | 'other';
225
+
226
+ /**
227
+ * Dispute type for incoming invoices
228
+ */
229
+ export type DisputeType =
230
+ | 'amount_dispute'
231
+ | 'quality_dispute'
232
+ | 'delivery_dispute'
233
+ | 'other';
234
+
235
+ /**
236
+ * Input for accepting an incoming invoice
237
+ */
238
+ export interface AcceptInvoiceInput {
239
+ /** Expected payment date (YYYY-MM-DD) */
240
+ payment_date?: DateString | undefined;
241
+ /** Optional note about the acceptance */
242
+ note?: string | undefined;
243
+ }
244
+
245
+ /**
246
+ * Input for rejecting an incoming invoice
247
+ */
248
+ export interface RejectInvoiceInput {
249
+ /** Reason for rejection */
250
+ reason: string;
251
+ /** Standardized rejection code */
252
+ reason_code: RejectionCode;
253
+ }
254
+
255
+ /**
256
+ * Input for disputing an incoming invoice
257
+ */
258
+ export interface DisputeInvoiceInput {
259
+ /** Reason for the dispute */
260
+ reason: string;
261
+ /** Type of dispute */
262
+ dispute_type: DisputeType;
263
+ /** Expected correct amount (if amount dispute) */
264
+ expected_amount?: number | undefined;
265
+ }
266
+
267
+ /**
268
+ * Input for marking an incoming invoice as paid
269
+ */
270
+ export interface MarkPaidInput {
271
+ /** Payment reference (bank transfer ID, check number, etc.) */
272
+ payment_reference?: string | undefined;
273
+ /** Payment date (ISO 8601) - defaults to current date/time if not provided */
274
+ paid_at?: DateTimeString | undefined;
275
+ /** Optional note about the payment */
276
+ note?: string | undefined;
277
+ }
278
+
279
+ /**
280
+ * Invoice file download format
281
+ */
282
+ export type InvoiceFileFormat = 'pdf' | 'xml';
@@ -4,13 +4,20 @@ import type { DateTimeString, Environment, UUID } from './common.js';
4
4
  * Available webhook events
5
5
  */
6
6
  export type WebhookEvent =
7
- // Invoice events
7
+ // Invoice events (outgoing)
8
8
  | 'invoice.created'
9
9
  | 'invoice.validated'
10
10
  | 'invoice.transmitted'
11
11
  | 'invoice.accepted'
12
12
  | 'invoice.rejected'
13
13
  | 'invoice.error'
14
+ // Invoice events (incoming)
15
+ | 'invoice.incoming.received'
16
+ | 'invoice.incoming.validated'
17
+ | 'invoice.incoming.accepted'
18
+ | 'invoice.incoming.rejected'
19
+ | 'invoice.incoming.disputed'
20
+ | 'invoice.incoming.paid'
14
21
  // Signature events
15
22
  | 'signature.created'
16
23
  | 'signature.waiting'
@@ -144,3 +151,25 @@ export interface BalanceWebhookData {
144
151
  currency: string;
145
152
  threshold: number;
146
153
  }
154
+
155
+ /**
156
+ * Invoice incoming paid webhook payload data
157
+ */
158
+ export interface InvoiceIncomingPaidPayload {
159
+ /** Invoice UUID */
160
+ invoice_id: string;
161
+ /** Invoice number */
162
+ invoice_number: string;
163
+ /** Seller company name */
164
+ seller_name: string;
165
+ /** Seller SIRET (14 digits) */
166
+ seller_siret: string;
167
+ /** Total amount including tax */
168
+ total_amount: number;
169
+ /** Currency code (ISO 4217) */
170
+ currency: string;
171
+ /** Date when the invoice was marked as paid (ISO 8601) */
172
+ paid_at: string;
173
+ /** Payment reference (if provided) */
174
+ payment_reference?: string | undefined;
175
+ }