@ozura/elements 1.2.4-next.48 → 1.2.4-next.50
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/dist/frame/element-frame.html +1 -1
- package/dist/frame/element-frame.js +47 -3
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.js +55 -8
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/react/server/index.d.ts +224 -0
- package/dist/server/index.cjs.js +314 -2
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +313 -3
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/server/index.d.ts +224 -0
- package/dist/types/server/index.d.ts +224 -0
- package/package.json +1 -1
|
@@ -174,6 +174,93 @@ export interface CardSaleInput {
|
|
|
174
174
|
*/
|
|
175
175
|
transactionChannel?: 'cardPresent' | 'ecommerce' | 'moto' | 'recurring';
|
|
176
176
|
}
|
|
177
|
+
/** Billing cadence for a recurring subscription plan. */
|
|
178
|
+
export type RecurringInterval = 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
179
|
+
export interface CreateRecurringPlanInput {
|
|
180
|
+
/** From TokenResponse.token (frontend SDK). */
|
|
181
|
+
token: string;
|
|
182
|
+
/** From TokenResponse.cvcSession (frontend SDK). */
|
|
183
|
+
cvcSession: string;
|
|
184
|
+
/** Decimal string — the base recurring charge amount, e.g. "19.99". */
|
|
185
|
+
amount: string;
|
|
186
|
+
/** ISO 4217, e.g. "USD". Default: "USD". */
|
|
187
|
+
currency?: string;
|
|
188
|
+
/**
|
|
189
|
+
* Customer billing details. All address fields are required by the Pay API
|
|
190
|
+
* for recurring plans (stricter than one-time card sales).
|
|
191
|
+
*/
|
|
192
|
+
billing: BillingDetails;
|
|
193
|
+
/** Client IP address — always obtain server-side, never from the browser. */
|
|
194
|
+
clientIpAddress: string;
|
|
195
|
+
/** Display name for the subscription plan (shown on statements and receipts). */
|
|
196
|
+
planName: string;
|
|
197
|
+
/** Billing cadence. */
|
|
198
|
+
interval: RecurringInterval;
|
|
199
|
+
/** Optional plan description shown to customers (max 100 chars). */
|
|
200
|
+
planDescription?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Amount for the initial trial period cycles. Requires `initialCycles`.
|
|
203
|
+
* Pass `"0.00"` for a free trial.
|
|
204
|
+
*/
|
|
205
|
+
initialAmount?: string;
|
|
206
|
+
/** Number of cycles to charge `initialAmount` before switching to `amount`. */
|
|
207
|
+
initialCycles?: number;
|
|
208
|
+
/** One-time setup fee charged alongside the first cycle (additive). */
|
|
209
|
+
setupFee?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Multiplier for the interval period. Default: 1.
|
|
212
|
+
* e.g. `interval: "monthly"` + `intervalCount: 3` → billed every 3 months.
|
|
213
|
+
*/
|
|
214
|
+
intervalCount?: number;
|
|
215
|
+
/** ISO 8601 end date. Must be at least one full cycle after the enrollment date. */
|
|
216
|
+
endDate?: string;
|
|
217
|
+
/** Maximum number of billing cycles. Omit for indefinite. */
|
|
218
|
+
maxCycles?: number;
|
|
219
|
+
/** Payment retry attempts on failure. Default: 3. */
|
|
220
|
+
maxAttempts?: number;
|
|
221
|
+
/** Hours between retry attempts. Default: 24. */
|
|
222
|
+
retryIntervalHours?: number;
|
|
223
|
+
/** Your own internal subscription reference ID (max 50 chars). */
|
|
224
|
+
merchantRecurringReference?: string;
|
|
225
|
+
salesTaxExempt?: boolean;
|
|
226
|
+
/** Defaults to "0.00". */
|
|
227
|
+
surchargePercent?: string;
|
|
228
|
+
/** Processor to use. If omitted, the Pay API auto-selects. */
|
|
229
|
+
processor?: 'elavon' | 'nuvei' | 'worldpay';
|
|
230
|
+
/**
|
|
231
|
+
* Transaction channel. Default: `"ecommerce"`.
|
|
232
|
+
* Use `"moto"` for mail/telephone-order enrollment flows.
|
|
233
|
+
*
|
|
234
|
+
* Note: `transactionInitiationType` is always `"cit"` for recurring
|
|
235
|
+
* enrollment and is not configurable — the Pay API enforces this.
|
|
236
|
+
* Subsequent MIT charges are handled by Ozura's recurring queue worker.
|
|
237
|
+
*/
|
|
238
|
+
transactionChannel?: 'ecommerce' | 'moto';
|
|
239
|
+
}
|
|
240
|
+
export interface CreateRecurringPlanResponseData {
|
|
241
|
+
/** Ozura's internal plan identifier. Store this to manage the subscription. */
|
|
242
|
+
planId: string;
|
|
243
|
+
planName: string;
|
|
244
|
+
planDescription?: string;
|
|
245
|
+
merchantRecurringReference?: string;
|
|
246
|
+
/** Base recurring charge amount as a decimal string. */
|
|
247
|
+
amount: string;
|
|
248
|
+
currency: string;
|
|
249
|
+
/** Transaction ID of the initial enrollment charge (Pay API's `citTransactionId`). */
|
|
250
|
+
transactionId: string;
|
|
251
|
+
/** Actual amount charged on enrollment (includes setup fee, initial amount, tax, surcharge). */
|
|
252
|
+
transactionAmount: string;
|
|
253
|
+
transactionSurchargeAmount?: string;
|
|
254
|
+
transactionSalesTaxAmount?: string;
|
|
255
|
+
/** ISO 8601 timestamp of the next scheduled billing cycle. */
|
|
256
|
+
nextCycleAt: string;
|
|
257
|
+
endDate?: string;
|
|
258
|
+
cardLastFour?: string;
|
|
259
|
+
cardBrand?: string;
|
|
260
|
+
cardExpMonth?: string;
|
|
261
|
+
cardExpYear?: string;
|
|
262
|
+
transDate?: string;
|
|
263
|
+
}
|
|
177
264
|
export interface ListTransactionsInput {
|
|
178
265
|
/** Look up a single transaction by ID. When set, dateFrom/dateTo are not required. */
|
|
179
266
|
transactionId?: string;
|
|
@@ -214,6 +301,32 @@ export declare class Ozura {
|
|
|
214
301
|
* Rate limit: 100 requests/minute per merchant.
|
|
215
302
|
*/
|
|
216
303
|
cardSale(input: CardSaleInput): Promise<CardSaleResponseData>;
|
|
304
|
+
/**
|
|
305
|
+
* Create a recurring subscription plan and execute the enrollment charge.
|
|
306
|
+
*
|
|
307
|
+
* The customer's card is charged immediately for the first billing cycle
|
|
308
|
+
* (at `initialAmount` if set, otherwise `amount`) and a subscription record
|
|
309
|
+
* is created in Ozura's billing engine for all future cycles.
|
|
310
|
+
*
|
|
311
|
+
* `transactionInitiationType` is always `"cit"` for enrollment — the Pay API
|
|
312
|
+
* enforces this. Subsequent MIT charges are processed automatically by Ozura's
|
|
313
|
+
* recurring queue worker; merchants do not need to trigger them.
|
|
314
|
+
*
|
|
315
|
+
* Rate limit: 100 requests/minute per merchant.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* const plan = await ozura.createRecurringPlan({
|
|
319
|
+
* token: tokenResponse.token,
|
|
320
|
+
* cvcSession: tokenResponse.cvcSession,
|
|
321
|
+
* amount: '19.99',
|
|
322
|
+
* billing: tokenResponse.billing,
|
|
323
|
+
* clientIpAddress: req.ip,
|
|
324
|
+
* planName: 'Pro Monthly',
|
|
325
|
+
* interval: 'monthly',
|
|
326
|
+
* });
|
|
327
|
+
* console.log(plan.planId, plan.transactionId);
|
|
328
|
+
*/
|
|
329
|
+
createRecurringPlan(input: CreateRecurringPlanInput): Promise<CreateRecurringPlanResponseData>;
|
|
217
330
|
/**
|
|
218
331
|
* List transactions by date range with pagination.
|
|
219
332
|
*
|
|
@@ -478,5 +591,116 @@ export declare function createCardSaleMiddleware(ozura: Ozura, options: CardSale
|
|
|
478
591
|
body?: unknown;
|
|
479
592
|
headers?: unknown;
|
|
480
593
|
}, res: NodeLikeResponse) => Promise<void>;
|
|
594
|
+
/**
|
|
595
|
+
* Server-side recurring plan configuration resolved per-request.
|
|
596
|
+
* Source all plan details from your own database — never trust client-supplied values.
|
|
597
|
+
*/
|
|
598
|
+
export interface RecurringPlanConfig {
|
|
599
|
+
/** Display name for the subscription. */
|
|
600
|
+
planName: string;
|
|
601
|
+
/** Billing cadence. */
|
|
602
|
+
interval: RecurringInterval;
|
|
603
|
+
planDescription?: string;
|
|
604
|
+
initialAmount?: string;
|
|
605
|
+
initialCycles?: number;
|
|
606
|
+
setupFee?: string;
|
|
607
|
+
intervalCount?: number;
|
|
608
|
+
endDate?: string;
|
|
609
|
+
maxCycles?: number;
|
|
610
|
+
maxAttempts?: number;
|
|
611
|
+
retryIntervalHours?: number;
|
|
612
|
+
merchantRecurringReference?: string;
|
|
613
|
+
salesTaxExempt?: boolean;
|
|
614
|
+
surchargePercent?: string;
|
|
615
|
+
processor?: 'elavon' | 'nuvei' | 'worldpay';
|
|
616
|
+
/**
|
|
617
|
+
* Transaction channel. Default: `"ecommerce"`.
|
|
618
|
+
* Use `"moto"` for mail/telephone-order enrollment flows.
|
|
619
|
+
* Source from your database via `getPlanConfig` — do not read from the request body.
|
|
620
|
+
*/
|
|
621
|
+
transactionChannel?: 'ecommerce' | 'moto';
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Options for {@link createRecurringPlanHandler} and {@link createRecurringPlanMiddleware}.
|
|
625
|
+
*
|
|
626
|
+
* Both `getAmount` and `getPlanConfig` must source their values from your own
|
|
627
|
+
* records — never trust the request body for plan price or configuration.
|
|
628
|
+
*/
|
|
629
|
+
export interface RecurringPlanHandlerOptions {
|
|
630
|
+
/**
|
|
631
|
+
* Return the base recurring charge amount as a decimal string (e.g. `"19.99"`).
|
|
632
|
+
* Source from your database; never trust the value the client sends.
|
|
633
|
+
*/
|
|
634
|
+
getAmount: (body: Record<string, unknown>) => string | Promise<string>;
|
|
635
|
+
/**
|
|
636
|
+
* Return the recurring plan configuration for this subscription.
|
|
637
|
+
* At minimum you must return `planName` and `interval`.
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* getPlanConfig: async (body) => {
|
|
641
|
+
* const plan = await db.plans.findById(body.planId as string);
|
|
642
|
+
* return { planName: plan.name, interval: plan.interval };
|
|
643
|
+
* }
|
|
644
|
+
*/
|
|
645
|
+
getPlanConfig: (body: Record<string, unknown>) => RecurringPlanConfig | Promise<RecurringPlanConfig>;
|
|
646
|
+
/** Return the ISO 4217 currency code. Default: `"USD"`. */
|
|
647
|
+
getCurrency?: (body: Record<string, unknown>) => string | Promise<string>;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Creates a ready-to-use Fetch API route handler for recurring subscription enrollment.
|
|
651
|
+
*
|
|
652
|
+
* Drop-in for Next.js App Router, Cloudflare Workers, Vercel Edge, and any runtime
|
|
653
|
+
* built on the standard Web API `Request` / `Response`.
|
|
654
|
+
*
|
|
655
|
+
* The handler reads `{ token, cvcSession, billing }` from the JSON request body,
|
|
656
|
+
* resolves the amount and plan configuration via your `options` callbacks (which
|
|
657
|
+
* must source data from your own database — never from the request body), calls
|
|
658
|
+
* `ozura.createRecurringPlan()`, and returns
|
|
659
|
+
* `{ planId, transactionId, transactionAmount, nextCycleAt }` on success.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* // app/api/subscribe/route.ts (Next.js App Router)
|
|
663
|
+
* import { Ozura, createRecurringPlanHandler } from '@ozura/elements/server';
|
|
664
|
+
*
|
|
665
|
+
* const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
666
|
+
*
|
|
667
|
+
* export const POST = createRecurringPlanHandler(ozura, {
|
|
668
|
+
* getAmount: async (body) => {
|
|
669
|
+
* const plan = await db.plans.findById(body.planId as string);
|
|
670
|
+
* return plan.price;
|
|
671
|
+
* },
|
|
672
|
+
* getPlanConfig: async (body) => {
|
|
673
|
+
* const plan = await db.plans.findById(body.planId as string);
|
|
674
|
+
* return { planName: plan.name, interval: plan.interval };
|
|
675
|
+
* },
|
|
676
|
+
* });
|
|
677
|
+
*/
|
|
678
|
+
export declare function createRecurringPlanHandler(ozura: Ozura, options: RecurringPlanHandlerOptions): (req: Request) => Promise<Response>;
|
|
679
|
+
/**
|
|
680
|
+
* Creates a ready-to-use Express / Connect middleware for recurring subscription enrollment.
|
|
681
|
+
*
|
|
682
|
+
* Requires `express.json()` (or equivalent body-parser) to be registered before
|
|
683
|
+
* this middleware so `req.body` is available.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* // Express
|
|
687
|
+
* import express from 'express';
|
|
688
|
+
* import { Ozura, createRecurringPlanMiddleware } from '@ozura/elements/server';
|
|
689
|
+
*
|
|
690
|
+
* const app = express();
|
|
691
|
+
* const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
692
|
+
*
|
|
693
|
+
* app.use(express.json());
|
|
694
|
+
* app.post('/api/subscribe', createRecurringPlanMiddleware(ozura, {
|
|
695
|
+
* getAmount: async (body) => db.plans.findById(body.planId).then(p => p.price),
|
|
696
|
+
* getPlanConfig: async (body) => db.plans.findById(body.planId).then(p => ({
|
|
697
|
+
* planName: p.name, interval: p.interval,
|
|
698
|
+
* })),
|
|
699
|
+
* }));
|
|
700
|
+
*/
|
|
701
|
+
export declare function createRecurringPlanMiddleware(ozura: Ozura, options: RecurringPlanHandlerOptions): (req: {
|
|
702
|
+
body?: unknown;
|
|
703
|
+
headers?: unknown;
|
|
704
|
+
}, res: NodeLikeResponse) => Promise<void>;
|
|
481
705
|
export type { BillingDetails, CardSaleResponseData, TransactionQueryPagination, TransactionType, TransactionBase, CardTransactionData, AchTransactionData, CryptoTransactionData, TransactionData, } from '../types';
|
|
482
706
|
export { normalizeCardSaleError } from '../sdk/errors';
|
package/dist/server/index.cjs.js
CHANGED
|
@@ -427,6 +427,113 @@ class Ozura {
|
|
|
427
427
|
// a second charge. No idempotency key is in play, so one attempt only.
|
|
428
428
|
return this.post('/api/v1/cardSale', body, true, 0);
|
|
429
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Create a recurring subscription plan and execute the enrollment charge.
|
|
432
|
+
*
|
|
433
|
+
* The customer's card is charged immediately for the first billing cycle
|
|
434
|
+
* (at `initialAmount` if set, otherwise `amount`) and a subscription record
|
|
435
|
+
* is created in Ozura's billing engine for all future cycles.
|
|
436
|
+
*
|
|
437
|
+
* `transactionInitiationType` is always `"cit"` for enrollment — the Pay API
|
|
438
|
+
* enforces this. Subsequent MIT charges are processed automatically by Ozura's
|
|
439
|
+
* recurring queue worker; merchants do not need to trigger them.
|
|
440
|
+
*
|
|
441
|
+
* Rate limit: 100 requests/minute per merchant.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* const plan = await ozura.createRecurringPlan({
|
|
445
|
+
* token: tokenResponse.token,
|
|
446
|
+
* cvcSession: tokenResponse.cvcSession,
|
|
447
|
+
* amount: '19.99',
|
|
448
|
+
* billing: tokenResponse.billing,
|
|
449
|
+
* clientIpAddress: req.ip,
|
|
450
|
+
* planName: 'Pro Monthly',
|
|
451
|
+
* interval: 'monthly',
|
|
452
|
+
* });
|
|
453
|
+
* console.log(plan.planId, plan.transactionId);
|
|
454
|
+
*/
|
|
455
|
+
async createRecurringPlan(input) {
|
|
456
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
457
|
+
if (!this.merchantId)
|
|
458
|
+
throw new OzuraError('merchantId is required for createRecurringPlan(). Add it to the Ozura constructor config.', 0);
|
|
459
|
+
if (!this.apiKey)
|
|
460
|
+
throw new OzuraError('apiKey is required for createRecurringPlan(). Add it to the Ozura constructor config.', 0);
|
|
461
|
+
this.log('createRecurringPlan', { merchantId: this.merchantId, planName: input.planName, interval: input.interval, amount: input.amount });
|
|
462
|
+
const billing = input.billing;
|
|
463
|
+
// Build the Pay API body with required fields.
|
|
464
|
+
// transactionInitiationType is hardcoded to 'cit' — the Pay API rejects any
|
|
465
|
+
// other value for recurring plan creation (400 with explicit error message).
|
|
466
|
+
const body = {
|
|
467
|
+
merchantId: this.merchantId,
|
|
468
|
+
amount: input.amount,
|
|
469
|
+
currency: input.currency || 'USD',
|
|
470
|
+
transactionInitiationType: 'cit',
|
|
471
|
+
transactionChannel: (_a = input.transactionChannel) !== null && _a !== void 0 ? _a : 'ecommerce',
|
|
472
|
+
ozuraVaultToken: input.token,
|
|
473
|
+
ozuraCvcSession: input.cvcSession,
|
|
474
|
+
planName: input.planName,
|
|
475
|
+
interval: input.interval,
|
|
476
|
+
billingFirstName: billing.firstName,
|
|
477
|
+
billingLastName: billing.lastName,
|
|
478
|
+
clientIpAddress: input.clientIpAddress,
|
|
479
|
+
salesTaxExempt: (_b = input.salesTaxExempt) !== null && _b !== void 0 ? _b : false,
|
|
480
|
+
surchargePercent: (_c = input.surchargePercent) !== null && _c !== void 0 ? _c : '0.00',
|
|
481
|
+
};
|
|
482
|
+
// Billing — recurring plans require all address fields (stricter than cardSale).
|
|
483
|
+
if (billing.email)
|
|
484
|
+
body.billingEmail = billing.email;
|
|
485
|
+
if (billing.phone)
|
|
486
|
+
body.billingPhone = billing.phone;
|
|
487
|
+
if ((_d = billing.address) === null || _d === void 0 ? void 0 : _d.line1)
|
|
488
|
+
body.billingAddress1 = billing.address.line1;
|
|
489
|
+
if ((_e = billing.address) === null || _e === void 0 ? void 0 : _e.line2)
|
|
490
|
+
body.billingAddress2 = billing.address.line2;
|
|
491
|
+
if ((_f = billing.address) === null || _f === void 0 ? void 0 : _f.city)
|
|
492
|
+
body.billingCity = billing.address.city;
|
|
493
|
+
if ((_g = billing.address) === null || _g === void 0 ? void 0 : _g.state)
|
|
494
|
+
body.billingState = billing.address.state;
|
|
495
|
+
if ((_h = billing.address) === null || _h === void 0 ? void 0 : _h.zip)
|
|
496
|
+
body.billingZipcode = billing.address.zip;
|
|
497
|
+
body.billingCountry = ((_j = billing.address) === null || _j === void 0 ? void 0 : _j.country) || 'US';
|
|
498
|
+
// Optional recurring-specific fields — omit rather than send empty/null.
|
|
499
|
+
if (input.processor)
|
|
500
|
+
body.processor = input.processor;
|
|
501
|
+
if (input.planDescription)
|
|
502
|
+
body.planDescription = input.planDescription;
|
|
503
|
+
if (input.initialAmount != null)
|
|
504
|
+
body.initialAmount = input.initialAmount;
|
|
505
|
+
if (input.initialCycles != null)
|
|
506
|
+
body.initialCycles = input.initialCycles;
|
|
507
|
+
if (input.setupFee != null)
|
|
508
|
+
body.setupFee = input.setupFee;
|
|
509
|
+
if (input.intervalCount != null)
|
|
510
|
+
body.intervalCount = input.intervalCount;
|
|
511
|
+
if (input.endDate)
|
|
512
|
+
body.endDate = input.endDate;
|
|
513
|
+
if (input.maxCycles != null)
|
|
514
|
+
body.maxCycles = input.maxCycles;
|
|
515
|
+
if (input.maxAttempts != null)
|
|
516
|
+
body.maxAttempts = input.maxAttempts;
|
|
517
|
+
if (input.retryIntervalHours != null)
|
|
518
|
+
body.retryIntervalHours = input.retryIntervalHours;
|
|
519
|
+
if (input.merchantRecurringReference)
|
|
520
|
+
body.merchantRecurringReference = input.merchantRecurringReference;
|
|
521
|
+
// maxRetries: 0 — same reasoning as cardSale. The Pay API creates a subscription
|
|
522
|
+
// record on every successful request; retrying a 5xx would risk a duplicate plan.
|
|
523
|
+
const raw = await this.post('/api/v1/recurring/plans', body, true, 0);
|
|
524
|
+
// Guard against a malformed success response — a missing planId means the
|
|
525
|
+
// subscription cannot be managed, and a missing citTransactionId means the
|
|
526
|
+
// enrollment charge cannot be traced. Both are required by the Pay API contract.
|
|
527
|
+
if (!raw.planId) {
|
|
528
|
+
throw new OzuraError('createRecurringPlan: API response missing planId', 0, JSON.stringify(raw));
|
|
529
|
+
}
|
|
530
|
+
if (!raw.citTransactionId) {
|
|
531
|
+
throw new OzuraError('createRecurringPlan: API response missing citTransactionId', 0, JSON.stringify(raw));
|
|
532
|
+
}
|
|
533
|
+
// Map Pay API's 'citTransactionId' / 'citTransactionAmount' to the SDK-consistent
|
|
534
|
+
// 'transactionId' / 'transactionAmount' names that mirror cardSale's response shape.
|
|
535
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ planId: String(raw.planId), planName: String((_k = raw.planName) !== null && _k !== void 0 ? _k : input.planName), amount: String((_l = raw.amount) !== null && _l !== void 0 ? _l : input.amount), currency: String((_o = (_m = raw.currency) !== null && _m !== void 0 ? _m : input.currency) !== null && _o !== void 0 ? _o : 'USD'), transactionId: String(raw.citTransactionId), transactionAmount: String((_p = raw.citTransactionAmount) !== null && _p !== void 0 ? _p : ''), nextCycleAt: String((_q = raw.nextCycleAt) !== null && _q !== void 0 ? _q : '') }, (raw.planDescription != null ? { planDescription: String(raw.planDescription) } : {})), (raw.merchantRecurringReference != null ? { merchantRecurringReference: String(raw.merchantRecurringReference) } : {})), (raw.citTransactionSurchargeAmount != null ? { transactionSurchargeAmount: String(raw.citTransactionSurchargeAmount) } : {})), (raw.citTransactionSalesTaxAmount != null ? { transactionSalesTaxAmount: String(raw.citTransactionSalesTaxAmount) } : {})), (raw.endDate != null ? { endDate: String(raw.endDate) } : {})), (raw.cardLastFour != null ? { cardLastFour: String(raw.cardLastFour) } : {})), (raw.cardBrand != null ? { cardBrand: String(raw.cardBrand) } : {})), (raw.cardExpMonth != null ? { cardExpMonth: String(raw.cardExpMonth) } : {})), (raw.cardExpYear != null ? { cardExpYear: String(raw.cardExpYear) } : {})), (raw.transDate != null ? { transDate: String(raw.transDate) } : {}));
|
|
536
|
+
}
|
|
430
537
|
/**
|
|
431
538
|
* List transactions by date range with pagination.
|
|
432
539
|
*
|
|
@@ -733,13 +840,17 @@ async function readJsonBody(req) {
|
|
|
733
840
|
if (raw.length > MAX_BODY_BYTES) {
|
|
734
841
|
return { ok: false, response: Response.json({ error: 'Request body too large' }, { status: 413 }) };
|
|
735
842
|
}
|
|
843
|
+
let parsed;
|
|
736
844
|
try {
|
|
737
|
-
|
|
738
|
-
return { ok: true, body };
|
|
845
|
+
parsed = JSON.parse(raw);
|
|
739
846
|
}
|
|
740
847
|
catch (_b) {
|
|
741
848
|
return { ok: false, response: Response.json({ error: 'Invalid JSON body' }, { status: 400 }) };
|
|
742
849
|
}
|
|
850
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
851
|
+
return { ok: false, response: Response.json({ error: 'Request body must be a JSON object' }, { status: 400 }) };
|
|
852
|
+
}
|
|
853
|
+
return { ok: true, body: parsed };
|
|
743
854
|
}
|
|
744
855
|
/**
|
|
745
856
|
* Creates a ready-to-use Fetch API route handler for payment session creation.
|
|
@@ -1188,6 +1299,205 @@ function createCardSaleMiddleware(ozura, options) {
|
|
|
1188
1299
|
res.json(outcome.data);
|
|
1189
1300
|
};
|
|
1190
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Validates the token/cvcSession/billing fields from a parsed request body.
|
|
1304
|
+
*/
|
|
1305
|
+
function parseRecurringPlanBody(body) {
|
|
1306
|
+
const { token, cvcSession, billing } = body;
|
|
1307
|
+
if (typeof token !== 'string' || !token) {
|
|
1308
|
+
return { ok: false, error: 'token is required' };
|
|
1309
|
+
}
|
|
1310
|
+
if (typeof cvcSession !== 'string' || !cvcSession) {
|
|
1311
|
+
return { ok: false, error: 'cvcSession is required' };
|
|
1312
|
+
}
|
|
1313
|
+
if (!billing || typeof billing.firstName !== 'string' || typeof billing.lastName !== 'string') {
|
|
1314
|
+
return { ok: false, error: 'billing with firstName and lastName is required' };
|
|
1315
|
+
}
|
|
1316
|
+
const billingValidation = validateBilling(billing);
|
|
1317
|
+
if (!billingValidation.valid) {
|
|
1318
|
+
return { ok: false, error: `Invalid billing details: ${billingValidation.errors.join('; ')}` };
|
|
1319
|
+
}
|
|
1320
|
+
return { ok: true, token, cvcSession, billing: billingValidation.normalized };
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Resolves amount, currency, and plan config then calls ozura.createRecurringPlan().
|
|
1324
|
+
* Both the handler and middleware factories delegate to this shared implementation.
|
|
1325
|
+
*/
|
|
1326
|
+
async function executeRecurringPlan(ozura, options, token, cvcSession, billing, rawBody, clientIpAddress) {
|
|
1327
|
+
let amount;
|
|
1328
|
+
try {
|
|
1329
|
+
amount = await options.getAmount(rawBody);
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
return { ok: false, error: err instanceof Error ? err.message : 'Failed to resolve amount', status: 500 };
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof amount !== 'string' || !amount.trim()) {
|
|
1335
|
+
return { ok: false, error: 'getAmount must return a non-empty decimal string', status: 500 };
|
|
1336
|
+
}
|
|
1337
|
+
if (!/^\d+(\.\d{1,2})?$/.test(amount.trim())) {
|
|
1338
|
+
return {
|
|
1339
|
+
ok: false,
|
|
1340
|
+
error: `getAmount returned an invalid amount: "${amount}". Expected a positive decimal string, e.g. "19.99".`,
|
|
1341
|
+
status: 500,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
amount = amount.trim();
|
|
1345
|
+
// Reject a zero base amount — a $0 recurring plan would charge nothing on every
|
|
1346
|
+
// future cycle. Free trials should use initialAmount:"0.00" + initialCycles, not
|
|
1347
|
+
// a zero base amount. A zero here almost always means a misconfigured getAmount().
|
|
1348
|
+
if (parseFloat(amount) === 0) {
|
|
1349
|
+
return {
|
|
1350
|
+
ok: false,
|
|
1351
|
+
error: 'getAmount returned "0" for a recurring plan base amount. For free trials use initialAmount + initialCycles in getPlanConfig.',
|
|
1352
|
+
status: 500,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
let currency = 'USD';
|
|
1356
|
+
if (options.getCurrency) {
|
|
1357
|
+
try {
|
|
1358
|
+
currency = await options.getCurrency(rawBody);
|
|
1359
|
+
}
|
|
1360
|
+
catch (err) {
|
|
1361
|
+
return { ok: false, error: err instanceof Error ? err.message : 'Failed to resolve currency', status: 500 };
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
let planConfig;
|
|
1365
|
+
try {
|
|
1366
|
+
planConfig = await options.getPlanConfig(rawBody);
|
|
1367
|
+
}
|
|
1368
|
+
catch (err) {
|
|
1369
|
+
return { ok: false, error: err instanceof Error ? err.message : 'Failed to resolve plan config', status: 500 };
|
|
1370
|
+
}
|
|
1371
|
+
if (!(planConfig === null || planConfig === void 0 ? void 0 : planConfig.planName) || !(planConfig === null || planConfig === void 0 ? void 0 : planConfig.interval)) {
|
|
1372
|
+
return { ok: false, error: 'getPlanConfig must return an object with planName and interval', status: 500 };
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
const result = await ozura.createRecurringPlan(Object.assign({ token, cvcSession, amount, currency, billing, clientIpAddress }, planConfig));
|
|
1376
|
+
return {
|
|
1377
|
+
ok: true,
|
|
1378
|
+
data: Object.assign(Object.assign({ planId: result.planId, transactionId: result.transactionId, transactionAmount: result.transactionAmount, nextCycleAt: result.nextCycleAt }, (result.cardLastFour ? { cardLastFour: result.cardLastFour } : {})), (result.cardBrand ? { cardBrand: result.cardBrand } : {})),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
catch (err) {
|
|
1382
|
+
if (err instanceof OzuraError) {
|
|
1383
|
+
if (err.statusCode === 429) {
|
|
1384
|
+
return { ok: false, error: normalizeCardSaleError(err.message), status: 429, retryAfter: err.retryAfter };
|
|
1385
|
+
}
|
|
1386
|
+
const status = err.statusCode >= 400 && err.statusCode < 600 ? err.statusCode : 502;
|
|
1387
|
+
return { ok: false, error: normalizeCardSaleError(err.message), status };
|
|
1388
|
+
}
|
|
1389
|
+
return { ok: false, error: 'Recurring plan creation failed', status: 500 };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Creates a ready-to-use Fetch API route handler for recurring subscription enrollment.
|
|
1394
|
+
*
|
|
1395
|
+
* Drop-in for Next.js App Router, Cloudflare Workers, Vercel Edge, and any runtime
|
|
1396
|
+
* built on the standard Web API `Request` / `Response`.
|
|
1397
|
+
*
|
|
1398
|
+
* The handler reads `{ token, cvcSession, billing }` from the JSON request body,
|
|
1399
|
+
* resolves the amount and plan configuration via your `options` callbacks (which
|
|
1400
|
+
* must source data from your own database — never from the request body), calls
|
|
1401
|
+
* `ozura.createRecurringPlan()`, and returns
|
|
1402
|
+
* `{ planId, transactionId, transactionAmount, nextCycleAt }` on success.
|
|
1403
|
+
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* // app/api/subscribe/route.ts (Next.js App Router)
|
|
1406
|
+
* import { Ozura, createRecurringPlanHandler } from '@ozura/elements/server';
|
|
1407
|
+
*
|
|
1408
|
+
* const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
1409
|
+
*
|
|
1410
|
+
* export const POST = createRecurringPlanHandler(ozura, {
|
|
1411
|
+
* getAmount: async (body) => {
|
|
1412
|
+
* const plan = await db.plans.findById(body.planId as string);
|
|
1413
|
+
* return plan.price;
|
|
1414
|
+
* },
|
|
1415
|
+
* getPlanConfig: async (body) => {
|
|
1416
|
+
* const plan = await db.plans.findById(body.planId as string);
|
|
1417
|
+
* return { planName: plan.name, interval: plan.interval };
|
|
1418
|
+
* },
|
|
1419
|
+
* });
|
|
1420
|
+
*/
|
|
1421
|
+
function createRecurringPlanHandler(ozura, options) {
|
|
1422
|
+
return async (req) => {
|
|
1423
|
+
var _a;
|
|
1424
|
+
if (req.method !== 'POST') {
|
|
1425
|
+
return Response.json({ error: 'Method Not Allowed' }, { status: 405 });
|
|
1426
|
+
}
|
|
1427
|
+
const contentType = (_a = req.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
|
|
1428
|
+
if (!contentType.includes('application/json')) {
|
|
1429
|
+
return Response.json({ error: 'Content-Type must be application/json' }, { status: 415 });
|
|
1430
|
+
}
|
|
1431
|
+
const bodyResult = await readJsonBody(req);
|
|
1432
|
+
if (!bodyResult.ok)
|
|
1433
|
+
return bodyResult.response;
|
|
1434
|
+
const body = bodyResult.body;
|
|
1435
|
+
const parsed = parseRecurringPlanBody(body);
|
|
1436
|
+
if (!parsed.ok) {
|
|
1437
|
+
return Response.json({ error: parsed.error }, { status: 400 });
|
|
1438
|
+
}
|
|
1439
|
+
const outcome = await executeRecurringPlan(ozura, options, parsed.token, parsed.cvcSession, parsed.billing, body, getClientIp(req));
|
|
1440
|
+
if (!outcome.ok) {
|
|
1441
|
+
const headers = outcome.retryAfter
|
|
1442
|
+
? { 'Retry-After': String(outcome.retryAfter) }
|
|
1443
|
+
: {};
|
|
1444
|
+
return Response.json({ error: outcome.error }, Object.assign({ status: outcome.status }, (Object.keys(headers).length > 0 ? { headers } : {})));
|
|
1445
|
+
}
|
|
1446
|
+
return Response.json(outcome.data);
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Creates a ready-to-use Express / Connect middleware for recurring subscription enrollment.
|
|
1451
|
+
*
|
|
1452
|
+
* Requires `express.json()` (or equivalent body-parser) to be registered before
|
|
1453
|
+
* this middleware so `req.body` is available.
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* // Express
|
|
1457
|
+
* import express from 'express';
|
|
1458
|
+
* import { Ozura, createRecurringPlanMiddleware } from '@ozura/elements/server';
|
|
1459
|
+
*
|
|
1460
|
+
* const app = express();
|
|
1461
|
+
* const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
|
|
1462
|
+
*
|
|
1463
|
+
* app.use(express.json());
|
|
1464
|
+
* app.post('/api/subscribe', createRecurringPlanMiddleware(ozura, {
|
|
1465
|
+
* getAmount: async (body) => db.plans.findById(body.planId).then(p => p.price),
|
|
1466
|
+
* getPlanConfig: async (body) => db.plans.findById(body.planId).then(p => ({
|
|
1467
|
+
* planName: p.name, interval: p.interval,
|
|
1468
|
+
* })),
|
|
1469
|
+
* }));
|
|
1470
|
+
*/
|
|
1471
|
+
function createRecurringPlanMiddleware(ozura, options) {
|
|
1472
|
+
return async (req, res) => {
|
|
1473
|
+
var _a, _b, _c;
|
|
1474
|
+
const method = req.method;
|
|
1475
|
+
if (method && method !== 'POST') {
|
|
1476
|
+
res.status(405).json({ error: 'Method Not Allowed' });
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const headers = req.headers;
|
|
1480
|
+
const ct = (_a = (typeof (headers === null || headers === void 0 ? void 0 : headers['content-type']) === 'string' ? headers['content-type'] : '')) !== null && _a !== void 0 ? _a : '';
|
|
1481
|
+
if (!ct.includes('application/json')) {
|
|
1482
|
+
res.status(415).json({ error: 'Content-Type must be application/json' });
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const body = ((_b = req.body) !== null && _b !== void 0 ? _b : {});
|
|
1486
|
+
const parsed = parseRecurringPlanBody(body);
|
|
1487
|
+
if (!parsed.ok) {
|
|
1488
|
+
res.status(400).json({ error: parsed.error });
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const outcome = await executeRecurringPlan(ozura, options, parsed.token, parsed.cvcSession, parsed.billing, body, getClientIp(req));
|
|
1492
|
+
if (!outcome.ok) {
|
|
1493
|
+
if (outcome.retryAfter)
|
|
1494
|
+
(_c = res.setHeader) === null || _c === void 0 ? void 0 : _c.call(res, 'Retry-After', String(outcome.retryAfter));
|
|
1495
|
+
res.status(outcome.status).json({ error: outcome.error });
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
res.json(outcome.data);
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1191
1501
|
|
|
1192
1502
|
exports.Ozura = Ozura;
|
|
1193
1503
|
exports.OzuraError = OzuraError;
|
|
@@ -1195,6 +1505,8 @@ exports.createCardSaleHandler = createCardSaleHandler;
|
|
|
1195
1505
|
exports.createCardSaleMiddleware = createCardSaleMiddleware;
|
|
1196
1506
|
exports.createMintWaxHandler = createMintWaxHandler;
|
|
1197
1507
|
exports.createMintWaxMiddleware = createMintWaxMiddleware;
|
|
1508
|
+
exports.createRecurringPlanHandler = createRecurringPlanHandler;
|
|
1509
|
+
exports.createRecurringPlanMiddleware = createRecurringPlanMiddleware;
|
|
1198
1510
|
exports.createSessionHandler = createSessionHandler;
|
|
1199
1511
|
exports.createSessionMiddleware = createSessionMiddleware;
|
|
1200
1512
|
exports.getClientIp = getClientIp;
|