@stacksjs/payments 0.75.367 → 0.75.369

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.
@@ -20,3 +20,8 @@ export declare interface DirectInvoicePurchaseOptions extends Pick<DirectInvoice
20
20
  metadata?: Record<string, string>
21
21
  }
22
22
  declare type PurchaseClient = Pick<Stripe, 'setupIntents' | 'prices' | 'paymentMethods' | 'invoices' | 'invoiceItems' | 'subscriptionSchedules' | 'subscriptions' | 'invoicePayments' | 'paymentIntents'>;
23
+ /** The quote expired without a bound purchase or an unbound provider resource
24
+ * that could be an interrupted purchase. Original local IDs still need review. */
25
+ export declare class DirectInvoicePurchaseExpiredError extends Error {
26
+
27
+ }
@@ -1 +1 @@
1
- import{stripe}from"../drivers/stripe";import{idempotencyFingerprint,stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{collectDirectInvoice}from"./invoice-collection";import{allPages}from"./pagination";import{manageDestinationSubscription}from"./subscription";export function directInvoicePurchaseMetadata(account,options){asAccount(account);if(typeof options.reference!=="string"||!/^[a-z0-9:._-]{1,160}$/i.test(options.reference)||!/^cus_[a-z0-9_]+$/i.test(options.customer)||!/^price_[a-z0-9_]+$/i.test(options.price)||!Number.isSafeInteger(options.expiresAt)||options.expiresAt<=0||!Number.isSafeInteger(options.amount)||options.amount<=0||!/^[a-z]{3}$/i.test(options.currency)||!/^txcd_\d{8}$/.test(options.taxCode)||options.interval!==void 0&&!["day","week","month","year"].includes(options.interval)||options.intervalCount!==void 0&&(!options.interval||!Number.isSafeInteger(options.intervalCount)||options.intervalCount<1)||options.trialDays!==void 0&&(!Number.isSafeInteger(options.trialDays)||options.trialDays<0||options.trialDays>730||!options.interval&&options.trialDays>0))throw Error("[payments/connect] Expected original customer, price and frozen invoice purchase terms");if(options.platformFeePercent==null)throw Error("[payments/connect] An explicit invoice fee percentage is required");manageDestinationSubscription.feePercentFor({platformFeePercent:options.platformFeePercent});const metadata=options.metadata??{};if(typeof metadata!=="object"||Array.isArray(metadata)||Object.keys(metadata).length>47||Object.entries(metadata).some(([key,value])=>!key||key.length>40||/[\[\]]/.test(key)||key.startsWith("stacks_purchase_")||typeof value!=="string"||value.length>500))throw Error("[payments/connect] Invalid purchase metadata or reserved binding fields");const terms={account,reference:options.reference,customer:options.customer,price:options.price,amount:options.amount,currency:options.currency.toLowerCase(),taxCode:options.taxCode,expiresAt:options.expiresAt,interval:options.interval??null,intervalCount:options.interval?options.intervalCount??1:null,trialDays:options.trialDays??0,platformFeePercent:options.platformFeePercent,metadata};return{...metadata,stacks_purchase_reference:options.reference,stacks_purchase_quote:idempotencyFingerprint(terms)}}function id(value){return typeof value==="string"?value:value?.id}export async function completeDirectInvoicePurchase(setupIntentId,account,options,client=stripe){const quote=directInvoicePurchaseMetadata(account,options),request=asAccount(account);if(!/^seti_[a-z0-9_]+$/i.test(setupIntentId))throw Error("[payments/connect] Expected original successful payment setup");const setup=await client.setupIntents.retrieve(setupIntentId,{},request),matches=(metadata,expected)=>Object.entries(expected).every(([key,value])=>metadata?.[key]===value);if(setup.id!==setupIntentId||setup.status!=="succeeded"||setup.usage!=="off_session"||!Number.isSafeInteger(setup.created)||setup.created<=0||id(setup.customer)!==options.customer||!/^pm_[a-z0-9_]+$/i.test(id(setup.payment_method)||"")||!matches(setup.metadata,quote))throw Error("[payments/connect] Payment setup does not match the original customer and frozen quote");const paymentMethod=id(setup.payment_method),metadata={...quote,stacks_purchase_setup:setupIntentId},key=(operation)=>stacksIdempotencyKey(`connect.invoice.purchase.${operation}`,account,options.reference,quote.stacks_purchase_quote),canCreate=()=>{if(Date.now()/1000>=Math.min(options.expiresAt,setup.created+86400))throw Error("[payments/connect] Expired purchase can only recover its original provider invoice or schedule")},existing=(rows)=>{const own=rows.filter((row)=>row.metadata?.stacks_purchase_reference===options.reference);if(own.length>1||own.some((row)=>!matches(row.metadata,metadata)))throw Error("[payments/connect] Purchase reference has ambiguous or different provider bindings");return own[0]},verifyPrice=async()=>{const price=await client.prices.retrieve(options.price,{expand:["product"]},request),product=price.product;if(price.id!==options.price||!price.active||price.currency!==options.currency.toLowerCase()||price.unit_amount!==options.amount||price.tax_behavior!=="inclusive"||price.type!==(options.interval?"recurring":"one_time")||(options.interval?price.recurring?.interval!==options.interval||price.recurring?.interval_count!==(options.intervalCount??1)||price.recurring?.usage_type!=="licensed":price.recurring!=null)||typeof product==="string"||product.deleted||!product.active||id(product.tax_code)!==options.taxCode)throw Error("[payments/connect] Catalog price does not match the approved inclusive purchase");const method=await client.paymentMethods.retrieve(paymentMethod,{},request);if(method.id!==paymentMethod||id(method.customer)!==options.customer)throw Error("[payments/connect] Purchase payment method belongs to another customer")};let invoice,subscription,reloadSubscription;if(options.interval){let schedule=existing(await allPages((after)=>client.subscriptionSchedules.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request)));if(!schedule){canCreate();await verifyPrice();const phase={items:[{price:options.price,quantity:1}],automatic_tax:{enabled:!0},metadata};schedule=await client.subscriptionSchedules.create({customer:options.customer,start_date:"now",end_behavior:"release",default_settings:{default_payment_method:paymentMethod},phases:options.trialDays?[{...phase,trial:!0,duration:{interval:"day",interval_count:options.trialDays}},phase]:[phase],metadata},{...request,idempotencyKey:key("schedule")})}const subscriptionId=id(schedule.subscription)??id(schedule.released_subscription);if(!/^sub_sched_[a-z0-9_]+$/i.test(schedule.id)||id(schedule.customer)!==options.customer||!matches(schedule.metadata,metadata)||!/^sub_[a-z0-9_]+$/i.test(subscriptionId||""))throw Error("[payments/connect] Purchase schedule has no original customer and subscription");const readSubscription=async()=>{const value=await client.subscriptions.retrieve(subscriptionId,{},request);if(value.id!==subscriptionId||id(value.customer)!==options.customer||!matches(value.metadata,metadata)||value.items.has_more||value.items.data.length!==1||id(value.items.data[0].price)!==options.price||value.items.data[0].quantity!==1)throw Error("[payments/connect] Subscription does not match the original frozen purchase");return value};reloadSubscription=readSubscription;subscription=await readSubscription();const initial=(await allPages((after)=>client.invoices.list({customer:options.customer,subscription:subscriptionId,limit:100,...after?{starting_after:after}:{}},request))).filter((value)=>value.billing_reason==="subscription_create"&&id(value.parent?.subscription_details?.subscription)===subscriptionId);if(initial.length!==1)throw Error("[payments/connect] Purchase has no unique original subscription invoice");invoice=await client.invoices.retrieve(initial[0].id,{},request);if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] First invoice does not match the original subscription quote");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection");if(invoice.status==="draft"&&invoice.auto_advance!==!1){invoice=await client.invoices.update(invoice.id,{auto_advance:!1},{...request,idempotencyKey:key("hold")});if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] Held invoice no longer matches the original subscription")}if(schedule.status==="active"){if(id(subscription.schedule)!==schedule.id)throw Error("[payments/connect] Subscription belongs to another schedule");const trialEnd=subscription.trial_end,released=await client.subscriptionSchedules.release(schedule.id,{},{...request,idempotencyKey:key("release")});if(released.id!==schedule.id||released.status!=="released"||id(released.released_subscription)!==subscriptionId)throw Error("[payments/connect] Schedule release did not preserve its original subscription");subscription=await readSubscription();if(subscription.schedule!=null||subscription.trial_end!==trialEnd)throw Error("[payments/connect] Released purchase changed its schedule or native trial");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection")}else if(!["released","completed","canceled"].includes(schedule.status))throw Error("[payments/connect] Purchase schedule has not started")}else{const previous=existing(await allPages((after)=>client.invoices.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request)));if(previous)invoice=await client.invoices.retrieve(previous.id,{},request);else{canCreate();await verifyPrice();invoice=await client.invoices.create({customer:options.customer,currency:options.currency.toLowerCase(),auto_advance:!1,automatic_tax:{enabled:!0},default_payment_method:paymentMethod,pending_invoice_items_behavior:"exclude",metadata},{...request,idempotencyKey:key("create")})}if(!/^in_[a-z0-9_]+$/i.test(invoice.id)||previous&&invoice.id!==previous.id||id(invoice.customer)!==options.customer||!matches(invoice.metadata,metadata))throw Error("[payments/connect] Purchase invoice belongs to another customer or quote");let lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request));if(lines.length===0&&invoice.status==="draft"){if(previous)await verifyPrice();await client.invoiceItems.create({customer:options.customer,invoice:invoice.id,pricing:{price:options.price},quantity:1},{...request,idempotencyKey:key("line")});lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request))}if(lines.length!==1||lines[0].quantity!==1||id(lines[0].pricing?.price_details?.price)!==options.price)throw Error("[payments/connect] Purchase invoice contains different catalog lines")}if(!invoice.automatic_tax.enabled)throw Error("[payments/connect] Purchase invoice no longer uses its original automatic tax policy");return{...await collectDirectInvoice(invoice.id,account,{customer:options.customer,...subscription?{subscription:subscription.id}:{},currency:options.currency,platformFeePercent:options.platformFeePercent,approvedTotal:options.trialDays?0:options.amount,paymentMethod},client),...reloadSubscription?{subscription:await reloadSubscription()}:{}}}
1
+ import{stripe}from"../drivers/stripe";import{idempotencyFingerprint,stacksIdempotencyKey}from"../idempotency";import{asAccount}from"./direct";import{collectDirectInvoice}from"./invoice-collection";import{allPages}from"./pagination";import{manageDestinationSubscription}from"./subscription";export class DirectInvoicePurchaseExpiredError extends Error{}export function directInvoicePurchaseMetadata(account,options){asAccount(account);if(typeof options.reference!=="string"||!/^[a-z0-9:._-]{1,160}$/i.test(options.reference)||!/^cus_[a-z0-9_]+$/i.test(options.customer)||!/^price_[a-z0-9_]+$/i.test(options.price)||!Number.isSafeInteger(options.expiresAt)||options.expiresAt<=0||!Number.isSafeInteger(options.amount)||options.amount<=0||!/^[a-z]{3}$/i.test(options.currency)||!/^txcd_\d{8}$/.test(options.taxCode)||options.interval!==void 0&&!["day","week","month","year"].includes(options.interval)||options.intervalCount!==void 0&&(!options.interval||!Number.isSafeInteger(options.intervalCount)||options.intervalCount<1)||options.trialDays!==void 0&&(!Number.isSafeInteger(options.trialDays)||options.trialDays<0||options.trialDays>730||!options.interval&&options.trialDays>0))throw Error("[payments/connect] Expected original customer, price and frozen invoice purchase terms");if(options.platformFeePercent==null)throw Error("[payments/connect] An explicit invoice fee percentage is required");manageDestinationSubscription.feePercentFor({platformFeePercent:options.platformFeePercent});const metadata=options.metadata??{};if(typeof metadata!=="object"||Array.isArray(metadata)||Object.keys(metadata).length>47||Object.entries(metadata).some(([key,value])=>!key||key.length>40||/[\[\]]/.test(key)||key.startsWith("stacks_purchase_")||typeof value!=="string"||value.length>500))throw Error("[payments/connect] Invalid purchase metadata or reserved binding fields");const terms={account,reference:options.reference,customer:options.customer,price:options.price,amount:options.amount,currency:options.currency.toLowerCase(),taxCode:options.taxCode,expiresAt:options.expiresAt,interval:options.interval??null,intervalCount:options.interval?options.intervalCount??1:null,trialDays:options.trialDays??0,platformFeePercent:options.platformFeePercent,metadata};return{...metadata,stacks_purchase_reference:options.reference,stacks_purchase_quote:idempotencyFingerprint(terms)}}function id(value){return typeof value==="string"?value:value?.id}export async function completeDirectInvoicePurchase(setupIntentId,account,options,client=stripe){const quote=directInvoicePurchaseMetadata(account,options),request=asAccount(account);if(!/^seti_[a-z0-9_]+$/i.test(setupIntentId))throw Error("[payments/connect] Expected original successful payment setup");const setup=await client.setupIntents.retrieve(setupIntentId,{},request),matches=(metadata,expected)=>Object.entries(expected).every(([key,value])=>metadata?.[key]===value);if(setup.id!==setupIntentId||setup.status!=="succeeded"||setup.usage!=="off_session"||!Number.isSafeInteger(setup.created)||setup.created<=0||id(setup.customer)!==options.customer||!/^pm_[a-z0-9_]+$/i.test(id(setup.payment_method)||"")||!matches(setup.metadata,quote))throw Error("[payments/connect] Payment setup does not match the original customer and frozen quote");const paymentMethod=id(setup.payment_method),metadata={...quote,stacks_purchase_setup:setupIntentId},key=(operation)=>stacksIdempotencyKey(`connect.invoice.purchase.${operation}`,account,options.reference,quote.stacks_purchase_quote),canCreate=(rows)=>{if(Date.now()/1000>=Math.min(options.expiresAt,setup.created+86400)){if(rows.some((row)=>!row.metadata?.stacks_purchase_reference||!row.metadata.stacks_purchase_quote||!row.metadata.stacks_purchase_setup||row.metadata.stacks_purchase_setup===setupIntentId))throw Error("[payments/connect] Expired purchase requires review of unbound original-customer resources");throw new DirectInvoicePurchaseExpiredError("[payments/connect] Expired purchase can only recover its original provider invoice or schedule")}},existing=(rows)=>{const own=rows.filter((row)=>row.metadata?.stacks_purchase_reference===options.reference);if(own.length>1||own.some((row)=>!matches(row.metadata,metadata)))throw Error("[payments/connect] Purchase reference has ambiguous or different provider bindings");return own[0]},verifyPrice=async()=>{const price=await client.prices.retrieve(options.price,{expand:["product"]},request),product=price.product;if(price.id!==options.price||!price.active||price.currency!==options.currency.toLowerCase()||price.unit_amount!==options.amount||price.tax_behavior!=="inclusive"||price.type!==(options.interval?"recurring":"one_time")||(options.interval?price.recurring?.interval!==options.interval||price.recurring?.interval_count!==(options.intervalCount??1)||price.recurring?.usage_type!=="licensed":price.recurring!=null)||typeof product==="string"||product.deleted||!product.active||id(product.tax_code)!==options.taxCode)throw Error("[payments/connect] Catalog price does not match the approved inclusive purchase");const method=await client.paymentMethods.retrieve(paymentMethod,{},request);if(method.id!==paymentMethod||id(method.customer)!==options.customer)throw Error("[payments/connect] Purchase payment method belongs to another customer")};let invoice,subscription,reloadSubscription;if(options.interval){const schedules=await allPages((after)=>client.subscriptionSchedules.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request));let schedule=existing(schedules);if(!schedule){canCreate(schedules);await verifyPrice();const phase={items:[{price:options.price,quantity:1}],automatic_tax:{enabled:!0},metadata};schedule=await client.subscriptionSchedules.create({customer:options.customer,start_date:"now",end_behavior:"release",default_settings:{default_payment_method:paymentMethod},phases:options.trialDays?[{...phase,trial:!0,duration:{interval:"day",interval_count:options.trialDays}},phase]:[phase],metadata},{...request,idempotencyKey:key("schedule")})}const subscriptionId=id(schedule.subscription)??id(schedule.released_subscription);if(!/^sub_sched_[a-z0-9_]+$/i.test(schedule.id)||id(schedule.customer)!==options.customer||!matches(schedule.metadata,metadata)||!/^sub_[a-z0-9_]+$/i.test(subscriptionId||""))throw Error("[payments/connect] Purchase schedule has no original customer and subscription");const readSubscription=async()=>{const value=await client.subscriptions.retrieve(subscriptionId,{},request);if(value.id!==subscriptionId||id(value.customer)!==options.customer||!matches(value.metadata,metadata)||value.items.has_more||value.items.data.length!==1||id(value.items.data[0].price)!==options.price||value.items.data[0].quantity!==1)throw Error("[payments/connect] Subscription does not match the original frozen purchase");return value};reloadSubscription=readSubscription;subscription=await readSubscription();const initial=(await allPages((after)=>client.invoices.list({customer:options.customer,subscription:subscriptionId,limit:100,...after?{starting_after:after}:{}},request))).filter((value)=>value.billing_reason==="subscription_create"&&id(value.parent?.subscription_details?.subscription)===subscriptionId);if(initial.length!==1)throw Error("[payments/connect] Purchase has no unique original subscription invoice");invoice=await client.invoices.retrieve(initial[0].id,{},request);if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] First invoice does not match the original subscription quote");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection");if(invoice.status==="draft"&&invoice.auto_advance!==!1){invoice=await client.invoices.update(invoice.id,{auto_advance:!1},{...request,idempotencyKey:key("hold")});if(invoice.id!==initial[0].id||id(invoice.customer)!==options.customer||id(invoice.parent?.subscription_details?.subscription)!==subscriptionId||!matches(invoice.parent?.subscription_details?.metadata,metadata))throw Error("[payments/connect] Held invoice no longer matches the original subscription")}if(schedule.status==="active"){if(id(subscription.schedule)!==schedule.id)throw Error("[payments/connect] Subscription belongs to another schedule");const trialEnd=subscription.trial_end,released=await client.subscriptionSchedules.release(schedule.id,{},{...request,idempotencyKey:key("release")});if(released.id!==schedule.id||released.status!=="released"||id(released.released_subscription)!==subscriptionId)throw Error("[payments/connect] Schedule release did not preserve its original subscription");subscription=await readSubscription();if(subscription.schedule!=null||subscription.trial_end!==trialEnd)throw Error("[payments/connect] Released purchase changed its schedule or native trial");if(invoice.status!=="paid"&&["canceled","incomplete_expired","unpaid"].includes(subscription.status))throw Error("[payments/connect] Subscription no longer permits its initial collection")}else if(!["released","completed","canceled"].includes(schedule.status))throw Error("[payments/connect] Purchase schedule has not started")}else{const invoices=await allPages((after)=>client.invoices.list({customer:options.customer,limit:100,...after?{starting_after:after}:{}},request)),previous=existing(invoices);if(previous)invoice=await client.invoices.retrieve(previous.id,{},request);else{canCreate(invoices);await verifyPrice();invoice=await client.invoices.create({customer:options.customer,currency:options.currency.toLowerCase(),auto_advance:!1,automatic_tax:{enabled:!0},default_payment_method:paymentMethod,pending_invoice_items_behavior:"exclude",metadata},{...request,idempotencyKey:key("create")})}if(!/^in_[a-z0-9_]+$/i.test(invoice.id)||previous&&invoice.id!==previous.id||id(invoice.customer)!==options.customer||!matches(invoice.metadata,metadata))throw Error("[payments/connect] Purchase invoice belongs to another customer or quote");let lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request));if(lines.length===0&&invoice.status==="draft"){if(previous)await verifyPrice();await client.invoiceItems.create({customer:options.customer,invoice:invoice.id,pricing:{price:options.price},quantity:1},{...request,idempotencyKey:key("line")});lines=await allPages((after)=>client.invoices.listLineItems(invoice.id,{limit:100,...after?{starting_after:after}:{}},request))}if(lines.length!==1||lines[0].quantity!==1||id(lines[0].pricing?.price_details?.price)!==options.price)throw Error("[payments/connect] Purchase invoice contains different catalog lines")}if(!invoice.automatic_tax.enabled)throw Error("[payments/connect] Purchase invoice no longer uses its original automatic tax policy");return{...await collectDirectInvoice(invoice.id,account,{customer:options.customer,...subscription?{subscription:subscription.id}:{},currency:options.currency,platformFeePercent:options.platformFeePercent,approvedTotal:options.trialDays?0:options.amount,paymentMethod},client),...reloadSubscription?{subscription:await reloadSubscription()}:{}}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/payments",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.367",
5
+ "version": "0.75.369",
6
6
  "description": "The Stacks payments package. Currently supporting Stripe.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,12 +60,12 @@
60
60
  "prepublishOnly": "bun run build"
61
61
  },
62
62
  "dependencies": {
63
- "@stacksjs/config": "0.75.367",
64
- "@stacksjs/database": "0.75.367",
65
- "@stacksjs/error-handling": "0.75.367",
66
- "@stacksjs/logging": "0.75.367",
67
- "@stacksjs/orm": "0.75.367",
68
- "@stacksjs/types": "0.75.367"
63
+ "@stacksjs/config": "0.75.369",
64
+ "@stacksjs/database": "0.75.369",
65
+ "@stacksjs/error-handling": "0.75.369",
66
+ "@stacksjs/logging": "0.75.369",
67
+ "@stacksjs/orm": "0.75.369",
68
+ "@stacksjs/types": "0.75.369"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@stripe/stripe-js": "^10.0.0",
@@ -80,7 +80,7 @@
80
80
  }
81
81
  },
82
82
  "devDependencies": {
83
- "@stacksjs/utils": "0.75.367",
83
+ "@stacksjs/utils": "0.75.369",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"