diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf7067..7dc4105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [4.20.0] 2026-08-12 + +### Added + +- Add invoice ZIP request methods: `invoices.createZipRequest`, `invoices.listZipRequests`, `invoices.retrieveZipRequest`, and `invoices.downloadZipRequest`. + ## [4.19.0] 2026-07-01 ### Added diff --git a/package.json b/package.json index d8f7cf6..1bec61a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "facturapi", - "version": "4.19.0", + "version": "4.20.0", "description": "SDK oficial de Facturapi para Node.js y navegadores. Integra facturación electrónica en México (CFDI) de forma simple y obtén una perspectiva fiscal completa de tu operación, con búsquedas indexadas, envío de documentos y trazabilidad.", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/src/resources/invoices.ts b/src/resources/invoices.ts index 960db70..5298371 100644 --- a/src/resources/invoices.ts +++ b/src/resources/invoices.ts @@ -1,10 +1,13 @@ import { BinaryDownload, CancelInvoiceOptions, + CreateZipRequestData, GenericResponse, Invoice, + ListZipRequestsParams, SearchResult, SendEmailBody, + ZipRequest, } from '../types'; import { WrapperClient } from '../wrapper'; @@ -96,6 +99,48 @@ export default class Invoices { return this.client.get('/invoices/' + id + '/zip'); } + /** + * Creates or retrieves a ZIP request for invoices matching the specified criteria. + * @param data ZIP request criteria + * @returns ZIP request object + */ + createZipRequest(data: CreateZipRequestData): Promise { + return this.client.post('/invoices/zip-requests', { body: data }); + } + + /** + * Gets a paginated list of invoice ZIP requests. + * @param params Search parameters + * @returns Search results containing ZIP requests + */ + listZipRequests( + params?: ListZipRequestsParams | null, + ): Promise> { + return this.client.get('/invoices/zip-requests', { + params: params || {}, + }); + } + + /** + * Gets a single invoice ZIP request. + * @param id ZIP request Id + * @returns ZIP request object + */ + retrieveZipRequest(id: string): Promise { + if (!id) return Promise.reject(new Error('id is required')); + return this.client.get('/invoices/zip-requests/' + id); + } + + /** + * Downloads the ZIP file generated by an invoice ZIP request. + * @param id ZIP request Id + * @returns ZIP file in a stream (Node.js) or Blob (browser) + */ + downloadZipRequest(id: string): Promise { + if (!id) return Promise.reject(new Error('id is required')); + return this.client.get('/invoices/zip-requests/' + id + '/zip'); + } + /** * Downloads the cancellation receipt of a canceled invoice in XML format * @param id Invoice Id diff --git a/src/types/invoice.ts b/src/types/invoice.ts index d0a298f..32655cb 100644 --- a/src/types/invoice.ts +++ b/src/types/invoice.ts @@ -92,3 +92,30 @@ export interface CancelInvoiceOptions { motive: CancellationMotive; substitution?: string; } + +export interface CreateZipRequestData { + year: number; + month: number; + issuer_type: IssuingType; + invoice_types?: InvoiceType[]; +} + +export interface ListZipRequestsParams { + year?: number; + month?: number; + status?: string; + limit?: number; + page?: number; +} + +export interface ZipRequest { + id: string; + year: number; + month: number; + issuer_type: IssuingType; + invoice_types: InvoiceType[]; + status: string; + created_at?: Date; + updated_at?: Date; + [key: string]: unknown; +} diff --git a/test-d/runtime-types.test-d.ts b/test-d/runtime-types.test-d.ts index 13ad533..0fe68ab 100644 --- a/test-d/runtime-types.test-d.ts +++ b/test-d/runtime-types.test-d.ts @@ -2,8 +2,12 @@ import { expectAssignable, expectType, expectError } from 'tsd'; import Facturapi, { BinaryDownload, FacturapiError, + InvoiceType, + IssuingType, NodeLikeReadableStream, + SearchResult, TaxFactor, + ZipRequest, } from '../dist'; const client = new Facturapi('sk_test_123'); @@ -11,6 +15,30 @@ const client = new Facturapi('sk_test_123'); const zipPromise = client.invoices.downloadZip('inv_123'); expectType>(zipPromise); +expectType>( + client.invoices.createZipRequest({ + year: 2025, + month: 3, + issuer_type: IssuingType.ISSUING, + invoice_types: [InvoiceType.INGRESO, InvoiceType.EGRESO], + }), +); +expectType>>( + client.invoices.listZipRequests({ + year: 2025, + month: 3, + status: 'finished', + limit: 20, + page: 1, + }), +); +expectType>( + client.invoices.retrieveZipRequest('zip_request_123'), +); +expectType>( + client.invoices.downloadZipRequest('zip_request_123'), +); + declare const nodeLike: NodeLikeReadableStream; nodeLike.on('data', (chunk) => { expectType(chunk); diff --git a/test/node/invoices-zip-requests.node.test.ts b/test/node/invoices-zip-requests.node.test.ts new file mode 100644 index 0000000..ecaa8f3 --- /dev/null +++ b/test/node/invoices-zip-requests.node.test.ts @@ -0,0 +1,112 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import Facturapi, { InvoiceType, IssuingType } from '../../src' + +const originalFetch = globalThis.fetch + +function createClient() { + const client = new Facturapi('sk_test_123') + client.BASE_URL = 'https://api.test.local/v2' + return client +} + +afterEach(() => { + globalThis.fetch = originalFetch + vi.restoreAllMocks() +}) + +describe('invoice ZIP requests', () => { + it('creates or retrieves a ZIP request', async () => { + const client = createClient() + const data = { + year: 2025, + month: 3, + issuer_type: IssuingType.ISSUING, + invoice_types: [InvoiceType.INGRESO, InvoiceType.EGRESO], + } + + globalThis.fetch = vi.fn(async (url, options) => { + expect(url).toBe('https://api.test.local/v2/invoices/zip-requests') + expect(options?.method).toBe('POST') + expect(options?.body).toBe(JSON.stringify(data)) + return Response.json({ + id: 'zip_request_123', + ...data, + status: 'pending', + }) + }) as typeof fetch + + const result = await client.invoices.createZipRequest(data) + expect(result.id).toBe('zip_request_123') + }) + + it('lists ZIP requests with query parameters', async () => { + const client = createClient() + + globalThis.fetch = vi.fn(async (url, options) => { + expect(url).toBe( + 'https://api.test.local/v2/invoices/zip-requests?year=2025&month=3&status=finished&limit=20&page=1', + ) + expect(options?.method).toBe('GET') + return Response.json({ + page: 1, + total_pages: 1, + total_results: 0, + data: [], + }) + }) as typeof fetch + + const result = await client.invoices.listZipRequests({ + year: 2025, + month: 3, + status: 'finished', + limit: 20, + page: 1, + }) + expect(result.data).toEqual([]) + }) + + it('retrieves a ZIP request', async () => { + const client = createClient() + + globalThis.fetch = vi.fn(async (url, options) => { + expect(url).toBe( + 'https://api.test.local/v2/invoices/zip-requests/zip_request_123', + ) + expect(options?.method).toBe('GET') + return Response.json({ id: 'zip_request_123' }) + }) as typeof fetch + + const result = await client.invoices.retrieveZipRequest('zip_request_123') + expect(result.id).toBe('zip_request_123') + }) + + it('downloads the generated ZIP', async () => { + const client = createClient() + + globalThis.fetch = vi.fn(async (url, options) => { + expect(url).toBe( + 'https://api.test.local/v2/invoices/zip-requests/zip_request_123/zip', + ) + expect(options?.method).toBe('GET') + return new Response(new Blob([Buffer.from('zip-binary-content')]), { + headers: { 'content-type': 'application/zip' }, + }) + }) as typeof fetch + + const zip = await client.invoices.downloadZipRequest('zip_request_123') + expect(zip instanceof Blob).toBe(false) + expect(typeof (zip as { pipe?: unknown }).pipe).toBe('function') + }) + + it('requires an id to retrieve or download a ZIP request', async () => { + const client = createClient() + + await expect(client.invoices.retrieveZipRequest('')).rejects.toThrow( + 'id is required', + ) + await expect(client.invoices.downloadZipRequest('')).rejects.toThrow( + 'id is required', + ) + }) +})