@qazuor/qzpay-core 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -16,6 +16,8 @@ pnpm add @qazuor/qzpay-core
16
16
  - **Metrics Service**: MRR, churn, and revenue calculations
17
17
  - **Health Service**: System health monitoring
18
18
  - **Logger**: Structured logging with customizable providers
19
+ - **Input Validation**: Zod-based validation for all create operations
20
+ - **Promo Code Validation**: Full validation with plan applicability and usage limits
19
21
  - **Utilities**: Date, money, validation, and hash helpers
20
22
  - **Runtime Agnostic**: Works in Node.js, Bun, Deno, and Edge runtimes (no direct `process.env` access)
21
23
 
@@ -78,8 +80,8 @@ await billing.subscriptions.cancel('sub_123');
78
80
  ### Payment Operations
79
81
 
80
82
  ```typescript
81
- // Create payment
82
- const payment = await billing.payments.create({
83
+ // Process a payment
84
+ const payment = await billing.payments.process({
83
85
  customerId: 'cus_123',
84
86
  amount: 2999, // $29.99 in cents
85
87
  currency: 'USD',
@@ -89,8 +91,26 @@ const payment = await billing.payments.create({
89
91
  // Retrieve payment
90
92
  const status = await billing.payments.get('pay_123');
91
93
 
94
+ // Get payments by customer
95
+ const payments = await billing.payments.getByCustomerId('cus_123');
96
+
97
+ // Record an external payment (already processed by provider)
98
+ const recorded = await billing.payments.record({
99
+ id: 'pay_external_123',
100
+ customerId: 'cus_123',
101
+ amount: 2999,
102
+ currency: 'USD',
103
+ status: 'succeeded',
104
+ providerPaymentId: 'pi_xxx',
105
+ provider: 'stripe'
106
+ });
107
+
92
108
  // Refund payment
93
- await billing.payments.refund('pay_123', { amount: 1500 });
109
+ await billing.payments.refund({
110
+ paymentId: 'pay_123',
111
+ amount: 1500, // Optional: partial refund
112
+ reason: 'requested_by_customer'
113
+ });
94
114
  ```
95
115
 
96
116
  ### Events
@@ -111,27 +131,44 @@ billing.on('payment.succeeded', (event) => {
111
131
 
112
132
  ### Metrics
113
133
 
114
- ```typescript
115
- import { createMetricsService } from '@qazuor/qzpay-core';
116
-
117
- const metrics = createMetricsService({ storage });
134
+ The `billing.metrics` service provides business intelligence and analytics:
118
135
 
119
- // Get MRR
120
- const mrr = await metrics.getMRR({ currency: 'USD' });
121
- console.log('Current MRR:', mrr.current);
136
+ ```typescript
137
+ // Get Monthly Recurring Revenue
138
+ const mrr = await billing.metrics.getMrr({ currency: 'USD' });
139
+ console.log('Current MRR:', mrr.currentMrr);
140
+ console.log('Previous MRR:', mrr.previousMrr);
141
+ console.log('Net New MRR:', mrr.netNewMrr);
142
+
143
+ // Get subscription metrics by status
144
+ const subMetrics = await billing.metrics.getSubscriptionMetrics();
145
+ console.log('Active:', subMetrics.active);
146
+ console.log('Trialing:', subMetrics.trialing);
147
+ console.log('Canceled:', subMetrics.canceled);
148
+ console.log('Past Due:', subMetrics.pastDue);
149
+
150
+ // Get revenue metrics for a period
151
+ const revenue = await billing.metrics.getRevenueMetrics({
152
+ startDate: new Date('2024-01-01'),
153
+ endDate: new Date('2024-12-31'),
154
+ currency: 'USD'
155
+ });
156
+ console.log('Total Revenue:', revenue.totalRevenue);
157
+ console.log('Refunds:', revenue.totalRefunds);
122
158
 
123
- // Get churn rate
124
- const churn = await metrics.getChurnRate({
125
- from: new Date('2024-01-01'),
126
- to: new Date('2024-12-31')
159
+ // Get churn metrics
160
+ const churn = await billing.metrics.getChurnMetrics({
161
+ startDate: new Date('2024-01-01'),
162
+ endDate: new Date('2024-12-31')
127
163
  });
164
+ console.log('Churn Rate:', churn.churnRate);
165
+ console.log('Churned MRR:', churn.churnedMrr);
128
166
 
129
- // Get revenue metrics
130
- const revenue = await metrics.getRevenueMetrics({
131
- from: new Date('2024-01-01'),
132
- to: new Date('2024-12-31'),
167
+ // Get all dashboard metrics aggregated
168
+ const dashboard = await billing.metrics.getDashboard({
133
169
  currency: 'USD'
134
170
  });
171
+ console.log('Dashboard:', dashboard);
135
172
  ```
136
173
 
137
174
  ### Health Checks
@@ -201,11 +238,266 @@ const billing = new QZPayBilling({
201
238
  | `billing.payments` | Payment processing |
202
239
  | `billing.invoices` | Invoice management |
203
240
  | `billing.plans` | Plan configuration |
204
- | `billing.prices` | Price management |
205
241
  | `billing.promoCodes` | Promotional codes |
206
242
  | `billing.entitlements` | Feature entitlements |
207
243
  | `billing.limits` | Usage limits |
208
244
  | `billing.addons` | Add-on management |
245
+ | `billing.paymentMethods` | Payment method management |
246
+ | `billing.metrics` | Business metrics and analytics |
247
+
248
+ ### Entitlements Service
249
+
250
+ Manage feature access based on subscriptions:
251
+
252
+ ```typescript
253
+ // Check if customer has an entitlement
254
+ const hasAccess = await billing.entitlements.check('cus_123', 'advanced_analytics');
255
+ if (hasAccess) {
256
+ // Show advanced analytics
257
+ }
258
+
259
+ // Get all entitlements for a customer
260
+ const entitlements = await billing.entitlements.getByCustomerId('cus_123');
261
+
262
+ // Grant an entitlement manually
263
+ await billing.entitlements.grant('cus_123', 'beta_features', 'manual', 'admin_grant');
264
+
265
+ // Revoke an entitlement
266
+ await billing.entitlements.revoke('cus_123', 'beta_features');
267
+ ```
268
+
269
+ ### Limits Service
270
+
271
+ Track and enforce usage limits:
272
+
273
+ ```typescript
274
+ // Check if customer is within limit
275
+ const result = await billing.limits.check('cus_123', 'api_calls');
276
+ console.log('Allowed:', result.allowed);
277
+ console.log('Current:', result.currentValue);
278
+ console.log('Max:', result.maxValue);
279
+ console.log('Remaining:', result.remaining);
280
+
281
+ // Get all limits for a customer
282
+ const limits = await billing.limits.getByCustomerId('cus_123');
283
+
284
+ // Increment usage
285
+ await billing.limits.increment('cus_123', 'api_calls', 1);
286
+
287
+ // Set a limit value
288
+ await billing.limits.set('cus_123', 'api_calls', 10000);
289
+
290
+ // Record usage (for audit/history)
291
+ await billing.limits.recordUsage('cus_123', 'api_calls', 5, 'increment');
292
+ ```
293
+
294
+ ### Add-ons Service
295
+
296
+ Manage subscription add-ons:
297
+
298
+ ```typescript
299
+ // Create an add-on definition
300
+ const addon = await billing.addons.create({
301
+ name: 'Extra Storage',
302
+ unitAmount: 500, // $5.00 in cents
303
+ currency: 'USD',
304
+ billingInterval: 'month',
305
+ compatiblePlanIds: ['plan_pro', 'plan_enterprise']
306
+ });
307
+
308
+ // Get add-ons compatible with a plan
309
+ const addons = await billing.addons.getByPlanId('plan_pro');
310
+
311
+ // Add an add-on to a subscription
312
+ const result = await billing.addons.addToSubscription({
313
+ subscriptionId: 'sub_123',
314
+ addOnId: 'addon_extra_storage',
315
+ quantity: 2
316
+ });
317
+ console.log('Proration amount:', result.prorationAmount);
318
+
319
+ // Get add-ons attached to a subscription
320
+ const subscriptionAddons = await billing.addons.getBySubscriptionId('sub_123');
321
+
322
+ // Update add-on quantity
323
+ await billing.addons.updateSubscriptionAddOn('sub_123', 'addon_extra_storage', {
324
+ quantity: 5
325
+ });
326
+
327
+ // Remove add-on from subscription
328
+ await billing.addons.removeFromSubscription('sub_123', 'addon_extra_storage');
329
+ ```
330
+
331
+ ### Payment Methods Service
332
+
333
+ Manage customer payment methods:
334
+
335
+ ```typescript
336
+ // Create a payment method
337
+ const paymentMethod = await billing.paymentMethods.create({
338
+ customerId: 'cus_123',
339
+ type: 'card',
340
+ providerPaymentMethodId: 'pm_xxx',
341
+ provider: 'stripe',
342
+ card: {
343
+ brand: 'visa',
344
+ last4: '4242',
345
+ expMonth: 12,
346
+ expYear: 2025
347
+ },
348
+ setAsDefault: true
349
+ });
350
+
351
+ // Get payment methods for a customer
352
+ const methods = await billing.paymentMethods.getByCustomerId('cus_123');
353
+
354
+ // Get the default payment method
355
+ const defaultMethod = await billing.paymentMethods.getDefault('cus_123');
356
+
357
+ // Set a payment method as default
358
+ await billing.paymentMethods.setDefault('cus_123', 'pm_456');
359
+
360
+ // Update a payment method
361
+ await billing.paymentMethods.update('pm_123', {
362
+ card: { expMonth: 1, expYear: 2026 }
363
+ });
364
+
365
+ // Delete a payment method
366
+ await billing.paymentMethods.delete('pm_123');
367
+ ```
368
+
369
+ ### Saved Card Service
370
+
371
+ Unified interface for saving and managing payment cards across providers.
372
+
373
+ ```typescript
374
+ import { createSavedCardService } from '@qazuor/qzpay-stripe'; // or '@qazuor/qzpay-mercadopago'
375
+
376
+ const cardService = createSavedCardService({
377
+ provider: 'stripe',
378
+ stripeSecretKey: 'sk_xxx',
379
+ getProviderCustomerId: async (customerId) => {
380
+ const customer = await db.customers.findById(customerId);
381
+ return customer.stripeCustomerId;
382
+ },
383
+ });
384
+
385
+ // Save a card
386
+ const card = await cardService.save({
387
+ customerId: 'local_cus_123',
388
+ paymentMethodId: 'pm_xxx', // From Stripe.js
389
+ setAsDefault: true,
390
+ });
391
+
392
+ // List all cards
393
+ const cards = await cardService.list('local_cus_123');
394
+
395
+ // Set card as default (Stripe only)
396
+ await cardService.setDefault('local_cus_123', 'pm_xxx');
397
+
398
+ // Remove a card
399
+ await cardService.remove('local_cus_123', 'pm_xxx');
400
+ ```
401
+
402
+ **Provider Differences:**
403
+
404
+ | Feature | Stripe | MercadoPago |
405
+ |---------|--------|-------------|
406
+ | Save card | `paymentMethodId` | `token` |
407
+ | List cards | ✅ | ✅ |
408
+ | Remove card | ✅ | ✅ |
409
+ | Set default | ✅ Native | ❌ Track in your DB |
410
+
411
+ **Note:** MercadoPago doesn't support default payment methods natively. Your application must track the default card ID in its database.
412
+
413
+ ### Subscription Lifecycle Service
414
+
415
+ Automates subscription renewals, trial conversions, and payment retries.
416
+
417
+ ```typescript
418
+ import { createSubscriptionLifecycle } from '@qazuor/qzpay-core';
419
+
420
+ const lifecycle = createSubscriptionLifecycle(billing, storage, {
421
+ gracePeriodDays: 7,
422
+ retryIntervals: [1, 3, 5], // Retry after 1, 3, and 5 days
423
+ trialConversionDays: 0, // Convert immediately when trial ends
424
+
425
+ // Process payment callback
426
+ processPayment: async (input) => {
427
+ const result = await stripe.paymentIntents.create({
428
+ amount: input.amount,
429
+ currency: input.currency,
430
+ customer: await getStripeCustomerId(input.customerId),
431
+ payment_method: input.paymentMethodId,
432
+ confirm: true,
433
+ off_session: true,
434
+ });
435
+ return {
436
+ success: result.status === 'succeeded',
437
+ paymentId: result.id,
438
+ error: result.last_payment_error?.message,
439
+ };
440
+ },
441
+
442
+ // Get default payment method callback
443
+ getDefaultPaymentMethod: async (customerId) => {
444
+ const pm = await db.paymentMethods.findDefault(customerId);
445
+ return pm ? {
446
+ id: pm.id,
447
+ providerPaymentMethodId: pm.stripePaymentMethodId
448
+ } : null;
449
+ },
450
+
451
+ // Optional: event callback for logging/notifications
452
+ onEvent: async (event) => {
453
+ console.log(`[${event.type}] Subscription: ${event.subscriptionId}`);
454
+
455
+ // Send notifications
456
+ if (event.type === 'subscription.renewal_failed') {
457
+ await sendEmail({
458
+ to: customer.email,
459
+ subject: 'Payment Failed',
460
+ body: 'Your subscription payment failed. Please update your payment method.'
461
+ });
462
+ }
463
+ },
464
+ });
465
+
466
+ // Run from a cron job
467
+ const results = await lifecycle.processAll();
468
+ console.log('Renewals:', results.renewals);
469
+ console.log('Trial conversions:', results.trialConversions);
470
+ console.log('Retries:', results.retries);
471
+ console.log('Cancellations:', results.cancellations);
472
+
473
+ // Or run individual operations
474
+ await lifecycle.processRenewals();
475
+ await lifecycle.processTrialConversions();
476
+ await lifecycle.processRetries();
477
+ await lifecycle.processCancellations();
478
+ ```
479
+
480
+ **Lifecycle Events:**
481
+
482
+ - `subscription.renewed` - Subscription successfully renewed
483
+ - `subscription.renewal_failed` - Renewal payment failed
484
+ - `subscription.trial_converted` - Trial converted to paid
485
+ - `subscription.trial_conversion_failed` - Trial conversion failed
486
+ - `subscription.entered_grace_period` - Entered grace period after failed payment
487
+ - `subscription.retry_scheduled` - Payment retry scheduled
488
+ - `subscription.retry_succeeded` - Retry payment succeeded
489
+ - `subscription.retry_failed` - All retries exhausted
490
+ - `subscription.canceled_nonpayment` - Canceled due to non-payment
491
+
492
+ **Cron Setup Example:**
493
+
494
+ ```typescript
495
+ // Run every hour
496
+ cron.schedule('0 * * * *', async () => {
497
+ const results = await lifecycle.processAll();
498
+ console.log(`Processed ${results.renewals.processed} renewals`);
499
+ });
500
+ ```
209
501
 
210
502
  ## Utility Functions
211
503
 
@@ -240,6 +532,73 @@ isValidAmount(100); // true
240
532
  isValidAmount(-50); // false
241
533
  ```
242
534
 
535
+ ### Input Validation
536
+
537
+ All create operations now include comprehensive Zod validation:
538
+
539
+ ```typescript
540
+ // Customer creation with validation
541
+ try {
542
+ const customer = await billing.customers.create({
543
+ email: 'invalid-email', // Will throw validation error
544
+ name: 'John Doe'
545
+ });
546
+ } catch (error) {
547
+ // error.code === 'VALIDATION_ERROR'
548
+ // error.details contains validation failure info
549
+ }
550
+
551
+ // Payment with validation
552
+ await billing.payments.process({
553
+ customerId: 'cus_123',
554
+ amount: -100, // Will throw validation error (negative amount)
555
+ currency: 'INVALID' // Will throw validation error (invalid currency)
556
+ });
557
+
558
+ // Invoice creation with validation
559
+ await billing.invoices.create({
560
+ customerId: 'cus_123',
561
+ items: [] // Will throw validation error (empty items array)
562
+ });
563
+ ```
564
+
565
+ ### Promo Code Validation
566
+
567
+ Promo codes are validated against multiple criteria:
568
+
569
+ ```typescript
570
+ // The system automatically validates:
571
+ // 1. Plan applicability
572
+ const promoCode = await billing.promoCodes.create({
573
+ code: 'SUMMER2024',
574
+ discountType: 'percentage',
575
+ discountValue: 20,
576
+ applicablePlans: ['plan_premium', 'plan_enterprise'] // Only works for these plans
577
+ });
578
+
579
+ // 2. Per-customer usage limits
580
+ await billing.promoCodes.validate({
581
+ code: 'SUMMER2024',
582
+ customerId: 'cus_123',
583
+ planId: 'plan_basic' // Will fail if not in applicablePlans
584
+ });
585
+
586
+ // 3. Date ranges
587
+ await billing.promoCodes.create({
588
+ code: 'NEWYEAR2024',
589
+ validFrom: new Date('2024-01-01'),
590
+ validTo: new Date('2024-01-31') // Only valid in January
591
+ });
592
+
593
+ // 4. Max uses and active status
594
+ await billing.promoCodes.create({
595
+ code: 'LIMITED',
596
+ maxUses: 100, // Can only be used 100 times total
597
+ maxUsesPerCustomer: 1, // Each customer can use it once
598
+ active: true // Must be active
599
+ });
600
+ ```
601
+
243
602
  ## Types
244
603
 
245
604
  ### Core Types