diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index 1933d1b..0000000 --- a/src/api.ts +++ /dev/null @@ -1,420 +0,0 @@ -import z from 'zod'; -import https from 'https'; -import fetch from 'node-fetch'; -import path from 'path'; -import fs from 'fs/promises'; -import { Prettify, capitalizeKeys } from './utils'; - -type TrxType = 'Sale' | 'Void' | 'Refund' | 'Hold'; - -const channel = z.string().max(3).optional().default('EC'); -const azulOrderId = z.string().max(8); -const amount = z.number().int().positive(); -const ITBIS = z.number().int().positive(); - -const PostSchema = z.object({ - channel, - azulOrderId, - amount, - ITBIS -}); - -const ProcessPaymentSchema = z.object({ - /** - * Canal de pago. - * Este valor es proporcionado por AZUL, junto a los - * datos de acceso a cada ambiente - */ - channel, - /** - * Número de tarjeta a la cual se le ha de cargar la - * transacción. - * La longitud del campo se determina por la tarjeta, - * no se debe rellenar con ceros (0), espacios, ni - * caracteres especiales - */ - cardNumber: z.string().max(19), - /** - * Fecha expiración/vencimiento de la tarjeta - * Formato YYYYMM Ej.: 201502 - */ - expiration: z.string().length(6), - /** - * Código de seguridad de la tarjeta (CVV2 o CVC). - */ - CVC: z.string().length(3), - /** - * Modo de ingreso. - * Este valor es proporcionado por AZUL, junto a los - * datos de acceso a cada ambiente - */ - posInputMode: z.string().max(10).optional().default('E-Commerce'), - /** - * Monto total de la transacción (Impuestos - * incluidos.) - * Se envía sin coma ni punto; los dos últimos dígitos - * representan los decimales. - * Ej. 1000 equivale a 10.00 - * Ej. 1748321 equivale a 17,483.21 - */ - amount, - /** - * Valor del ITBIS. Mismo formato que el campo - * Amount. El valor enviado en el campo de ITBIS no - * se carga como un monto adicional en la - * transacción. En el campo de Amount debe enviarse - * total a cargar incluyendo el ITBIS. - * Si la transacción o el negocio están exentos, se - * envía en cero colocando el valo “000”. - * - * Este valor deberá también ser incluido en el cálculo - * del hash. - */ - ITBIS, - /** - * Número de orden asociado a la transacción. Puede - * viajar nulo, pero siempre debe de estar presente. - */ - orderNumber: z.string().max(15).optional(), - /** - * Uso Interno AZUL - * Valor Fijo: 1 - */ - acquirerRefData: z.enum(['0', '1']).optional(), - /** - * Número de servicio para atención telefónica del - * establecimiento. Ej.: 8095442985 - */ - customerServicePhone: z.string().max(32).optional(), - /** - * Dirección web del afiliado. - */ - ECommerceURL: z.string().max(32).optional(), - /** - * Numero Identificador dada por el afiliado a la - * transacción. Este campo debe ser enviado si se - * desea implementar el método de VerifyPayment. - */ - customOrderId: z.string().max(75).optional(), - /** - * Campo que permite al Comercio colocar un - * nombre más descriptivo para que el - * tarjetahabiente pueda identificarle en su estado de - * cuenta. Se sugiere siempre colocar su nombre - * comercial adecuadamente a fin de evitar disputas. - * Si lo desea, puede agregar a su nombre algún - * indicador único de orden. - * El campo solo acepta un máximo de 25 caracteres. - * No utilizar los siguientes caracteres especiales: - * “ Genera un error en el request. - * \ Genera un error en el request. - * ' Este carácter no se muestra en el mensaje del - * emisor. - */ - altMerchantName: z.string().max(30).optional(), - /** - * Valor del token generado por SDP en caso de que - * se desee realizar una transacción con dicho token. - * Si se manda el valor de esto, no se deben enviar los - * valores de CardNumber, Expiration. El envío de - * CVC si es ecommerce puede ser o no mandatorio - * depende de lo conversado con Negocios SDP. Si es - * MOTO la transacción, no se debería enviar CVC. - */ - dataVaultToken: z.string().max(100).optional(), - /** - * Valores posibles 1 = si, 2 = no. Si se manda este - * valor en 1, SDP le devolverá el token generado en - * el campo DataVaultToken - */ - saveToDataVault: z.enum(['1', '2']).optional(), - /** - * Valores posibles 0 =no, 1 = Si. Si se envía el valor en - * ‘0’, la transacción se procesa con 3D Secure. Si se - * envía el valor en ‘1’ la transacción se procesa sin - * 3D Secure. - */ - forceNo3DS: z.enum(['0', '1']).optional() -}); - -const RefundRequestSchema = z - .object({ - OriginalDate: z.string().length(8), - OriginalTrxTicketNr: z.string().length(4).optional() - }) - .merge(ProcessPaymentSchema); - -type ProcessPaymentSchemaInput = z.input; - -type AzulResponse = { - /** - * Código de autorización generado por el centro autorizador para la - * transacción. - * Sólo presente si la transacción fue aprobada. ISOCode = ISO8583 y - * ResponseCode = 00 - */ - AuthorizationCode: string; - /** - * Numero Identificador dada por el afiliado a la transacción. Si no - * fue provisto en la transacción, este campo viaja en blanco - */ - CustomOrderId: string; - /** - * Fecha y hora de la transacción. - * Formato YYYYMMDDHHMMSS. - */ - DateTime: string; - /** - * Descripción del error. - * Valor sólo presente si la transacción produjo un error. En caso de - * no presentar error ese campo viaja en blanco - */ - ErrorDescription: string; - /** - * Código ISO-8583 recibido de respuesta. - * Nota: En la documentación de Azul, este campo se llama ISOCode - */ - IsoCode: string; - /** - * Número de lote en que se registró la transacción - */ - LotNumber: string; - /** - * Número de referencia - * (Reference referral number). - */ - RRN: string; - /** - * # de orden Azul. Puede ser usado en vez del RRN para generar una - * devolución. Importante dar prioridad a este valor sobre el RRN. - */ - AzulOrderId: string; - /** - * Código de respuesta. - * Puede contener uno de los siguientes valores: - * Iso8583 = la transacción fue procesada. Se debe revisar el - * campo ISOCode para ver la respuesta de la transacción Error = - * La transacción no fue procesada. - */ - ResponseCode: 'ISO8583' | 'Error'; - /** - * Mensaje de respuesta ISO-8583. - * Valor sólo presente si el ResponseCode = ISO8583 - */ - ReponseMessage: string; - /** - * Número del ticket correspondiente a la transacción - */ - Ticket: string; - /** - * Tarjeta usada para la transacción, enmascarada - * (XXXXXX******XXXX) - */ - // CardNumber: string; -}; - -type AzulResponseWithOk = Prettify< - Partial & { - /** - * Indica que la transacción fue exitosa. - * Su valor es `true` si ResponseCode no es `Error` y ISOCode es `00`. - */ - ok: boolean; - } ->; - -type AzulConfig = { - auth1: string; - auth2: string; - merchantId: string; - certificatePath: string; - keyPath: string; - environment?: 'dev' | 'prod'; - channel?: string; -}; - -enum AzulURL { - DEV = 'https://pruebas.azul.com.do/webservices/JSON/Default.aspx', - PROD = 'https://pagos.azul.com.do/webservices/JSON/Default.aspx' -} - -class AzulAPI { - private readonly config: AzulConfig; - private readonly azulURL: string; - private certificate: Buffer | undefined; - private certificateKey: Buffer | undefined; - - constructor(config: AzulConfig) { - this.config = config; - - if (this.config.channel === undefined) { - this.config.channel = 'EC'; - } - - if (config.environment === undefined || config.environment === 'dev') { - this.azulURL = AzulURL.DEV; - } else { - this.azulURL = AzulURL.PROD; - } - } - - /** - * ### SALE: Transacción de venta - * Esta es la transacción principal utilizada para someter una autorización de una tarjeta - * por la venta de un bien o servicio. - * Las ventas realizadas con la transacción “Sale” son capturadas automáticamente para - * su liquidación, por lo que sólo pueden ser anuladas con una transacción de “Void” en - * un lapso de no más de 20 minutos luego de recibir respuesta de aprobación. - * - * Luego de transcurridos estos 20 minutos, la transacción será liquidada y se debe realizar - * una transacción de “Refund” o devolución para devolver los fondos a la tarjeta. - * - * @param saleRequest Datos de la transacción de venta - * @returns Respuesta de la transacción - */ - async sale(saleRequest: ProcessPaymentSchemaInput): Promise { - const response = await this.safeRequest({ - body: ProcessPaymentSchema.parse(saleRequest), - trxType: 'Sale' - }); - - return this.checkAzulResponse(response); - } - - /** - * #### Hold: Transacción para retención o reserva de fondos en la tarjeta - * Se puede separar la autorización del posteo o captura en dos mensajes distintos: - * 1. Hold: pre-autorización y reserva de los fondos en la tarjeta del cliente. - * 2. Post: se hace la captura o el “posteo” de la transacción. - * - * Al utilizar el Hold y Post se deben considerar los siguientes puntos: - * 1. Para evitar que el banco emisor elimine la pre-autorización, el Post debe ser - * realizado antes de 7 días de haber hecho el Hold. - * 2. Luego de realizado el Hold, el comercio no va a recibir la liquidación de los - * fondos hasta que someta el Post. - * 3. El Post solamente se puede hacer una vez por cada Hold realizado. Si se - * desea dividir el posteo en múltiples capturas, se debe usar la - * funcionalidad de Captura Múltiple o Split Shipment. - * 4. El Post puede ser igual, menor o mayor al monto original. El posteo por un - * monto mayor no debe sobrepasar el 15% del monto original. - * 5. El Void libera o cancela los fondos retenidos. - */ - async hold(saleRequest: ProcessPaymentSchemaInput): Promise { - const response = await this.safeRequest({ - body: ProcessPaymentSchema.parse(saleRequest), - trxType: 'Hold' - }); - - return this.checkAzulResponse(response); - } - - async void(azulOrderId: string): Promise { - const response = await this.safeRequest({ - body: { azulOrderId }, - trxType: 'Void' - }); - - return this.checkAzulResponse(response); - } - - async refund(refundRequest: z.input): Promise { - const response = await this.safeRequest({ - body: RefundRequestSchema.parse(refundRequest), - trxType: 'Refund' - }); - - return this.checkAzulResponse(response); - } - - async post(postRequest: z.input): Promise { - const response = await this.safeRequest({ - body: PostSchema.parse(postRequest) - }); - - return this.checkAzulResponse(response); - } - - async verifyPayment(customOrderId: string): Promise { - const response = await this.safeRequest({ - body: { customOrderId } - }); - - return this.checkAzulResponse(response); - } - - /** - * Este método permite extraer los detalles de una o varias transacciones - * vía Webservices, anteriormente procesadas de un rango de fechas - * previamente seleccionado. - */ - async search({ from, to }: { from: string; to: string }): Promise { - const response = await this.safeRequest({ - url: this.azulURL + '?SearchPayments', - body: { - dateFrom: from, - dateTo: to - } - }); - - return this.checkAzulResponse(response); - } - - private checkAzulResponse(json: any): AzulResponseWithOk { - return { - ...json, - ok: json.ResponseCode !== 'Error' && json.IsoCode === '00' - }; - } - - private async safeRequest({ - body, - trxType, - url - }: { - body: any; - trxType?: TrxType; - url?: string; - }) { - if (trxType) { - body.trxType = trxType; - } - - const fullBody = capitalizeKeys({ - channel: this.config.channel, - store: this.config.merchantId, - ...body - }); - - const response = await fetch(url || this.azulURL, { - method: 'POST', - headers: { - Auth1: this.config.auth1, - Auth2: this.config.auth2, - 'Content-Type': 'application/json' - }, - agent: new https.Agent(await this.getCertificates()), - body: JSON.stringify(capitalizeKeys(fullBody)) - }); - - return await response.json(); - } - - private async getCertificates(): Promise<{ cert: Buffer; key: Buffer }> { - if (this.certificate && this.certificateKey) { - return { - cert: this.certificate, - key: this.certificateKey - }; - } - - this.certificate = await fs.readFile(path.resolve(__dirname, this.config.certificatePath)); - this.certificateKey = await fs.readFile(path.resolve(__dirname, this.config.keyPath)); - - return { - cert: this.certificate, - key: this.certificateKey - }; - } -} - -export default AzulAPI; diff --git a/src/azul-api/api.ts b/src/azul-api/api.ts new file mode 100644 index 0000000..cc4a732 --- /dev/null +++ b/src/azul-api/api.ts @@ -0,0 +1,84 @@ +import { PostSchema, PostSchemaInput } from './schemas'; +import AzulRequester, { Config } from './request'; +import DataVault from './data-vault/data-vault'; +import ProcessPayment from './process-payment/process-payment '; +import { ProcessPaymentResponse } from './process-payment/schemas'; + +class AzulAPI { + private requester: AzulRequester; + + public valut: DataVault; + public payments: ProcessPayment; + + constructor(config: Config) { + this.requester = new AzulRequester(config); + this.valut = new DataVault(this.requester); + this.payments = new ProcessPayment(this.requester); + } + + /** + * ### Transacción para anular venta, post o hold + * Las transacciones de venta o post se pueden anular antes de los 20 minutos de haber + * recibido la respuesta de aprobación. + * Las transacciones de hold que no han sido posteadas no tienen límite de tiempo para + * anularse. + */ + async void(azulOrderId: string): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?ProcessVoid', + body: { + azulOrderId + } + }); + } + + /** + * ### Transacción para hacer captura o posteo del Hold + * El método “Post” permite capturar un “Hold” realizado previamente para su liquidación. + * El monto del “Post” puede ser igual o menor al monto del “Hold”. En caso de que el + * monto del Post sea menor al Hold, se envía un mensaje de reverso para liberar los + * fondos retenidos a la tarjeta. + */ + async post(input: PostSchemaInput): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?ProcessPost', + body: { + ...PostSchema.parse(input) + } + }); + } + + /** + * Método VerifyPayment + * Este método permite verificar la respuesta enviada por el webservice de una + * transacción anterior (procesada por el método ProccesPayment), identificada por el + * campo CustomOrderId. + * Si existe más de una transacción con este identificador este método devolverá los + * valores de la última transacción (más reciente) de ellas. + */ + async verifyPayment(customOrderId: string): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?VerifyPayment', + body: { + customOrderId + } + }); + } + + /** + * Este método permite extraer los detalles de una o varias transacciones + * vía Webservices, anteriormente procesadas de un rango de fechas + * previamente seleccionado. + */ + async search({ from, to }: { from: string; to: string }): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?SearchPayments', + body: { + dateFrom: from, + dateTo: to + } + }); + } +} + +export default AzulAPI; diff --git a/src/azul-api/data-vault/data-vault.ts b/src/azul-api/data-vault/data-vault.ts new file mode 100644 index 0000000..94c8dab --- /dev/null +++ b/src/azul-api/data-vault/data-vault.ts @@ -0,0 +1,46 @@ +import AzulRequester from '../request'; +import { Create, CreateInput, Delete, DeleteInput, DataVaultResponse } from './shemas'; + +enum DataVaultTransaction { + CREATE = 'CREATE', + DELETE = 'DELETE' +} + +class DataVault { + private readonly requester: AzulRequester; + + constructor(requester: AzulRequester) { + this.requester = requester; + } + + /** + * ### Create: Creación de Token con Bóveda de Datos (DataVault) + * Con esta transacción se solicita un token para ser utilizado en sustitución de la tarjeta, + * sin necesidad de realizar una venta. + */ + async create(input: CreateInput): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?ProcessDatavault', + body: { + ...Create.parse(input), + trxType: DataVaultTransaction.CREATE + } + }); + } + + /** + * ### Delete: Eliminación de Token de Bóveda de Datos (DataVault) + * Con esta transacción se solicita la eliminación de un token de la Bóveda de Datos. + */ + async delete(input: DeleteInput): Promise { + return await this.requester.safeRequest({ + url: this.requester.url + '?ProcessDatavault', + body: { + ...Delete.parse(input), + trxType: DataVaultTransaction.DELETE + } + }); + } +} + +export default DataVault; diff --git a/src/azul-api/data-vault/shemas.ts b/src/azul-api/data-vault/shemas.ts new file mode 100644 index 0000000..bbd39b1 --- /dev/null +++ b/src/azul-api/data-vault/shemas.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; +import { CVC, cardNumber, expiration } from '../schemas'; + +export const Create = z.object({ + /** + * Número de tarjeta a la cual se le ha de cargar la + * transacción. + * La longitud del campo se determina por la tarjeta, + * no se debe rellenar con ceros (0), espacios, ni + * caracteres especiales + */ + cardNumber, + /** + * Fecha expiración/vencimiento de la tarjeta + * Formato YYYYMM Ej.: 201502 + */ + expiration, + /** + * Código de seguridad de la tarjeta (CVV2 o CVC). + */ + CVC +}); + +export const Delete = z.object({ + /** + * Token generado por Azul. + */ + dataVaultToken: z.string() +}); + +export type DataVaultResponse = Partial<{ + /** + * Marca de la tarjeta. + */ + Brand: string; + /** + * Número de tarjeta enmascarada (ej. XXXXXX…XXXX). + */ + CardNumber: string; + /** + * Token generado por SDP. + */ + DataVaultToken: string; + /** + * Descripción del error. + * Valor sólo presente si la transacción produjo un error. En caso + * de no presentar error ese campo viaja en blanco + */ + ErrorDescription: string; + /** + * Fecha expiración del token. Formato YYYYMM + */ + Expiration: string; + /** + * Indica si el Token fue creado con CVV. + */ + HasCVV: boolean; + /** + * Código ISO-8583 recibido de respuesta. + * Cuando la transacción es exitosa se recibe el valor “00” + */ + ISOCode: string; + /** + * Mensaje de respuesta + */ + ReponseMessage: string; +}>; + +export type CreateInput = z.input; +export type DeleteInput = z.input; diff --git a/src/azul-api/process-payment/process-payment .ts b/src/azul-api/process-payment/process-payment .ts new file mode 100644 index 0000000..606c36d --- /dev/null +++ b/src/azul-api/process-payment/process-payment .ts @@ -0,0 +1,89 @@ +import AzulRequester from '../request'; +import { ProcessPaymentResponse, ProcessPaymentSchemaInput, ProcessPaymentSchema } from './schemas'; + +enum ProcessPaymentTransaction { + SALE = 'Sale', + HOLD = 'Hold', + REFUND = 'Refund' +} + +class ProcessPayment { + private readonly requester: AzulRequester; + + constructor(requester: AzulRequester) { + this.requester = requester; + } + + /** + * ### SALE: Transacción de venta + * Esta es la transacción principal utilizada para someter una autorización de una tarjeta + * por la venta de un bien o servicio. + * Las ventas realizadas con la transacción “Sale” son capturadas automáticamente para + * su liquidación, por lo que sólo pueden ser anuladas con una transacción de “Void” en + * un lapso de no más de 20 minutos luego de recibir respuesta de aprobación. + * + * Luego de transcurridos estos 20 minutos, la transacción será liquidada y se debe realizar + * una transacción de “Refund” o devolución para devolver los fondos a la tarjeta. + */ + async sale(input: ProcessPaymentSchemaInput): Promise { + return await this.requester.safeRequest({ + body: { + ...ProcessPaymentSchema.parse(input), + trxType: ProcessPaymentTransaction.SALE + } + }); + } + + /** + * ### Refund: Transacción Devolución + * La devolución o “Refund” permite reembolsarle los fondos a una tarjeta luego de haberse + * liquidado la transacción. + * + * Para poder realizar una devolución se debe haber procesado exitosamente una + * transacción de Venta o Post, y se deben utilizar los datos de la transacción original + * para enviar la devolución. + * 1. El monto a devolver puede ser el mismo o menor. + * 2. Se permite hacer una devolución, múltiples devoluciones o devoluciones + * + * parciales para cada transacción realizada. + * El límite de tiempo para procesar una devolución es de 6 meses transcurridos + * después de la transacción original. + */ + async refund(input: ProcessPaymentSchemaInput): Promise { + return await this.requester.safeRequest({ + body: { + ...ProcessPaymentSchema.parse(input), + trxType: ProcessPaymentTransaction.REFUND + } + }); + } + + /** + * ### Hold: Transacción para retención o reserva de fondos en la tarjeta + * Se puede separar la autorización del posteo o captura en dos mensajes distintos: + * 1. Hold: pre-autorización y reserva de los fondos en la tarjeta del cliente. + * 2. Post: se hace la captura o el “posteo” de la transacción. + * + * Al utilizar el Hold y Post se deben considerar los siguientes puntos: + * 1. Para evitar que el banco emisor elimine la pre-autorización, el Post debe ser + * realizado antes de 7 días de haber hecho el Hold. + * 2. Luego de realizado el Hold, el comercio no va a recibir la liquidación de los + * fondos hasta que someta el Post. + * 3. El Post solamente se puede hacer una vez por cada Hold realizado. Si se + * desea dividir el posteo en múltiples capturas, se debe usar la + * funcionalidad de Captura Múltiple o Split Shipment. + * 4. El Post puede ser igual, menor o mayor al monto original. El posteo por un + * monto mayor no debe sobrepasar el 15% del monto original. + * 5. El Void libera o cancela los fondos retenidos. + */ + async hold(input: ProcessPaymentSchemaInput): Promise { + return await this.requester.safeRequest({ + body: { + ...ProcessPaymentSchema.parse(input), + trxType: ProcessPaymentTransaction.HOLD + } + }); + } +} + +export default ProcessPayment; diff --git a/src/azul-api/process-payment/schemas.ts b/src/azul-api/process-payment/schemas.ts new file mode 100644 index 0000000..dd30a6e --- /dev/null +++ b/src/azul-api/process-payment/schemas.ts @@ -0,0 +1,213 @@ +import z from 'zod'; +import { + CVC, + ECommerceURL, + ITBIS, + acquirerRefData, + altMerchantName, + amount, + cardNumber, + channel, + customOrderId, + customerServicePhone, + dataVaultToken, + expiration, + forceNo3DS, + orderNumber, + posInputMode, + saveToDataVault +} from '../schemas'; + +export const ProcessPaymentSchema = z.object({ + /** + * Canal de pago. + * Este valor es proporcionado por AZUL, junto a los + * datos de acceso a cada ambiente + */ + channel, + /** + * Número de tarjeta a la cual se le ha de cargar la + * transacción. + * La longitud del campo se determina por la tarjeta, + * no se debe rellenar con ceros (0), espacios, ni + * caracteres especiales + */ + cardNumber, + /** + * Fecha expiración/vencimiento de la tarjeta + * Formato YYYYMM Ej.: 201502 + */ + expiration, + /** + * Código de seguridad de la tarjeta (CVV2 o CVC). + */ + CVC, + /** + * Modo de ingreso. + * Este valor es proporcionado por AZUL, junto a los + * datos de acceso a cada ambiente + */ + posInputMode, + /** + * Monto total de la transacción (Impuestos + * incluidos.) + * Se envía sin coma ni punto; los dos últimos dígitos + * representan los decimales. + * Ej. 1000 equivale a 10.00 + * Ej. 1748321 equivale a 17,483.21 + */ + amount, + /** + * Valor del ITBIS. Mismo formato que el campo + * Amount. El valor enviado en el campo de ITBIS no + * se carga como un monto adicional en la + * transacción. En el campo de Amount debe enviarse + * total a cargar incluyendo el ITBIS. + * Si la transacción o el negocio están exentos, se + * envía en cero colocando el valo “000”. + * + * Este valor deberá también ser incluido en el cálculo + * del hash. + */ + ITBIS, + /** + * Número de orden asociado a la transacción. Puede + * viajar nulo, pero siempre debe de estar presente. + */ + orderNumber, + /** + * Uso Interno AZUL + * Valor Fijo: 1 + */ + acquirerRefData, + /** + * Número de servicio para atención telefónica del + * establecimiento. Ej.: 8095442985 + */ + customerServicePhone, + /** + * Dirección web del afiliado. + */ + ECommerceURL, + /** + * Numero Identificador dada por el afiliado a la + * transacción. Este campo debe ser enviado si se + * desea implementar el método de VerifyPayment. + */ + customOrderId, + /** + * Campo que permite al Comercio colocar un + * nombre más descriptivo para que el + * tarjetahabiente pueda identificarle en su estado de + * cuenta. Se sugiere siempre colocar su nombre + * comercial adecuadamente a fin de evitar disputas. + * Si lo desea, puede agregar a su nombre algún + * indicador único de orden. + * El campo solo acepta un máximo de 25 caracteres. + * No utilizar los siguientes caracteres especiales: + * “ Genera un error en el request. + * \ Genera un error en el request. + * ' Este carácter no se muestra en el mensaje del + * emisor. + */ + altMerchantName, + /** + * Valor del token generado por SDP en caso de que + * se desee realizar una transacción con dicho token. + * Si se manda el valor de esto, no se deben enviar los + * valores de CardNumber, Expiration. El envío de + * CVC si es ecommerce puede ser o no mandatorio + * depende de lo conversado con Negocios SDP. Si es + * MOTO la transacción, no se debería enviar CVC. + */ + dataVaultToken, + /** + * Valores posibles 1 = si, 2 = no. Si se manda este + * valor en 1, SDP le devolverá el token generado en + * el campo DataVaultToken + */ + saveToDataVault, + /** + * Valores posibles 0 =no, 1 = Si. Si se envía el valor en + * ‘0’, la transacción se procesa con 3D Secure. Si se + * envía el valor en ‘1’ la transacción se procesa sin + * 3D Secure. + */ + forceNo3DS +}); + +export const RefundRequestSchema = z + .object({ + OriginalDate: z.string().length(8), + OriginalTrxTicketNr: z.string().length(4).optional() + }) + .merge(ProcessPaymentSchema); + +export type ProcessPaymentResponse = Partial<{ + /** + * Código de autorización generado por el centro autorizador para la + * transacción. + * Sólo presente si la transacción fue aprobada. ISOCode = ISO8583 y + * ResponseCode = 00 + */ + AuthorizationCode: string; + /** + * Numero Identificador dada por el afiliado a la transacción. Si no + * fue provisto en la transacción, este campo viaja en blanco + */ + CustomOrderId: string; + /** + * Fecha y hora de la transacción. + * Formato YYYYMMDDHHMMSS. + */ + DateTime: string; + /** + * Descripción del error. + * Valor sólo presente si la transacción produjo un error. En caso de + * no presentar error ese campo viaja en blanco + */ + ErrorDescription: string; + /** + * Código ISO-8583 recibido de respuesta. + * Nota: En la documentación de Azul, este campo se llama ISOCode + */ + IsoCode: string; + /** + * Número de lote en que se registró la transacción + */ + LotNumber: string; + /** + * Número de referencia + * (Reference referral number). + */ + RRN: string; + /** + * # de orden Azul. Puede ser usado en vez del RRN para generar una + * devolución. Importante dar prioridad a este valor sobre el RRN. + */ + AzulOrderId: string; + /** + * Código de respuesta. + * Puede contener uno de los siguientes valores: + * Iso8583 = la transacción fue procesada. Se debe revisar el + * campo ISOCode para ver la respuesta de la transacción Error = + * La transacción no fue procesada. + */ + ResponseCode: 'ISO8583' | 'Error'; + /** + * Mensaje de respuesta ISO-8583. + * Valor sólo presente si el ResponseCode = ISO8583 + */ + ReponseMessage: string; + /** + * Número del ticket correspondiente a la transacción + */ + Ticket: string; + /** + * Tarjeta usada para la transacción, enmascarada + * (XXXXXX******XXXX) + */ + CardNumber: string; +}>; + +export type ProcessPaymentSchemaInput = z.input; diff --git a/src/azul-api/request.ts b/src/azul-api/request.ts new file mode 100644 index 0000000..7de7e5a --- /dev/null +++ b/src/azul-api/request.ts @@ -0,0 +1,93 @@ +import path from 'path'; +import https from 'https'; +import fs from 'fs/promises'; +import fetch from 'node-fetch'; +import { capitalizeKeys } from '../utils'; + +enum AzulURL { + DEV = 'https://pruebas.azul.com.do/webservices/JSON/Default.aspx', + PROD = 'https://pagos.azul.com.do/webservices/JSON/Default.aspx' +} + +export type Config = { + auth1: string; + auth2: string; + merchantId: string; + certificatePath: string; + keyPath: string; + environment?: 'dev' | 'prod'; + channel?: string; +}; + +class AzulRequester { + public readonly url: string; + + private auth1: string; + private auth2: string; + private channel: string; + private merchantId: string; + private certificatePath: string; + private keyPath: string; + private certificate: Buffer | undefined; + private certificateKey: Buffer | undefined; + + constructor(config: Config) { + this.auth1 = config.auth1; + this.auth2 = config.auth2; + this.merchantId = config.merchantId; + this.certificatePath = config.certificatePath; + this.keyPath = config.keyPath; + + if (config.channel === undefined) { + this.channel = 'EC'; + } else { + this.channel = config.channel; + } + + if (config.environment === undefined || config.environment === 'dev') { + this.url = AzulURL.DEV; + } else { + this.url = AzulURL.PROD; + } + } + + async safeRequest({ body, url }: { body: any; url?: string }) { + const response = await fetch(url || this.url, { + method: 'POST', + headers: { + Auth1: this.auth1, + Auth2: this.auth2, + 'Content-Type': 'application/json' + }, + agent: new https.Agent(await this.getCertificates()), + body: JSON.stringify( + capitalizeKeys({ + channel: this.channel, + store: this.merchantId, + ...body + }) + ) + }); + + return await response.json(); + } + + private async getCertificates(): Promise<{ cert: Buffer; key: Buffer }> { + if (this.certificate && this.certificateKey) { + return { + cert: this.certificate, + key: this.certificateKey + }; + } + + this.certificate = await fs.readFile(path.resolve(__dirname, this.certificatePath)); + this.certificateKey = await fs.readFile(path.resolve(__dirname, this.keyPath)); + + return { + cert: this.certificate, + key: this.certificateKey + }; + } +} + +export default AzulRequester; diff --git a/src/azul-api/schemas.ts b/src/azul-api/schemas.ts new file mode 100644 index 0000000..5f0368a --- /dev/null +++ b/src/azul-api/schemas.ts @@ -0,0 +1,57 @@ +import z from 'zod'; + +export const channel = z.string().max(3).optional().default('EC'); +export const azulOrderId = z.string().max(8); +export const amount = z.number().int().positive(); +export const ITBIS = z.number().int().positive(); +export const cardNumber = z.string().max(19); +export const expiration = z.string().length(6); +export const CVC = z.string().length(3); +export const posInputMode = z.string().max(10).optional().default('E-Commerce'); +export const orderNumber = z.string().max(15).optional(); +export const acquirerRefData = z.enum(['0', '1']).optional(); +export const customerServicePhone = z.string().max(32).optional(); +export const ECommerceURL = z.string().max(32).optional(); +export const customOrderId = z.string().max(75).optional(); +export const altMerchantName = z.string().max(30).optional(); +export const dataVaultToken = z.string().max(100).optional(); +export const saveToDataVault = z.enum(['1', '2']).optional(); +export const forceNo3DS = z.enum(['0', '1']).optional(); + +export const PostSchema = z.object({ + /** + * Canal de pago. + * Este valor es proporcionado por AZUL, junto a los + * datos de acceso a cada ambiente + */ + channel, + /** + * # de orden Azul. Identificador único de la transacción + * tipo Hold previa. + */ + azulOrderId, + /** + * Monto total de la transacción (Impuestos + * incluidos.) + * Se envía sin coma ni punto; los dos últimos dígitos + * representan los decimales. + * Ej. 1000 equivale a 10.00 + * Ej. 1748321 equivale a 17,483.21 + */ + amount, + /** + * Valor del ITBIS. Mismo formato que el campo + * Amount. El valor enviado en el campo de ITBIS no + * se carga como un monto adicional en la + * transacción. En el campo de Amount debe enviarse + * total a cargar incluyendo el ITBIS. + * Si la transacción o el negocio están exentos, se + * envía en cero colocando el valo “000”. + * + * Este valor deberá también ser incluido en el cálculo + * del hash. + */ + ITBIS +}); + +export type PostSchemaInput = z.infer; diff --git a/src/examples/api-example.ts b/src/examples/api-example.ts index 6d9195c..d6ee244 100644 --- a/src/examples/api-example.ts +++ b/src/examples/api-example.ts @@ -1,6 +1,6 @@ -import express from "express"; -import AzulAPI from "../api"; -import "dotenv/config"; +import express from 'express'; +import AzulAPI from '../azul-api/api'; +import 'dotenv/config'; const app = express(); @@ -9,17 +9,17 @@ const azul = new AzulAPI({ auth2: process.env.AUTH2!, merchantId: process.env.MERCHANT_ID!, certificatePath: process.env.CERTIFICATE_PATH!, - keyPath: process.env.KEY_PATH!, + keyPath: process.env.KEY_PATH! }); -app.get("/buy-ticket", async (req, res) => { - const result = await azul.sale({ - cardNumber: "6011000990099818", - expiration: "202412", - CVC: "818", - customOrderId: "1234", +app.get('/buy-ticket', async (req, res) => { + const result = await azul.payments.sale({ + cardNumber: '6011000990099818', + expiration: '202412', + CVC: '818', + customOrderId: '1234', amount: 1000, - ITBIS: 100, + ITBIS: 100 }); res.send(result);