recurrente-js 1.0.1 → 1.0.3

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.
@@ -1,4 +1,4 @@
1
- import { ProductSubscription, CreateSubscriptionResponse, SubscriptionStatusResponse, CreateProductRequest, CreateProductResponse, GetProductResponse, GetAllProductsResponse, UpdateProductRequest } from '../types/globals';
1
+ import { ProductSubscription, CreateSubscriptionResponse, SubscriptionStatusResponse, CreateProductRequest, CreateProductResponse, GetProductResponse, GetAllProductsResponse, UpdateProductRequest, CreateCheckoutRequest, CreateCheckoutResponse } from '../types/globals';
2
2
  /**
3
3
  * Recurrente API utility for managing product subscriptions, cancellations, and product deletions.
4
4
  *
@@ -127,5 +127,16 @@ declare const recurrente: {
127
127
  * @see getSubscription
128
128
  */
129
129
  getSubscription: (subscriptionId: string) => Promise<SubscriptionStatusResponse>;
130
+ /**
131
+ * Creates a new checkout session.
132
+ *
133
+ * @function
134
+ * @memberof recurrente.checkouts
135
+ * @see createCheckout
136
+ * @param {CreateCheckoutRequest} checkoutData - The details for the checkout session.
137
+ * @returns {Promise<CreateCheckoutResponse>} A promise that resolves with the checkout ID and URL.
138
+ * @throws {ErrorResponse} Throws an error if the checkout creation fails.
139
+ */
140
+ createCheckout: (checkoutData: CreateCheckoutRequest) => Promise<CreateCheckoutResponse>;
130
141
  };
131
142
  export default recurrente;
@@ -15,6 +15,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const axios_1 = __importDefault(require("axios"));
16
16
  const axiosInstance_1 = __importDefault(require("../config/axiosInstance"));
17
17
  const conversion_1 = require("../utils/conversion");
18
+ // TODO: Implement namespaces
19
+ /**
20
+ * Creates a new checkout session.
21
+ *
22
+ * This function takes checkout data, including items (by product_id or details),
23
+ * converts it to snake_case, and sends it to the API to create a new checkout session.
24
+ * It returns the checkout ID and the URL for redirection.
25
+ *
26
+ * @param {CreateCheckoutRequest} checkoutData - The details for the checkout session.
27
+ * @returns {Promise<CreateCheckoutResponse>} The response containing the checkout ID and URL.
28
+ * @throws {ErrorResponse} Throws an error if the checkout creation fails.
29
+ */
30
+ const createCheckout = (checkoutData) => __awaiter(void 0, void 0, void 0, function* () {
31
+ try {
32
+ const checkoutDataInSnakeCase = (0, conversion_1.toSnakeCase)(checkoutData);
33
+ const response = yield axiosInstance_1.default.post('/checkouts/', checkoutDataInSnakeCase);
34
+ return (0, conversion_1.toCamelCase)(response.data);
35
+ }
36
+ catch (error) {
37
+ throw handleAxiosError(error);
38
+ }
39
+ });
18
40
  /**
19
41
  * Creates a new product with a one-time payment.
20
42
  *
@@ -351,5 +373,16 @@ const recurrente = {
351
373
  * @see getSubscription
352
374
  */
353
375
  getSubscription,
376
+ /**
377
+ * Creates a new checkout session.
378
+ *
379
+ * @function
380
+ * @memberof recurrente.checkouts
381
+ * @see createCheckout
382
+ * @param {CreateCheckoutRequest} checkoutData - The details for the checkout session.
383
+ * @returns {Promise<CreateCheckoutResponse>} A promise that resolves with the checkout ID and URL.
384
+ * @throws {ErrorResponse} Throws an error if the checkout creation fails.
385
+ */
386
+ createCheckout,
354
387
  };
355
388
  exports.default = recurrente;
@@ -15,13 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
15
15
  }) : function(o, v) {
16
16
  o["default"] = v;
17
17
  });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
25
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
37
  };
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { default as recurrente } from './api/recurrente';
2
+ export * from './types/globals';
package/dist/index.js CHANGED
@@ -1,4 +1,18 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
18
  };
@@ -6,3 +20,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
20
  exports.recurrente = void 0;
7
21
  var recurrente_1 = require("./api/recurrente");
8
22
  Object.defineProperty(exports, "recurrente", { enumerable: true, get: function () { return __importDefault(recurrente_1).default; } });
23
+ __exportStar(require("./types/globals"), exports); // <-- Add this line
@@ -1262,6 +1262,64 @@ export interface SubscriptionCancel {
1262
1262
  */
1263
1263
  customerName: string;
1264
1264
  }
1265
+ /**
1266
+ * Represents the payload for creating a checkout session.
1267
+ */
1268
+ export interface CreateCheckoutRequest {
1269
+ /**
1270
+ * An array of items to be included in the checkout.
1271
+ * Each item can be defined by its product_id or by its details.
1272
+ * @required
1273
+ */
1274
+ items: {
1275
+ /**
1276
+ * The ID of a pre-existing product. Use this to create a checkout for a master plan.
1277
+ * @optional
1278
+ */
1279
+ productId?: string;
1280
+ }[];
1281
+ /**
1282
+ * URL to redirect the user after a successful transaction.
1283
+ * @optional
1284
+ */
1285
+ successUrl?: string;
1286
+ /**
1287
+ * URL to redirect the user after canceling the transaction.
1288
+ * @optional
1289
+ */
1290
+ cancelUrl?: string;
1291
+ /**
1292
+ * The ID of the user to whom the checkout belongs.
1293
+ * Pre-populates user information fields.
1294
+ * @optional
1295
+ */
1296
+ userId?: string;
1297
+ /**
1298
+ * Optional metadata for additional information.
1299
+ * @optional
1300
+ */
1301
+ metadata?: Record<string, any>;
1302
+ /**
1303
+ * Optional expiration date for the checkout in ISO 8601 format.
1304
+ * @optional
1305
+ */
1306
+ expiresAt?: string;
1307
+ }
1308
+ /**
1309
+ * Represents the response after creating a checkout session.
1310
+ */
1311
+ export interface CreateCheckoutResponse {
1312
+ /**
1313
+ * The unique identifier for the checkout session (e.g., "ch_...").
1314
+ * @required
1315
+ */
1316
+ id: string;
1317
+ /**
1318
+ * The URL to which you should redirect your user to complete the payment.
1319
+ * @required
1320
+ */
1321
+ checkoutUrl: string;
1322
+ }
1265
1323
  /**
1266
1324
  * Represents the possible webhook events that can be triggered in the Recurrente system.
1267
1325
  * These events correspond to various stages of the payment or subscription lifecycle.
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "recurrente-js",
3
- "version": "1.0.1",
4
- "main": "./dist/index.js",
5
- "types": "./dist/globals.d.ts",
6
- "exports": {
7
- ".": "./dist/index.js",
8
- "./webhooks": "./dist/webhooks.js"
9
- },
10
- "files": [
11
- "dist/**/*",
12
- "src/**/*.d.ts"
13
- ],
14
- "scripts": {
15
- "test": "jest",
16
- "lint": "gts lint",
17
- "clean": "gts clean",
18
- "compile": "tsc",
19
- "fix": "gts fix",
20
- "prepare": "npm.cmd run compile",
21
- "pretest": "npm.cmd run compile",
22
- "posttest": "npm.cmd run lint",
23
- "dev": "nodemon -w *.ts -e ts -x ts-node --files -H -T ./src/index.ts"
24
- },
25
- "author": "Axel Aguilar",
26
- "license": "MIT",
27
- "description": "Recurrente API Wrapper",
28
- "devDependencies": {
29
- "@types/jest": "^29.5.12",
30
- "@types/node": "20.12.7",
31
- "gts": "^5.3.1",
32
- "jest": "^29.7.0",
33
- "nodemon": "^3.1.4",
34
- "ts-jest": "^29.2.5",
35
- "ts-node": "^10.9.2",
36
- "typescript": "^5.6.2"
37
- },
38
- "dependencies": {
39
- "axios": "^1.7.7",
40
- "dotenv": "^16.4.5",
41
- "svix": "^1.34.0"
42
- }
43
- }
1
+ {
2
+ "name": "recurrente-js",
3
+ "version": "1.0.3",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/globals.d.ts",
6
+ "exports": {
7
+ ".": "./dist/index.js",
8
+ "./webhooks": "./dist/webhooks.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*",
12
+ "src/**/*.d.ts"
13
+ ],
14
+ "scripts": {
15
+ "test": "jest",
16
+ "lint": "gts lint",
17
+ "clean": "gts clean",
18
+ "compile": "tsc",
19
+ "fix": "gts fix",
20
+ "prepare": "npm.cmd run compile",
21
+ "pretest": "npm.cmd run compile",
22
+ "posttest": "npm.cmd run lint",
23
+ "dev": "nodemon -w *.ts -e ts -x ts-node --files -H -T ./src/index.ts"
24
+ },
25
+ "author": "Axel Aguilar",
26
+ "license": "MIT",
27
+ "description": "Recurrente API Wrapper",
28
+ "devDependencies": {
29
+ "@types/jest": "^29.5.12",
30
+ "@types/node": "20.12.7",
31
+ "gts": "^5.3.1",
32
+ "jest": "^29.7.0",
33
+ "nodemon": "^3.1.4",
34
+ "ts-jest": "^29.2.5",
35
+ "ts-node": "^10.9.2",
36
+ "typescript": "^5.6.2"
37
+ },
38
+ "dependencies": {
39
+ "axios": "^1.7.7",
40
+ "dotenv": "^16.4.5",
41
+ "svix": "^1.34.0"
42
+ }
43
+ }
package/readme.md CHANGED
@@ -1,352 +1,352 @@
1
- # Recurrente.js - Wrapper para la API de Recurrente
2
-
3
- Este proyecto es un wrapper para la API de pagos recurrentes Recurrente, creado para facilitar la integración de pagos en proyectos. Con este paquete, puedes crear, actualizar, obtener y eliminar productos y suscripciones de manera eficiente utilizando solo Node.js y gestionando tus credenciales a través de un archivo .env.
4
-
5
- ## Beneficios del Proyecto
6
-
7
- - Totalmente Tipado: El paquete está desarrollado con TypeScript, lo que proporciona un tipado fuerte en todas las funciones y objetos. Esto te ayuda a detectar errores durante el desarrollo y a mantener un código limpio y seguro.
8
-
9
- - Promesas y Flujo Asíncrono: El wrapper sigue un enfoque basado en promesas, lo que permite una integración asíncrona y fluida con tu aplicación. Puedes gestionar productos y suscripciones utilizando async/await o .then()/.catch().
10
-
11
- - Configuración Simple: Usa un archivo .env para gestionar tus claves API y URL base, lo que simplifica la configuración.
12
- Integración Sencilla: Con unas pocas líneas de código, puedes interactuar con la API de Recurrente para manejar productos y suscripciones.
13
-
14
- - Modularidad y Flexibilidad: Funciones claras y bien organizadas que permiten crear productos, suscripciones y gestionar estas entidades de manera independiente.
15
-
16
- - Manejo de Errores Centralizado: Simplifica la depuración y el manejo de errores con un sistema centralizado que gestiona las respuestas incorrectas de la API.
17
-
18
- - Manejo de Webhooks Personalizado con Svix: El paquete incluye una implementación para manejar webhooks de Recurrente, que utiliza Svix para la entrega segura y fiable de webhooks. Esto te permite verificar las firmas de los webhooks, registrar manejadores personalizados para diferentes tipos de eventos y procesar eventos de webhook de manera eficiente y segura.
19
-
20
- ## Instalación
21
-
22
- Instala el paquete directamente desde npm o yarn.
23
-
24
- `npm install recurrente-js`
25
-
26
- ## Configuración de Variables de Entorno
27
-
28
- Crea un archivo .env en la raíz de tu proyecto y añade las siguientes claves:
29
-
30
- ```
31
- RECURRENTE_BASE_URL=https://app.recurrente.com
32
- RECURRENTE_PUBLIC_KEY=tu-public-key
33
- RECURRENTE_SECRET_KEY=tu-secret-key
34
- RECURRENTE_SVIX_SIGNING_SECRET=tu-svix-signing-key
35
- ```
36
-
37
- ## Ejemplos de Uso
38
-
39
- ### Crear un Producto
40
-
41
- El siguiente código muestra cómo crear un producto de pago único utilizando este wrapper de forma tipada y asíncrona.
42
-
43
- ```
44
- import { recurrente } from 'recurrente-js';
45
- import { CreateProductRequest } from 'recurrente-js/types/globals';
46
-
47
- // Datos de ejemplo para crear un producto
48
- const productData: CreateProductRequest = {
49
- name: 'Producto Ejemplo',
50
- pricesAttributes: [
51
- {
52
- currency: 'GTQ',
53
- chargeType: 'one_time',
54
- amountInCents: 1000,
55
- },
56
- ],
57
- successUrl: 'https://www.example.com/success',
58
- cancelUrl: 'https://www.example.com/cancel',
59
- phoneRequirement: 'none',
60
- addressRequirement: 'none',
61
- billingInfoRequirement: 'none',
62
- };
63
-
64
- // Función asíncrona para crear el producto
65
- async function createProduct() {
66
- try {
67
- const productResponse = await recurrente.createProduct(productData);
68
- console.log('Producto creado:', productResponse);
69
- } catch (error) {
70
- console.error('Error al crear el producto:', error);
71
- }
72
- }
73
-
74
- createProduct();
75
- ```
76
-
77
- ### Crear una Suscripción
78
-
79
- El siguiente ejemplo muestra cómo crear una suscripción recurrente para un producto de forma asíncrona:
80
-
81
- ```import { recurrente } from 'recurrente-js';
82
- import { ProductSubscription } from 'recurrente-js/types/globals';
83
-
84
- // Datos de ejemplo para crear una suscripción
85
- const subscriptionData: ProductSubscription = {
86
- product: {
87
- name: 'Producto Suscripción',
88
- pricesAttributes: [
89
- {
90
- currency: 'GTQ',
91
- chargeType: 'recurring',
92
- amountInCents: 500,
93
- billingIntervalCount: 1,
94
- billingInterval: 'month',
95
- },
96
- ],
97
- successUrl: 'https://www.example.com/success',
98
- cancelUrl: 'https://www.example.com/cancel',
99
- },
100
- };
101
-
102
- // Función asíncrona para crear la suscripción
103
- async function createSubscription() {
104
- try {
105
- const subscriptionResponse = await recurrente.createSubscription(subscriptionData);
106
- console.log('Suscripción creada:', subscriptionResponse);
107
- } catch (error) {
108
- console.error('Error al crear la suscripción:', error);
109
- }
110
- }
111
-
112
- createSubscription();
113
- ```
114
-
115
- ### Recuperar y Actualizar Productos
116
-
117
- Puedes obtener la lista de productos existentes y también actualizar los productos con la API:
118
-
119
- ```
120
- // Obtener todos los productos
121
- async function getAllProducts() {
122
- try {
123
- const products = await recurrente.getAllProducts();
124
- console.log('Productos recuperados:', products);
125
- } catch (error) {
126
- console.error('Error al recuperar productos:', error);
127
- }
128
- }
129
-
130
- // Actualizar un producto existente
131
- async function updateProduct(productId: string) {
132
- const updatedData = {
133
- name: 'Producto Actualizado',
134
- pricesAttributes: [
135
- {
136
- currency: 'GTQ',
137
- chargeType: 'one_time',
138
- amountInCents: 1500,
139
- },
140
- ],
141
- };
142
-
143
- try {
144
- const updatedProduct = await recurrente.updateProduct(productId, updatedData);
145
- console.log('Producto actualizado:', updatedProduct);
146
- } catch (error) {
147
- console.error('Error al actualizar el producto:', error);
148
- }
149
- }
150
-
151
- getAllProducts();
152
- ```
153
-
154
- ### Eliminar Productos y Cancelar Suscripciones
155
-
156
- Eliminar un producto o cancelar una suscripción es tan simple como llamar a las funciones adecuadas con el ID correspondiente.
157
-
158
- ```
159
- // Cancelar una suscripción
160
- async function cancelSubscription(subscriptionId: string) {
161
- try {
162
- const cancelResponse = await recurrente.cancelSubscription(subscriptionId);
163
- console.log('Suscripción cancelada:', cancelResponse.message);
164
- } catch (error) {
165
- console.error('Error al cancelar la suscripción:', error);
166
- }
167
- }
168
-
169
- // Eliminar un producto
170
- async function deleteProduct(productId: string) {
171
- try {
172
- const deleteResponse = await recurrente.deleteProduct(productId);
173
- console.log('Producto eliminado:', deleteResponse.message);
174
- } catch (error) {
175
- console.error('Error al eliminar el producto:', error);
176
- }
177
- }
178
-
179
- cancelSubscription('subscription-id');
180
- deleteProduct('product-id');
181
- ```
182
-
183
- ### Manejo de Webhooks
184
-
185
- Recurrente utiliza Svix para la entrega de webhooks, lo que proporciona una capa adicional de seguridad y fiabilidad en la comunicación. Svix ayuda a garantizar que los webhooks que recibes sean legítimos y no hayan sido manipulados durante el tránsito.
186
-
187
- #### Verificación de la Firma del Webhook
188
-
189
- La implementación asume un enfoque de un solo endpoint, donde todos los webhooks se manejan a través de una única ruta, y se utiliza un solo `SVIX_SIGNING_SECRET` global para verificar todos los webhooks entrantes.
190
-
191
- Para asegurar que los webhooks son auténticos, puedes utilizar la función verifySvixSignature para verificar la firma del
192
-
193
- ```
194
- import { verifySvixSignature, handleWebhookEvent } from 'recurrente-webhooks';
195
-
196
- // En tu controlador de webhooks
197
- app.post('/webhook', (req, res) => {
198
- try {
199
- const payload = JSON.stringify(req.body);
200
- const headers = req.headers;
201
-
202
- // Verificar la firma y obtener el evento
203
- const event = verifySvixSignature(payload, headers);
204
-
205
- // Procesar el evento
206
- handleWebhookEvent(event);
207
-
208
- res.status(200).send('Webhook procesado');
209
- } catch (error) {
210
- console.error('Error al procesar el webhook:', error);
211
- res.status(400).send('Firma de webhook inválida');
212
- }
213
- });
214
- ```
215
-
216
- #### Manejar Eventos de Webhook
217
-
218
- La función handleWebhookEvent se encarga de despachar el evento al manejador registrado correspondiente basado en el tipo de evento recibido.
219
-
220
- #### Registrar Manejadores de Eventos de Webhook
221
-
222
- ##### En Next.js (v14+)
223
-
224
- En Next.js 14, los handlers no se registran automáticamente en cada solicitud. Debes asegurarte de registrarlos en cada petición, una posible solución es registrarlos bajo una función al mismo nivel de tu ruta de API:
225
-
226
- ```
227
- // api/webhook/handlers.ts
228
- import { registerWebhookHandler } from 'recurrente-js/webhooks';
229
-
230
- let handlersRegistered = false;
231
-
232
- export function initializeWebhookHandlers() {
233
- if (handlersRegistered) return;
234
-
235
- registerWebhookHandler('payment_intent.failed', (event) => {
236
- console.log('Manejador personalizado de payment_intent.failed activado!');
237
- });
238
-
239
- registerWebhookHandler('payment_intent.succeeded', (event) => {
240
- console.log('Manejador personalizado de payment_intent.succeeded activado!');
241
- });
242
-
243
- handlersRegistered = true;
244
- }
245
- ```
246
-
247
- Para luego llamar esa función en tu ruta:
248
-
249
- ```
250
-
251
- // /api/webhook/route.ts
252
- import { NextResponse } from 'next/server';
253
- import { initializeWebhookHandlers } from '../../handlers';
254
- import { handleWebhookEvent, verifySvixSignature } from 'recurrente-js/webhooks';
255
-
256
- export async function POST(req: Request) {
257
- initializeWebhookHandlers(); // Asegura que los handlers estén registrados
258
-
259
- const body = await req.text();
260
- const headers = {
261
- 'svix-id': req.headers.get('svix-id'),
262
- 'svix-timestamp': req.headers.get('svix-timestamp'),
263
- 'svix-signature': req.headers.get('svix-signature'),
264
- };
265
-
266
- try {
267
- const event = verifySvixSignature(body, headers);
268
- handleWebhookEvent(event);
269
- return NextResponse.json({ status: 'Webhook procesado' });
270
- } catch (error) {
271
- return NextResponse.json({ error: 'Firma de webhook inválida' }, { status: 400 });
272
- }
273
- }
274
- ```
275
-
276
- ##### En una App Normal con Express
277
-
278
- En Express, puedes registrar los handlers globalmente al iniciar el servidor:
279
-
280
- ```
281
- // app.ts
282
- import express from 'express';
283
- import { verifySvixSignature, handleWebhookEvent, registerWebhookHandler } from 'recurrente-js/webhooks';
284
-
285
- const app = express();
286
- app.use(express.json());
287
-
288
- // Registra los handlers una vez al iniciar la app
289
- registerWebhookHandler('payment_intent.failed', (event) => {
290
- console.log('¡Handler personalizado de payment_intent.failed activado!');
291
- });
292
-
293
- registerWebhookHandler('payment_intent.succeeded', (event) => {
294
- console.log('¡Handler personalizado de payment_intent.succeeded activado!');
295
- });
296
-
297
- app.post('/webhook', (req, res) => {
298
- const payload = JSON.stringify(req.body);
299
- const headers = req.headers;
300
-
301
- try {
302
- const event = verifySvixSignature(payload, headers);
303
- handleWebhookEvent(event);
304
- res.status(200).send('Webhook procesado');
305
- } catch (error) {
306
- res.status(400).send('Firma de webhook inválida');
307
- }
308
- });
309
-
310
- app.listen(3000, () => console.log('Servidor corriendo en el puerto 3000'));
311
- ```
312
-
313
- #### Eventos Disponibles
314
-
315
- - payment_intent.succeeded
316
- - payment_intent.failed
317
- - subscription.create
318
- - subscription.past_due
319
- - subscription.paused
320
- - subscription.cancel
321
-
322
- Puedes registrar manejadores para estos eventos según tus necesidades utilizando `registerWebhookHandler`.
323
-
324
- ### Contribuir
325
-
326
- Si deseas contribuir al proyecto, sigue estas pautas y asegúrate de cumplir con los estándares y buenas prácticas definidos:
327
-
328
- 1. **Haz un fork del repositorio** y clona el proyecto localmente.
329
- 2. **Crea una nueva rama** para tu funcionalidad o corrección (`git checkout -b nueva-funcionalidad`).
330
- 3. **Realiza los cambios necesarios** en el código, asegurándote de seguir las guías de estilo y prácticas establecidas:
331
- - **Mensajes de Commit**: Asegúrate de que tus commits sigan la convención de [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) para mantener un historial claro y estructurado.
332
- - **Linting y Estilo**: Este proyecto utiliza la [Google TypeScript Style Guide](https://github.com/google/gts) y tiene integrado el sistema `gts` para asegurar un código consistente. Utiliza los siguientes comandos:
333
- 1. `npm run lint`: Verifica que el código siga las reglas de estilo.
334
- 2. `npm run fix`: Corrige automáticamente los errores de estilo.
335
- 3. `npm run clean`: Limpia archivos generados automáticamente.
336
- - **Nomenclatura**: Las interfaces, variables y funciones deben seguir la convención de `camelCase`. Para manejar casos en los que se requiera `snake_case`, el proyecto incluye utilidades para convertir entre estos formatos.
337
- - **Escribe tests**: Antes de integrar nuevas funcionalidades, asegúrate de crear los tests correspondientes. El proyecto utiliza `jest` para pruebas unitarias. Ejecuta los tests con `npm run test` y verifica que todo funcione correctamente antes de enviar los cambios.
338
- 4. **Haz commit de tus cambios** usando un mensaje descriptivo (`git commit -am 'feat: agrega nueva funcionalidad'`).
339
- 5. **Haz push a tu rama** (`git push origin nueva-funcionalidad`).
340
- 6. **Crea un pull request** desde tu repositorio forkeado hacia el repositorio original para revisión.
341
-
342
- ## Contacto
343
-
344
- Si tienes preguntas o sugerencias, no dudes en contactarme:
345
-
346
- Correo electrónico: herdezx@gmail.com
347
-
348
- Invitame un almuerzo: https://app.recurrente.com/s/pc-store/almuerzo
349
-
350
- ---
351
-
352
- Este proyecto fue creado para facilitar la integración de pagos recurrentes en una SaaS que me encuentro desarrollando. Si te ha sido útil, considera compartirlo o contribuir para mejorarlo. ¡Gracias!
1
+ # Recurrente.js - Wrapper para la API de Recurrente
2
+
3
+ Este proyecto es un wrapper para la API de pagos recurrentes Recurrente, creado para facilitar la integración de pagos en proyectos. Con este paquete, puedes crear, actualizar, obtener y eliminar productos y suscripciones de manera eficiente utilizando solo Node.js y gestionando tus credenciales a través de un archivo .env.
4
+
5
+ ## Beneficios del Proyecto
6
+
7
+ - Totalmente Tipado: El paquete está desarrollado con TypeScript, lo que proporciona un tipado fuerte en todas las funciones y objetos. Esto te ayuda a detectar errores durante el desarrollo y a mantener un código limpio y seguro.
8
+
9
+ - Promesas y Flujo Asíncrono: El wrapper sigue un enfoque basado en promesas, lo que permite una integración asíncrona y fluida con tu aplicación. Puedes gestionar productos y suscripciones utilizando async/await o .then()/.catch().
10
+
11
+ - Configuración Simple: Usa un archivo .env para gestionar tus claves API y URL base, lo que simplifica la configuración.
12
+ Integración Sencilla: Con unas pocas líneas de código, puedes interactuar con la API de Recurrente para manejar productos y suscripciones.
13
+
14
+ - Modularidad y Flexibilidad: Funciones claras y bien organizadas que permiten crear productos, suscripciones y gestionar estas entidades de manera independiente.
15
+
16
+ - Manejo de Errores Centralizado: Simplifica la depuración y el manejo de errores con un sistema centralizado que gestiona las respuestas incorrectas de la API.
17
+
18
+ - Manejo de Webhooks Personalizado con Svix: El paquete incluye una implementación para manejar webhooks de Recurrente, que utiliza Svix para la entrega segura y fiable de webhooks. Esto te permite verificar las firmas de los webhooks, registrar manejadores personalizados para diferentes tipos de eventos y procesar eventos de webhook de manera eficiente y segura.
19
+
20
+ ## Instalación
21
+
22
+ Instala el paquete directamente desde npm o yarn.
23
+
24
+ `npm install recurrente-js`
25
+
26
+ ## Configuración de Variables de Entorno
27
+
28
+ Crea un archivo .env en la raíz de tu proyecto y añade las siguientes claves:
29
+
30
+ ```
31
+ RECURRENTE_BASE_URL=https://app.recurrente.com
32
+ RECURRENTE_PUBLIC_KEY=tu-public-key
33
+ RECURRENTE_SECRET_KEY=tu-secret-key
34
+ RECURRENTE_SVIX_SIGNING_SECRET=tu-svix-signing-key
35
+ ```
36
+
37
+ ## Ejemplos de Uso
38
+
39
+ ### Crear un Producto
40
+
41
+ El siguiente código muestra cómo crear un producto de pago único utilizando este wrapper de forma tipada y asíncrona.
42
+
43
+ ```
44
+ import { recurrente } from 'recurrente-js';
45
+ import { CreateProductRequest } from 'recurrente-js/types/globals';
46
+
47
+ // Datos de ejemplo para crear un producto
48
+ const productData: CreateProductRequest = {
49
+ name: 'Producto Ejemplo',
50
+ pricesAttributes: [
51
+ {
52
+ currency: 'GTQ',
53
+ chargeType: 'one_time',
54
+ amountInCents: 1000,
55
+ },
56
+ ],
57
+ successUrl: 'https://www.example.com/success',
58
+ cancelUrl: 'https://www.example.com/cancel',
59
+ phoneRequirement: 'none',
60
+ addressRequirement: 'none',
61
+ billingInfoRequirement: 'none',
62
+ };
63
+
64
+ // Función asíncrona para crear el producto
65
+ async function createProduct() {
66
+ try {
67
+ const productResponse = await recurrente.createProduct(productData);
68
+ console.log('Producto creado:', productResponse);
69
+ } catch (error) {
70
+ console.error('Error al crear el producto:', error);
71
+ }
72
+ }
73
+
74
+ createProduct();
75
+ ```
76
+
77
+ ### Crear una Suscripción
78
+
79
+ El siguiente ejemplo muestra cómo crear una suscripción recurrente para un producto de forma asíncrona:
80
+
81
+ ```import { recurrente } from 'recurrente-js';
82
+ import { ProductSubscription } from 'recurrente-js/types/globals';
83
+
84
+ // Datos de ejemplo para crear una suscripción
85
+ const subscriptionData: ProductSubscription = {
86
+ product: {
87
+ name: 'Producto Suscripción',
88
+ pricesAttributes: [
89
+ {
90
+ currency: 'GTQ',
91
+ chargeType: 'recurring',
92
+ amountInCents: 500,
93
+ billingIntervalCount: 1,
94
+ billingInterval: 'month',
95
+ },
96
+ ],
97
+ successUrl: 'https://www.example.com/success',
98
+ cancelUrl: 'https://www.example.com/cancel',
99
+ },
100
+ };
101
+
102
+ // Función asíncrona para crear la suscripción
103
+ async function createSubscription() {
104
+ try {
105
+ const subscriptionResponse = await recurrente.createSubscription(subscriptionData);
106
+ console.log('Suscripción creada:', subscriptionResponse);
107
+ } catch (error) {
108
+ console.error('Error al crear la suscripción:', error);
109
+ }
110
+ }
111
+
112
+ createSubscription();
113
+ ```
114
+
115
+ ### Recuperar y Actualizar Productos
116
+
117
+ Puedes obtener la lista de productos existentes y también actualizar los productos con la API:
118
+
119
+ ```
120
+ // Obtener todos los productos
121
+ async function getAllProducts() {
122
+ try {
123
+ const products = await recurrente.getAllProducts();
124
+ console.log('Productos recuperados:', products);
125
+ } catch (error) {
126
+ console.error('Error al recuperar productos:', error);
127
+ }
128
+ }
129
+
130
+ // Actualizar un producto existente
131
+ async function updateProduct(productId: string) {
132
+ const updatedData = {
133
+ name: 'Producto Actualizado',
134
+ pricesAttributes: [
135
+ {
136
+ currency: 'GTQ',
137
+ chargeType: 'one_time',
138
+ amountInCents: 1500,
139
+ },
140
+ ],
141
+ };
142
+
143
+ try {
144
+ const updatedProduct = await recurrente.updateProduct(productId, updatedData);
145
+ console.log('Producto actualizado:', updatedProduct);
146
+ } catch (error) {
147
+ console.error('Error al actualizar el producto:', error);
148
+ }
149
+ }
150
+
151
+ getAllProducts();
152
+ ```
153
+
154
+ ### Eliminar Productos y Cancelar Suscripciones
155
+
156
+ Eliminar un producto o cancelar una suscripción es tan simple como llamar a las funciones adecuadas con el ID correspondiente.
157
+
158
+ ```
159
+ // Cancelar una suscripción
160
+ async function cancelSubscription(subscriptionId: string) {
161
+ try {
162
+ const cancelResponse = await recurrente.cancelSubscription(subscriptionId);
163
+ console.log('Suscripción cancelada:', cancelResponse.message);
164
+ } catch (error) {
165
+ console.error('Error al cancelar la suscripción:', error);
166
+ }
167
+ }
168
+
169
+ // Eliminar un producto
170
+ async function deleteProduct(productId: string) {
171
+ try {
172
+ const deleteResponse = await recurrente.deleteProduct(productId);
173
+ console.log('Producto eliminado:', deleteResponse.message);
174
+ } catch (error) {
175
+ console.error('Error al eliminar el producto:', error);
176
+ }
177
+ }
178
+
179
+ cancelSubscription('subscription-id');
180
+ deleteProduct('product-id');
181
+ ```
182
+
183
+ ### Manejo de Webhooks
184
+
185
+ Recurrente utiliza Svix para la entrega de webhooks, lo que proporciona una capa adicional de seguridad y fiabilidad en la comunicación. Svix ayuda a garantizar que los webhooks que recibes sean legítimos y no hayan sido manipulados durante el tránsito.
186
+
187
+ #### Verificación de la Firma del Webhook
188
+
189
+ La implementación asume un enfoque de un solo endpoint, donde todos los webhooks se manejan a través de una única ruta, y se utiliza un solo `SVIX_SIGNING_SECRET` global para verificar todos los webhooks entrantes.
190
+
191
+ Para asegurar que los webhooks son auténticos, puedes utilizar la función verifySvixSignature para verificar la firma del
192
+
193
+ ```
194
+ import { verifySvixSignature, handleWebhookEvent } from 'recurrente-webhooks';
195
+
196
+ // En tu controlador de webhooks
197
+ app.post('/webhook', (req, res) => {
198
+ try {
199
+ const payload = JSON.stringify(req.body);
200
+ const headers = req.headers;
201
+
202
+ // Verificar la firma y obtener el evento
203
+ const event = verifySvixSignature(payload, headers);
204
+
205
+ // Procesar el evento
206
+ handleWebhookEvent(event);
207
+
208
+ res.status(200).send('Webhook procesado');
209
+ } catch (error) {
210
+ console.error('Error al procesar el webhook:', error);
211
+ res.status(400).send('Firma de webhook inválida');
212
+ }
213
+ });
214
+ ```
215
+
216
+ #### Manejar Eventos de Webhook
217
+
218
+ La función handleWebhookEvent se encarga de despachar el evento al manejador registrado correspondiente basado en el tipo de evento recibido.
219
+
220
+ #### Registrar Manejadores de Eventos de Webhook
221
+
222
+ ##### En Next.js (v14+)
223
+
224
+ En Next.js 14, los handlers no se registran automáticamente en cada solicitud. Debes asegurarte de registrarlos en cada petición, una posible solución es registrarlos bajo una función al mismo nivel de tu ruta de API:
225
+
226
+ ```
227
+ // api/webhook/handlers.ts
228
+ import { registerWebhookHandler } from 'recurrente-js/webhooks';
229
+
230
+ let handlersRegistered = false;
231
+
232
+ export function initializeWebhookHandlers() {
233
+ if (handlersRegistered) return;
234
+
235
+ registerWebhookHandler('payment_intent.failed', (event) => {
236
+ console.log('Manejador personalizado de payment_intent.failed activado!');
237
+ });
238
+
239
+ registerWebhookHandler('payment_intent.succeeded', (event) => {
240
+ console.log('Manejador personalizado de payment_intent.succeeded activado!');
241
+ });
242
+
243
+ handlersRegistered = true;
244
+ }
245
+ ```
246
+
247
+ Para luego llamar esa función en tu ruta:
248
+
249
+ ```
250
+
251
+ // /api/webhook/route.ts
252
+ import { NextResponse } from 'next/server';
253
+ import { initializeWebhookHandlers } from '../../handlers';
254
+ import { handleWebhookEvent, verifySvixSignature } from 'recurrente-js/webhooks';
255
+
256
+ export async function POST(req: Request) {
257
+ initializeWebhookHandlers(); // Asegura que los handlers estén registrados
258
+
259
+ const body = await req.text();
260
+ const headers = {
261
+ 'svix-id': req.headers.get('svix-id'),
262
+ 'svix-timestamp': req.headers.get('svix-timestamp'),
263
+ 'svix-signature': req.headers.get('svix-signature'),
264
+ };
265
+
266
+ try {
267
+ const event = verifySvixSignature(body, headers);
268
+ handleWebhookEvent(event);
269
+ return NextResponse.json({ status: 'Webhook procesado' });
270
+ } catch (error) {
271
+ return NextResponse.json({ error: 'Firma de webhook inválida' }, { status: 400 });
272
+ }
273
+ }
274
+ ```
275
+
276
+ ##### En una App Normal con Express
277
+
278
+ En Express, puedes registrar los handlers globalmente al iniciar el servidor:
279
+
280
+ ```
281
+ // app.ts
282
+ import express from 'express';
283
+ import { verifySvixSignature, handleWebhookEvent, registerWebhookHandler } from 'recurrente-js/webhooks';
284
+
285
+ const app = express();
286
+ app.use(express.json());
287
+
288
+ // Registra los handlers una vez al iniciar la app
289
+ registerWebhookHandler('payment_intent.failed', (event) => {
290
+ console.log('¡Handler personalizado de payment_intent.failed activado!');
291
+ });
292
+
293
+ registerWebhookHandler('payment_intent.succeeded', (event) => {
294
+ console.log('¡Handler personalizado de payment_intent.succeeded activado!');
295
+ });
296
+
297
+ app.post('/webhook', (req, res) => {
298
+ const payload = JSON.stringify(req.body);
299
+ const headers = req.headers;
300
+
301
+ try {
302
+ const event = verifySvixSignature(payload, headers);
303
+ handleWebhookEvent(event);
304
+ res.status(200).send('Webhook procesado');
305
+ } catch (error) {
306
+ res.status(400).send('Firma de webhook inválida');
307
+ }
308
+ });
309
+
310
+ app.listen(3000, () => console.log('Servidor corriendo en el puerto 3000'));
311
+ ```
312
+
313
+ #### Eventos Disponibles
314
+
315
+ - payment_intent.succeeded
316
+ - payment_intent.failed
317
+ - subscription.create
318
+ - subscription.past_due
319
+ - subscription.paused
320
+ - subscription.cancel
321
+
322
+ Puedes registrar manejadores para estos eventos según tus necesidades utilizando `registerWebhookHandler`.
323
+
324
+ ### Contribuir
325
+
326
+ Si deseas contribuir al proyecto, sigue estas pautas y asegúrate de cumplir con los estándares y buenas prácticas definidos:
327
+
328
+ 1. **Haz un fork del repositorio** y clona el proyecto localmente.
329
+ 2. **Crea una nueva rama** para tu funcionalidad o corrección (`git checkout -b nueva-funcionalidad`).
330
+ 3. **Realiza los cambios necesarios** en el código, asegurándote de seguir las guías de estilo y prácticas establecidas:
331
+ - **Mensajes de Commit**: Asegúrate de que tus commits sigan la convención de [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) para mantener un historial claro y estructurado.
332
+ - **Linting y Estilo**: Este proyecto utiliza la [Google TypeScript Style Guide](https://github.com/google/gts) y tiene integrado el sistema `gts` para asegurar un código consistente. Utiliza los siguientes comandos:
333
+ 1. `npm run lint`: Verifica que el código siga las reglas de estilo.
334
+ 2. `npm run fix`: Corrige automáticamente los errores de estilo.
335
+ 3. `npm run clean`: Limpia archivos generados automáticamente.
336
+ - **Nomenclatura**: Las interfaces, variables y funciones deben seguir la convención de `camelCase`. Para manejar casos en los que se requiera `snake_case`, el proyecto incluye utilidades para convertir entre estos formatos.
337
+ - **Escribe tests**: Antes de integrar nuevas funcionalidades, asegúrate de crear los tests correspondientes. El proyecto utiliza `jest` para pruebas unitarias. Ejecuta los tests con `npm run test` y verifica que todo funcione correctamente antes de enviar los cambios.
338
+ 4. **Haz commit de tus cambios** usando un mensaje descriptivo (`git commit -am 'feat: agrega nueva funcionalidad'`).
339
+ 5. **Haz push a tu rama** (`git push origin nueva-funcionalidad`).
340
+ 6. **Crea un pull request** desde tu repositorio forkeado hacia el repositorio original para revisión.
341
+
342
+ ## Contacto
343
+
344
+ Si tienes preguntas o sugerencias, no dudes en contactarme:
345
+
346
+ Correo electrónico: herdezx@gmail.com
347
+
348
+ Invitame un almuerzo: https://app.recurrente.com/s/pc-store/almuerzo
349
+
350
+ ---
351
+
352
+ Este proyecto fue creado para facilitar la integración de pagos recurrentes en una SaaS que me encuentro desarrollando. Si te ha sido útil, considera compartirlo o contribuir para mejorarlo. ¡Gracias!