@stacksjs/payments 0.75.74 → 0.75.76

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,64 @@
1
+ import type { ChargeOptions, CheckoutRequest, CheckoutSession, CreatePaymentOptions, Customer, Money, Payer, PaymentDriver, PaymentEvent, PaymentResult, PaymentStatus, RefundOptions, RefundResult, StoredPaymentMethod, WebhookRequest } from './types';
2
+ /**
3
+ * Adyen's `resultCode` in our terms. Only `Authorised`, `Refused`, `Error`
4
+ * and `Cancelled` are final; everything else waits on the payer or a webhook.
5
+ */
6
+ export declare function adyenStatus(resultCode: string | undefined): PaymentStatus;
7
+ /**
8
+ * The string Adyen signs for a standard webhook item: eight fields joined by
9
+ * colons, empty ones as empty strings.
10
+ */
11
+ export declare function adyenSigningPayload(item: NotificationItem): string;
12
+ /** HMAC-SHA256 of the signing payload under the hex key, base64. */
13
+ export declare function adyenSignature(item: NotificationItem, hmacKey: string): string;
14
+ export declare const ADYEN_API_VERSION: 'v72';
15
+ /**
16
+ * The Adyen driver (stacksjs/stacks#450), on Checkout API v72.
17
+ *
18
+ * Adyen is a processor like Stripe, not a merchant of record, so payments,
19
+ * hosted checkout, refunds, stored cards and webhooks map across. It has no
20
+ * product catalog, no subscriptions, no invoices and no customer object:
21
+ * a payer is a `shopperReference` you choose, and recurring billing is charging
22
+ * a stored card on your own schedule. Those operations throw
23
+ * {@link PaymentUnsupportedError}.
24
+ *
25
+ * Verified against Adyen's API reference and its published HMAC example; not
26
+ * yet run against a live merchant account.
27
+ */
28
+ export declare interface AdyenConfig {
29
+ apiKey: string
30
+ merchantAccount: string
31
+ environment: 'test' | 'live'
32
+ liveUrlPrefix?: string
33
+ hmacKey?: string
34
+ shopperReferencePrefix?: string
35
+ }
36
+ declare interface NotificationItem {
37
+ pspReference?: string
38
+ originalReference?: string
39
+ merchantAccountCode?: string
40
+ merchantReference?: string
41
+ amount?: { value?: number, currency?: string }
42
+ eventCode?: string
43
+ success?: string | boolean
44
+ additionalData?: Record<string, string>
45
+ }
46
+ export declare class AdyenDriver implements PaymentDriver {
47
+ readonly name: string;
48
+ readonly capabilities: unknown;
49
+ constructor(config: AdyenConfig, options?: { fetch?: typeof fetch });
50
+ get baseUrl(): string;
51
+ shopperReference(payer: Payer): string;
52
+ customer(payer: Payer): Promise<Customer>;
53
+ charge(payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions): Promise<PaymentResult>;
54
+ createPayment(payer: Payer, amount: Money, options?: CreatePaymentOptions): Promise<PaymentResult>;
55
+ refund(paymentId: string, options?: RefundOptions): Promise<RefundResult>;
56
+ checkout(payer: Payer, request: CheckoutRequest): Promise<CheckoutSession>;
57
+ paymentMethods(payer: Payer): Promise<StoredPaymentMethod[]>;
58
+ removePaymentMethod(payer: Payer, paymentMethodId: string): Promise<void>;
59
+ subscribe(): Promise<never>;
60
+ cancelSubscription(): Promise<never>;
61
+ subscriptions(): Promise<never>;
62
+ verifyWebhook(request: WebhookRequest): Promise<PaymentEvent[]>;
63
+ acknowledgeWebhook(): Response;
64
+ }
@@ -0,0 +1 @@
1
+ import{createHmac,randomUUID,timingSafeEqual}from"node:crypto";import{assertMoney,PaymentProviderError,PaymentUnsupportedError,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}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.");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.currency)throw TypeError("An Adyen checkout needs a currency.");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 Error("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{id:`${item.eventCode}:${item.pspReference}`,type:pair?pair[success?0:1]:"unknown",providerType:item.eventCode??"",reference:item.pspReference??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"}})}}
@@ -0,0 +1,23 @@
1
+ import { stripe } from '../drivers/stripe';
2
+ import type { AdyenConfig } from './adyen';
3
+ import type { PaymentDriver } from './types';
4
+ import type { StripeOperations } from './stripe';
5
+ /**
6
+ * Make a driver of your own available as `config.payment.driver`. A built-in
7
+ * name cannot be replaced, so `stripe` always means Stacks' Stripe driver.
8
+ */
9
+ export declare function registerPaymentDriver(name: string, factory: DriverFactory): void;
10
+ /** Adyen's settings: config/payment.ts, then the ADYEN_* environment. */
11
+ export declare function adyenConfig(settings?: Partial<AdyenConfig>, env?: Record<string, string | undefined>): AdyenConfig;
12
+ /**
13
+ * The payment driver `config.payment.driver` names - `stripe` unless it says
14
+ * otherwise - or the one named here. This is the first thing to read that
15
+ * setting: it used to be declared, typed as `'stripe'`, and ignored.
16
+ */
17
+ export declare function paymentDriver(name?: string): PaymentDriver;
18
+ /** Stripe through the billable modules Stacks already had, so behaviour does not change. */
19
+ export declare const defaultStripeOperations: StripeOperations;
20
+ declare type DriverFactory = () => PaymentDriver;
21
+ export * from './adyen';
22
+ export * from './stripe';
23
+ export * from './types';
@@ -0,0 +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?stripe.subscriptions.update(subscriptionId,{cancel_at_period_end:!0}):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 paymentDriver(name){const settings=config.payment??{},chosen=name??settings.driver??"stripe";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")return new AdyenDriver(adyenConfig(settings.adyen));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()}
@@ -0,0 +1,46 @@
1
+ import type { ChargeOptions, CheckoutRequest, CheckoutSession, CreatePaymentOptions, Customer, Money, Payer, PaymentDriver, PaymentEvent, PaymentResult, PaymentStatus, RefundOptions, RefundResult, StoredPaymentMethod, SubscriptionSummary, WebhookRequest } from './types';
2
+ import type Stripe from 'stripe';
3
+ /**
4
+ * A PaymentIntent's status in our terms. `requires_capture` is authorised but
5
+ * not yet captured - no money has moved - so it is still `processing`.
6
+ */
7
+ export declare function stripePaymentStatus(intent: Pick<Stripe.PaymentIntent, 'status' | 'last_payment_error'>): PaymentStatus;
8
+ /**
9
+ * The Stripe calls the driver needs, separated from the mapping so the mapping
10
+ * is testable without Stripe. The defaults delegate to the billable modules
11
+ * Stacks already had, so a Stripe app keeps their idempotency keys,
12
+ * same-origin redirect checks and `stripe_id` bookkeeping unchanged.
13
+ */
14
+ export declare interface StripeOperations {
15
+ customer: (payer: Payer) => Promise<Stripe.Customer>
16
+ createIntent: (payer: Payer, params: Stripe.PaymentIntentCreateParams, idempotencyKey?: string) => Promise<Stripe.PaymentIntent>
17
+ refund: (paymentIntentId: string, params: Stripe.RefundCreateParams & { idempotencyKey?: string }) => Promise<Stripe.Refund>
18
+ createCheckout: (payer: Payer, params: Stripe.Checkout.SessionCreateParams) => Promise<Stripe.Checkout.Session>
19
+ listPaymentMethods: (customerId: string) => Promise<Stripe.PaymentMethod[]>
20
+ deletePaymentMethod: (payer: Payer, paymentMethodId: string) => Promise<unknown>
21
+ subscribe: (payer: Payer, type: string, lookupKey: string) => Promise<Stripe.Subscription>
22
+ cancelSubscription: (subscriptionId: string, atPeriodEnd: boolean) => Promise<Stripe.Subscription>
23
+ listSubscriptions: (customerId: string) => Promise<Stripe.Subscription[]>
24
+ constructEvent: (payload: string, signature: string, secret: string) => Promise<Stripe.Event>
25
+ }
26
+ export declare interface StripeDriverOptions {
27
+ operations: StripeOperations
28
+ webhookSecret?: string
29
+ }
30
+ export declare class StripeDriver implements PaymentDriver {
31
+ readonly name: string;
32
+ readonly capabilities: unknown;
33
+ constructor(options: StripeDriverOptions);
34
+ customer(payer: Payer): Promise<Customer>;
35
+ charge(payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions): Promise<PaymentResult>;
36
+ createPayment(payer: Payer, amount: Money, options?: CreatePaymentOptions): Promise<PaymentResult>;
37
+ refund(paymentId: string, options?: RefundOptions): Promise<RefundResult>;
38
+ checkout(payer: Payer, request: CheckoutRequest): Promise<CheckoutSession>;
39
+ paymentMethods(payer: Payer): Promise<StoredPaymentMethod[]>;
40
+ removePaymentMethod(payer: Payer, paymentMethodId: string): Promise<void>;
41
+ subscribe(payer: Payer, price: string, options?: { type?: string }): Promise<SubscriptionSummary>;
42
+ cancelSubscription(subscriptionId: string, options?: { atPeriodEnd?: boolean }): Promise<SubscriptionSummary>;
43
+ subscriptions(payer: Payer): Promise<SubscriptionSummary[]>;
44
+ verifyWebhook(request: WebhookRequest): Promise<PaymentEvent[]>;
45
+ acknowledgeWebhook(): Response;
46
+ }
@@ -0,0 +1 @@
1
+ import{assertMoney,headerOf,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"}}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"}function summarizeSubscription(subscription){const item=subscription.items?.data?.[0],periodEnd=item?.current_period_end??subscription.current_period_end;return{id:subscription.id,status:subscriptionStatus(subscription.status),price:item?.price?.id??null,currentPeriodEnd:periodEnd?new Date(periodEnd*1000):null,cancelAtPeriodEnd:Boolean(subscription.cancel_at_period_end),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"};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){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}}}}),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}:{}});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)=>({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}))}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 Error("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"?object.payment_intent??object.id:object.id,amount=typeof object.amount==="number"&&object.currency?{amount:object.amount,currency:String(object.currency)}:typeof object.amount_total==="number"&&object.currency?{amount:object.amount_total,currency:String(object.currency)}:null;return[{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,raw:event}]}acknowledgeWebhook(){return Response.json({received:!0})}}
@@ -0,0 +1,188 @@
1
+ /** Read a header from either a `Headers` or a plain object, case-insensitively. */
2
+ export declare function headerOf(headers: WebhookRequest['headers'], name: string): string | null;
3
+ /** Validate an amount before it reaches a provider: a positive whole number of minor units. */
4
+ export declare function assertMoney(money: Money, label?: string): void;
5
+ /**
6
+ * The provider-neutral payments contract (stacksjs/stacks#665, #450).
7
+ *
8
+ * `@stacksjs/payments` was Stripe's SDK with a facade on it: every function
9
+ * returned a `Stripe.*` type, and `config.payment.driver` was read by nothing.
10
+ * A `PaymentDriver` describes what an application needs from a payment
11
+ * provider in the application's terms - money in minor units, statuses of our
12
+ * own, events of our own - and each provider implements it.
13
+ *
14
+ * Providers are not the same kind of thing, and the interface does not
15
+ * pretend otherwise. A driver lists what it can do in `capabilities`; calling
16
+ * anything else throws a {@link PaymentUnsupportedError} naming the driver and
17
+ * the operation, rather than a stack trace from an SDK the app never
18
+ * installed or a result that only looks like success.
19
+ */
20
+ /** An amount in the currency's minor unit (cents), never a float. */
21
+ export declare interface Money {
22
+ amount: number
23
+ currency: string
24
+ }
25
+ /** Who is paying, as the application knows them. Any Stacks user model fits. */
26
+ export declare interface Payer {
27
+ id: number | string
28
+ email?: string | null
29
+ name?: string | null
30
+ stripe_id?: string | null
31
+ update?: (values: Record<string, unknown>) => Promise<unknown>
32
+ }
33
+ export declare interface Customer {
34
+ id: string
35
+ email: string | null
36
+ name: string | null
37
+ }
38
+ export declare interface PaymentResult {
39
+ id: string
40
+ status: PaymentStatus
41
+ amount: Money
42
+ clientConfirmation?: ClientConfirmation
43
+ redirectUrl?: string
44
+ failureReason?: string
45
+ raw: unknown
46
+ }
47
+ export declare interface RefundResult {
48
+ id: string
49
+ paymentId: string
50
+ status: RefundStatus
51
+ amount: Money | null
52
+ raw: unknown
53
+ }
54
+ export declare interface CheckoutRequest {
55
+ mode: 'payment' | 'subscription' | 'setup'
56
+ lines: CheckoutLine[]
57
+ currency?: string
58
+ successUrl: string
59
+ cancelUrl?: string
60
+ reference?: string
61
+ metadata?: Record<string, string>
62
+ }
63
+ export declare interface CheckoutSession {
64
+ id: string
65
+ url: string
66
+ expiresAt: Date | null
67
+ raw: unknown
68
+ }
69
+ export declare interface StoredPaymentMethod {
70
+ id: string
71
+ type: string
72
+ brand: string | null
73
+ last4: string | null
74
+ expMonth: number | null
75
+ expYear: number | null
76
+ isDefault: boolean
77
+ raw: unknown
78
+ }
79
+ export declare interface SubscriptionSummary {
80
+ id: string
81
+ status: SubscriptionStatus
82
+ price: string | null
83
+ currentPeriodEnd: Date | null
84
+ cancelAtPeriodEnd: boolean
85
+ raw: unknown
86
+ }
87
+ /** A verified webhook notification, in our terms. */
88
+ export declare interface PaymentEvent {
89
+ id: string
90
+ type: PaymentEventType
91
+ providerType: string
92
+ reference: string | null
93
+ merchantReference: string | null
94
+ amount: Money | null
95
+ raw: unknown
96
+ }
97
+ export declare interface WebhookRequest {
98
+ payload: string
99
+ headers: Headers | Record<string, string | undefined>
100
+ }
101
+ export declare interface ChargeOptions {
102
+ reference?: string
103
+ description?: string
104
+ metadata?: Record<string, string>
105
+ idempotencyKey?: string
106
+ }
107
+ export declare interface CreatePaymentOptions extends ChargeOptions {
108
+ returnUrl?: string
109
+ }
110
+ export declare interface RefundOptions {
111
+ amount?: Money
112
+ reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer'
113
+ idempotencyKey?: string
114
+ }
115
+ export declare interface PaymentDriver {
116
+ readonly name: string
117
+ readonly capabilities: ReadonlySet<PaymentOperation>
118
+ customer: (payer: Payer) => Promise<Customer>
119
+ charge: (payer: Payer, amount: Money, paymentMethod: string, options?: ChargeOptions) => Promise<PaymentResult>
120
+ createPayment: (payer: Payer, amount: Money, options?: CreatePaymentOptions) => Promise<PaymentResult>
121
+ refund: (paymentId: string, options?: RefundOptions) => Promise<RefundResult>
122
+ checkout: (payer: Payer, request: CheckoutRequest) => Promise<CheckoutSession>
123
+ paymentMethods: (payer: Payer) => Promise<StoredPaymentMethod[]>
124
+ removePaymentMethod: (payer: Payer, paymentMethodId: string) => Promise<void>
125
+ subscribe: (payer: Payer, price: string, options?: { type?: string }) => Promise<SubscriptionSummary>
126
+ cancelSubscription: (subscriptionId: string, options?: { atPeriodEnd?: boolean }) => Promise<SubscriptionSummary>
127
+ subscriptions: (payer: Payer) => Promise<SubscriptionSummary[]>
128
+ verifyWebhook: (request: WebhookRequest) => Promise<PaymentEvent[]>
129
+ acknowledgeWebhook: () => Response
130
+ }
131
+ export type PaymentOperation = | 'customers'
132
+ | 'charge'
133
+ | 'createPayment'
134
+ | 'refund'
135
+ | 'checkout'
136
+ | 'paymentMethods'
137
+ | 'subscriptions'
138
+ | 'webhooks';
139
+ /**
140
+ * Where a payment stands.
141
+ *
142
+ * `requires_action`: the payer has to do something - authenticate, be
143
+ * redirected. `processing`: accepted, outcome not known yet (it arrives by
144
+ * webhook). Only `succeeded`, `failed` and `canceled` are final.
145
+ */
146
+ export type PaymentStatus = 'succeeded' | 'processing' | 'requires_action' | 'requires_payment_method' | 'failed' | 'canceled';
147
+ /**
148
+ * What the browser needs to finish a payment on the provider's own client SDK.
149
+ * Tagged by provider, because the SDKs differ: Stripe.js confirms a client
150
+ * secret, Adyen's Drop-in mounts a session.
151
+ */
152
+ export type ClientConfirmation = | { provider: 'stripe', clientSecret: string }
153
+ | { provider: 'adyen', sessionId: string, sessionData: string }
154
+ export type RefundStatus = 'pending' | 'succeeded' | 'failed' | 'canceled';
155
+ /** A line on a checkout: priced here, or a price the provider's catalog holds. */
156
+ export type CheckoutLine = | { name: string, unitAmount: number, quantity: number }
157
+ | { price: string, quantity: number }
158
+ /** `unknown`: a status the provider added after this driver was written; `raw` has it. */
159
+ export type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'unpaid' | 'paused' | 'unknown';
160
+ export type PaymentEventType = | 'payment.succeeded'
161
+ | 'payment.failed'
162
+ | 'payment.canceled'
163
+ | 'refund.succeeded'
164
+ | 'refund.failed'
165
+ | 'checkout.completed'
166
+ | 'subscription.created'
167
+ | 'subscription.updated'
168
+ | 'subscription.canceled'
169
+ | 'invoice.paid'
170
+ | 'invoice.payment_failed'
171
+ | 'unknown';
172
+ /** An operation the configured provider cannot do. */
173
+ export declare class PaymentUnsupportedError extends Error {
174
+ readonly driver: string;
175
+ readonly operation: string;
176
+ constructor(driver: string, operation: string, detail?: string);
177
+ }
178
+ /** A provider rejected a request. Carries its status and code. */
179
+ export declare class PaymentProviderError extends Error {
180
+ readonly driver: string;
181
+ readonly status: number;
182
+ readonly code: string | null;
183
+ constructor(driver: string, status: number, message: string, code?: string | null);
184
+ }
185
+ /** A webhook delivery whose signature does not verify. */
186
+ export declare class WebhookSignatureError extends Error {
187
+ constructor(driver: string, detail: string);
188
+ }
@@ -0,0 +1 @@
1
+ export class PaymentUnsupportedError extends Error{driver;operation;constructor(driver,operation,detail){super(`The ${driver} payment driver does not support ${operation}${detail?`: ${detail}`:"."}`);this.name="PaymentUnsupportedError";this.driver=driver;this.operation=operation}}export class PaymentProviderError extends Error{driver;status;code;constructor(driver,status,message,code=null){super(`${driver} rejected the request (${status}${code?` ${code}`:""}): ${message}`);this.name="PaymentProviderError";this.driver=driver;this.status=status;this.code=code}}export class WebhookSignatureError extends Error{constructor(driver,detail){super(`The ${driver} webhook signature does not verify: ${detail}`);this.name="WebhookSignatureError"}}export function headerOf(headers,name){if(typeof headers.get==="function")return headers.get(name);const wanted=name.toLowerCase();for(const[key,value]of Object.entries(headers))if(key.toLowerCase()===wanted)return value??null;return null}export function assertMoney(money,label="amount"){if(!Number.isInteger(money.amount)||money.amount<0)throw TypeError(`${label} must be a whole number of minor units (cents), got ${money.amount}`);if(!/^[a-z]{3}$/i.test(money.currency))throw TypeError(`${label} currency must be an ISO 4217 code, got "${money.currency}"`)}
package/dist/index.d.ts CHANGED
@@ -6,6 +6,10 @@ export type { StripeSdk as Stripe };
6
6
  // Main Payment facade
7
7
  export * from './payment';
8
8
  export { default as Payment } from './payment';
9
+ // The provider-neutral driver contract, and the Stripe and Adyen drivers
10
+ // (stacksjs/stacks#665, #450). `paymentDriver()` is the one config.payment.driver
11
+ // selects.
12
+ export * from './driver/index';
9
13
  // Billable modules
10
14
  export * from './billable/index';
11
15
  // Stripe Connect (marketplaces)
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./payment";export{default as Payment}from"./payment";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";
1
+ export*from"./payment";export{default as Payment}from"./payment";export*from"./driver";export*from"./billable";export*from"./connect";export*from"./drivers/stripe";export{freshIdempotencyKey,stacksIdempotencyKey}from"./idempotency";
package/dist/payment.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { connect } from './connect/index';
2
- import { onCharge, onCheckout, onInvoice, onPaymentIntent, onSubscription, processWebhook } from './billable/webhook';
3
- import { stripe } from './drivers/stripe';
4
1
  import type { UserModel } from '@stacksjs/orm';
5
2
  import type Stripe from 'stripe';
6
3
  /**
@@ -143,51 +140,5 @@ export declare function billingPortal(customer: string | UserModel, options?: {
143
140
  // =============================================================================
144
141
  // Payment Facade Object
145
142
  // =============================================================================
146
- export declare const Payment: {
147
- charge: typeof charge;
148
- createPayment: typeof createPayment;
149
- refund: typeof refund;
150
- checkout: typeof checkout;
151
- subscriptionCheckout: typeof subscriptionCheckout;
152
- subscribe: typeof subscribe;
153
- cancelSubscription: typeof cancelSubscription;
154
- hasActiveSubscription: typeof hasActiveSubscription;
155
- changeSubscription: typeof changeSubscription;
156
- getOrCreateCustomer: typeof getOrCreateCustomer;
157
- updateCustomer: typeof updateCustomer;
158
- deleteCustomer: typeof deleteCustomer;
159
- addPaymentMethod: typeof addPaymentMethod;
160
- setDefaultPaymentMethod: typeof setDefaultPaymentMethod;
161
- removePaymentMethod: typeof removePaymentMethod;
162
- createSetupIntent: typeof createSetupIntent;
163
- getInvoices: typeof getInvoices;
164
- createInvoice: typeof createInvoice;
165
- payInvoice: typeof payInvoice;
166
- createProduct: typeof createProduct;
167
- getPrice: typeof getPrice;
168
- listProducts: typeof listProducts;
169
- billingPortal: typeof billingPortal;
170
- createCoupon: typeof createCoupon;
171
- createPromoCode: typeof createPromoCode;
172
- validatePromoCode: typeof validatePromoCode;
173
- formatAmount: typeof formatAmount;
174
- toCents: typeof toCents;
175
- toDollars: typeof toDollars;
176
- webhook: unknown;
177
- onPaymentIntent: typeof onPaymentIntent;
178
- onSubscription: typeof onSubscription;
179
- onInvoice: typeof onInvoice;
180
- onCheckout: typeof onCheckout;
181
- onCharge: typeof onCharge;
182
- processWebhook: typeof processWebhook;
183
- connect: typeof connect;
184
- stripe: typeof stripe;
185
- customer: unknown;
186
- subscription: unknown;
187
- invoice: unknown;
188
- paymentMethod: unknown;
189
- product: unknown;
190
- price: unknown;
191
- coupon: unknown
192
- };
143
+ export declare const Payment: {};
193
144
  export default Payment;
package/dist/payment.js CHANGED
@@ -1 +1 @@
1
- import{stripe}from"./drivers/stripe";import{manageCharge}from"./billable/charge";import{manageCheckout}from"./billable/checkout";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";export async function charge(user,amount,paymentMethod,options={}){return manageCharge.charge(user,amount,paymentMethod,options)}export async function createPayment(user,amount,options={}){return manageCharge.createPayment(user,amount,options)}export async function refund(paymentIntentId,amount,options={}){return manageCharge.refund(paymentIntentId,{amount,...options})}export async function checkout(user,lineItems,options={}){return manageCheckout.create(user,{line_items:lineItems,mode:"payment",...options})}export async function subscriptionCheckout(user,priceId,options={}){return manageCheckout.create(user,{line_items:[{price:priceId,quantity:1}],mode:"subscription",...options})}export async function subscribe(user,lookupKey,options={}){return manageSubscription.create(user,"default",lookupKey,options)}export async function cancelSubscription(subscriptionId,immediately=!1){return manageSubscription.cancel(subscriptionId,{prorate:!immediately,invoice_now:immediately})}export async function hasActiveSubscription(user,type="default"){return manageSubscription.isValid(user,type)}export async function changeSubscription(user,newLookupKey,type="default"){return manageSubscription.update(user,type,newLookupKey,{})}export async function getOrCreateCustomer(user,options={}){return manageCustomer.createOrGetStripeUser(user,options)}export async function updateCustomer(user,options){return manageCustomer.updateStripeCustomer(user,options)}export async function deleteCustomer(user){return manageCustomer.deleteStripeUser(user)}export async function addPaymentMethod(user,paymentMethodId){return managePaymentMethod.addPaymentMethod(user,paymentMethodId)}export async function setDefaultPaymentMethod(user,paymentMethodId){return managePaymentMethod.setUserDefaultPayment(user,paymentMethodId)}export async function removePaymentMethod(user,paymentMethodId){return managePaymentMethod.deletePaymentMethod(user,paymentMethodId)}export async function createSetupIntent(user,options={}){return manageSetupIntent.create(user,options)}export async function getInvoices(user){return manageInvoice.list(user)}export async function createInvoice(customerId,options={}){return stripe.invoices.create({customer:customerId,...options})}export async function payInvoice(invoiceId){return stripe.invoices.pay(invoiceId)}export async function createProduct(name,price,options={}){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){return managePrice.retrieveByLookupKey(lookupKey)}export async function listProducts(options={}){return manageProduct.list(options)}export async function createCoupon(options){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={}){return manageCoupon.createPromotionCode({promotion:{type:"coupon",coupon:couponId},code,...options})}export async function validatePromoCode(code){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={}){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={charge,createPayment,refund,checkout,subscriptionCheckout,subscribe,cancelSubscription,hasActiveSubscription,changeSubscription,getOrCreateCustomer,updateCustomer,deleteCustomer,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;
1
+ import{stripe}from"./drivers/stripe";import{manageCharge}from"./billable/charge";import{manageCheckout}from"./billable/checkout";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{paymentDriver}from"./driver";export async function charge(user,amount,paymentMethod,options={}){return manageCharge.charge(user,amount,paymentMethod,options)}export async function createPayment(user,amount,options={}){return manageCharge.createPayment(user,amount,options)}export async function refund(paymentIntentId,amount,options={}){return manageCharge.refund(paymentIntentId,{amount,...options})}export async function checkout(user,lineItems,options={}){return manageCheckout.create(user,{line_items:lineItems,mode:"payment",...options})}export async function subscriptionCheckout(user,priceId,options={}){return manageCheckout.create(user,{line_items:[{price:priceId,quantity:1}],mode:"subscription",...options})}export async function subscribe(user,lookupKey,options={}){return manageSubscription.create(user,"default",lookupKey,options)}export async function cancelSubscription(subscriptionId,immediately=!1){return manageSubscription.cancel(subscriptionId,{prorate:!immediately,invoice_now:immediately})}export async function hasActiveSubscription(user,type="default"){return manageSubscription.isValid(user,type)}export async function changeSubscription(user,newLookupKey,type="default"){return manageSubscription.update(user,type,newLookupKey,{})}export async function getOrCreateCustomer(user,options={}){return manageCustomer.createOrGetStripeUser(user,options)}export async function updateCustomer(user,options){return manageCustomer.updateStripeCustomer(user,options)}export async function deleteCustomer(user){return manageCustomer.deleteStripeUser(user)}export async function addPaymentMethod(user,paymentMethodId){return managePaymentMethod.addPaymentMethod(user,paymentMethodId)}export async function setDefaultPaymentMethod(user,paymentMethodId){return managePaymentMethod.setUserDefaultPayment(user,paymentMethodId)}export async function removePaymentMethod(user,paymentMethodId){return managePaymentMethod.deletePaymentMethod(user,paymentMethodId)}export async function createSetupIntent(user,options={}){return manageSetupIntent.create(user,options)}export async function getInvoices(user){return manageInvoice.list(user)}export async function createInvoice(customerId,options={}){return stripe.invoices.create({customer:customerId,...options})}export async function payInvoice(invoiceId){return stripe.invoices.pay(invoiceId)}export async function createProduct(name,price,options={}){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){return managePrice.retrieveByLookupKey(lookupKey)}export async function listProducts(options={}){return manageProduct.list(options)}export async function createCoupon(options){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={}){return manageCoupon.createPromotionCode({promotion:{type:"coupon",coupon:couponId},code,...options})}export async function validatePromoCode(code){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={}){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,hasActiveSubscription,changeSubscription,getOrCreateCustomer,updateCustomer,deleteCustomer,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.74",
5
+ "version": "0.75.76",
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.74",
64
- "@stacksjs/database": "0.75.74",
65
- "@stacksjs/error-handling": "0.75.74",
66
- "@stacksjs/logging": "0.75.74",
67
- "@stacksjs/orm": "0.75.74",
68
- "@stacksjs/types": "0.75.74"
63
+ "@stacksjs/config": "0.75.76",
64
+ "@stacksjs/database": "0.75.76",
65
+ "@stacksjs/error-handling": "0.75.76",
66
+ "@stacksjs/logging": "0.75.76",
67
+ "@stacksjs/orm": "0.75.76",
68
+ "@stacksjs/types": "0.75.76"
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.74",
83
+ "@stacksjs/utils": "0.75.76",
84
84
  "@stripe/stripe-js": "^10.0.0",
85
85
  "better-dx": "^0.2.24",
86
86
  "stripe": "23.0.0"