diff --git a/CHANGELOG.md b/CHANGELOG.md index 52628eb..38db545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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.19.0] 2026-07-16 +### Added +- Export documented API root error-code catalogs and `ValidationErrorCode` for structured error handling. `FacturapiError.code` remains forward-compatible with future API codes. + ## [4.18.0] 2026-06-06 ### Added - Expose structured API error metadata through `FacturapiError`, including `status`, `code`, `path`, `location`, `errors`, `logId`, and response `headers`. diff --git a/package.json b/package.json index b6c17f7..d8f7cf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "facturapi", - "version": "4.18.0", + "version": "4.19.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/errorCodes.ts b/src/errorCodes.ts new file mode 100644 index 0000000..313145e --- /dev/null +++ b/src/errorCodes.ts @@ -0,0 +1,342 @@ +// Public API root error.code constants. External PAC/SAT codes belong in +// errors[].code and are intentionally not included here. + +type ErrorCodeValue = T[keyof T] + +export const CommonErrorCode = { + CONFLICT: 'conflict', + FORBIDDEN: 'forbidden', + INTERNAL_ERROR: 'internal_error', + NOT_FOUND: 'not_found', + UNAUTHORIZED: 'unauthorized', +} as const +export type CommonErrorCode = ErrorCodeValue + +export const AuthErrorCode = { + API_KEY_INVALID: 'api_key_invalid', + API_KEY_NOT_ALLOWED: 'api_key_not_allowed', + FEATURE_NOT_AVAILABLE: 'feature_not_available', + LIVE_API_KEY_REQUIRED: 'live_api_key_required', + MCP_PERMISSION_DENIED: 'mcp_permission_denied', + MISSING_CREDENTIALS: 'missing_credentials', + ORGANIZATION_INCOMPLETE: 'organization_incomplete', + SUBSCRIPTION_REQUIRED: 'subscription_required', + SUBSCRIPTION_LIVE_ACCESS_REQUIRED: 'subscription_live_access_required', + USER_KEY_INVALID: 'user_key_invalid', + USER_SUSPENDED: 'user_suspended', +} as const +export type AuthErrorCode = ErrorCodeValue + +export const RequestErrorCode = { + IDEMPOTENCY_KEY_IN_USE: 'idempotency_key_in_use', + RATE_LIMIT_EXCEEDED: 'rate_limit_exceeded', + DATE_RANGE_TOO_LARGE: 'date_range_too_large', + IMAGE_FILE_REQUIRED: 'image_file_required', + INVALID_COUNTRY_CODE: 'invalid_country_code', + INVALID_DATE: 'invalid_date', + INVALID_DATE_RANGE: 'invalid_date_range', + INVALID_IMAGE_FILE: 'invalid_image_file', + INVALID_JSON: 'invalid_json', + INVALID_REQUEST: 'invalid_request', + INVALID_MULTIPART_FORM_DATA: 'invalid_multipart_form_data', + INVALID_STATE_CODE: 'invalid_state_code', + INVALID_TIMEZONE: 'invalid_timezone', + MULTIPART_LIMIT_EXCEEDED: 'multipart_limit_exceeded', + PAGE_TOO_LARGE: 'page_too_large', + PAYLOAD_TOO_LARGE: 'payload_too_large', +} as const +export type RequestErrorCode = ErrorCodeValue + +export const CustomerErrorCode = { + CUSTOMER_COULD_NOT_BE_RESOLVED: 'customer_could_not_be_resolved', + CUSTOMER_EDIT_LINK_NOT_FOUND: 'customer_edit_link_not_found', + CUSTOMER_EDIT_LINK_UNAVAILABLE: 'customer_edit_link_unavailable', + CUSTOMER_EMAIL_REQUIRED: 'customer_email_required', + CUSTOMER_HAS_INVOICES: 'customer_has_invoices', + CUSTOMER_NOT_FOUND: 'customer_not_found', + CUSTOMER_TAX_INFO_UNAVAILABLE: 'customer_tax_info_unavailable', +} as const +export type CustomerErrorCode = ErrorCodeValue + +export const TaxInfoValidationCode = { + LEGAL_NAME_MISMATCH: 'legal_name_mismatch', + TAX_ADDRESS_ZIP_MISMATCH: 'tax_address_zip_mismatch', + TAX_ID_NOT_FOUND: 'tax_id_not_found', + TAX_SYSTEM_NOT_ALLOWED_FOR_TAX_ID: 'tax_system_not_allowed_for_tax_id', + TAX_SYSTEM_NOT_IN_CATALOG: 'tax_system_not_in_catalog', +} as const +export type TaxInfoValidationCode = ErrorCodeValue + +export const ProductErrorCode = { + PRODUCT_NOT_FOUND: 'product_not_found', +} as const +export type ProductErrorCode = ErrorCodeValue + +export const SupplierErrorCode = { + SUPPLIER_NOT_FOUND: 'supplier_not_found', +} as const +export type SupplierErrorCode = ErrorCodeValue + +export const CatalogErrorCode = { + PRODUCT_KEY_NOT_FOUND: 'product_key_not_found', + TARIFF_CODE_NOT_FOUND: 'tariff_code_not_found', + UNIT_KEY_NOT_FOUND: 'unit_key_not_found', +} as const +export type CatalogErrorCode = ErrorCodeValue + +export const InvoiceErrorCode = { + INVOICE_ALREADY_STAMPED: 'invoice_already_stamped', + INVOICE_NOT_DRAFT: 'invoice_not_draft', + INVOICE_NOT_FOUND: 'invoice_not_found', + INVOICE_NOT_STAMPED: 'invoice_not_stamped', +} as const +export type InvoiceErrorCode = ErrorCodeValue + +export const InvoiceDraftErrorCode = { + DRAFT_NOT_READY_TO_STAMP: 'draft_not_ready_to_stamp', + DRAFT_UPDATE_IN_PROGRESS: 'draft_update_in_progress', +} as const +export type InvoiceDraftErrorCode = ErrorCodeValue + +export const InvoiceStampingErrorCode = { + INVOICE_STAMPING_FAILED: 'invoice_stamping_failed', + INVOICE_STAMPING_SERVICE_UNAVAILABLE: 'invoice_stamping_service_unavailable', + INVOICE_STAMPING_VALIDATION_ERROR: 'invoice_stamping_validation_error', + MANIFESTO_SIGNATURE_FAILED: 'manifesto_signature_failed', + STAMPING_IN_PROGRESS: 'stamping_in_progress', +} as const +export type InvoiceStampingErrorCode = ErrorCodeValue< + typeof InvoiceStampingErrorCode +> + +export const InvoiceDeliveryErrorCode = { + INVOICE_EMAIL_DELIVERY_FAILED: 'invoice_email_delivery_failed', + INVOICE_EMAIL_RECIPIENT_REQUIRED: 'invoice_email_recipient_required', + INVOICE_EMAIL_STATUS_NOT_ALLOWED: 'invoice_email_status_not_allowed', +} as const +export type InvoiceDeliveryErrorCode = ErrorCodeValue< + typeof InvoiceDeliveryErrorCode +> + +export const InvoiceCancellationErrorCode = { + INVOICE_CANCELLATION_IN_PROGRESS: 'invoice_cancellation_in_progress', + INVOICE_CANCELLATION_RECEIPT_UNAVAILABLE: + 'invoice_cancellation_receipt_unavailable', + INVOICE_CANCELLATION_FAILED: 'invoice_cancellation_failed', + INVOICE_CANCELLATION_NOT_ALLOWED: 'invoice_cancellation_not_allowed', + INVOICE_CANCELLATION_NOT_FOUND: 'invoice_cancellation_not_found', + INVOICE_CANCELLATION_RFC_MISMATCH: 'invoice_cancellation_rfc_mismatch', + INVOICE_CANCELLATION_SERVICE_UNAVAILABLE: + 'invoice_cancellation_service_unavailable', + INVOICE_NOT_CANCELABLE: 'invoice_not_cancelable', + INVOICE_NOT_CANCELABLE_BY_SAT: 'invoice_not_cancelable_by_sat', + SUBSTITUTION_INVOICE_CANCELED: 'substitution_invoice_canceled', + SUBSTITUTION_INVOICE_NOT_FOUND: 'substitution_invoice_not_found', + SUBSTITUTION_INVOICE_REQUIRED: 'substitution_invoice_required', + SUBSTITUTION_INVOICE_STATUS_NOT_ALLOWED: + 'substitution_invoice_status_not_allowed', +} as const +export type InvoiceCancellationErrorCode = ErrorCodeValue< + typeof InvoiceCancellationErrorCode +> + +export const ReceiptErrorCode = { + RECEIPT_EXPIRED: 'receipt_expired', + RECEIPT_NOT_FOUND: 'receipt_not_found', + RECEIPT_NOT_OPEN: 'receipt_not_open', +} as const +export type ReceiptErrorCode = ErrorCodeValue + +export const ReceiptInvoicingErrorCode = { + RECEIPT_INVOICING_ADDRESS_MISMATCH: 'receipt_invoicing_address_mismatch', + RECEIPT_INVOICING_CUSTOMER_MISMATCH: 'receipt_invoicing_customer_mismatch', + RECEIPT_INVOICING_TOO_MANY_ITEMS: 'receipt_invoicing_too_many_items', + RECEIPT_KEYS_NOT_FOUND: 'receipt_keys_not_found', +} as const +export type ReceiptInvoicingErrorCode = ErrorCodeValue< + typeof ReceiptInvoicingErrorCode +> + +export const ReceiptGlobalInvoiceErrorCode = { + GLOBAL_INVOICE_TOO_MANY_ITEMS: 'global_invoice_too_many_items', + INVALID_GLOBAL_INVOICE_PERIOD: 'invalid_global_invoice_period', +} as const +export type ReceiptGlobalInvoiceErrorCode = ErrorCodeValue< + typeof ReceiptGlobalInvoiceErrorCode +> + +export const RetentionErrorCode = { + INVALID_RETENTION_COMPLEMENT: 'invalid_retention_complement', + RETENTION_NOT_FOUND: 'retention_not_found', + RETENTION_NOT_STAMPED: 'retention_not_stamped', + RETENTION_NOT_CANCELABLE: 'retention_not_cancelable', +} as const +export type RetentionErrorCode = ErrorCodeValue + +export const RetentionDeliveryErrorCode = { + RETENTION_EMAIL_DELIVERY_FAILED: 'retention_email_delivery_failed', + RETENTION_EMAIL_RECIPIENT_REQUIRED: 'retention_email_recipient_required', + RETENTION_EMAIL_STATUS_NOT_ALLOWED: 'retention_email_status_not_allowed', +} as const +export type RetentionDeliveryErrorCode = ErrorCodeValue< + typeof RetentionDeliveryErrorCode +> + +export const RetentionCancellationErrorCode = { + RETENTION_CANCELLATION_FAILED: 'retention_cancellation_failed', + RETENTION_CANCELLATION_SERVICE_UNAVAILABLE: + 'retention_cancellation_service_unavailable', +} as const +export type RetentionCancellationErrorCode = ErrorCodeValue< + typeof RetentionCancellationErrorCode +> + +export const RetentionStampingErrorCode = { + RETENTION_STAMPING_FAILED: 'retention_stamping_failed', + RETENTION_STAMPING_SERVICE_UNAVAILABLE: + 'retention_stamping_service_unavailable', + RETENTION_STAMPING_VALIDATION_ERROR: 'retention_stamping_validation_error', +} as const +export type RetentionStampingErrorCode = ErrorCodeValue< + typeof RetentionStampingErrorCode +> + +export const OrganizationErrorCode = { + INVALID_OPERATION: 'invalid_operation', + INVALID_USER_ID: 'invalid_user_id', + ORGANIZATION_NOT_FOUND: 'organization_not_found', + SUBSCRIPTION_ACTIVE_REQUIRED: 'subscription_active_required', +} as const +export type OrganizationErrorCode = ErrorCodeValue + +export const OrganizationSettingsErrorCode = { + CERTIFICATE_EXPIRED: 'certificate_expired', + CERTIFICATE_FILE_REQUIRED: 'certificate_file_required', + CERTIFICATE_FILES_INVALID: 'certificate_files_invalid', + CERTIFICATE_FILES_REQUIRED: 'certificate_files_required', + CERTIFICATE_FIEL_RFC_MISMATCH: 'certificate_fiel_rfc_mismatch', + CERTIFICATE_INVALID: 'certificate_invalid', + CERTIFICATE_NOT_YET_VALID: 'certificate_not_yet_valid', + CERTIFICATE_PREVIOUS_RFC_MISMATCH: 'certificate_previous_rfc_mismatch', + CSD_REQUIRED: 'csd_required', + FIEL_INVALID: 'fiel_invalid', + FIEL_RFC_MISMATCH: 'fiel_rfc_mismatch', + FIEL_REQUIRED: 'fiel_required', + ORGANIZATION_DOMAIN_CHANGE_NOT_ALLOWED: + 'organization_domain_change_not_allowed', + ORGANIZATION_DOMAIN_UNAVAILABLE: 'organization_domain_unavailable', + ORGANIZATION_SETTINGS_INVALID: 'organization_settings_invalid', + ORGANIZATION_SUPPORT_EMAIL_REQUIRED: 'organization_support_email_required', + ORGANIZATION_TAX_INFO_INVALID: 'organization_tax_info_invalid', + PRIVATE_KEY_CERTIFICATE_MISMATCH: 'private_key_certificate_mismatch', + PRIVATE_KEY_FILE_REQUIRED: 'private_key_file_required', + PRIVATE_KEY_PASSWORD_INCORRECT: 'private_key_password_incorrect', +} as const +export type OrganizationSettingsErrorCode = ErrorCodeValue< + typeof OrganizationSettingsErrorCode +> + +export const OrganizationInviteErrorCode = { + INVITE_EMAIL_DELIVERY_FAILED: 'invite_email_delivery_failed', + INVITE_EMAIL_MISMATCH: 'invite_email_mismatch', + INVITE_EXPIRED: 'invite_expired', + INVITE_NOT_FOUND: 'invite_not_found', + INVITE_ROLE_UNAVAILABLE: 'invite_role_unavailable', + USER_ALREADY_IN_ORGANIZATION: 'user_already_in_organization', +} as const +export type OrganizationInviteErrorCode = ErrorCodeValue< + typeof OrganizationInviteErrorCode +> + +export const OrganizationAccessErrorCode = { + ORGANIZATION_ADMIN_ACCESS_CANNOT_BE_REMOVED: + 'organization_admin_access_cannot_be_removed', + ORGANIZATION_ADMIN_ASSIGNMENT_CANNOT_BE_EDITED: + 'organization_admin_assignment_cannot_be_edited', + ORGANIZATION_ADMIN_ROLE_CANNOT_BE_DELETED: + 'organization_admin_role_cannot_be_deleted', + ORGANIZATION_ADMIN_ROLE_CANNOT_BE_EDITED: + 'organization_admin_role_cannot_be_edited', + ORGANIZATION_ADMIN_ROLE_REQUIRED: 'organization_admin_role_required', + ORGANIZATION_ID_NOT_ALLOWED: 'organization_id_not_allowed', + ORGANIZATION_ID_REQUIRED: 'organization_id_required', + OWNER_ACCESS_CANNOT_BE_REASSIGNED: 'owner_access_cannot_be_reassigned', + OWNER_ACCESS_CANNOT_BE_REMOVED: 'owner_access_cannot_be_removed', + ROLE_HAS_ASSIGNED_USERS: 'role_has_assigned_users', + ROLE_TEMPLATE_NOT_FOUND: 'role_template_not_found', + ROLE_NOT_FOUND: 'role_not_found', + USER_ACCESS_NOT_FOUND: 'user_access_not_found', +} as const +export type OrganizationAccessErrorCode = ErrorCodeValue< + typeof OrganizationAccessErrorCode +> + +export const OrganizationSeriesErrorCode = { + SERIES_ALREADY_EXISTS: 'series_already_exists', + SERIES_NOT_FOUND: 'series_not_found', +} as const +export type OrganizationSeriesErrorCode = ErrorCodeValue< + typeof OrganizationSeriesErrorCode +> + +export const WebhookErrorCode = { + WEBHOOK_DELIVERY_ATTEMPT_NOT_FOUND: 'webhook_delivery_attempt_not_found', + WEBHOOK_NOT_FOUND: 'webhook_not_found', + WEBHOOK_SIGNATURE_INVALID: 'webhook_signature_invalid', +} as const +export type WebhookErrorCode = ErrorCodeValue + +export const ToolErrorCode = { + TAX_ID_VALIDATION_FAILED: 'tax_id_validation_failed', + TAX_ID_VALIDATION_SERVICE_UNAVAILABLE: + 'tax_id_validation_service_unavailable', +} as const +export type ToolErrorCode = ErrorCodeValue + +export type ApiErrorCode = + | CommonErrorCode + | AuthErrorCode + | RequestErrorCode + | CustomerErrorCode + | TaxInfoValidationCode + | ProductErrorCode + | SupplierErrorCode + | CatalogErrorCode + | InvoiceErrorCode + | InvoiceDraftErrorCode + | InvoiceStampingErrorCode + | InvoiceDeliveryErrorCode + | InvoiceCancellationErrorCode + | ReceiptErrorCode + | ReceiptInvoicingErrorCode + | ReceiptGlobalInvoiceErrorCode + | RetentionErrorCode + | RetentionDeliveryErrorCode + | RetentionCancellationErrorCode + | RetentionStampingErrorCode + | OrganizationErrorCode + | OrganizationSettingsErrorCode + | OrganizationInviteErrorCode + | OrganizationAccessErrorCode + | OrganizationSeriesErrorCode + | WebhookErrorCode + | ToolErrorCode + +// Public errors[].code constants for Facturapi-owned validation details. +export const ValidationErrorCode = { + AMOUNT_EXCEEDS_RELATED_DOCUMENT_BALANCE: + 'amount_exceeds_related_document_balance', + EXCHANGE_RATE_TOO_LARGE: 'exchange_rate_too_large', + EXCHANGE_RATE_TOO_SMALL: 'exchange_rate_too_small', + INVALID_FORMAT: 'invalid_format', + INVALID_LENGTH: 'invalid_length', + INVALID_TYPE: 'invalid_type', + NOT_FOUND: 'not_found', + NOT_ALLOWED: 'not_allowed', + REQUIRED: 'required', + TOO_LARGE: 'too_large', + TOO_SMALL: 'too_small', + UNKNOWN_FIELD: 'unknown_field', + INVALID_VALUE: 'invalid_value', +} as const +export type ValidationErrorCode = ErrorCodeValue diff --git a/src/index.ts b/src/index.ts index 13075f8..9d2163b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import CartaPorteCatalogs from './tools/cartaPorteCatalogs'; import ComercioExteriorCatalogs from './tools/comercioExteriorCatalogs'; export * from './enums'; +export * from './errorCodes'; export * from './types'; export { FacturapiError } from './wrapper'; export type { FacturapiErrorDetail } from './wrapper'; diff --git a/src/wrapper.ts b/src/wrapper.ts index 7a6ec75..a56a851 100644 --- a/src/wrapper.ts +++ b/src/wrapper.ts @@ -3,6 +3,7 @@ import { BASE_URL_V1, DEFAULT_API_VERSION, } from './constants'; +import type { ApiErrorCode } from './errorCodes'; const getRuntimeFetch = () => { const runtimeFetch = globalThis.fetch; @@ -36,7 +37,7 @@ export interface FacturapiErrorDetail { export interface FacturapiErrorOptions { message: string; status: number; - code?: string; + code?: ApiErrorCode | (string & {}); path?: string; location?: string; errors?: FacturapiErrorDetail[]; @@ -46,7 +47,7 @@ export interface FacturapiErrorOptions { export class FacturapiError extends Error { status: number; - code?: string; + code?: ApiErrorCode | (string & {}); path?: string; location?: string; errors?: FacturapiErrorDetail[]; diff --git a/test-d/runtime-types.test-d.ts b/test-d/runtime-types.test-d.ts index 13ad533..9d346a1 100644 --- a/test-d/runtime-types.test-d.ts +++ b/test-d/runtime-types.test-d.ts @@ -1,6 +1,7 @@ import { expectAssignable, expectType, expectError } from 'tsd'; import Facturapi, { BinaryDownload, + CommonErrorCode, FacturapiError, NodeLikeReadableStream, TaxFactor, @@ -33,7 +34,8 @@ expectAssignable(TaxFactor.EXENTO); declare const apiError: FacturapiError; expectType(apiError.status); -expectType(apiError.code); +expectAssignable(CommonErrorCode.NOT_FOUND); +expectAssignable('future_error_code'); expectType(apiError.path); expectType(apiError.location); expectType(apiError.logId); diff --git a/test/node/runtime-compat.node.test.ts b/test/node/runtime-compat.node.test.ts index 0e5f97f..7698f98 100644 --- a/test/node/runtime-compat.node.test.ts +++ b/test/node/runtime-compat.node.test.ts @@ -231,7 +231,7 @@ describe('runtime compatibility (node)', () => { JSON.stringify({ message: 'Se excedió el límite de solicitudes.', status: 429, - code: 'RATE_LIMIT_EXCEEDED', + code: 'rate_limit_exceeded', path: 'date', location: 'query', errors: [ @@ -263,7 +263,7 @@ describe('runtime compatibility (node)', () => { 'Se excedió el límite de solicitudes.', ) expect((error as FacturapiError).status).toBe(429) - expect((error as FacturapiError).code).toBe('RATE_LIMIT_EXCEEDED') + expect((error as FacturapiError).code).toBe('rate_limit_exceeded') expect((error as FacturapiError).path).toBe('date') expect((error as FacturapiError).location).toBe('query') expect((error as FacturapiError).logId).toBe('log_123') diff --git a/test/web/runtime-compat.web.test.ts b/test/web/runtime-compat.web.test.ts index d855cc9..e2cec3c 100644 --- a/test/web/runtime-compat.web.test.ts +++ b/test/web/runtime-compat.web.test.ts @@ -149,7 +149,7 @@ describe('runtime compatibility (web simulation)', () => { return JSON.stringify({ message: 'Se excedió el límite de solicitudes.', status: 429, - code: 'RATE_LIMIT_EXCEEDED', + code: 'rate_limit_exceeded', }) }, } as unknown as Response @@ -161,7 +161,7 @@ describe('runtime compatibility (web simulation)', () => { } catch (error) { expect(error).toBeInstanceOf(FacturapiError) expect((error as FacturapiError).status).toBe(429) - expect((error as FacturapiError).code).toBe('RATE_LIMIT_EXCEEDED') + expect((error as FacturapiError).code).toBe('rate_limit_exceeded') expect((error as FacturapiError).logId).toBe('log_123') expect((error as FacturapiError).headers['retry-after']).toBe('3') expect((error as FacturapiError).headers['x-facturapi-log-id']).toBe(