@stacksjs/payments 0.74.69 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * What a payments endpoint answers when the signed-in user's model does not
3
+ * declare `traits.billable`.
4
+ *
5
+ * The default User model leaves the trait off, because not every app bills
6
+ * through its users. Without it a user record has none of the billable
7
+ * methods, so an endpoint that reaches for `user.checkout()` has to ask first
8
+ * (`isBillable(user)` from `@stacksjs/orm`) and say which switch is missing,
9
+ * rather than fail with "user.checkout is not a function". One message, so
10
+ * every endpoint names the same fix.
11
+ */
12
+ export declare const BILLING_NOT_ENABLED: 'Billing is not enabled. Set `traits.billable` on the User model to use payments.';
@@ -0,0 +1 @@
1
+ export const BILLING_NOT_ENABLED="Billing is not enabled. Set `traits.billable` on the User model to use payments.";
@@ -1 +1 @@
1
- import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{stripe}from"../drivers/stripe";import{freshIdempotencyKey}from"../idempotency";function defaultCurrency(){const cfg=config||{};return(cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase()}export const manageCharge=(()=>{async function createPayment(user,amount,options){const defaultOptions={currency:defaultCurrency(),amount};if(user.hasStripeId())defaultOptions.customer=user.stripe_id??void 0;const mergedOptions={...defaultOptions,...options};return await stripe.paymentIntents.create(mergedOptions,{idempotencyKey:freshIdempotencyKey("payment_intent.create",user.id,amount)})}async function findPayment(id){try{return await stripe.paymentIntents.retrieve(id)}catch(error){log.error(error);return null}}async function refund(paymentIntentId,options={}){if(!paymentIntentId||typeof paymentIntentId!=="string")throw Error("[payments/refund] paymentIntentId is required");if(options.amount!=null){if(typeof options.amount!=="number"||!Number.isFinite(options.amount)||options.amount<=0)throw Error("[payments/refund] amount must be a positive finite number");try{const original=await stripe.paymentIntents.retrieve(paymentIntentId),captured=original.amount_received??original.amount;if(captured&&options.amount>captured)throw Error(`[payments/refund] amount ${options.amount} exceeds captured amount ${captured}`)}catch(err){if(err instanceof Error&&err.message.startsWith("[payments/refund]"))throw err}}const{idempotencyKey,...rest}=options,refundParams={payment_intent:paymentIntentId,...rest},requestOptions={idempotencyKey:idempotencyKey||`refund:${paymentIntentId}:${rest.amount??"full"}`};return await stripe.refunds.create(refundParams,requestOptions)}async function charge(user,amount,paymentMethod,options){const mergedOptions={...{confirmation_method:"automatic",confirm:!0,payment_method:paymentMethod,currency:defaultCurrency(),amount},...options};return await createPayment(user,amount,mergedOptions)}return{createPayment,charge,findPayment,refund}})();
1
+ import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";import{freshIdempotencyKey}from"../idempotency";function defaultCurrency(){const cfg=config||{};return(cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase()}export const manageCharge=(()=>{async function createPayment(user,amount,options){const defaultOptions={currency:defaultCurrency(),amount};if(manageCustomer.hasStripeId(user))defaultOptions.customer=user.stripe_id??void 0;const mergedOptions={...defaultOptions,...options};return await stripe.paymentIntents.create(mergedOptions,{idempotencyKey:freshIdempotencyKey("payment_intent.create",user.id,amount)})}async function findPayment(id){try{return await stripe.paymentIntents.retrieve(id)}catch(error){log.error(error);return null}}async function refund(paymentIntentId,options={}){if(!paymentIntentId||typeof paymentIntentId!=="string")throw Error("[payments/refund] paymentIntentId is required");if(options.amount!=null){if(typeof options.amount!=="number"||!Number.isFinite(options.amount)||options.amount<=0)throw Error("[payments/refund] amount must be a positive finite number");try{const original=await stripe.paymentIntents.retrieve(paymentIntentId),captured=original.amount_received??original.amount;if(captured&&options.amount>captured)throw Error(`[payments/refund] amount ${options.amount} exceeds captured amount ${captured}`)}catch(err){if(err instanceof Error&&err.message.startsWith("[payments/refund]"))throw err}}const{idempotencyKey,...rest}=options,refundParams={payment_intent:paymentIntentId,...rest},requestOptions={idempotencyKey:idempotencyKey||`refund:${paymentIntentId}:${rest.amount??"full"}`};return await stripe.refunds.create(refundParams,requestOptions)}async function charge(user,amount,paymentMethod,options){const mergedOptions={...{confirmation_method:"automatic",confirm:!0,payment_method:paymentMethod,currency:defaultCurrency(),amount},...options};return await createPayment(user,amount,mergedOptions)}return{createPayment,charge,findPayment,refund}})();
@@ -1,4 +1,5 @@
1
1
  // Core Billable Modules
2
+ export * from './billing-enabled';
2
3
  export * from './charge';
3
4
  export * from './checkout';
4
5
  export * from './customer';
@@ -1 +1 @@
1
- export*from"./charge";export*from"./checkout";export*from"./customer";export*from"./intent";export*from"./invoice";export*from"./payment-method";export*from"./price";export*from"./product";export*from"./setup-products";export*from"./subscription";export*from"./transaction";export*from"./webhook";
1
+ export*from"./billing-enabled";export*from"./charge";export*from"./checkout";export*from"./customer";export*from"./intent";export*from"./invoice";export*from"./payment-method";export*from"./price";export*from"./product";export*from"./setup-products";export*from"./subscription";export*from"./transaction";export*from"./webhook";
@@ -1 +1 @@
1
- import{stripe}from"../drivers/stripe";export const manageInvoice=(()=>{async function list(user){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");if(!user.stripe_id)throw Error("User has no Stripe ID");return await stripe.invoices.list({customer:user.stripe_id,expand:["data.payment_intent.payment_method"]})}return{list}})();
1
+ import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";export const manageInvoice=(()=>{async function list(user){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");if(!user.stripe_id)throw Error("User has no Stripe ID");return await stripe.invoices.list({customer:user.stripe_id,expand:["data.payment_intent.payment_method"]})}return{list}})();
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";import{PaymentMethod}from"@stacksjs/orm";import{stripe}from"../drivers/stripe";export const managePaymentMethod=(()=>{async function addPaymentMethod(user,paymentMethod){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");let stripePaymentMethod;if(typeof paymentMethod==="string")stripePaymentMethod=await stripe.paymentMethods.retrieve(paymentMethod);else stripePaymentMethod=paymentMethod;if(stripePaymentMethod.customer!==user.stripe_id)stripePaymentMethod=await stripe.paymentMethods.attach(stripePaymentMethod.id,{customer:user.stripe_id});await storePaymentMethod(user,stripePaymentMethod);return stripePaymentMethod}async function setUserDefaultPayment(user,paymentMethodId){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");const paymentMethod=await stripe.paymentMethods.retrieve(paymentMethodId),paymentMethodModel=await db.selectFrom("payment_methods").where("provider_id","=",paymentMethodId).selectAll().executeTakeFirst();if(!user.stripe_id)throw Error("User has no Stripe ID");if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});const updatedCustomer=await stripe.customers.update(user.stripe_id,{invoice_settings:{default_payment_method:paymentMethodId}});await db.updateTable("payment_methods").set({is_default:!1}).where("user_id","=",user.id).executeTakeFirst();if(paymentMethodModel)await updateDefault(paymentMethodModel);return updatedCustomer}async function setDefaultPaymentMethod(user,paymentId){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");const pm=await db.selectFrom("payment_methods").where("id","=",paymentId).selectAll().executeTakeFirst();if(!pm?.provider_id)throw Error(`Payment method with id ${paymentId} not found`);const paymentMethod=await stripe.paymentMethods.retrieve(String(pm.provider_id));if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});if(!user.stripe_id)throw Error("User has no Stripe ID");const updatedCustomer=await stripe.customers.update(user.stripe_id,{invoice_settings:{default_payment_method:String(pm.provider_id)}});await db.updateTable("payment_methods").set({is_default:!1}).where("user_id","=",user.id).executeTakeFirst();await db.updateTable("payment_methods").set({is_default:!0}).where("id","=",paymentId).executeTakeFirst();return updatedCustomer}async function storePaymentMethod(user,paymentMethod){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");if(!paymentMethod.card?.brand||!paymentMethod.card?.exp_year||!paymentMethod.card?.exp_month)throw Error("Invalid payment method: missing required card details");const method={type:"card",lastFour:String(paymentMethod.card.last4),brand:paymentMethod.card.brand,expYear:paymentMethod.card.exp_year,expMonth:paymentMethod.card.exp_month,user_id:user.id,providerId:paymentMethod.id};if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});return await PaymentMethod.create(method)}async function updateDefault(paymentMethodModel){return await paymentMethodModel.update({is_default:!0})}async function deletePaymentMethod(user,paymentMethodId){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");const pm=typeof paymentMethodId==="string"?await db.selectFrom("payment_methods").where("provider_id","=",paymentMethodId).selectAll().executeTakeFirst():await PaymentMethod.find(paymentMethodId);if(!pm?.provider_id)throw Error(`Payment method with id ${paymentMethodId} not found`);if((await stripe.paymentMethods.retrieve(String(pm.provider_id))).customer!==user.stripe_id)throw Error("Payment method does not belong to this customer");await pm.delete();return await stripe.paymentMethods.detach(String(pm.provider_id))}async function updatePaymentMethod(user,paymentMethodId,updateParams){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");if((await stripe.paymentMethods.retrieve(paymentMethodId)).customer!==user.stripe_id)throw Error("Payment method does not belong to this customer");return await stripe.paymentMethods.update(paymentMethodId,updateParams)}async function listPaymentMethods(user){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");return await db.selectFrom("payment_methods").selectAll().where("user_id","=",user.id).execute()}async function retrievePaymentMethod(user,paymentMethodId){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");return await db.selectFrom("payment_methods").where("id","=",paymentMethodId).selectAll().executeTakeFirst()}async function retrieveDefaultPaymentMethod(user){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");return await PaymentMethod.where("user_id",user.id).where("is_default",!0).first()}return{addPaymentMethod,deletePaymentMethod,retrieveDefaultPaymentMethod,updatePaymentMethod,listPaymentMethods,setDefaultPaymentMethod,storePaymentMethod,retrievePaymentMethod,setUserDefaultPayment}})();
1
+ import{db}from"@stacksjs/database/runtime";import{PaymentMethod}from"@stacksjs/orm";import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";export const managePaymentMethod=(()=>{async function addPaymentMethod(user,paymentMethod){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");let stripePaymentMethod;if(typeof paymentMethod==="string")stripePaymentMethod=await stripe.paymentMethods.retrieve(paymentMethod);else stripePaymentMethod=paymentMethod;if(stripePaymentMethod.customer!==user.stripe_id)stripePaymentMethod=await stripe.paymentMethods.attach(stripePaymentMethod.id,{customer:user.stripe_id});await storePaymentMethod(user,stripePaymentMethod);return stripePaymentMethod}async function setUserDefaultPayment(user,paymentMethodId){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");const paymentMethod=await stripe.paymentMethods.retrieve(paymentMethodId),paymentMethodModel=await db.selectFrom("payment_methods").where("provider_id","=",paymentMethodId).selectAll().executeTakeFirst();if(!user.stripe_id)throw Error("User has no Stripe ID");if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});const updatedCustomer=await stripe.customers.update(user.stripe_id,{invoice_settings:{default_payment_method:paymentMethodId}});await db.updateTable("payment_methods").set({is_default:!1}).where("user_id","=",user.id).executeTakeFirst();if(paymentMethodModel)await updateDefault(paymentMethodModel);return updatedCustomer}async function setDefaultPaymentMethod(user,paymentId){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");const pm=await db.selectFrom("payment_methods").where("id","=",paymentId).selectAll().executeTakeFirst();if(!pm?.provider_id)throw Error(`Payment method with id ${paymentId} not found`);const paymentMethod=await stripe.paymentMethods.retrieve(String(pm.provider_id));if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});if(!user.stripe_id)throw Error("User has no Stripe ID");const updatedCustomer=await stripe.customers.update(user.stripe_id,{invoice_settings:{default_payment_method:String(pm.provider_id)}});await db.updateTable("payment_methods").set({is_default:!1}).where("user_id","=",user.id).executeTakeFirst();await db.updateTable("payment_methods").set({is_default:!0}).where("id","=",paymentId).executeTakeFirst();return updatedCustomer}async function storePaymentMethod(user,paymentMethod){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");if(!paymentMethod.card?.brand||!paymentMethod.card?.exp_year||!paymentMethod.card?.exp_month)throw Error("Invalid payment method: missing required card details");const method={type:"card",lastFour:String(paymentMethod.card.last4),brand:paymentMethod.card.brand,expYear:paymentMethod.card.exp_year,expMonth:paymentMethod.card.exp_month,user_id:user.id,providerId:paymentMethod.id};if(paymentMethod.customer!==user.stripe_id)await stripe.paymentMethods.attach(paymentMethod.id,{customer:user.stripe_id});return await PaymentMethod.create(method)}async function updateDefault(paymentMethodModel){return await paymentMethodModel.update({is_default:!0})}async function deletePaymentMethod(user,paymentMethodId){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");const pm=typeof paymentMethodId==="string"?await db.selectFrom("payment_methods").where("provider_id","=",paymentMethodId).selectAll().executeTakeFirst():await PaymentMethod.find(paymentMethodId);if(!pm?.provider_id)throw Error(`Payment method with id ${paymentMethodId} not found`);if((await stripe.paymentMethods.retrieve(String(pm.provider_id))).customer!==user.stripe_id)throw Error("Payment method does not belong to this customer");await pm.delete();return await stripe.paymentMethods.detach(String(pm.provider_id))}async function updatePaymentMethod(user,paymentMethodId,updateParams){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");if((await stripe.paymentMethods.retrieve(paymentMethodId)).customer!==user.stripe_id)throw Error("Payment method does not belong to this customer");return await stripe.paymentMethods.update(paymentMethodId,updateParams)}async function listPaymentMethods(user){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");return await db.selectFrom("payment_methods").selectAll().where("user_id","=",user.id).execute()}async function retrievePaymentMethod(user,paymentMethodId){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");return await db.selectFrom("payment_methods").where("id","=",paymentMethodId).selectAll().executeTakeFirst()}async function retrieveDefaultPaymentMethod(user){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");return await PaymentMethod.where("user_id",user.id).where("is_default",!0).first()}return{addPaymentMethod,deletePaymentMethod,retrieveDefaultPaymentMethod,updatePaymentMethod,listPaymentMethods,setDefaultPaymentMethod,storePaymentMethod,retrievePaymentMethod,setUserDefaultPayment}})();
@@ -1 +1 @@
1
- import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";import{managePrice}from"./price";import{stacksIdempotencyKey}from"../idempotency";export const ENTITLING_STATUSES=["active","trialing"],INCOMPLETE_STATUSES=["incomplete"],manageSubscription=(()=>{async function create(user,type,lookupKey,params){const price=await managePrice.retrieveByLookupKey(lookupKey);if(!price)throw Error("Price does not exist in Stripe");const subscriptionItems=[{price:price.id,quantity:1}],mergedParams={...{customer:await manageCustomer.createOrGetStripeUser(user,{}).then((customer)=>{if(!customer||!customer.id)throw Error("Customer does not exist in Stripe");return customer.id}),payment_behavior:"allow_incomplete",expand:["latest_invoice.payment_intent"],items:subscriptionItems},...params},subscription=await stripe.subscriptions.create(mergedParams,{idempotencyKey:stacksIdempotencyKey("subscription.create",user.id,type,lookupKey)});await storeSubscription(user,type,lookupKey,subscription);return subscription}async function update(user,type,lookupKey,_params={}){const newPrice=await managePrice.retrieveByLookupKey(lookupKey),activeSubscription=await user?.activeSubscription();if(!newPrice)throw Error("New price does not exist in Stripe");if(!activeSubscription)throw Error("No active subscription for user!");const subscriptionId=activeSubscription.subscription?.provider_id;if(!subscriptionId)throw Error("Active subscription has no provider ID");const subscription=await stripe.subscriptions.retrieve(subscriptionId);if(!subscription)throw Error("Subscription does not exist in Stripe");const subscriptionItemId=subscription.items.data[0]?.id;if(!subscriptionItemId)throw Error("No subscription items found in the subscription");await stripe.subscriptions.update(subscriptionId,{items:[{id:subscriptionItemId,price:newPrice.id,quantity:1}],proration_behavior:"create_prorations"},{idempotencyKey:stacksIdempotencyKey("subscription.update",subscriptionId,newPrice.id)});const updatedSubscription=await stripe.subscriptions.retrieve(subscriptionId);if(!activeSubscription.subscription?.id)throw Error("Active subscription has no database ID");await updateSubscription(activeSubscription.subscription.id,type,updatedSubscription);return updatedSubscription}async function cancel(subscriptionId,params){if(!await stripe.subscriptions.retrieve(subscriptionId))throw Error("Subscription does not exist or does not belong to the user");const updatedSubscription=await stripe.subscriptions.cancel(subscriptionId,params,{idempotencyKey:stacksIdempotencyKey("subscription.cancel",subscriptionId)});await updateStoredSubscription(subscriptionId);return updatedSubscription}async function retrieve(user,subscriptionId){if(!user.hasStripeId())throw Error("Customer does not exist in Stripe");return await stripe.subscriptions.retrieve(subscriptionId)}async function updateStoredSubscription(subscriptionId){await db.updateTable("subscriptions").set({provider_status:"canceled"}).where("provider_id","=",subscriptionId).executeTakeFirst()}function isActive(subscription){return subscription.provider_status==="active"}function isTrial(subscription){return subscription.provider_status==="trialing"}async function hasSubscriptionInStatus(user,type,statuses){const match=await db.selectFrom("subscriptions").where("user_id","=",user.id).where("type","=",type).whereIn("provider_status",[...statuses]).select(["id"]).limit(1).executeTakeFirst();return Boolean(match)}async function isIncomplete(user,type){return await hasSubscriptionInStatus(user,type,INCOMPLETE_STATUSES)}async function isValid(user,type){return await hasSubscriptionInStatus(user,type,ENTITLING_STATUSES)}async function storeSubscription(user,type,_lookupKey,options){const firstItem=options.items.data[0];if(!firstItem)throw Error("Stripe subscription contains no line items - cannot store subscription");const data=removeNullValues({user_id:user.id,type,unit_price:Number(firstItem.price.unit_amount),provider_id:options.id,provider_status:options.status,provider_price_id:firstItem.price.id,quantity:firstItem.quantity,trial_ends_at:options.trial_end!=null?String(options.trial_end):void 0,ends_at:options.current_period_end!=null?String(options.current_period_end):void 0,provider_type:"stripe",last_used_at:options.current_period_end!=null?String(options.current_period_end):void 0});let subscriptionModelCreated;try{subscriptionModelCreated=await db.insertInto("subscriptions").values(data).executeTakeFirst()}catch(error){if(isUniqueViolation(error))throw new HttpError(409,"A subscription with this provider ID already exists");throw error}if(!subscriptionModelCreated)throw Error("Failed to insert subscription record");return await db.selectFrom("subscriptions").where("id","=",Number(subscriptionModelCreated.insertId)).selectAll().executeTakeFirst()}async function updateSubscription(activeSubId,type,options){const subscription=await db.selectFrom("subscriptions").where("id","=",activeSubId).selectAll().executeTakeFirst(),firstItem=options.items.data[0];if(!firstItem)throw Error("Stripe subscription contains no line items - cannot update subscription");await db?.updateTable("subscriptions").set({type,provider_price_id:firstItem.price.id,unit_price:Number(firstItem.price.unit_amount)}).where("id","=",activeSubId).executeTakeFirst();return subscription}function removeNullValues(obj){return Object.fromEntries(Object.entries(obj).filter(([_,value])=>value!=null))}return{create,update,isValid,isIncomplete,cancel,retrieve}})();
1
+ import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{isUniqueViolation}from"@stacksjs/orm";import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";import{managePrice}from"./price";import{stacksIdempotencyKey}from"../idempotency";export const ENTITLING_STATUSES=["active","trialing"],INCOMPLETE_STATUSES=["incomplete"],manageSubscription=(()=>{async function create(user,type,lookupKey,params){const price=await managePrice.retrieveByLookupKey(lookupKey);if(!price)throw Error("Price does not exist in Stripe");const subscriptionItems=[{price:price.id,quantity:1}],mergedParams={...{customer:await manageCustomer.createOrGetStripeUser(user,{}).then((customer)=>{if(!customer||!customer.id)throw Error("Customer does not exist in Stripe");return customer.id}),payment_behavior:"allow_incomplete",expand:["latest_invoice.payment_intent"],items:subscriptionItems},...params},subscription=await stripe.subscriptions.create(mergedParams,{idempotencyKey:stacksIdempotencyKey("subscription.create",user.id,type,lookupKey)});await storeSubscription(user,type,lookupKey,subscription);return subscription}async function update(user,type,lookupKey,_params={}){const newPrice=await managePrice.retrieveByLookupKey(lookupKey),activeSubscription=await user?.activeSubscription();if(!newPrice)throw Error("New price does not exist in Stripe");if(!activeSubscription)throw Error("No active subscription for user!");const subscriptionId=activeSubscription.subscription?.provider_id;if(!subscriptionId)throw Error("Active subscription has no provider ID");const subscription=await stripe.subscriptions.retrieve(subscriptionId);if(!subscription)throw Error("Subscription does not exist in Stripe");const subscriptionItemId=subscription.items.data[0]?.id;if(!subscriptionItemId)throw Error("No subscription items found in the subscription");await stripe.subscriptions.update(subscriptionId,{items:[{id:subscriptionItemId,price:newPrice.id,quantity:1}],proration_behavior:"create_prorations"},{idempotencyKey:stacksIdempotencyKey("subscription.update",subscriptionId,newPrice.id)});const updatedSubscription=await stripe.subscriptions.retrieve(subscriptionId);if(!activeSubscription.subscription?.id)throw Error("Active subscription has no database ID");await updateSubscription(activeSubscription.subscription.id,type,updatedSubscription);return updatedSubscription}async function cancel(subscriptionId,params){if(!await stripe.subscriptions.retrieve(subscriptionId))throw Error("Subscription does not exist or does not belong to the user");const updatedSubscription=await stripe.subscriptions.cancel(subscriptionId,params,{idempotencyKey:stacksIdempotencyKey("subscription.cancel",subscriptionId)});await updateStoredSubscription(subscriptionId);return updatedSubscription}async function retrieve(user,subscriptionId){if(!manageCustomer.hasStripeId(user))throw Error("Customer does not exist in Stripe");return await stripe.subscriptions.retrieve(subscriptionId)}async function updateStoredSubscription(subscriptionId){await db.updateTable("subscriptions").set({provider_status:"canceled"}).where("provider_id","=",subscriptionId).executeTakeFirst()}function isActive(subscription){return subscription.provider_status==="active"}function isTrial(subscription){return subscription.provider_status==="trialing"}async function hasSubscriptionInStatus(user,type,statuses){const match=await db.selectFrom("subscriptions").where("user_id","=",user.id).where("type","=",type).whereIn("provider_status",[...statuses]).select(["id"]).limit(1).executeTakeFirst();return Boolean(match)}async function isIncomplete(user,type){return await hasSubscriptionInStatus(user,type,INCOMPLETE_STATUSES)}async function isValid(user,type){return await hasSubscriptionInStatus(user,type,ENTITLING_STATUSES)}async function storeSubscription(user,type,_lookupKey,options){const firstItem=options.items.data[0];if(!firstItem)throw Error("Stripe subscription contains no line items - cannot store subscription");const data=removeNullValues({user_id:user.id,type,unit_price:Number(firstItem.price.unit_amount),provider_id:options.id,provider_status:options.status,provider_price_id:firstItem.price.id,quantity:firstItem.quantity,trial_ends_at:options.trial_end!=null?String(options.trial_end):void 0,ends_at:options.current_period_end!=null?String(options.current_period_end):void 0,provider_type:"stripe",last_used_at:options.current_period_end!=null?String(options.current_period_end):void 0});let subscriptionModelCreated;try{subscriptionModelCreated=await db.insertInto("subscriptions").values(data).executeTakeFirst()}catch(error){if(isUniqueViolation(error))throw new HttpError(409,"A subscription with this provider ID already exists");throw error}if(!subscriptionModelCreated)throw Error("Failed to insert subscription record");return await db.selectFrom("subscriptions").where("id","=",Number(subscriptionModelCreated.insertId)).selectAll().executeTakeFirst()}async function updateSubscription(activeSubId,type,options){const subscription=await db.selectFrom("subscriptions").where("id","=",activeSubId).selectAll().executeTakeFirst(),firstItem=options.items.data[0];if(!firstItem)throw Error("Stripe subscription contains no line items - cannot update subscription");await db?.updateTable("subscriptions").set({type,provider_price_id:firstItem.price.id,unit_price:Number(firstItem.price.unit_amount)}).where("id","=",activeSubId).executeTakeFirst();return subscription}function removeNullValues(obj){return Object.fromEntries(Object.entries(obj).filter(([_,value])=>value!=null))}return{create,update,isValid,isIncomplete,cancel,retrieve}})();
@@ -1 +1 @@
1
- import{config}from"@stacksjs/config";import{stripe}from"../drivers/stripe";import{freshIdempotencyKey}from"../idempotency";import{requireAccountId}from"./account";function defaultCurrency(){const cfg=config||{};return(cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase()}function configuredFeePercent(){return(config||{}).payment?.connect?.platformFeePercent}export const manageDestinationCharge=(()=>{function applicationFeeFor(amount,options={}){if(options.applicationFeeAmount!=null){if(!Number.isFinite(options.applicationFeeAmount)||options.applicationFeeAmount<0)throw Error("[payments/connect] applicationFeeAmount must be a non-negative number of minor units");if(options.applicationFeeAmount>amount)throw Error(`[payments/connect] applicationFeeAmount ${options.applicationFeeAmount} exceeds the charge amount ${amount}`);return Math.floor(options.applicationFeeAmount)}const percent=options.platformFeePercent??configuredFeePercent();if(percent==null)return;if(!Number.isFinite(percent)||percent<0||percent>100)throw Error(`[payments/connect] platformFeePercent must be between 0 and 100, got ${percent}`);return Math.floor(amount*percent/100)}async function destinationCharge(user,amount,destinationAccountId,options={}){requireAccountId(destinationAccountId,"destinationCharge");if(!Number.isFinite(amount)||amount<=0)throw Error("[payments/connect] destinationCharge requires a positive amount in minor units");const{applicationFeeAmount,platformFeePercent,transferGroup,...intentParams}=options,fee=applicationFeeFor(amount,{applicationFeeAmount,platformFeePercent}),params={amount,currency:defaultCurrency(),transfer_data:{destination:destinationAccountId},...fee!=null&&fee>0?{application_fee_amount:fee}:{},...transferGroup?{transfer_group:transferGroup}:{},...intentParams};if(!params.customer&&user.hasStripeId())params.customer=user.stripe_id??void 0;return await stripe.paymentIntents.create(params,{idempotencyKey:freshIdempotencyKey("connect.destination_charge",user.id,amount)})}return{destinationCharge,applicationFeeFor}})();
1
+ import{config}from"@stacksjs/config";import{stripe}from"../drivers/stripe";import{manageCustomer}from"../billable/customer";import{freshIdempotencyKey}from"../idempotency";import{requireAccountId}from"./account";function defaultCurrency(){const cfg=config||{};return(cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase()}function configuredFeePercent(){return(config||{}).payment?.connect?.platformFeePercent}export const manageDestinationCharge=(()=>{function applicationFeeFor(amount,options={}){if(options.applicationFeeAmount!=null){if(!Number.isFinite(options.applicationFeeAmount)||options.applicationFeeAmount<0)throw Error("[payments/connect] applicationFeeAmount must be a non-negative number of minor units");if(options.applicationFeeAmount>amount)throw Error(`[payments/connect] applicationFeeAmount ${options.applicationFeeAmount} exceeds the charge amount ${amount}`);return Math.floor(options.applicationFeeAmount)}const percent=options.platformFeePercent??configuredFeePercent();if(percent==null)return;if(!Number.isFinite(percent)||percent<0||percent>100)throw Error(`[payments/connect] platformFeePercent must be between 0 and 100, got ${percent}`);return Math.floor(amount*percent/100)}async function destinationCharge(user,amount,destinationAccountId,options={}){requireAccountId(destinationAccountId,"destinationCharge");if(!Number.isFinite(amount)||amount<=0)throw Error("[payments/connect] destinationCharge requires a positive amount in minor units");const{applicationFeeAmount,platformFeePercent,transferGroup,...intentParams}=options,fee=applicationFeeFor(amount,{applicationFeeAmount,platformFeePercent}),params={amount,currency:defaultCurrency(),transfer_data:{destination:destinationAccountId},...fee!=null&&fee>0?{application_fee_amount:fee}:{},...transferGroup?{transfer_group:transferGroup}:{},...intentParams};if(!params.customer&&manageCustomer.hasStripeId(user))params.customer=user.stripe_id??void 0;return await stripe.paymentIntents.create(params,{idempotencyKey:freshIdempotencyKey("connect.destination_charge",user.id,amount)})}return{destinationCharge,applicationFeeFor}})();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/payments",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.69",
5
+ "version": "0.75.0",
6
6
  "description": "The Stacks payments package. Currently supporting Stripe.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,11 +60,11 @@
60
60
  "prepublishOnly": "bun run build"
61
61
  },
62
62
  "dependencies": {
63
- "@stacksjs/config": "0.74.69",
64
- "@stacksjs/database": "0.74.69",
65
- "@stacksjs/error-handling": "0.74.69",
66
- "@stacksjs/logging": "0.74.69",
67
- "@stacksjs/orm": "0.74.69"
63
+ "@stacksjs/config": "0.75.0",
64
+ "@stacksjs/database": "0.75.0",
65
+ "@stacksjs/error-handling": "0.75.0",
66
+ "@stacksjs/logging": "0.75.0",
67
+ "@stacksjs/orm": "0.75.0"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@stripe/stripe-js": "^9.10.0",
@@ -79,7 +79,7 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@stacksjs/utils": "0.74.69",
82
+ "@stacksjs/utils": "0.75.0",
83
83
  "@stripe/stripe-js": "^9.10.0",
84
84
  "better-dx": "^0.2.24",
85
85
  "stripe": "22.3.2"