{"_id":"@apexara/stripe","_rev":"4-002a9aa0a3c7788bd8b8e99f5ca496fc","name":"@apexara/stripe","dist-tags":{"latest":"1.0.4"},"versions":{"1.0.2":{"name":"@apexara/stripe","version":"1.0.2","keywords":["stripe","subscriptions","webhooks"],"author":{"name":"Apexara LLC"},"license":"ISC","_id":"@apexara/stripe@1.0.2","maintainers":[{"name":"simeondspasov","email":"simeon.spasov@apexara.com"},{"name":"ivozhulev","email":"ivo@apexara.com"},{"name":"nikolay-kostadinov","email":"nikolay.kostadinov@apexara.com"},{"name":"martinpetrovapex","email":"martin.petrov@apexara.com"}],"dist":{"shasum":"5288b8aeb339f0564de1eab025962fc988b3fa79","tarball":"https://registry.npmjs.org/@apexara/stripe/-/stripe-1.0.2.tgz","fileCount":65,"integrity":"sha512-FicrkaXXK/uyhpiCGLDEoIzcxEoVHdnF5YlB97rLqnjZh++W6g0ejC509mZ0hA5ndIGrXmQyDW8o+uxs1UB1FA==","signatures":[{"sig":"MEQCIDZZdSxsZ8d2ISOSWfE0/F7Njcr5f1uj1UnRdpEg5S2YAiABU/ij/EpFuSBz9dPfV46kAcp5ZRDS4UUwYjqLlBZmKQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122630},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js"}},"gitHead":"786e540cc63c63c39f82c776d790cb89fbe366db","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","test:watch":"vitest","type-check":"tsc --noEmit","build:watch":"tsc -p tsconfig.build.json --watch","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"ivozhulev","email":"ivo@apexara.com"},"_npmVersion":"10.7.0","description":"Stripe service layer and webhook dispatcher","directories":{},"_nodeVersion":"18.20.3","dependencies":{"stripe":"^18.5.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.4","typescript":"^5.0.0","@types/node":"^22.0.0","@types/express":"^5.0.0","@vitest/coverage-v8":"^3.2.4"},"peerDependencies":{"express":"^5.0.0","@types/express":"^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true},"@types/express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/stripe_1.0.2_1775050559885_0.21136664457494359","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@apexara/stripe","version":"1.0.3","keywords":["stripe","subscriptions","webhooks"],"author":{"name":"Apexara LLC"},"license":"ISC","_id":"@apexara/stripe@1.0.3","maintainers":[{"name":"simeondspasov","email":"simeon.spasov@apexara.com"},{"name":"ivozhulev","email":"ivo@apexara.com"},{"name":"nikolay-kostadinov","email":"nikolay.kostadinov@apexara.com"},{"name":"martinpetrovapex","email":"martin.petrov@apexara.com"}],"dist":{"shasum":"8326c5d2b39b015f9cdab715e233e636d425f756","tarball":"https://registry.npmjs.org/@apexara/stripe/-/stripe-1.0.3.tgz","fileCount":65,"integrity":"sha512-/tXwvQtJjkocgESaDpoVEE5wv1/SZMV4B6CHtxnM4FDUh8I5IrMJ1n37OWn3Vl1jcOjdO77V0JlQ681IH94FmA==","signatures":[{"sig":"MEYCIQCfsI+zys3R9awtMVlVdLPZ+bo9sUnhupX8g+TKiOP0xAIhAKcgS+jlnqIzjmBp9Ysa7/aGiR/sppw5DAEk+CMf7Tan","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122698},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js"}},"gitHead":"cb9a2deba3e191decbed0efc9ef71245158e76d4","private":false,"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","test:watch":"vitest","type-check":"tsc --noEmit","build:watch":"tsc -p tsconfig.build.json --watch","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"ivozhulev","email":"ivo@apexara.com"},"_npmVersion":"10.7.0","description":"Stripe service layer and webhook dispatcher","directories":{},"_nodeVersion":"18.20.3","dependencies":{"stripe":"^18.5.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.4","typescript":"^5.0.0","@types/node":"^22.0.0","@types/express":"^5.0.0","@vitest/coverage-v8":"^3.2.4"},"peerDependencies":{"express":"^5.0.0","@types/express":"^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true},"@types/express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/stripe_1.0.3_1779786286973_0.7427534406742264","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@apexara/stripe","version":"1.0.4","description":"Stripe service layer and webhook dispatcher","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.build.json","build:watch":"tsc -p tsconfig.build.json --watch","type-check":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage"},"keywords":["stripe","subscriptions","webhooks"],"author":{"name":"Apexara LLC"},"license":"ISC","private":false,"engines":{"node":">=18"},"dependencies":{"stripe":"^18.5.0"},"peerDependencies":{"@types/express":"^5.0.0","express":"^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true},"@types/express":{"optional":true}},"devDependencies":{"@types/express":"^5.0.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.2.4","typescript":"^5.0.0","vitest":"^3.2.4"},"_id":"@apexara/stripe@1.0.4","gitHead":"7405dcae88a1881710b7f709683fc054ba0630da","_nodeVersion":"18.20.3","_npmVersion":"10.7.0","dist":{"integrity":"sha512-aD0Vq4DCqvU+P9M8uD2qlKE7/pdrM66fKaMjEkkZhMTV9jz7wYNq9bsKEsMiCgaV1WGROY/S3G9xYugx7UsVKQ==","shasum":"a2b88d8d54d857b3b2dc7981652dd4cdf1fd3946","tarball":"https://registry.npmjs.org/@apexara/stripe/-/stripe-1.0.4.tgz","fileCount":65,"unpackedSize":122769,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGXYoUOeG5iyC5fHURgLoH/8NnXxtc2CHJmcMViv6GHYAiEA6GMlefSwe9rvRqm3dOm5FB14Vp+PJl66pHwCSkXBmSk="}]},"_npmUser":{"name":"ivozhulev","email":"ivo@apexara.com"},"directories":{},"maintainers":[{"name":"simeondspasov","email":"simeon.spasov@apexara.com"},{"name":"ivozhulev","email":"ivo@apexara.com"},{"name":"nikolay-kostadinov","email":"nikolay.kostadinov@apexara.com"},{"name":"martinpetrovapex","email":"martin.petrov@apexara.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/stripe_1.0.4_1782132025661_0.771376395696503"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-01T13:35:59.885Z","modified":"2026-06-22T12:40:26.007Z","1.0.2":"2026-04-01T13:36:00.041Z","1.0.3":"2026-05-26T09:04:47.096Z","1.0.4":"2026-06-22T12:40:25.805Z"},"author":{"name":"Apexara LLC"},"license":"ISC","keywords":["stripe","subscriptions","webhooks"],"description":"Stripe service layer and webhook dispatcher","maintainers":[{"name":"simeondspasov","email":"simeon.spasov@apexara.com"},{"name":"ivozhulev","email":"ivo@apexara.com"},{"name":"nikolay-kostadinov","email":"nikolay.kostadinov@apexara.com"},{"name":"martinpetrovapex","email":"martin.petrov@apexara.com"}],"readme":"# @apexara/stripe\n\nA typed Stripe service layer for Node.js. Wraps the official Stripe SDK into focused, independently injectable services with built-in webhook handling, idempotency, and affiliate payout math.\n\n## Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Configuration](#configuration)\n- [Services](#services)\n  - [CheckoutService](#checkoutservice)\n  - [CustomerService](#customerservice)\n  - [InvoiceService](#invoiceservice)\n  - [PaymentLinkService](#paymentlinkservice)\n  - [PaymentMethodService](#paymentmethodservice)\n  - [PriceService](#priceservice)\n  - [ConnectedAccountService](#connectedaccountservice)\n  - [SubscriptionService](#subscriptionservice)\n- [Webhooks](#webhooks)\n- [Advanced Usage](#advanced-usage)\n- [API Reference — Types](#api-reference--types)\n- [Development](#development)\n\n---\n\n## Installation\n\n```bash\nnpm install @apexara/stripe\n```\n\nExpress is an optional peer dependency — only required if you use `StripeWebhookHandler`:\n\n```bash\nnpm install express\n```\n\n**Requirements:** Node.js 18+, TypeScript 5+\n\n---\n\n## Quick Start\n\n```typescript\nimport { ApexStripe } from '@apexara/stripe';\n\nconst stripe = new ApexStripe(\n  { secretKey: process.env.STRIPE_SECRET_KEY! },\n  { currency: 'usd' },\n);\n\n// Create a customer\nconst customer = await stripe.customers.createCustomer('Jane', 'Doe', 'jane@example.com');\n\n// Create a checkout session\nconst clientSecret = await stripe.checkout.createCheckoutSession({\n  mode: 'payment',\n  customerId: customer.id,\n  name: 'Order #1234',\n  cost: 4900,       // $49.00 in cents\n  quantity: 1,\n  returnUrl: 'https://example.com/payment-result',\n});\n\n// Access the raw Stripe client if needed\nconst balance = await stripe.client.balance.retrieve();\n```\n\n---\n\n## Configuration\n\n### `StripeClientConfig`\n\nPassed as the first argument to `ApexStripe`. Controls how the Stripe SDK client is created.\n\n```typescript\ninterface StripeClientConfig {\n  secretKey: string;\n  apiVersion?: Stripe.LatestApiVersion; // Default: '2025-08-27.basil'\n}\n```\n\n### `StripeServiceConfig`\n\nPassed as the second (optional) argument to `ApexStripe`. All fields are optional — defaults are applied for any field not provided.\n\n```typescript\ninterface StripeServiceConfig {\n  // Stripe settings\n  currency?: string;                    // Default: 'usd'\n\n  // Payment methods per context\n  orderPaymentMethods?: string[];       // Default: ['card', 'link', 'cashapp', 'klarna']\n  subscriptionPaymentMethods?: string[]; // Default: ['card', 'link', 'cashapp']\n  setupIntentPaymentMethods?: string[]; // Default: ['card']\n\n  // Checkout behavior\n  checkoutUiMode?: 'custom' | 'embedded' | 'hosted'; // Default: 'custom'\n  allowPromotionCodes?: boolean;        // Default: true\n  savePaymentMethod?: boolean;          // Default: true\n  orderInvoiceCreation?: boolean;       // Default: true\n\n  // Invoices\n  invoiceCollectionMethod?: 'send_invoice' | 'charge_automatically'; // Default: 'send_invoice'\n  invoiceDaysUntilDue?: number;         // Default: 3\n  invoicePaymentMethodTypes?: string[]; // Default: ['card', 'link']\n\n  // Connected accounts\n  connectedAccountType?: 'express' | 'standard' | 'custom'; // Default: 'express'\n\n}\n```\n\n**Full example with all defaults overridden:**\n\n```typescript\nconst stripe = new ApexStripe(\n  { secretKey: process.env.STRIPE_SECRET_KEY! },\n  {\n    currency: 'eur',\n    orderPaymentMethods: ['card'],\n    subscriptionPaymentMethods: ['card'],\n    setupIntentPaymentMethods: ['card'],\n    checkoutUiMode: 'embedded',\n    allowPromotionCodes: false,\n    savePaymentMethod: false,\n    orderInvoiceCreation: false,\n    invoiceCollectionMethod: 'charge_automatically',\n    invoiceDaysUntilDue: 7,\n    invoicePaymentMethodTypes: ['card'],\n    connectedAccountType: 'standard',\n  },\n);\n```\n\n---\n\n## Services\n\nAll services are available as properties on the `ApexStripe` instance. They can also be instantiated independently — see [Advanced Usage](#advanced-usage).\n\n---\n\n### CheckoutService\n\n`stripe.checkout`\n\nHandles Stripe Checkout sessions, Setup Intents, and affiliate payout calculations.\n\n---\n\n#### `createCheckoutSession(params)`\n\nCreates a payment or subscription checkout session. Returns the `client_secret` for use with Stripe.js.\n\n**Payment mode:**\n\n```typescript\nconst clientSecret = await stripe.checkout.createCheckoutSession({\n  mode: 'payment',\n  customerId: 'cus_xxx',\n  name: 'Order #1234',\n  cost: 4900,           // Amount in cents\n  quantity: 1,\n  returnUrl: 'https://example.com/result',\n  metadata: { orderId: '1234' },\n  // Optional: affiliate payout\n  affiliate: {\n    accountId: 'acct_xxx',  // Connected account ID\n    amount: 490,             // Payout amount in cents\n  },\n});\n```\n\n**Subscription mode:**\n\n```typescript\nconst clientSecret = await stripe.checkout.createCheckoutSession({\n  mode: 'subscription',\n  customerId: 'cus_xxx',\n  priceIds: ['price_xxx'],\n  quantity: 1,\n  returnUrl: 'https://example.com/result',\n  metadata: { userId: 'user_123' },\n  // Optional: affiliate payout\n  affiliate: {\n    transferData: { destination: 'acct_xxx', amount_percent: 19 },\n    metadata: { affiliateId: 'aff_xxx' },\n  },\n});\n```\n\n---\n\n#### `createSetupIntentSession(customerId)`\n\nCreates a Setup Intent for saving a payment method without charging. Returns the `client_secret`.\n\n```typescript\nconst clientSecret = await stripe.checkout.createSetupIntentSession('cus_xxx');\n```\n\n---\n\n#### `expireCheckoutSession(sessionId)`\n\nExpires an open checkout session. Silently ignores sessions that are already expired — safe to call unconditionally.\n\n```typescript\nawait stripe.checkout.expireCheckoutSession('cs_xxx');\n```\n\n---\n\n#### `retrieveCheckoutSession(sessionId, options?)`\n\nRetrieves a checkout session with optional expansion.\n\n```typescript\nconst session = await stripe.checkout.retrieveCheckoutSession('cs_xxx', {\n  expand: ['payment_intent.latest_charge', 'invoice'],\n});\n```\n\n---\n\n#### `getPaymentIntent(paymentIntentId, options?)`\n\nRetrieves a payment intent.\n\n```typescript\nconst intent = await stripe.checkout.getPaymentIntent('pi_xxx');\n```\n\n---\n\n### CustomerService\n\n`stripe.customers`\n\n---\n\n#### `createCustomer(firstName, lastName, email)`\n\nCreates a Stripe customer. The full name is stored as `${firstName} ${lastName}`.\n\n```typescript\nconst customer = await stripe.customers.createCustomer('Jane', 'Doe', 'jane@example.com');\n// → Stripe.Customer\n```\n\n---\n\n#### `updateCustomerDefaultPaymentMethod(customerId, paymentMethodId)`\n\nSets the default payment method on a customer's invoice settings.\n\n```typescript\nawait stripe.customers.updateCustomerDefaultPaymentMethod('cus_xxx', 'pm_xxx');\n```\n\n---\n\n### InvoiceService\n\n`stripe.invoices`\n\n---\n\n#### `createInvoice(params)`\n\nCreates an invoice with a single line item and either sends it or finalizes it, depending on `invoiceCollectionMethod`.\n\n- `send_invoice` — creates, adds line item, sends — customer receives an email with payment link\n- `charge_automatically` — creates, adds line item, finalizes — Stripe charges the default payment method\n\n```typescript\nconst invoice = await stripe.invoices.createInvoice({\n  customerId: 'cus_xxx',\n  amount: 2000,            // In cents\n  description: 'Credit top-up — 500 records',\n  metadata: { origin: 'admin', credits: '500' },\n});\n// → Stripe.Invoice (sent or finalized)\n```\n\n---\n\n#### `listInvoicePayments(invoiceId, params?)`\n\nLists all payments recorded against an invoice.\n\n```typescript\nconst payments = await stripe.invoices.listInvoicePayments('in_xxx');\n// → Stripe.ApiList<Stripe.InvoicePayment>\n```\n\n---\n\n#### `findPendingActivationInvoice(customerId)`\n\nChecks whether a customer has an open invoice from a `subscription_create` event whose subscription is still active or incomplete. Returns the invoice data or `null`.\n\nUse this to prompt the user to pay their pending subscription invoice after an admin-created invoice-based subscription.\n\n```typescript\nconst pending = await stripe.invoices.findPendingActivationInvoice('cus_xxx');\n\nif (pending) {\n  // { subscriptionId: 'sub_xxx', hostedInvoiceUrl: 'https://...' }\n  redirectToInvoice(pending.hostedInvoiceUrl);\n}\n```\n\nReturns `null` when:\n- No open invoices exist\n- No invoice has `billing_reason === 'subscription_create'`\n- The subscription is `canceled` or `incomplete_expired`\n\n---\n\n### PaymentLinkService\n\n`stripe.paymentLinks`\n\n---\n\n#### `createPaymentLink(params)`\n\nCreates a Stripe Price and Payment Link in one call. Returns the link ID, URL, and the underlying price/product IDs needed to archive later.\n\n```typescript\nconst link = await stripe.paymentLinks.createPaymentLink({\n  name: 'Order #1234',\n  cost: 4900,\n  redirectUrl: 'https://example.com/result',\n  metadata: { orderId: '1234' },\n  // Optional: affiliate payout\n  affiliate: {\n    accountId: 'acct_xxx',\n    amount: 490,\n  },\n});\n// → { id, url, productId, priceId }\n```\n\n---\n\n#### `archivePaymentLink(linkId, productId, priceId)`\n\nDeactivates the payment link, product, and price so they no longer appear in Stripe or accept payments.\n\n```typescript\nawait stripe.paymentLinks.archivePaymentLink(\n  link.id,\n  link.productId,\n  link.priceId,\n);\n```\n\n---\n\n### PaymentMethodService\n\n`stripe.paymentMethods`\n\n---\n\n#### `getPaymentMethod(paymentMethodId)`\n\n```typescript\nconst pm = await stripe.paymentMethods.getPaymentMethod('pm_xxx');\n// → Stripe.PaymentMethod\n```\n\n#### `listPaymentMethods(customerId)`\n\nReturns the customer's saved payment methods as a flat array.\n\n```typescript\nconst methods = await stripe.paymentMethods.listPaymentMethods('cus_xxx');\n// → Stripe.PaymentMethod[]\n```\n\n#### `attachPaymentMethod(paymentMethodId, customerId)`\n\nAttaches a payment method to a customer.\n\n```typescript\nconst pm = await stripe.paymentMethods.attachPaymentMethod('pm_xxx', 'cus_xxx');\n```\n\n#### `setDefaultPaymentMethodForCustomerAndSubscription(customerId, subscriptionId, paymentMethodId)`\n\nAttaches the payment method, sets it as the customer's invoice default, and sets it as the subscription's default — all in one call.\n\n```typescript\nawait stripe.paymentMethods.setDefaultPaymentMethodForCustomerAndSubscription(\n  'cus_xxx',\n  'sub_xxx',\n  'pm_xxx',\n);\n```\n\n#### `setDefaultPaymentMethod(subscriptionId, paymentMethodId)`\n\nSets the subscription's default payment method unconditionally.\n\n```typescript\nawait stripe.paymentMethods.setDefaultPaymentMethod('sub_xxx', 'pm_xxx');\n```\n\n#### `setDefaultPaymentMethodIfMissing(subscriptionId, paymentMethodId)`\n\nSets the default only if the subscription has no default payment method currently set.\n\n```typescript\nconst { changed } = await stripe.paymentMethods.setDefaultPaymentMethodIfMissing('sub_xxx', 'pm_xxx');\n```\n\n#### `removePaymentMethod(customerId, subscriptionId, paymentMethodId)`\n\nRemoves a payment method safely. If it is the subscription's default, another payment method is assigned first. Throws if it is the only payment method on the customer.\n\n```typescript\nconst { newDefaultId } = await stripe.paymentMethods.removePaymentMethod(\n  'cus_xxx',\n  'sub_xxx',\n  'pm_xxx',\n);\n```\n\n---\n\n### PriceService\n\n`stripe.prices`\n\n---\n\n#### `createCustomSubscriptionPrice(params)`\n\nCreates a recurring price on an existing Stripe product.\n\n```typescript\nconst price = await stripe.prices.createCustomSubscriptionPrice({\n  cost: 9900,                // In cents\n  billingPeriod: 'month',\n  billingIntervalCount: 1,\n  label: 'Pro Monthly',\n  stripeProductId: 'prod_xxx',\n  metadata: { label: 'Pro Monthly', credits: '1000' },\n});\n// → Stripe.Price\n```\n\n`billingPeriod` accepts: `'day' | 'week' | 'month' | 'year'`\n\n---\n\n#### `archivePrice(priceId)`\n\nSets a price to `active: false` so it can no longer be used for new subscriptions.\n\n```typescript\nawait stripe.prices.archivePrice('price_xxx');\n```\n\n---\n\n### ConnectedAccountService\n\n`stripe.connectedAccounts`\n\nHandles Stripe Connect for marketplace and affiliate payout scenarios.\n\n---\n\n#### `createConnectedAccount(metadata?)`\n\nCreates a new connected account (type from config, defaults to `'express'`). Returns the account ID.\n\n```typescript\nconst accountId = await stripe.connectedAccounts.createConnectedAccount({\n  userId: 'user_123',\n});\n// → 'acct_xxx'\n```\n\n---\n\n#### `deleteConnectedAccount(accountId)`\n\nPermanently deletes a connected account.\n\n```typescript\nawait stripe.connectedAccounts.deleteConnectedAccount('acct_xxx');\n```\n\n---\n\n#### `createAccountLink(accountId, refreshUrl, returnUrl, type?)`\n\nGenerates a Stripe-hosted onboarding or management URL for a connected account. Returns the URL string.\n\n```typescript\nconst url = await stripe.connectedAccounts.createAccountLink(\n  'acct_xxx',\n  'https://example.com/onboarding/refresh',\n  'https://example.com/onboarding/return',\n  // type defaults to 'account_onboarding'\n);\n```\n\n`type` accepts: `'account_onboarding' | 'account_update'`\n\n---\n\n#### `createLoginLink(accountId)`\n\nGenerates a single-use Express Dashboard login URL for a connected account. Returns the URL string.\n\n```typescript\nconst url = await stripe.connectedAccounts.createLoginLink('acct_xxx');\n```\n\n---\n\n### SubscriptionService\n\n`stripe.subscriptions`\n\nThe most comprehensive service. Covers the full subscription lifecycle including scheduling, upgrades, downgrades, and payment method management.\n\n---\n\n#### Basic operations\n\n```typescript\n// Get a subscription (expands items.data.price)\nconst sub = await stripe.subscriptions.getSubscriptionById('sub_xxx');\n\n// Get all subscriptions for a customer (status: 'all', expands default_payment_method)\nconst subs = await stripe.subscriptions.getAllUserSubscriptions('cus_xxx');\n// → Stripe.Subscription[] (empty array if customerId is falsy)\n\n// Update a subscription\nconst updated = await stripe.subscriptions.updateSubscription('sub_xxx', {\n  metadata: { userId: 'user_123' },\n});\n\n// Cancel immediately\nconst cancelled = await stripe.subscriptions.cancelSubscription('sub_xxx');\n```\n\n---\n\n#### Lifecycle — cancel at period end / resume\n\n```typescript\n// Schedule cancellation at end of billing period.\n// Throws if status is not 'active' or 'past_due'.\nconst sub = await stripe.subscriptions.cancelSubscriptionAtPeriodEnd('sub_xxx');\n\n// Undo a scheduled cancellation.\n// Throws if cancel_at_period_end is false or status is not resumable.\nconst sub = await stripe.subscriptions.resumeSubscription('sub_xxx');\n```\n\n---\n\n#### Plan switching and credits\n\nUse `applyPlanCredit` to create a credit invoice item (a negative charge) before switching plans, so the customer is credited for unused days on their current plan:\n\n```typescript\nawait stripe.subscriptions.applyPlanCredit(\n  'cus_xxx',\n  'sub_xxx',\n  3200,             // Amount in cents — stored as a negative invoice item\n  'usd',\n  'Credit for unused Pro plan days',  // Optional\n  { type: 'upgrade_credit' },         // Optional metadata\n);\n```\n\nThen switch the subscription to the new price immediately (billing cycle resets to now, no proration by default):\n\n```typescript\nconst updated = await stripe.subscriptions.switchSubscriptionPlan(\n  'sub_xxx',\n  'si_xxx',         // The subscription item being replaced\n  'price_xxx',\n  {\n    quantity: 1,                // Optional, defaults to 1\n    proration: 'none',          // Optional — pass 'create_prorations' to let Stripe calculate\n  },\n);\n```\n\n---\n\n#### Downgrades (scheduled)\n\nDowngrades are scheduled to take effect at the end of the current billing period using Stripe Subscription Schedules.\n\n```typescript\n// Schedule a downgrade to a lower price at period end.\n// Tags the schedule with { type: 'downgrade' } for later identification.\nconst schedule = await stripe.subscriptions.scheduleDowngrade(\n  subscription,\n  'price_lower_xxx',\n  {\n    transferData: { destination: 'acct_xxx', amount_percent: 19 }, // Optional\n    phaseMetadata: { affiliateId: 'aff_xxx' },                     // Optional\n    scheduleMetadata: { initiatedBy: 'user' },                     // Optional\n  },\n);\n\n// Check if a downgrade is scheduled (returns null if not, or if the\n// schedule is not tagged as a downgrade).\nconst schedule = await stripe.subscriptions.getDowngradeSchedule(subscription);\nif (schedule) {\n  // schedule.phases[1].items[0].price → the new lower price\n}\n\n// Cancel a scheduled downgrade (e.g. the user upgrades instead).\n// Returns true if a downgrade schedule was found and released, false otherwise.\nconst released = await stripe.subscriptions.releaseScheduleIfDowngrade(subscription);\n```\n\nFor more control, get or create the schedule directly:\n\n```typescript\nconst schedule = await stripe.subscriptions.getOrCreateSubscriptionSchedule(subscription);\n```\n\n---\n\n#### Invoice-based subscriptions (admin-created)\n\nCreates a subscription using `send_invoice` collection and immediately emails the invoice to the customer. Use this for admin-created subscriptions where the customer pays via an emailed link rather than through a checkout session.\n\n```typescript\nconst { subscription, invoiceId, invoiceUrl } =\n  await stripe.subscriptions.createInvoiceBasedSubscription(\n    'cus_xxx',\n    'price_xxx',\n    {\n      daysUntilDue: 3,                         // Overrides config default\n      paymentMethodTypes: ['card', 'link'],\n      metadata: { adminId: 'admin_123', userId: 'user_456' },\n      transferData: { destination: 'acct_xxx', amount_percent: 19 }, // Optional affiliate\n      quantity: 1,\n    },\n  );\n\n// invoiceUrl — the hosted_invoice_url to show the customer\n// invoiceId  — the Stripe invoice ID\n```\n\n---\n\n#### Utilities\n\n```typescript\n// Extract fields needed to keep a local subscription document in sync.\n// Falls back to provided values when the subscription has no items yet.\nconst fields = stripe.subscriptions.buildSubscriptionSyncFields(subscription, {\n  currentPeriodStart: existingDoc.currentPeriodStart,\n  currentPeriodEnd: existingDoc.currentPeriodEnd,\n  itemId: existingDoc.itemId,\n});\n// → { status, cancelAtPeriodEnd, currentPeriodStart, currentPeriodEnd, itemId }\n\n// Get the primary subscription item, preferring a known item ID.\nconst item = stripe.subscriptions.getPrimarySubscriptionItem(subscription, 'si_xxx');\n// → Stripe.SubscriptionItem | null\n```\n\n---\n\n## Affiliate Payout Helpers\n\nTwo standalone functions are exported for calculating affiliate payouts after deducting Stripe processing fees. Both require a `processingFees` object (`{ flat: number, percent: number }`).\n\n```typescript\nimport {\n  calculateAffiliatePaymentPayoutAmount,\n  calculateAffiliateSubscriptionPayoutPercent,\n} from '@apexara/stripe';\n\nconst fees = { flat: 30, percent: 0.029 }; // Standard Stripe rates\n\n// One-time payment payout — returns flat amount in cents\n// cost = 10000 ($100), affiliatePayoutPercent = 20\n// stripeFee = round(10000 * 0.029 + 30) = 320\n// payout = round((10000 - 320) * 0.20) = 1936 ($19.36)\nconst amount = calculateAffiliatePaymentPayoutAmount(10000, 20, fees);\n// → 1936\n\n// Subscription payout — returns percentage to pass to transfer_data.amount_percent\n// subscriptionCost = 10000, payoutPercent = 0.20\n// result = ((10000 - 320) * 0.20) / 10000 ≈ 0.19\nconst percent = calculateAffiliateSubscriptionPayoutPercent(10000, 0.20, fees);\n// → 0.19\n```\n\n---\n\n## Webhooks\n\n`StripeWebhookHandler` handles the main Stripe webhook and the connected account webhook. It performs signature verification, idempotency checks, and event dispatch.\n\n### Setup\n\n```typescript\nimport { StripeWebhookHandler, WebhookHooks, WebhookHandlerConfig } from '@apexara/stripe';\n\nconst config: WebhookHandlerConfig = {\n  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,\n  connectedWebhookSecret: process.env.STRIPE_WEBHOOK_CONNECTED_SECRET, // Optional\n};\n\nconst hooks: WebhookHooks = {\n  // Required: pluggable idempotency store\n  eventStore: {\n    tryInsert: async (eventId, type) => {\n      // Return true if this is a new event (insert succeeded).\n      // Return false if the event was already processed (duplicate).\n      // Use a unique index to make this atomic.\n      return db.stripeEvents.tryInsert({ eventId, type });\n    },\n  },\n\n  // Optional: implement only the events you care about\n\n  onCheckoutCompleted: async ({ session, metadata }) => {\n    // session is pre-expanded: payment_intent.latest_charge + invoice\n    await handleOrder(session, metadata);\n  },\n\n  onInvoicePaid: async ({ event, invoice }) => {\n    await grantCredits(invoice);\n  },\n\n  onInvoicePaymentFailed: async ({ event, invoice, failReason }) => {\n    // failReason is pre-extracted from the payment intent (decline_code or error code)\n    await notifyUser(invoice, failReason);\n  },\n\n  onSubscriptionDeleted: async ({ event, subscription }) => {\n    await deactivateSubscription(subscription.id);\n  },\n\n  onConnectedAccountUpdated: async ({ account }) => {\n    await syncAffiliateStatus(account);\n  },\n\n  // Optional: called when a non-critical internal error occurs (e.g. fail reason extraction)\n  onError: (context, error) => {\n    logger.error(`Webhook error in ${context}`, error);\n  },\n};\n\nconst handler = new StripeWebhookHandler(\n  hooks,\n  stripeClient,    // Raw Stripe instance — use ApexStripe.client or createStripeClient()\n  config,\n  checkoutService, // ApexStripe.checkout or a standalone CheckoutService\n  invoiceService,  // ApexStripe.invoices or a standalone InvoiceService\n);\n```\n\n### Express routes\n\nThe handler plugs directly into Express. **The raw body must be available on `req`** — register `express.raw()` before the webhook routes.\n\n```typescript\napp.use('/stripe/webhook', express.raw({ type: 'application/json' }));\napp.use('/stripe/webhook/connected', express.raw({ type: 'application/json' }));\n\n// Express mode — pass next to delegate errors to the app's error middleware\napp.post('/stripe/webhook', (req, res, next) => handler.handle(req, res, next));\napp.post('/stripe/webhook/connected', (req, res, next) => handler.handleConnected(req, res, next));\n\n// Standalone mode — omit next and the handler manages error responses itself\napp.post('/stripe/webhook', (req, res) => handler.handle(req, res));\n```\n\n### Idempotency\n\nEvery incoming event is checked against the event store before being dispatched. If `tryInsert` returns `false`, the event is acknowledged with HTTP 200 and skipped — no hook is called. This prevents duplicate processing when Stripe retries delivery.\n\n**The `tryInsert` implementation must be atomic.** Use a unique index (MongoDB, PostgreSQL) or a Redis `SET NX` to avoid race conditions when Stripe delivers the same event to multiple server instances simultaneously.\n\n**MongoDB example:**\n\n```typescript\neventStore: {\n  tryInsert: async (eventId, type) => {\n    try {\n      await StripeEventModel.create({ eventId, type, processedAt: new Date() });\n      return true;\n    } catch (err: any) {\n      if (err.code === 11000) return false; // Duplicate key — already processed\n      throw err;\n    }\n  },\n},\n```\n\n### Events dispatched\n\n| Stripe event | Hook called | Notes |\n|---|---|---|\n| `checkout.session.completed` | `onCheckoutCompleted` | Session re-fetched and expanded with `payment_intent.latest_charge` and `invoice` before the hook is called |\n| `invoice.paid` | `onInvoicePaid` | Raw invoice from event data |\n| `invoice.payment_failed` | `onInvoicePaymentFailed` | Raw invoice + `failReason` pre-extracted from the payment intent (`decline_code` or `code`) |\n| `customer.subscription.deleted` | `onSubscriptionDeleted` | Raw subscription from event data |\n| `account.updated` (connected) | `onConnectedAccountUpdated` | Raw account from event data |\n| All other event types | — | Silently ignored — HTTP 200 returned. Throwing for unknown events would cause Stripe to retry for up to 72 hours. |\n\n---\n\n## Advanced Usage\n\n### Using services independently\n\nEvery service can be instantiated on its own with an injected Stripe client. This is useful when you only need part of the package or when injecting the client from elsewhere.\n\n```typescript\nimport Stripe from 'stripe';\nimport { createStripeClient, SubscriptionService, InvoiceService } from '@apexara/stripe';\n\nconst client = createStripeClient({ secretKey: process.env.STRIPE_SECRET_KEY! });\n\n// Services that need config\nconst subscriptions = new SubscriptionService(client, {\n  currency: 'usd',\n  invoiceDaysUntilDue: 7,\n  invoiceCollectionMethod: 'send_invoice',\n  invoicePaymentMethodTypes: ['card', 'link'],\n  // All other StripeServiceConfig fields are required when constructing directly —\n  // use ApexStripe to have defaults applied automatically.\n});\n\n// Services that only need the client\nimport { CustomerService, PaymentMethodService } from '@apexara/stripe';\n\nconst customers = new CustomerService(client);\nconst paymentMethods = new PaymentMethodService(client);\n```\n\n### Using the raw Stripe client\n\nThe raw Stripe instance is accessible via `ApexStripe.client`:\n\n```typescript\nconst apex = new ApexStripe({ secretKey: '...' });\n\n// Full Stripe SDK — use for anything not covered by the services\nconst products = await apex.client.products.list({ limit: 10 });\n```\n\nOr create it independently:\n\n```typescript\nimport { createStripeClient } from '@apexara/stripe';\n\nconst stripe = createStripeClient({\n  secretKey: process.env.STRIPE_SECRET_KEY!,\n  apiVersion: '2025-08-27.basil', // Optional\n});\n```\n\n---\n\n## API Reference — Types\n\nAll types are exported from the package root.\n\n### Config\n\n```typescript\nimport {\n  StripeClientConfig,\n  StripeServiceConfig,\n  StripeProcessingFees,\n  WebhookHandlerConfig,\n} from '@apexara/stripe';\n```\n\n### Checkout\n\n```typescript\nimport {\n  CreateCheckoutSessionParams,\n  CreatePaymentCheckoutSessionParams,\n  CreateSubscriptionCheckoutSessionParams,\n  AffiliatePaymentParams,\n  AffiliateSubscriptionParams,\n} from '@apexara/stripe';\n```\n\n### Subscriptions\n\n```typescript\nimport {\n  ScheduleDowngradeOptions,\n  SwitchSubscriptionPlanOptions,\n  CreateInvoiceSubscriptionOptions,\n  CreateInvoiceSubscriptionResult,\n  SubscriptionSyncFields,\n} from '@apexara/stripe';\n```\n\n### Invoices\n\n```typescript\nimport {\n  CreateInvoiceParams,\n  PendingActivationInvoice,\n} from '@apexara/stripe';\n```\n\n### Payment links\n\n```typescript\nimport {\n  CreatePaymentLinkParams,\n  PaymentLinkResult,\n} from '@apexara/stripe';\n```\n\n### Prices\n\n```typescript\nimport { CreateCustomPriceParams } from '@apexara/stripe';\n```\n\n### Webhooks\n\n```typescript\nimport {\n  WebhookHooks,\n  WebhookEventStore,\n  CheckoutCompletedPayload,\n  InvoicePaidPayload,\n  InvoicePaymentFailedPayload,\n  SubscriptionDeletedPayload,\n  ConnectedAccountUpdatedPayload,\n} from '@apexara/stripe';\n```\n\n### Utilities\n\n```typescript\nimport { getBillingCycleLabel } from '@apexara/stripe';\n\ngetBillingCycleLabel('month', 1);  // → 'Monthly'\ngetBillingCycleLabel('year', 1);   // → 'Yearly'\ngetBillingCycleLabel('week', 1);   // → 'Weekly'\ngetBillingCycleLabel('day', 1);    // → 'Daily'\ngetBillingCycleLabel('month', 3);  // → 'Quarterly'\ngetBillingCycleLabel('month', 6);  // → 'Semi-Annually'\ngetBillingCycleLabel('month', 2);  // → 'Every 2 months'\n```\n\n---\n\n## Development\n\n```bash\n# Install dependencies\nnpm install\n\n# Build\nnpm run build\n\n# Build in watch mode\nnpm run build:watch\n\n# Type check (no emit)\nnpm run type-check\n\n# Run tests\nnpm test\n\n# Run tests with coverage\nnpm run test:coverage\n```\n\n**Test stack:** Vitest with v8 coverage. Tests live in `src/tests/` and are excluded from the build output.\n\n**Stripe API version:** `2025-08-27.basil` — pinned in `src/client.ts`. To use a different version, pass `apiVersion` in `StripeClientConfig`.\n\n---\n\n## License\n\nISC\n","readmeFilename":"README.md"}