Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
45 changes: 45 additions & 0 deletions src/resources/invoices.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
BinaryDownload,
CancelInvoiceOptions,
CreateZipRequestData,
GenericResponse,
Invoice,
ListZipRequestsParams,
SearchResult,
SendEmailBody,
ZipRequest,
} from '../types';
import { WrapperClient } from '../wrapper';

Expand Down Expand Up @@ -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<ZipRequest> {
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<SearchResult<ZipRequest>> {
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<ZipRequest> {
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<BinaryDownload> {
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
Expand Down
27 changes: 27 additions & 0 deletions src/types/invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
28 changes: 28 additions & 0 deletions test-d/runtime-types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,43 @@ 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');

const zipPromise = client.invoices.downloadZip('inv_123');
expectType<Promise<BinaryDownload>>(zipPromise);

expectType<Promise<ZipRequest>>(
client.invoices.createZipRequest({
year: 2025,
month: 3,
issuer_type: IssuingType.ISSUING,
invoice_types: [InvoiceType.INGRESO, InvoiceType.EGRESO],
}),
);
expectType<Promise<SearchResult<ZipRequest>>>(
client.invoices.listZipRequests({
year: 2025,
month: 3,
status: 'finished',
limit: 20,
page: 1,
}),
);
expectType<Promise<ZipRequest>>(
client.invoices.retrieveZipRequest('zip_request_123'),
);
expectType<Promise<BinaryDownload>>(
client.invoices.downloadZipRequest('zip_request_123'),
);

declare const nodeLike: NodeLikeReadableStream;
nodeLike.on('data', (chunk) => {
expectType<unknown>(chunk);
Expand Down
112 changes: 112 additions & 0 deletions test/node/invoices-zip-requests.node.test.ts
Original file line number Diff line number Diff line change
@@ -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',
)
})
})