@stacksjs/payments 0.75.82 → 0.75.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billable/subscription.d.ts +7 -0
- package/dist/billable/subscription.js +1 -1
- package/dist/driver/adyen.js +1 -1
- package/dist/driver/index.d.ts +8 -0
- package/dist/driver/index.js +1 -1
- package/dist/driver/stripe.d.ts +8 -0
- package/dist/driver/stripe.js +1 -1
- package/dist/driver/types.d.ts +1 -0
- package/dist/payment.d.ts +58 -85
- package/dist/payment.js +1 -1
- package/package.json +8 -8
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { UserModel } from '@stacksjs/orm';
|
|
2
2
|
import type Stripe from 'stripe';
|
|
3
|
+
/**
|
|
4
|
+
* A Stripe timestamp (Unix seconds) as a database timestamp. These columns
|
|
5
|
+
* are timestamps, and were written the raw seconds as text (`"1790000000"`),
|
|
6
|
+
* which nothing reading them as dates could use.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stripeTimestamp(seconds: number | null | undefined): string | undefined;
|
|
3
9
|
/**
|
|
4
10
|
* The provider statuses that mean the customer currently has what they paid for.
|
|
5
11
|
*
|
|
@@ -25,6 +31,7 @@ export declare interface SubscriptionManager {
|
|
|
25
31
|
create: (user: UserModel, type: string, lookupKey: string, params: Partial<Stripe.SubscriptionCreateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
26
32
|
update: (user: UserModel, type: string, lookupKey: string, params: Partial<Stripe.SubscriptionUpdateParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
27
33
|
cancel: (subscriptionId: string, params?: Partial<Stripe.SubscriptionCancelParams>) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
34
|
+
cancelAtPeriodEnd: (subscriptionId: string) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
28
35
|
retrieve: (user: UserModel, subscriptionId: string) => Promise<Stripe.Response<Stripe.Subscription>>
|
|
29
36
|
isValid: (user: UserModel, type: string) => Promise<boolean>
|
|
30
37
|
isIncomplete: (user: UserModel, type: string) => Promise<boolean>
|
|
@@ -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"],SUBSCRIPTION_CREATE_EXPAND=["latest_invoice.confirmation_secret"]
|
|
1
|
+
import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{formatDate,isUniqueViolation}from"@stacksjs/orm";import{stripe}from"../drivers/stripe";import{manageCustomer}from"./customer";import{managePrice}from"./price";import{subscriptionPeriodEnd}from"../driver/stripe";import{stacksIdempotencyKey}from"../idempotency";export const ENTITLING_STATUSES=["active","trialing"],INCOMPLETE_STATUSES=["incomplete"],SUBSCRIPTION_CREATE_EXPAND=["latest_invoice.confirmation_secret"];export function stripeTimestamp(seconds){return typeof seconds==="number"?formatDate(new Date(seconds*1000)):void 0}export const 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:[...SUBSCRIPTION_CREATE_EXPAND],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 cancelAtPeriodEnd(subscriptionId){const subscription=await stripe.subscriptions.update(subscriptionId,{cancel_at_period_end:!0},{idempotencyKey:stacksIdempotencyKey("subscription.cancel_at_period_end",subscriptionId)}),endsAt=stripeTimestamp(subscriptionPeriodEnd(subscription));if(endsAt)await db.updateTable("subscriptions").set({ends_at:endsAt}).where("provider_id","=",subscriptionId).executeTakeFirst();return subscription}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:stripeTimestamp(options.trial_end),ends_at:stripeTimestamp(subscriptionPeriodEnd(options)),provider_type:"stripe",last_used_at:stripeTimestamp(subscriptionPeriodEnd(options))});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,cancelAtPeriodEnd,retrieve}})();
|
package/dist/driver/adyen.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";import{assertMoney,PaymentProviderError,PaymentUnsupportedError,WebhookNotConfiguredError,WebhookSignatureError}from"./types";export const ADYEN_API_VERSION="v72";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","webhooks"]);export function adyenStatus(resultCode){switch(resultCode){case"Authorised":return"succeeded";case"Refused":case"Error":return"failed";case"Cancelled":return"canceled";case"RedirectShopper":case"ChallengeShopper":case"IdentifyShopper":case"PresentToShopper":return"requires_action";default:return"processing"}}const EVENT_TYPES={AUTHORISATION:["payment.succeeded","payment.failed"],CANCELLATION:["payment.canceled","unknown"],REFUND:["refund.succeeded","refund.failed"],REFUND_FAILED:["refund.failed","refund.failed"],CANCEL_OR_REFUND:["refund.succeeded","refund.failed"]};export function adyenSigningPayload(item){return[item.pspReference??"",item.originalReference??"",item.merchantAccountCode??"",item.merchantReference??"",item.amount?.value??"",item.amount?.currency??"",item.eventCode??"",String(item.success??"")].join(":")}export function adyenSignature(item,hmacKey){return createHmac("sha256",Buffer.from(hmacKey,"hex")).update(adyenSigningPayload(item),"utf8").digest("base64")}export class AdyenDriver{name="adyen";capabilities=CAPABILITIES;config;fetch;constructor(config,options={}){if(!config.apiKey)throw Error("The Adyen payment driver needs an API key: set ADYEN_API_KEY or payment.adyen.apiKey.");if(!config.merchantAccount)throw Error("The Adyen payment driver needs a merchant account: set ADYEN_MERCHANT_ACCOUNT or payment.adyen.merchantAccount.");if(config.environment==="live"&&!config.liveUrlPrefix)throw Error("Adyen live requests go to your account's own endpoint: set ADYEN_LIVE_URL_PREFIX or payment.adyen.liveUrlPrefix.");this.config=config;this.fetch=options.fetch??fetch}get baseUrl(){return this.config.environment==="live"?`https://${this.config.liveUrlPrefix}-checkout-live.adyenpayments.com/checkout/${ADYEN_API_VERSION}`:`https://checkout-test.adyen.com/${ADYEN_API_VERSION}`}shopperReference(payer){return`${this.config.shopperReferencePrefix??"user-"}${payer.id}`}async request(method,path,options={}){const url=new URL(`${this.baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{}))url.searchParams.set(key,value);const headers={"X-API-Key":this.config.apiKey,Accept:"application/json"};if(options.body!==void 0)headers["Content-Type"]="application/json";if(options.idempotencyKey)headers["Idempotency-Key"]=options.idempotencyKey;const response=await this.fetch(url,{method,headers,body:options.body===void 0?void 0:JSON.stringify(options.body)});if(response.status===204)return;const text=await response.text(),body=text?JSON.parse(text):void 0;if(!response.ok){const error=body;throw new PaymentProviderError("adyen",response.status,error?.message??response.statusText,error?.errorCode??null)}return body}assertReturnUrl(url,label){let target;try{target=new URL(url)}catch{throw TypeError(`${label} is not a valid URL: ${url}`)}if(!this.config.appUrl)return;const app=new URL(/^https?:\/\//.test(this.config.appUrl)?this.config.appUrl:`https://${this.config.appUrl}`);if(target.origin!==app.origin)throw TypeError(`${label} (${target.origin}) is not on the application's origin (${app.origin})`)}amount(money){assertMoney(money);return{value:money.amount,currency:money.currency.toUpperCase()}}async customer(payer){return{id:this.shopperReference(payer),email:payer.email??null,name:payer.name??null}}async charge(payer,amount,paymentMethod,options={}){const response=await this.request("POST","/payments",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),paymentMethod:{type:"scheme",storedPaymentMethodId:paymentMethod},shopperReference:this.shopperReference(payer),shopperInteraction:"ContAuth",recurringProcessingModel:"UnscheduledCardOnFile",...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,status:adyenStatus(response.resultCode),amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},...response.action?.url?{redirectUrl:response.action.url}:{},...response.refusalReason?{failureReason:response.refusalReason}:{},raw:response}}async createPayment(payer,amount,options={}){if(!options.returnUrl)throw TypeError("An Adyen payment session needs a returnUrl: where the payer comes back after authenticating.");this.assertReturnUrl(options.returnUrl,"returnUrl");const session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),returnUrl:options.returnUrl,mode:"embedded",shopperReference:this.shopperReference(payer),...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:session.id,status:"requires_payment_method",amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},clientConfirmation:{provider:"adyen",sessionId:session.id,sessionData:session.sessionData},raw:session}}async refund(paymentId,options={}){const reference=randomUUID(),response=options.amount?await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/refunds`,{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(options.amount),reference},idempotencyKey:options.idempotencyKey}):await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/reversals`,{body:{merchantAccount:this.config.merchantAccount,reference},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,paymentId,status:"pending",amount:options.amount?{amount:options.amount.amount,currency:options.amount.currency.toLowerCase()}:null,raw:response}}async checkout(payer,request){if(request.mode==="subscription")throw new PaymentUnsupportedError("adyen","subscription checkout",'Adyen has no subscriptions; store a card with mode "setup" and charge it on your schedule');if(request.cancelUrl&&request.cancelUrl!==request.successUrl)throw new PaymentUnsupportedError("adyen","a separate cancelUrl","Adyen's hosted checkout returns to one URL, with the outcome in its query string");if(request.allowPromotionCodes)throw new PaymentUnsupportedError("adyen","promotion codes","Adyen has no coupons; discount the line amounts instead");if(request.automaticTax)throw new PaymentUnsupportedError("adyen","automatic tax","Adyen does not calculate tax; price each line including it");if(!request.currency)throw TypeError("An Adyen checkout needs a currency.");this.assertReturnUrl(request.successUrl,"successUrl");const lines=request.lines.map((line)=>{if("price"in line)throw new PaymentUnsupportedError("adyen","catalog prices","Adyen has no product catalog; price each line with name and unitAmount");return line}),total=lines.reduce((sum,line)=>sum+line.unitAmount*line.quantity,0),setup=request.mode==="setup",session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount({amount:setup?0:total,currency:request.currency}),reference:request.reference??randomUUID(),returnUrl:request.successUrl,mode:"hosted",shopperReference:this.shopperReference(payer),...payer.email?{shopperEmail:payer.email}:{},...setup?{storePaymentMethodMode:"enabled",recurringProcessingModel:"UnscheduledCardOnFile"}:{},...setup?{}:{lineItems:lines.map((line)=>({description:line.name,quantity:line.quantity,amountIncludingTax:line.unitAmount}))},...request.metadata?{metadata:request.metadata}:{}}});if(!session.url)throw new PaymentProviderError("adyen",200,"the hosted session came back without a url");return{id:session.id,url:session.url,expiresAt:session.expiresAt?new Date(session.expiresAt):null,raw:session}}async paymentMethods(payer){return((await this.request("GET","/storedPaymentMethods",{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}}))?.storedPaymentMethods??[]).map((method)=>({id:String(method.id),type:String(method.type??"scheme"),brand:method.brand??null,last4:method.lastFour??null,expMonth:method.expiryMonth?Number(method.expiryMonth):null,expYear:method.expiryYear?Number(String(method.expiryYear).length===2?`20${method.expiryYear}`:method.expiryYear):null,isDefault:!1,raw:method}))}async removePaymentMethod(payer,paymentMethodId){await this.request("DELETE",`/storedPaymentMethods/${encodeURIComponent(paymentMethodId)}`,{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}})}async subscribe(){throw new PaymentUnsupportedError("adyen","subscriptions","charge a stored card on your own schedule with charge()")}async cancelSubscription(){throw new PaymentUnsupportedError("adyen","subscriptions")}async subscriptions(){throw new PaymentUnsupportedError("adyen","subscriptions")}async verifyWebhook(request){if(!this.config.hmacKey)throw new WebhookNotConfiguredError("adyen","Adyen webhooks cannot be verified without the HMAC key: set ADYEN_HMAC_KEY or payment.adyen.hmacKey.");let body;try{body=JSON.parse(request.payload)}catch{throw new WebhookSignatureError("adyen","the body is not JSON")}const items=(body.notificationItems??[]).map((entry)=>entry.NotificationRequestItem).filter((item)=>Boolean(item));if(items.length===0)throw new WebhookSignatureError("adyen","the delivery holds no notification items");return items.map((item)=>{const given=item.additionalData?.hmacSignature;if(!given)throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} has no hmacSignature`);const expected=Buffer.from(adyenSignature(item,this.config.hmacKey)),actual=Buffer.from(given);if(expected.length!==actual.length||!timingSafeEqual(expected,actual))throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} is signed with another key`);const success=String(item.success)==="true",pair=EVENT_TYPES[item.eventCode??""];return{provider:this.name,id:`${item.eventCode}:${item.pspReference}:${success}`,type:pair?pair[success?0:1]:"unknown",providerType:item.eventCode??"",reference:item.originalReference||item.pspReference||null,reason:item.reason||null,merchantReference:item.merchantReference??null,amount:item.amount?.value!==void 0&&item.amount.currency?{amount:Number(item.amount.value),currency:item.amount.currency.toLowerCase()}:null,raw:item}})}acknowledgeWebhook(){return new Response("[accepted]",{status:202,headers:{"Content-Type":"text/plain"}})}}
|
|
1
|
+
import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";import{assertMoney,PaymentProviderError,PaymentUnsupportedError,WebhookNotConfiguredError,WebhookSignatureError}from"./types";export const ADYEN_API_VERSION="v72";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","webhooks"]);export function adyenStatus(resultCode){switch(resultCode){case"Authorised":return"succeeded";case"Refused":case"Error":return"failed";case"Cancelled":return"canceled";case"RedirectShopper":case"ChallengeShopper":case"IdentifyShopper":case"PresentToShopper":return"requires_action";default:return"processing"}}const EVENT_TYPES={AUTHORISATION:["payment.succeeded","payment.failed"],CANCELLATION:["payment.canceled","unknown"],REFUND:["refund.succeeded","refund.failed"],REFUND_FAILED:["refund.failed","refund.failed"],CANCEL_OR_REFUND:["refund.succeeded","refund.failed"]};export function adyenSigningPayload(item){return[item.pspReference??"",item.originalReference??"",item.merchantAccountCode??"",item.merchantReference??"",item.amount?.value??"",item.amount?.currency??"",item.eventCode??"",String(item.success??"")].join(":")}export function adyenSignature(item,hmacKey){return createHmac("sha256",Buffer.from(hmacKey,"hex")).update(adyenSigningPayload(item),"utf8").digest("base64")}export class AdyenDriver{name="adyen";capabilities=CAPABILITIES;config;fetch;constructor(config,options={}){if(!config.apiKey)throw Error("The Adyen payment driver needs an API key: set ADYEN_API_KEY or payment.adyen.apiKey.");if(!config.merchantAccount)throw Error("The Adyen payment driver needs a merchant account: set ADYEN_MERCHANT_ACCOUNT or payment.adyen.merchantAccount.");if(config.environment==="live"&&!config.liveUrlPrefix)throw Error("Adyen live requests go to your account's own endpoint: set ADYEN_LIVE_URL_PREFIX or payment.adyen.liveUrlPrefix.");this.config=config;this.fetch=options.fetch??fetch}get baseUrl(){return this.config.environment==="live"?`https://${this.config.liveUrlPrefix}-checkout-live.adyenpayments.com/checkout/${ADYEN_API_VERSION}`:`https://checkout-test.adyen.com/${ADYEN_API_VERSION}`}shopperReference(payer){return`${this.config.shopperReferencePrefix??"user-"}${payer.id}`}async request(method,path,options={}){const url=new URL(`${this.baseUrl}${path}`);for(const[key,value]of Object.entries(options.query??{}))url.searchParams.set(key,value);const headers={"X-API-Key":this.config.apiKey,Accept:"application/json"};if(options.body!==void 0)headers["Content-Type"]="application/json";if(options.idempotencyKey)headers["Idempotency-Key"]=options.idempotencyKey;const response=await this.fetch(url,{method,headers,body:options.body===void 0?void 0:JSON.stringify(options.body)});if(response.status===204)return;const text=await response.text(),body=text?JSON.parse(text):void 0;if(!response.ok){const error=body;throw new PaymentProviderError("adyen",response.status,error?.message??response.statusText,error?.errorCode??null)}return body}assertReturnUrl(url,label){let target;try{target=new URL(url)}catch{throw TypeError(`${label} is not a valid URL: ${url}`)}if(!this.config.appUrl)return;const app=new URL(/^https?:\/\//.test(this.config.appUrl)?this.config.appUrl:`https://${this.config.appUrl}`);if(target.origin!==app.origin)throw TypeError(`${label} (${target.origin}) is not on the application's origin (${app.origin})`)}amount(money){assertMoney(money);return{value:money.amount,currency:money.currency.toUpperCase()}}async customer(payer){return{id:this.shopperReference(payer),email:payer.email??null,name:payer.name??null}}async charge(payer,amount,paymentMethod,options={}){const response=await this.request("POST","/payments",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),paymentMethod:{type:"scheme",storedPaymentMethodId:paymentMethod},shopperReference:this.shopperReference(payer),shopperInteraction:"ContAuth",recurringProcessingModel:"UnscheduledCardOnFile",...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,status:adyenStatus(response.resultCode),amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},...response.action?.url?{redirectUrl:response.action.url}:{},...response.refusalReason?{failureReason:response.refusalReason}:{},raw:response}}async createPayment(payer,amount,options={}){if(!options.returnUrl)throw TypeError("An Adyen payment session needs a returnUrl: where the payer comes back after authenticating.");this.assertReturnUrl(options.returnUrl,"returnUrl");const session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(amount),reference:options.reference??randomUUID(),returnUrl:options.returnUrl,mode:"embedded",shopperReference:this.shopperReference(payer),...options.metadata?{metadata:options.metadata}:{}},idempotencyKey:options.idempotencyKey});return{id:session.id,status:"requires_payment_method",amount:{amount:amount.amount,currency:amount.currency.toLowerCase()},clientConfirmation:{provider:"adyen",sessionId:session.id,sessionData:session.sessionData},raw:session}}async refund(paymentId,options={}){const reference=randomUUID(),response=options.amount?await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/refunds`,{body:{merchantAccount:this.config.merchantAccount,amount:this.amount(options.amount),reference},idempotencyKey:options.idempotencyKey}):await this.request("POST",`/payments/${encodeURIComponent(paymentId)}/reversals`,{body:{merchantAccount:this.config.merchantAccount,reference},idempotencyKey:options.idempotencyKey});return{id:response.pspReference,paymentId,status:"pending",amount:options.amount?{amount:options.amount.amount,currency:options.amount.currency.toLowerCase()}:null,raw:response}}async checkout(payer,request){if(request.trialDays&&request.mode!=="subscription")throw TypeError("trialDays applies to a subscription checkout.");if(request.mode==="subscription")throw new PaymentUnsupportedError("adyen","subscription checkout",'Adyen has no subscriptions; store a card with mode "setup" and charge it on your schedule');if(request.cancelUrl&&request.cancelUrl!==request.successUrl)throw new PaymentUnsupportedError("adyen","a separate cancelUrl","Adyen's hosted checkout returns to one URL, with the outcome in its query string");if(request.allowPromotionCodes)throw new PaymentUnsupportedError("adyen","promotion codes","Adyen has no coupons; discount the line amounts instead");if(request.automaticTax)throw new PaymentUnsupportedError("adyen","automatic tax","Adyen does not calculate tax; price each line including it");if(!request.currency)throw TypeError("An Adyen checkout needs a currency.");this.assertReturnUrl(request.successUrl,"successUrl");const lines=request.lines.map((line)=>{if("price"in line)throw new PaymentUnsupportedError("adyen","catalog prices","Adyen has no product catalog; price each line with name and unitAmount");return line}),total=lines.reduce((sum,line)=>sum+line.unitAmount*line.quantity,0),setup=request.mode==="setup",session=await this.request("POST","/sessions",{body:{merchantAccount:this.config.merchantAccount,amount:this.amount({amount:setup?0:total,currency:request.currency}),reference:request.reference??randomUUID(),returnUrl:request.successUrl,mode:"hosted",shopperReference:this.shopperReference(payer),...payer.email?{shopperEmail:payer.email}:{},...setup?{storePaymentMethodMode:"enabled",recurringProcessingModel:"UnscheduledCardOnFile"}:{},...setup?{}:{lineItems:lines.map((line)=>({description:line.name,quantity:line.quantity,amountIncludingTax:line.unitAmount}))},...request.metadata?{metadata:request.metadata}:{}}});if(!session.url)throw new PaymentProviderError("adyen",200,"the hosted session came back without a url");return{id:session.id,url:session.url,expiresAt:session.expiresAt?new Date(session.expiresAt):null,raw:session}}async paymentMethods(payer){return((await this.request("GET","/storedPaymentMethods",{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}}))?.storedPaymentMethods??[]).map((method)=>({id:String(method.id),type:String(method.type??"scheme"),brand:method.brand??null,last4:method.lastFour??null,expMonth:method.expiryMonth?Number(method.expiryMonth):null,expYear:method.expiryYear?Number(String(method.expiryYear).length===2?`20${method.expiryYear}`:method.expiryYear):null,isDefault:!1,raw:method}))}async removePaymentMethod(payer,paymentMethodId){await this.request("DELETE",`/storedPaymentMethods/${encodeURIComponent(paymentMethodId)}`,{query:{merchantAccount:this.config.merchantAccount,shopperReference:this.shopperReference(payer)}})}async subscribe(){throw new PaymentUnsupportedError("adyen","subscriptions","charge a stored card on your own schedule with charge()")}async cancelSubscription(){throw new PaymentUnsupportedError("adyen","subscriptions")}async subscriptions(){throw new PaymentUnsupportedError("adyen","subscriptions")}async verifyWebhook(request){if(!this.config.hmacKey)throw new WebhookNotConfiguredError("adyen","Adyen webhooks cannot be verified without the HMAC key: set ADYEN_HMAC_KEY or payment.adyen.hmacKey.");let body;try{body=JSON.parse(request.payload)}catch{throw new WebhookSignatureError("adyen","the body is not JSON")}const items=(body.notificationItems??[]).map((entry)=>entry.NotificationRequestItem).filter((item)=>Boolean(item));if(items.length===0)throw new WebhookSignatureError("adyen","the delivery holds no notification items");return items.map((item)=>{const given=item.additionalData?.hmacSignature;if(!given)throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} has no hmacSignature`);const expected=Buffer.from(adyenSignature(item,this.config.hmacKey)),actual=Buffer.from(given);if(expected.length!==actual.length||!timingSafeEqual(expected,actual))throw new WebhookSignatureError("adyen",`item ${item.pspReference??"?"} is signed with another key`);const success=String(item.success)==="true",pair=EVENT_TYPES[item.eventCode??""];return{provider:this.name,id:`${item.eventCode}:${item.pspReference}:${success}`,type:pair?pair[success?0:1]:"unknown",providerType:item.eventCode??"",reference:item.originalReference||item.pspReference||null,reason:item.reason||null,merchantReference:item.merchantReference??null,amount:item.amount?.value!==void 0&&item.amount.currency?{amount:Number(item.amount.value),currency:item.amount.currency.toLowerCase()}:null,raw:item}})}acknowledgeWebhook(){return new Response("[accepted]",{status:202,headers:{"Content-Type":"text/plain"}})}}
|
package/dist/driver/index.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ import type { StripeOperations } from './stripe';
|
|
|
9
9
|
export declare function registerPaymentDriver(name: string, factory: DriverFactory): void;
|
|
10
10
|
/** Adyen's settings: config/payment.ts, then the ADYEN_* environment. */
|
|
11
11
|
export declare function adyenConfig(settings?: Partial<AdyenConfig>, env?: Record<string, string | undefined>): AdyenConfig;
|
|
12
|
+
/** The driver `config.payment.driver` names: `stripe` unless it says otherwise. */
|
|
13
|
+
export declare function configuredPaymentDriver(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Refuse a Stripe-only operation when the app pays through another provider,
|
|
16
|
+
* naming the operation - rather than reaching Stripe with keys the app does
|
|
17
|
+
* not have, or quietly mixing two providers' customers.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertStripeDriver(operation: string): void;
|
|
12
20
|
/**
|
|
13
21
|
* The payment driver `config.payment.driver` names - `stripe` unless it says
|
|
14
22
|
* otherwise - or the one named here. This is the first thing to read that
|
package/dist/driver/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{config,services}from"@stacksjs/config";import{manageCharge}from"../billable/charge";import{manageCheckout}from"../billable/checkout";import{manageCustomer}from"../billable/customer";import{managePaymentMethod}from"../billable/payment-method";import{manageSubscription}from"../billable/subscription";import{constructEvent}from"../billable/webhook";import{stripe}from"../drivers/stripe";import{freshIdempotencyKey}from"../idempotency";import{AdyenDriver}from"./adyen";import{StripeDriver}from"./stripe";export*from"./adyen";export*from"./stripe";export*from"./types";export const defaultStripeOperations={customer:(payer)=>manageCustomer.createOrGetStripeUser(payer,{}),createIntent:(payer,params,idempotencyKey)=>stripe.paymentIntents.create(params,{idempotencyKey:idempotencyKey??freshIdempotencyKey("payment_intent.create",payer.id,params.amount)}),refund:(paymentIntentId,params)=>manageCharge.refund(paymentIntentId,params),createCheckout:(payer,params)=>manageCheckout.create(payer,params),listPaymentMethods:async(customerId)=>(await stripe.paymentMethods.list({customer:customerId,limit:100})).data,deletePaymentMethod:(payer,paymentMethodId)=>managePaymentMethod.deletePaymentMethod(payer,paymentMethodId),subscribe:(payer,type,lookupKey)=>manageSubscription.create(payer,type,lookupKey,{}),cancelSubscription:(subscriptionId,atPeriodEnd)=>atPeriodEnd?
|
|
1
|
+
import process from"node:process";import{config,services}from"@stacksjs/config";import{manageCharge}from"../billable/charge";import{manageCheckout}from"../billable/checkout";import{manageCustomer}from"../billable/customer";import{managePaymentMethod}from"../billable/payment-method";import{manageSubscription}from"../billable/subscription";import{constructEvent}from"../billable/webhook";import{stripe}from"../drivers/stripe";import{freshIdempotencyKey}from"../idempotency";import{AdyenDriver}from"./adyen";import{PaymentUnsupportedError}from"./types";import{StripeDriver}from"./stripe";export*from"./adyen";export*from"./stripe";export*from"./types";export const defaultStripeOperations={customer:(payer)=>manageCustomer.createOrGetStripeUser(payer,{}),createIntent:(payer,params,idempotencyKey)=>stripe.paymentIntents.create(params,{idempotencyKey:idempotencyKey??freshIdempotencyKey("payment_intent.create",payer.id,params.amount)}),refund:(paymentIntentId,params)=>manageCharge.refund(paymentIntentId,params),createCheckout:(payer,params)=>manageCheckout.create(payer,params),listPaymentMethods:async(customerId)=>(await stripe.paymentMethods.list({customer:customerId,limit:100})).data,deletePaymentMethod:(payer,paymentMethodId)=>managePaymentMethod.deletePaymentMethod(payer,paymentMethodId),subscribe:(payer,type,lookupKey)=>manageSubscription.create(payer,type,lookupKey,{}),cancelSubscription:(subscriptionId,atPeriodEnd)=>atPeriodEnd?manageSubscription.cancelAtPeriodEnd(subscriptionId):manageSubscription.cancel(subscriptionId),listSubscriptions:async(customerId)=>(await stripe.subscriptions.list({customer:customerId,status:"all",limit:100})).data,constructEvent:(payload,signature,secret)=>constructEvent(payload,signature,secret)};const custom=new Map;export function registerPaymentDriver(name,factory){if(name==="stripe"||name==="adyen")throw Error(`"${name}" is a built-in payment driver and cannot be replaced.`);custom.set(name,factory)}export function adyenConfig(settings={},env=process.env){const environment=settings.environment??(env.ADYEN_ENVIRONMENT==="live"?"live":"test");return{apiKey:settings.apiKey||env.ADYEN_API_KEY||"",merchantAccount:settings.merchantAccount||env.ADYEN_MERCHANT_ACCOUNT||"",environment,liveUrlPrefix:settings.liveUrlPrefix||env.ADYEN_LIVE_URL_PREFIX||void 0,hmacKey:settings.hmacKey||env.ADYEN_HMAC_KEY||void 0,shopperReferencePrefix:settings.shopperReferencePrefix}}export function configuredPaymentDriver(){return(config.payment??{}).driver??"stripe"}export function assertStripeDriver(operation){const configured=configuredPaymentDriver();if(configured!=="stripe")throw new PaymentUnsupportedError(configured,operation,"it is a Stripe-only operation")}export function paymentDriver(name){const settings=config.payment??{},chosen=name??configuredPaymentDriver();if(chosen==="stripe")return new StripeDriver({operations:defaultStripeOperations,webhookSecret:settings.stripe?.webhookSecret||services?.stripe?.webhookSecret||process.env.STRIPE_WEBHOOK_SECRET||void 0});if(chosen==="adyen"){const appUrl=config.app?.url;return new AdyenDriver({...adyenConfig(settings.adyen),...appUrl?{appUrl}:{}})}const factory=custom.get(chosen);if(!factory)throw Error(`No payment driver named "${chosen}". Built in: stripe, adyen${custom.size?`; registered: ${[...custom.keys()].join(", ")}`:""}.`);return factory()}
|
package/dist/driver/stripe.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export declare function stripePaymentStatus(intent: Pick<Stripe.PaymentIntent, '
|
|
|
11
11
|
* itself; the event's `previous_attributes` holds the total before it.
|
|
12
12
|
*/
|
|
13
13
|
export declare function refundedThisTime(charge: Record<string, any>, previous?: Record<string, any> | null): number | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* When the current billing period ends, in Unix seconds.
|
|
16
|
+
*
|
|
17
|
+
* Read from the subscription's item: Stripe moved `current_period_end` there
|
|
18
|
+
* in API 2025-03-31.basil, and the subscription no longer carries it. Reading
|
|
19
|
+
* the subscription's own field left `ends_at` empty on every row stored since.
|
|
20
|
+
*/
|
|
21
|
+
export declare function subscriptionPeriodEnd(subscription: Stripe.Subscription): number | undefined;
|
|
14
22
|
export declare function summarizeSubscription(subscription: Stripe.Subscription): SubscriptionSummary;
|
|
15
23
|
/** A Stripe PaymentMethod in our terms. */
|
|
16
24
|
export declare function toStoredPaymentMethod(method: Stripe.PaymentMethod, defaultId?: string | null): StoredPaymentMethod;
|
package/dist/driver/stripe.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{assertMoney,headerOf,WebhookNotConfiguredError,WebhookSignatureError}from"./types";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","subscriptions","webhooks"]);export function stripePaymentStatus(intent){switch(intent.status){case"succeeded":return"succeeded";case"canceled":return"canceled";case"requires_action":return"requires_action";case"processing":case"requires_capture":return"processing";default:return intent.last_payment_error?"failed":"requires_payment_method"}}export function refundedThisTime(charge,previous){if(typeof charge.amount_refunded!=="number")return;const before=typeof previous?.amount_refunded==="number"?previous.amount_refunded:0;return charge.amount_refunded-before}function refundStatus(status){if(status==="succeeded"||status==="failed"||status==="canceled")return status;return"pending"}const SUBSCRIPTION_STATUSES=new Set(["active","trialing","past_due","canceled","incomplete","unpaid","paused"]);function subscriptionStatus(status){if(status==="incomplete_expired")return"canceled";return SUBSCRIPTION_STATUSES.has(status)?status:"unknown"}const INTERVALS=new Set(["day","week","month","year"]);export function summarizeSubscription(subscription){const item=subscription.items?.data?.[0],periodEnd=
|
|
1
|
+
import{assertMoney,headerOf,WebhookNotConfiguredError,WebhookSignatureError}from"./types";const CAPABILITIES=new Set(["customers","charge","createPayment","refund","checkout","paymentMethods","subscriptions","webhooks"]);export function stripePaymentStatus(intent){switch(intent.status){case"succeeded":return"succeeded";case"canceled":return"canceled";case"requires_action":return"requires_action";case"processing":case"requires_capture":return"processing";default:return intent.last_payment_error?"failed":"requires_payment_method"}}export function refundedThisTime(charge,previous){if(typeof charge.amount_refunded!=="number")return;const before=typeof previous?.amount_refunded==="number"?previous.amount_refunded:0;return charge.amount_refunded-before}function refundStatus(status){if(status==="succeeded"||status==="failed"||status==="canceled")return status;return"pending"}const SUBSCRIPTION_STATUSES=new Set(["active","trialing","past_due","canceled","incomplete","unpaid","paused"]);function subscriptionStatus(status){if(status==="incomplete_expired")return"canceled";return SUBSCRIPTION_STATUSES.has(status)?status:"unknown"}const INTERVALS=new Set(["day","week","month","year"]);export function subscriptionPeriodEnd(subscription){const fromItem=subscription.items?.data?.[0]?.current_period_end;if(typeof fromItem==="number")return fromItem;const legacy=subscription.current_period_end;return typeof legacy==="number"?legacy:void 0}export function summarizeSubscription(subscription){const item=subscription.items?.data?.[0],periodEnd=subscriptionPeriodEnd(subscription),price=item?.price,invoice=subscription.latest_invoice,secret=invoice&&typeof invoice==="object"?invoice.confirmation_secret?.client_secret:void 0;return{id:subscription.id,status:subscriptionStatus(subscription.status),price:price?{id:price.id,name:price.nickname??price.lookup_key??null,amount:typeof price.unit_amount==="number"?{amount:price.unit_amount,currency:price.currency}:null,interval:INTERVALS.has(price.recurring?.interval??"")?price.recurring.interval:null}:null,currentPeriodEnd:periodEnd?new Date(periodEnd*1000):null,cancelAtPeriodEnd:Boolean(subscription.cancel_at_period_end),...secret&&subscription.status==="incomplete"?{clientConfirmation:{provider:"stripe",clientSecret:secret}}:{},raw:subscription}}const EVENT_TYPES={"payment_intent.succeeded":"payment.succeeded","payment_intent.payment_failed":"payment.failed","payment_intent.canceled":"payment.canceled","charge.refunded":"refund.succeeded","refund.failed":"refund.failed","checkout.session.completed":"checkout.completed","customer.subscription.created":"subscription.created","customer.subscription.updated":"subscription.updated","customer.subscription.deleted":"subscription.canceled","invoice.paid":"invoice.paid","invoice.payment_failed":"invoice.payment_failed"};export function toStoredPaymentMethod(method,defaultId=null){return{id:method.id,type:method.type,brand:method.card?.brand??null,last4:method.card?.last4??null,expMonth:method.card?.exp_month??null,expYear:method.card?.exp_year??null,isDefault:method.id===defaultId,raw:method}}function toPayment(intent){const status=stripePaymentStatus(intent);return{id:intent.id,status,amount:{amount:intent.amount,currency:intent.currency},...intent.client_secret&&(status==="requires_payment_method"||status==="requires_action")?{clientConfirmation:{provider:"stripe",clientSecret:intent.client_secret}}:{},...intent.next_action?.redirect_to_url?.url?{redirectUrl:intent.next_action.redirect_to_url.url}:{},...intent.last_payment_error?.message?{failureReason:intent.last_payment_error.message}:{},raw:intent}}export class StripeDriver{name="stripe";capabilities=CAPABILITIES;ops;webhookSecret;constructor(options){this.ops=options.operations;this.webhookSecret=options.webhookSecret}async customer(payer){const customer=await this.ops.customer(payer);return{id:customer.id,email:customer.email??null,name:customer.name??null}}async charge(payer,amount,paymentMethod,options={}){assertMoney(amount);const customer=await this.ops.customer(payer),intent=await this.ops.createIntent(payer,{amount:amount.amount,currency:amount.currency.toLowerCase(),customer:customer.id,payment_method:paymentMethod,confirm:!0,automatic_payment_methods:{enabled:!0,allow_redirects:"never"},off_session:!0,...options.description?{description:options.description}:{},metadata:{...options.metadata,...options.reference?{reference:options.reference}:{}}},options.idempotencyKey);return toPayment(intent)}async createPayment(payer,amount,options={}){assertMoney(amount);const customer=await this.ops.customer(payer),intent=await this.ops.createIntent(payer,{amount:amount.amount,currency:amount.currency.toLowerCase(),customer:customer.id,automatic_payment_methods:{enabled:!0},...options.description?{description:options.description}:{},metadata:{...options.metadata,...options.reference?{reference:options.reference}:{}}},options.idempotencyKey);return toPayment(intent)}async refund(paymentId,options={}){if(options.amount)assertMoney(options.amount,"refund amount");const refund=await this.ops.refund(paymentId,{...options.amount?{amount:options.amount.amount}:{},...options.reason?{reason:options.reason}:{},...options.idempotencyKey?{idempotencyKey:options.idempotencyKey}:{}});return{id:refund.id,paymentId,status:refundStatus(refund.status),amount:{amount:refund.amount,currency:refund.currency},raw:refund}}async checkout(payer,request){if(request.trialDays!==void 0&&(!Number.isSafeInteger(request.trialDays)||request.trialDays<1))throw TypeError("trialDays is a whole number of days, at least 1.");if(request.trialDays&&request.mode!=="subscription")throw TypeError("trialDays applies to a subscription checkout.");const customer=await this.ops.customer(payer),lineItems=request.lines.map((line)=>{if("price"in line)return{price:line.price,quantity:line.quantity};if(!request.currency)throw TypeError("A checkout line priced with unitAmount needs the checkout's currency.");return{quantity:line.quantity,price_data:{currency:request.currency.toLowerCase(),unit_amount:line.unitAmount,product_data:{name:line.name}}}}),carried={...request.metadata,...request.reference?{reference:request.reference}:{}},hasCarried=Object.keys(carried).length>0,session=await this.ops.createCheckout(payer,{customer:customer.id,mode:request.mode,success_url:request.successUrl,...request.cancelUrl?{cancel_url:request.cancelUrl}:{},...request.mode==="setup"?{currency:request.currency?.toLowerCase()}:{line_items:lineItems},...request.reference?{client_reference_id:request.reference}:{},...request.metadata?{metadata:request.metadata}:{},...hasCarried&&request.mode==="payment"?{payment_intent_data:{metadata:carried}}:{},...request.mode==="subscription"&&(hasCarried||request.trialDays)?{subscription_data:{...hasCarried?{metadata:carried}:{},...request.trialDays?{trial_period_days:request.trialDays}:{}}}:{},...hasCarried&&request.mode==="setup"?{setup_intent_data:{metadata:carried}}:{},...request.allowPromotionCodes?{allow_promotion_codes:!0}:{},...request.automaticTax?{automatic_tax:{enabled:!0}}:{}});if(!session.url)throw Error("Stripe returned a checkout session without a url.");return{id:session.id,url:session.url,expiresAt:session.expires_at?new Date(session.expires_at*1000):null,raw:session}}async paymentMethods(payer){const customer=await this.ops.customer(payer),defaultId=typeof customer.invoice_settings?.default_payment_method==="string"?customer.invoice_settings.default_payment_method:customer.invoice_settings?.default_payment_method?.id??null;return(await this.ops.listPaymentMethods(customer.id)).map((method)=>toStoredPaymentMethod(method,defaultId))}async removePaymentMethod(payer,paymentMethodId){await this.ops.deletePaymentMethod(payer,paymentMethodId)}async subscribe(payer,price,options={}){return summarizeSubscription(await this.ops.subscribe(payer,options.type??"default",price))}async cancelSubscription(subscriptionId,options={}){return summarizeSubscription(await this.ops.cancelSubscription(subscriptionId,options.atPeriodEnd??!1))}async subscriptions(payer){const customer=await this.ops.customer(payer);return(await this.ops.listSubscriptions(customer.id)).map(summarizeSubscription)}async verifyWebhook(request){if(!this.webhookSecret)throw new WebhookNotConfiguredError("stripe","Stripe webhooks cannot be verified without the signing secret: set STRIPE_WEBHOOK_SECRET or payment.stripe.webhookSecret.");const signature=headerOf(request.headers,"stripe-signature");if(!signature)throw new WebhookSignatureError("stripe","there is no Stripe-Signature header");let event;try{event=await this.ops.constructEvent(request.payload,signature,this.webhookSecret)}catch(error){throw new WebhookSignatureError("stripe",error.message)}const object=event.data.object,reference=event.type==="charge.refunded"||event.type==="refund.failed"?object.payment_intent??object.id:object.id,minor=event.type==="charge.refunded"?refundedThisTime(object,event.data.previous_attributes):object.amount??object.amount_total,amount=typeof minor==="number"&&object.currency?{amount:minor,currency:String(object.currency)}:null,reason=object.last_payment_error?.message??object.failure_reason??null;return[{provider:this.name,id:event.id,type:EVENT_TYPES[event.type]??"unknown",providerType:event.type,reference:typeof reference==="string"?reference:null,merchantReference:object.client_reference_id??object.metadata?.reference??null,amount,reason,raw:event}]}acknowledgeWebhook(){return Response.json({received:!0})}}
|
package/dist/driver/types.d.ts
CHANGED
package/dist/payment.d.ts
CHANGED
|
@@ -7,105 +7,82 @@ import { manageSubscription } from './billable/subscription';
|
|
|
7
7
|
import { manageWebhook, onCharge, onCheckout, onInvoice, onPaymentIntent, onSubscription, processWebhook } from './billable/webhook';
|
|
8
8
|
import { paymentDriver } from './driver/index';
|
|
9
9
|
import { stripe } from './drivers/stripe';
|
|
10
|
+
import type { ChargeOptions, CheckoutRequest, CheckoutSession, CreatePaymentOptions, Customer, Money, Payer, PaymentResult, RefundOptions, RefundResult, StoredPaymentMethod, SubscriptionSummary } from './driver/index';
|
|
10
11
|
import type { UserModel } from '@stacksjs/orm';
|
|
11
12
|
import type Stripe from 'stripe';
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
export declare function subscribe(user: UserModel, lookupKey: string, options?: Partial<Stripe.SubscriptionCreateParams>): Promise<Stripe.Subscription>;
|
|
36
|
-
/**
|
|
37
|
-
* Cancel a subscription
|
|
13
|
+
/** Charge a stored payment method, server side. */
|
|
14
|
+
export declare function charge(payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions): Promise<PaymentResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Start a payment the browser completes with the provider's SDK. The result's
|
|
17
|
+
* `clientConfirmation` says how: a Stripe client secret, or an Adyen session.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createPayment(payer: Payer, amount: Money, options?: CreatePaymentOptions): Promise<PaymentResult>;
|
|
20
|
+
/** Refund a payment: all of it, or `options.amount` of it. */
|
|
21
|
+
export declare function refund(paymentId: string, options?: RefundOptions): Promise<RefundResult>;
|
|
22
|
+
/** A hosted checkout page; send the payer to the session's `url`. */
|
|
23
|
+
export declare function checkout(payer: Payer, request: CheckoutRequest): Promise<CheckoutSession>;
|
|
24
|
+
/** A hosted checkout that starts a subscription to one price. */
|
|
25
|
+
export declare function subscriptionCheckout(payer: Payer, price: string, options: Omit<CheckoutRequest, 'mode' | 'lines'> & { quantity?: number }): Promise<CheckoutSession>;
|
|
26
|
+
/** Subscribe to `price` (a Stripe lookup key). `type` names it locally, `default` unless given. */
|
|
27
|
+
export declare function subscribe(payer: Payer, price: string, options?: { type?: string }): Promise<SubscriptionSummary>;
|
|
28
|
+
/**
|
|
29
|
+
* Cancel now, or with `atPeriodEnd` stop renewing and let the customer keep
|
|
30
|
+
* what they paid for until the period ends.
|
|
31
|
+
*
|
|
32
|
+
* Replaces `cancelSubscription(id, immediately)`, whose `false` cancelled
|
|
33
|
+
* immediately anyway, only with a proration. Stripe's `prorate` and
|
|
34
|
+
* `invoice_now` are `manageSubscription.cancel`'s to pass.
|
|
38
35
|
*/
|
|
39
|
-
export declare function cancelSubscription(subscriptionId: string,
|
|
36
|
+
export declare function cancelSubscription(subscriptionId: string, options?: { atPeriodEnd?: boolean }): Promise<SubscriptionSummary>;
|
|
37
|
+
/** The provider's subscriptions for this payer, every status included. */
|
|
38
|
+
export declare function subscriptions(payer: Payer): Promise<SubscriptionSummary[]>;
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
40
|
+
* Whether the user holds an entitling subscription of `type`. Answered from
|
|
41
|
+
* the local `subscriptions` table, which the webhook keeps current, so it does
|
|
42
|
+
* not ask the provider.
|
|
42
43
|
*/
|
|
43
44
|
export declare function hasActiveSubscription(user: UserModel, type?: string): Promise<boolean>;
|
|
44
|
-
/**
|
|
45
|
-
* Update subscription to a new price
|
|
46
|
-
*/
|
|
45
|
+
/** Switch the user's subscription of `type` to another price, prorated. Stripe only. */
|
|
47
46
|
export declare function changeSubscription(user: UserModel, newLookupKey: string, type?: string): Promise<Stripe.Subscription>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
*/
|
|
51
|
-
export declare function
|
|
52
|
-
/**
|
|
53
|
-
* Update customer details
|
|
54
|
-
*/
|
|
55
|
-
export declare function updateCustomer(user: UserModel, options: Stripe.CustomerCreateParams): Promise<Stripe.Customer>;
|
|
56
|
-
/**
|
|
57
|
-
* Delete a customer from Stripe
|
|
58
|
-
*/
|
|
47
|
+
/** The provider's customer for this payer, created on first use. */
|
|
48
|
+
export declare function getOrCreateCustomer(payer: Payer): Promise<Customer>;
|
|
49
|
+
/** Update the Stripe customer's details. Stripe only. */
|
|
50
|
+
export declare function updateCustomer(user: UserModel, options: Stripe.CustomerUpdateParams): Promise<Stripe.Customer>;
|
|
51
|
+
/** Delete the Stripe customer. Stripe only. */
|
|
59
52
|
export declare function deleteCustomer(user: UserModel): Promise<Stripe.DeletedCustomer>;
|
|
53
|
+
/** The payment methods the provider holds for this payer, the default marked. */
|
|
54
|
+
export declare function paymentMethods(payer: Payer): Promise<StoredPaymentMethod[]>;
|
|
60
55
|
/**
|
|
61
|
-
*
|
|
56
|
+
* Detach a payment method, by the provider's id for it (as `paymentMethods`
|
|
57
|
+
* lists it). It used to take the local `payment_methods` row id as well; that
|
|
58
|
+
* is `managePaymentMethod.deletePaymentMethod`'s to do.
|
|
62
59
|
*/
|
|
60
|
+
export declare function removePaymentMethod(payer: Payer, paymentMethodId: string): Promise<void>;
|
|
61
|
+
/** Attach a card the browser collected with a setup intent. Stripe only. */
|
|
63
62
|
export declare function addPaymentMethod(user: UserModel, paymentMethodId: string): Promise<Stripe.PaymentMethod>;
|
|
64
|
-
/**
|
|
65
|
-
* Set the default payment method
|
|
66
|
-
*/
|
|
63
|
+
/** Make a payment method the customer's default. Stripe only. */
|
|
67
64
|
export declare function setDefaultPaymentMethod(user: UserModel, paymentMethodId: string): Promise<Stripe.Customer>;
|
|
68
|
-
/**
|
|
69
|
-
* Remove a payment method.
|
|
70
|
-
* Accepts either a numeric database ID or a Stripe payment method ID string (pm_xxx).
|
|
71
|
-
*/
|
|
72
|
-
export declare function removePaymentMethod(user: UserModel, paymentMethodId: string | number): Promise<Stripe.PaymentMethod>;
|
|
73
|
-
/**
|
|
74
|
-
* Create a setup intent for adding payment methods
|
|
75
|
-
*/
|
|
65
|
+
/** A setup intent, for collecting a card in the browser without charging it. Stripe only. */
|
|
76
66
|
export declare function createSetupIntent(user: UserModel, options?: Partial<Stripe.SetupIntentCreateParams>): Promise<Stripe.SetupIntent>;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Invoices - Stripe only
|
|
69
|
+
// =============================================================================
|
|
80
70
|
export declare function getInvoices(user: UserModel): Promise<Stripe.ApiList<Stripe.Invoice>>;
|
|
81
|
-
/**
|
|
82
|
-
* Create an invoice
|
|
83
|
-
*/
|
|
84
71
|
export declare function createInvoice(customerId: string, options?: Partial<Stripe.InvoiceCreateParams>): Promise<Stripe.Invoice>;
|
|
85
|
-
/**
|
|
86
|
-
* Pay an invoice
|
|
87
|
-
*/
|
|
88
72
|
export declare function payInvoice(invoiceId: string): Promise<Stripe.Invoice>;
|
|
89
|
-
/**
|
|
90
|
-
* Create a product with a price
|
|
91
|
-
*/
|
|
73
|
+
/** Create a product with a price. Amounts in minor units. */
|
|
92
74
|
export declare function createProduct(name: string, price: number, options?: {
|
|
93
75
|
currency?: string
|
|
94
76
|
interval?: 'day' | 'week' | 'month' | 'year'
|
|
95
77
|
description?: string
|
|
96
78
|
metadata?: Stripe.MetadataParam
|
|
97
79
|
}): Promise<{ product: Stripe.Product, price: Stripe.Price }>;
|
|
98
|
-
/**
|
|
99
|
-
* Get a price by lookup key
|
|
100
|
-
*/
|
|
80
|
+
/** A price by its lookup key. */
|
|
101
81
|
export declare function getPrice(lookupKey: string): Promise<Stripe.Price | undefined>;
|
|
102
|
-
/**
|
|
103
|
-
* List all products
|
|
104
|
-
*/
|
|
105
82
|
export declare function listProducts(options?: Stripe.ProductListParams): Promise<Stripe.ApiList<Stripe.Product>>;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Coupons - Stripe only
|
|
85
|
+
// =============================================================================
|
|
109
86
|
export declare function createCoupon(options: {
|
|
110
87
|
percentOff?: number
|
|
111
88
|
amountOff?: number
|
|
@@ -115,13 +92,7 @@ export declare function createCoupon(options: {
|
|
|
115
92
|
name?: string
|
|
116
93
|
maxRedemptions?: number
|
|
117
94
|
}): Promise<Stripe.Coupon>;
|
|
118
|
-
/**
|
|
119
|
-
* Create a promotion code from a coupon
|
|
120
|
-
*/
|
|
121
95
|
export declare function createPromoCode(couponId: string, code: string, options?: Partial<Stripe.PromotionCodeCreateParams>): Promise<Stripe.PromotionCode>;
|
|
122
|
-
/**
|
|
123
|
-
* Validate a promo code
|
|
124
|
-
*/
|
|
125
96
|
export declare function validatePromoCode(code: string): Promise<Stripe.PromotionCode | null>;
|
|
126
97
|
/**
|
|
127
98
|
* Format amount for display.
|
|
@@ -141,7 +112,7 @@ export declare function toCents(dollars: number): number;
|
|
|
141
112
|
*/
|
|
142
113
|
export declare function toDollars(cents: number): number;
|
|
143
114
|
/**
|
|
144
|
-
* A link into the Stripe customer portal, where the customer manages their
|
|
115
|
+
* A link into the Stripe customer portal (Stripe only), where the customer manages their
|
|
145
116
|
* subscription, card and invoices. Takes a Stripe customer id, or a user with
|
|
146
117
|
* one. The link is single-use and short-lived: create it when they click.
|
|
147
118
|
*/
|
|
@@ -151,9 +122,9 @@ export declare function billingPortal(customer: string | UserModel, options?: {
|
|
|
151
122
|
// =============================================================================
|
|
152
123
|
export declare const Payment: {
|
|
153
124
|
/**
|
|
154
|
-
* The
|
|
155
|
-
*
|
|
156
|
-
*
|
|
125
|
+
* The driver `config.payment.driver` selects - Stripe or Adyen - for what
|
|
126
|
+
* the functions below do not cover (webhooks: `verifyWebhook`,
|
|
127
|
+
* `acknowledgeWebhook`).
|
|
157
128
|
*/
|
|
158
129
|
driver: typeof paymentDriver;
|
|
159
130
|
charge: typeof charge;
|
|
@@ -163,11 +134,13 @@ export declare const Payment: {
|
|
|
163
134
|
subscriptionCheckout: typeof subscriptionCheckout;
|
|
164
135
|
subscribe: typeof subscribe;
|
|
165
136
|
cancelSubscription: typeof cancelSubscription;
|
|
137
|
+
subscriptions: typeof subscriptions;
|
|
166
138
|
hasActiveSubscription: typeof hasActiveSubscription;
|
|
167
139
|
changeSubscription: typeof changeSubscription;
|
|
168
140
|
getOrCreateCustomer: typeof getOrCreateCustomer;
|
|
169
141
|
updateCustomer: typeof updateCustomer;
|
|
170
142
|
deleteCustomer: typeof deleteCustomer;
|
|
143
|
+
paymentMethods: typeof paymentMethods;
|
|
171
144
|
addPaymentMethod: typeof addPaymentMethod;
|
|
172
145
|
setDefaultPaymentMethod: typeof setDefaultPaymentMethod;
|
|
173
146
|
removePaymentMethod: typeof removePaymentMethod;
|
package/dist/payment.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripe}from"./drivers/stripe";import{
|
|
1
|
+
import{stripe}from"./drivers/stripe";import{manageCustomer}from"./billable/customer";import{manageInvoice}from"./billable/invoice";import{managePaymentMethod}from"./billable/payment-method";import{managePrice}from"./billable/price";import{manageBillingPortal}from"./billable/portal";import{manageCoupon,managePriceExtended,manageProduct}from"./billable/product";import{manageSubscription}from"./billable/subscription";import{manageSetupIntent}from"./billable/intent";import{manageWebhook,onCharge,onCheckout,onInvoice,onPaymentIntent,onSubscription,processWebhook}from"./billable/webhook";import{connect}from"./connect";import{assertStripeDriver,paymentDriver}from"./driver";export async function charge(payer,amount,paymentMethod,options={}){return paymentDriver().charge(payer,amount,paymentMethod,options)}export async function createPayment(payer,amount,options={}){return paymentDriver().createPayment(payer,amount,options)}export async function refund(paymentId,options={}){return paymentDriver().refund(paymentId,options)}export async function checkout(payer,request){return paymentDriver().checkout(payer,request)}export async function subscriptionCheckout(payer,price,options){const{quantity=1,...request}=options;return paymentDriver().checkout(payer,{...request,mode:"subscription",lines:[{price,quantity}]})}export async function subscribe(payer,price,options={}){return paymentDriver().subscribe(payer,price,options)}export async function cancelSubscription(subscriptionId,options={}){return paymentDriver().cancelSubscription(subscriptionId,options)}export async function subscriptions(payer){return paymentDriver().subscriptions(payer)}export async function hasActiveSubscription(user,type="default"){return manageSubscription.isValid(user,type)}export async function changeSubscription(user,newLookupKey,type="default"){assertStripeDriver("changeSubscription");return manageSubscription.update(user,type,newLookupKey,{})}export async function getOrCreateCustomer(payer){return paymentDriver().customer(payer)}export async function updateCustomer(user,options){assertStripeDriver("updateCustomer");return manageCustomer.updateStripeCustomer(user,options)}export async function deleteCustomer(user){assertStripeDriver("deleteCustomer");return manageCustomer.deleteStripeUser(user)}export async function paymentMethods(payer){return paymentDriver().paymentMethods(payer)}export async function removePaymentMethod(payer,paymentMethodId){return paymentDriver().removePaymentMethod(payer,paymentMethodId)}export async function addPaymentMethod(user,paymentMethodId){assertStripeDriver("addPaymentMethod");return managePaymentMethod.addPaymentMethod(user,paymentMethodId)}export async function setDefaultPaymentMethod(user,paymentMethodId){assertStripeDriver("setDefaultPaymentMethod");return managePaymentMethod.setUserDefaultPayment(user,paymentMethodId)}export async function createSetupIntent(user,options={}){assertStripeDriver("createSetupIntent");return manageSetupIntent.create(user,options)}export async function getInvoices(user){assertStripeDriver("getInvoices");return manageInvoice.list(user)}export async function createInvoice(customerId,options={}){assertStripeDriver("createInvoice");return stripe.invoices.create({customer:customerId,...options})}export async function payInvoice(invoiceId){assertStripeDriver("payInvoice");return stripe.invoices.pay(invoiceId)}export async function createProduct(name,price,options={}){assertStripeDriver("createProduct");const{currency="usd",interval,description,metadata}=options,priceParams={unit_amount:price,currency};if(interval)priceParams.recurring={interval};return manageProduct.createWithPrice({name,description,metadata},priceParams)}export async function getPrice(lookupKey){assertStripeDriver("getPrice");return managePrice.retrieveByLookupKey(lookupKey)}export async function listProducts(options={}){assertStripeDriver("listProducts");return manageProduct.list(options)}export async function createCoupon(options){assertStripeDriver("createCoupon");const params={duration:options.duration||"once"};if(options.percentOff)params.percent_off=options.percentOff;else if(options.amountOff){params.amount_off=options.amountOff;params.currency=options.currency||"usd"}if(options.name)params.name=options.name;if(options.durationInMonths)params.duration_in_months=options.durationInMonths;if(options.maxRedemptions)params.max_redemptions=options.maxRedemptions;return manageCoupon.create(params)}export async function createPromoCode(couponId,code,options={}){assertStripeDriver("createPromoCode");return manageCoupon.createPromotionCode({promotion:{type:"coupon",coupon:couponId},code,...options})}export async function validatePromoCode(code){assertStripeDriver("validatePromoCode");return manageCoupon.retrievePromotionCode(code)}export function formatAmount(amount,currency){const cfg=globalThis.config||{},ccy=(currency||cfg.payment?.currency||cfg.billing?.currency||process.env.STRIPE_CURRENCY||"usd").toLowerCase(),locale=cfg.app?.locale||"en-US";return new Intl.NumberFormat(locale,{style:"currency",currency:ccy.toUpperCase()}).format(amount/100)}export function toCents(dollars){return Math.round(dollars*100)}export function toDollars(cents){return cents/100}export async function billingPortal(customer,options={}){assertStripeDriver("billingPortal");const customerId=typeof customer==="string"?customer:customer.stripe_id;if(!customerId)throw Error("billingPortal needs a Stripe customer: this user has no stripe_id yet.");return manageBillingPortal.createSession(customerId,options)}export const Payment={driver:paymentDriver,charge,createPayment,refund,checkout,subscriptionCheckout,subscribe,cancelSubscription,subscriptions,hasActiveSubscription,changeSubscription,getOrCreateCustomer,updateCustomer,deleteCustomer,paymentMethods,addPaymentMethod,setDefaultPaymentMethod,removePaymentMethod,createSetupIntent,getInvoices,createInvoice,payInvoice,createProduct,getPrice,listProducts,billingPortal,createCoupon,createPromoCode,validatePromoCode,formatAmount,toCents,toDollars,webhook:manageWebhook,onPaymentIntent,onSubscription,onInvoice,onCheckout,onCharge,processWebhook,connect,stripe,customer:manageCustomer,subscription:manageSubscription,invoice:manageInvoice,paymentMethod:managePaymentMethod,product:manageProduct,price:managePriceExtended,coupon:manageCoupon};export default Payment;
|
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.
|
|
5
|
+
"version": "0.75.84",
|
|
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.
|
|
64
|
-
"@stacksjs/database": "0.75.
|
|
65
|
-
"@stacksjs/error-handling": "0.75.
|
|
66
|
-
"@stacksjs/logging": "0.75.
|
|
67
|
-
"@stacksjs/orm": "0.75.
|
|
68
|
-
"@stacksjs/types": "0.75.
|
|
63
|
+
"@stacksjs/config": "0.75.84",
|
|
64
|
+
"@stacksjs/database": "0.75.84",
|
|
65
|
+
"@stacksjs/error-handling": "0.75.84",
|
|
66
|
+
"@stacksjs/logging": "0.75.84",
|
|
67
|
+
"@stacksjs/orm": "0.75.84",
|
|
68
|
+
"@stacksjs/types": "0.75.84"
|
|
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.
|
|
83
|
+
"@stacksjs/utils": "0.75.84",
|
|
84
84
|
"@stripe/stripe-js": "^10.0.0",
|
|
85
85
|
"better-dx": "^0.2.24",
|
|
86
86
|
"stripe": "23.0.0"
|