recurrente-js 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /*
3
+
4
+ Products and Subscriptions
5
+
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Recursively converts all keys in an object or array from camelCase to snake_case.
3
+ *
4
+ * This utility function is designed to handle objects, arrays, and other values.
5
+ * It maps camelCase keys to snake_case, which is commonly used in API requests.
6
+ *
7
+ * It processes nested objects and arrays recursively while preserving the original type structure.
8
+ *
9
+ * @param {T} obj - The object or array to convert.
10
+ * @returns {T} The converted object or array with snake_case keys, maintaining the original structure.
11
+ */
12
+ export declare const toSnakeCase: <T>(obj: T) => T;
13
+ /**
14
+ * Recursively converts all keys in an object or array from snake_case to camelCase.
15
+ *
16
+ * This utility function handles arrays and objects. It converts snake_case keys
17
+ * (e.g., "my_key") to camelCase keys (e.g., "myKey"), which is commonly used in
18
+ * JavaScript-based applications.
19
+ *
20
+ * It processes nested objects and arrays recursively while preserving the original type structure.
21
+ *
22
+ * @param {T} data - The object or array to convert.
23
+ * @returns {T} The converted object or array with camelCase keys, maintaining the original structure.
24
+ */
25
+ export declare const toCamelCase: <T>(data: T) => T;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toCamelCase = exports.toSnakeCase = void 0;
4
+ /**
5
+ * Recursively converts all keys in an object or array from camelCase to snake_case.
6
+ *
7
+ * This utility function is designed to handle objects, arrays, and other values.
8
+ * It maps camelCase keys to snake_case, which is commonly used in API requests.
9
+ *
10
+ * It processes nested objects and arrays recursively while preserving the original type structure.
11
+ *
12
+ * @param {T} obj - The object or array to convert.
13
+ * @returns {T} The converted object or array with snake_case keys, maintaining the original structure.
14
+ */
15
+ const toSnakeCase = (obj) => {
16
+ if (Array.isArray(obj)) {
17
+ // Recursively convert each element in the array
18
+ return obj.map(v => (0, exports.toSnakeCase)(v));
19
+ }
20
+ else if (obj !== null && typeof obj === 'object') {
21
+ // Convert each key in the object to snake_case and recursively process values
22
+ return Object.keys(obj).reduce((result, key) => {
23
+ const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
24
+ result[snakeKey] = (0, exports.toSnakeCase)(obj[key]);
25
+ return result;
26
+ }, {});
27
+ }
28
+ // Return non-object values as they are
29
+ return obj;
30
+ };
31
+ exports.toSnakeCase = toSnakeCase;
32
+ /**
33
+ * Recursively converts all keys in an object or array from snake_case to camelCase.
34
+ *
35
+ * This utility function handles arrays and objects. It converts snake_case keys
36
+ * (e.g., "my_key") to camelCase keys (e.g., "myKey"), which is commonly used in
37
+ * JavaScript-based applications.
38
+ *
39
+ * It processes nested objects and arrays recursively while preserving the original type structure.
40
+ *
41
+ * @param {T} data - The object or array to convert.
42
+ * @returns {T} The converted object or array with camelCase keys, maintaining the original structure.
43
+ */
44
+ const toCamelCase = (data) => {
45
+ if (Array.isArray(data)) {
46
+ return data.map(item => (0, exports.toCamelCase)(item));
47
+ }
48
+ else if (data !== null && typeof data === 'object') {
49
+ return Object.keys(data).reduce((acc, key) => {
50
+ const camelKey = key.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
51
+ acc[camelKey] = (0, exports.toCamelCase)(data[key]);
52
+ return acc;
53
+ }, {});
54
+ }
55
+ // Return non-object values as they are
56
+ return data;
57
+ };
58
+ exports.toCamelCase = toCamelCase;
@@ -0,0 +1 @@
1
+ export { handleWebhookEvent, registerWebhookHandler, verifySvixSignature, } from './api/recurrente-webhooks';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifySvixSignature = exports.registerWebhookHandler = exports.handleWebhookEvent = void 0;
4
+ var recurrente_webhooks_1 = require("./api/recurrente-webhooks");
5
+ Object.defineProperty(exports, "handleWebhookEvent", { enumerable: true, get: function () { return recurrente_webhooks_1.handleWebhookEvent; } });
6
+ Object.defineProperty(exports, "registerWebhookHandler", { enumerable: true, get: function () { return recurrente_webhooks_1.registerWebhookHandler; } });
7
+ Object.defineProperty(exports, "verifySvixSignature", { enumerable: true, get: function () { return recurrente_webhooks_1.verifySvixSignature; } });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "recurrente-js",
3
+ "version": "1.0.0",
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 ADDED
@@ -0,0 +1,353 @@
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
+ BASE_URL=https://app.recurrente.com
32
+ RECURRENTE_PUBLIC_KEY=tu-public-key
33
+ RECURRENTE_SECRET_KEY=tu-secret-key
34
+ 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
+ const signingSecret = process.env.SVIX_SIGNING_SECRET as string;
202
+
203
+ // Verificar la firma y obtener el evento
204
+ const event = verifySvixSignature(payload, headers, signingSecret);
205
+
206
+ // Procesar el evento
207
+ handleWebhookEvent(event);
208
+
209
+ res.status(200).send('Webhook procesado');
210
+ } catch (error) {
211
+ console.error('Error al procesar el webhook:', error);
212
+ res.status(400).send('Firma de webhook inválida');
213
+ }
214
+ });
215
+ ```
216
+
217
+ #### Manejar Eventos de Webhook
218
+
219
+ La función handleWebhookEvent se encarga de despachar el evento al manejador registrado correspondiente basado en el tipo de evento recibido.
220
+
221
+ #### Registrar Manejadores de Eventos de Webhook
222
+
223
+ ##### En Next.js (v14+)
224
+
225
+ 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:
226
+
227
+ ```
228
+ // api/webhook/handlers.ts
229
+ import { registerWebhookHandler } from 'recurrente-js/webhooks';
230
+
231
+ let handlersRegistered = false;
232
+
233
+ export function initializeWebhookHandlers() {
234
+ if (handlersRegistered) return;
235
+
236
+ registerWebhookHandler('payment_intent.failed', (event) => {
237
+ console.log('Manejador personalizado de payment_intent.failed activado!');
238
+ });
239
+
240
+ registerWebhookHandler('payment_intent.succeeded', (event) => {
241
+ console.log('Manejador personalizado de payment_intent.succeeded activado!');
242
+ });
243
+
244
+ handlersRegistered = true;
245
+ }
246
+ ```
247
+
248
+ Para luego llamar esa función en tu ruta:
249
+
250
+ ```
251
+
252
+ // /api/webhook/route.ts
253
+ import { NextResponse } from 'next/server';
254
+ import { initializeWebhookHandlers } from '../../handlers';
255
+ import { handleWebhookEvent, verifySvixSignature } from 'recurrente-js/webhooks';
256
+
257
+ export async function POST(req: Request) {
258
+ initializeWebhookHandlers(); // Asegura que los handlers estén registrados
259
+
260
+ const body = await req.text();
261
+ const headers = {
262
+ 'svix-id': req.headers.get('svix-id'),
263
+ 'svix-timestamp': req.headers.get('svix-timestamp'),
264
+ 'svix-signature': req.headers.get('svix-signature'),
265
+ };
266
+
267
+ try {
268
+ const event = verifySvixSignature(body, headers);
269
+ handleWebhookEvent(event);
270
+ return NextResponse.json({ status: 'Webhook procesado' });
271
+ } catch (error) {
272
+ return NextResponse.json({ error: 'Firma de webhook inválida' }, { status: 400 });
273
+ }
274
+ }
275
+ ```
276
+
277
+ ##### En una App Normal con Express
278
+
279
+ En Express, puedes registrar los handlers globalmente al iniciar el servidor:
280
+
281
+ ```
282
+ // app.ts
283
+ import express from 'express';
284
+ import { verifySvixSignature, handleWebhookEvent, registerWebhookHandler } from 'recurrente-js/webhooks';
285
+
286
+ const app = express();
287
+ app.use(express.json());
288
+
289
+ // Registra los handlers una vez al iniciar la app
290
+ registerWebhookHandler('payment_intent.failed', (event) => {
291
+ console.log('¡Handler personalizado de payment_intent.failed activado!');
292
+ });
293
+
294
+ registerWebhookHandler('payment_intent.succeeded', (event) => {
295
+ console.log('¡Handler personalizado de payment_intent.succeeded activado!');
296
+ });
297
+
298
+ app.post('/webhook', (req, res) => {
299
+ const payload = JSON.stringify(req.body);
300
+ const headers = req.headers;
301
+
302
+ try {
303
+ const event = verifySvixSignature(payload, headers, process.env.SVIX_SIGNING_SECRET!);
304
+ handleWebhookEvent(event);
305
+ res.status(200).send('Webhook procesado');
306
+ } catch (error) {
307
+ res.status(400).send('Firma de webhook inválida');
308
+ }
309
+ });
310
+
311
+ app.listen(3000, () => console.log('Servidor corriendo en el puerto 3000'));
312
+ ```
313
+
314
+ #### Eventos Disponibles
315
+
316
+ - payment_intent.succeeded
317
+ - payment_intent.failed
318
+ - subscription.create
319
+ - subscription.past_due
320
+ - subscription.paused
321
+ - subscription.cancel
322
+
323
+ Puedes registrar manejadores para estos eventos según tus necesidades utilizando `registerWebhookHandler`.
324
+
325
+ ### Contribuir
326
+
327
+ Si deseas contribuir al proyecto, sigue estas pautas y asegúrate de cumplir con los estándares y buenas prácticas definidos:
328
+
329
+ 1. **Haz un fork del repositorio** y clona el proyecto localmente.
330
+ 2. **Crea una nueva rama** para tu funcionalidad o corrección (`git checkout -b nueva-funcionalidad`).
331
+ 3. **Realiza los cambios necesarios** en el código, asegurándote de seguir las guías de estilo y prácticas establecidas:
332
+ - **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.
333
+ - **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:
334
+ 1. `npm run lint`: Verifica que el código siga las reglas de estilo.
335
+ 2. `npm run fix`: Corrige automáticamente los errores de estilo.
336
+ 3. `npm run clean`: Limpia archivos generados automáticamente.
337
+ - **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.
338
+ - **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.
339
+ 4. **Haz commit de tus cambios** usando un mensaje descriptivo (`git commit -am 'feat: agrega nueva funcionalidad'`).
340
+ 5. **Haz push a tu rama** (`git push origin nueva-funcionalidad`).
341
+ 6. **Crea un pull request** desde tu repositorio forkeado hacia el repositorio original para revisión.
342
+
343
+ ## Contacto
344
+
345
+ Si tienes preguntas o sugerencias, no dudes en contactarme:
346
+
347
+ Correo electrónico: herdezx@gmail.com
348
+
349
+ Invitame un almuerzo: https://app.recurrente.com/s/pc-store/almuerzo
350
+
351
+ ---
352
+
353
+ 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!