@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.
@@ -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';
@@ -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
- const body = JSON.parse(raw);
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;