@shware/purchase 7.3.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,22 +4,27 @@ const require_stripe_decline = require("./decline.cjs");
4
4
  const require_stripe_expandable = require("./expandable.cjs");
5
5
  const require_stripe_handler = require("./handler.cjs");
6
6
  const require_stripe_mapper = require("./mapper.cjs");
7
+ const require_stripe_subscription = require("./subscription.cjs");
8
+ const require_stripe_payment_method = require("./payment-method.cjs");
7
9
  const require_stripe_thin_handler = require("./thin-handler.cjs");
8
10
  const require_stripe_schema = require("./schema.cjs");
9
11
  const require_stripe_metadata = require("./metadata.cjs");
10
12
  const require_stripe_pending_checkout = require("./pending-checkout.cjs");
11
13
  exports.DECLINE_REASONS = require_stripe_decline.DECLINE_REASONS;
14
+ exports.DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = require_stripe_payment_method.DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES;
12
15
  exports.METADATA_KEYS = require_stripe_metadata.METADATA_KEYS;
13
16
  exports.StripeConfig = require_stripe_config.StripeConfig;
14
17
  exports.StripeEventHandler = require_stripe_handler.StripeEventHandler;
15
18
  exports.StripeThinEventHandler = require_stripe_thin_handler.StripeThinEventHandler;
16
19
  exports.addPendingCheckout = require_stripe_pending_checkout.addPendingCheckout;
20
+ exports.cancellationComment = require_stripe_mapper.cancellationComment;
17
21
  exports.cancellationDetailsSchema = require_stripe_schema.cancellationDetailsSchema;
18
22
  exports.createCheckoutSessionSchema = require_stripe_schema.createCheckoutSessionSchema;
19
23
  exports.expanded = require_stripe_expandable.expanded;
20
24
  exports.getBeginCheckoutProperties = require_stripe_mapper.getBeginCheckoutProperties;
21
25
  exports.getPendingCheckouts = require_stripe_pending_checkout.getPendingCheckouts;
22
26
  exports.getPurchaseProperties = require_stripe_mapper.getPurchaseProperties;
27
+ exports.hasDelayedNotification = require_stripe_payment_method.hasDelayedNotification;
23
28
  exports.idOf = require_stripe_expandable.idOf;
24
29
  exports.mapCancellationDetails = require_stripe_mapper.mapCancellationDetails;
25
30
  exports.mapCharge = require_stripe_mapper.mapCharge;
@@ -28,10 +33,12 @@ exports.mapDecline = require_stripe_decline.mapDecline;
28
33
  exports.mapDisputeStatus = require_stripe_mapper.mapDisputeStatus;
29
34
  exports.mapInvoice = require_stripe_mapper.mapInvoice;
30
35
  exports.mapLineItem = require_stripe_mapper.mapLineItem;
36
+ exports.mapListPrice = require_stripe_subscription.mapListPrice;
31
37
  exports.mapPaymentIntent = require_stripe_mapper.mapPaymentIntent;
32
38
  exports.mapPaymentType = require_stripe_mapper.mapPaymentType;
33
39
  exports.mapPrice = require_stripe_mapper.mapPrice;
34
40
  exports.mapRefundStatus = require_stripe_mapper.mapRefundStatus;
41
+ exports.mapSubscription = require_stripe_subscription.mapSubscription;
35
42
  exports.mapSubscriptionStatus = require_stripe_mapper.mapSubscriptionStatus;
36
43
  exports.mapTime = require_stripe_mapper.mapTime;
37
44
  exports.minorUnits = require_stripe_mapper.minorUnits;
@@ -3,10 +3,12 @@ import { PriceId, StripeConfig, StripeMode } from "./config.cjs";
3
3
  import { CardFailure, DECLINE_REASONS, Decline, DeclineReason, mapDecline } from "./decline.cjs";
4
4
  import { expanded, idOf } from "./expandable.cjs";
5
5
  import { StripeEventHandler } from "./handler.cjs";
6
- import { BeginCheckoutProperties, CheckoutSession, LineItem, Money, PaymentStatus, Price, ProductPrice, PurchaseProperties, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.cjs";
6
+ import { BeginCheckoutProperties, CheckoutSession, LineItem, Money, PaymentStatus, Price, ProductPrice, PurchaseProperties, cancellationComment, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.cjs";
7
+ import { StripeSubscriptionState, mapListPrice, mapSubscription } from "./subscription.cjs";
8
+ import { DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, hasDelayedNotification } from "./payment-method.cjs";
7
9
  import { StripeThinEventHandler, StripeThinEventNotificationOf, StripeThinEventType } from "./thin-handler.cjs";
8
10
  import { AllowedOrigins, CreateCheckoutSessionDTO, CreateCheckoutSessionSchemaOptions, cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.cjs";
9
11
  import { METADATA_KEYS } from "./metadata.cjs";
10
12
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.cjs";
11
13
  import { Stripe } from "stripe";
12
- export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CardFailure, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, DECLINE_REASONS, type Decline, type DeclineReason, type LineItem, METADATA_KEYS, type Money, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
14
+ export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CardFailure, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, DECLINE_REASONS, DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, type Decline, type DeclineReason, type LineItem, METADATA_KEYS, type Money, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, type StripeSubscriptionState, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationComment, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, hasDelayedNotification, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapListPrice, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscription, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
@@ -3,10 +3,12 @@ import { PriceId, StripeConfig, StripeMode } from "./config.mjs";
3
3
  import { CardFailure, DECLINE_REASONS, Decline, DeclineReason, mapDecline } from "./decline.mjs";
4
4
  import { expanded, idOf } from "./expandable.mjs";
5
5
  import { StripeEventHandler } from "./handler.mjs";
6
- import { BeginCheckoutProperties, CheckoutSession, LineItem, Money, PaymentStatus, Price, ProductPrice, PurchaseProperties, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.mjs";
6
+ import { BeginCheckoutProperties, CheckoutSession, LineItem, Money, PaymentStatus, Price, ProductPrice, PurchaseProperties, cancellationComment, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.mjs";
7
+ import { StripeSubscriptionState, mapListPrice, mapSubscription } from "./subscription.mjs";
8
+ import { DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, hasDelayedNotification } from "./payment-method.mjs";
7
9
  import { StripeThinEventHandler, StripeThinEventNotificationOf, StripeThinEventType } from "./thin-handler.mjs";
8
10
  import { AllowedOrigins, CreateCheckoutSessionDTO, CreateCheckoutSessionSchemaOptions, cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.mjs";
9
11
  import { METADATA_KEYS } from "./metadata.mjs";
10
12
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.mjs";
11
13
  import { Stripe } from "stripe";
12
- export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CardFailure, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, DECLINE_REASONS, type Decline, type DeclineReason, type LineItem, METADATA_KEYS, type Money, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
14
+ export { type AllowedOrigins, type BeginCheckoutProperties, type CancellationDetails, type CardFailure, type CheckoutSession, type CreateCheckoutSessionDTO, type CreateCheckoutSessionSchemaOptions, DECLINE_REASONS, DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, type Decline, type DeclineReason, type LineItem, METADATA_KEYS, type Money, type PaymentStatus, type Price, type PriceId, type ProductPrice, type PurchaseProperties, type Stripe, StripeConfig, StripeEventHandler, type StripeMode, type StripeSubscriptionState, StripeThinEventHandler, type StripeThinEventNotificationOf, type StripeThinEventType, addPendingCheckout, cancellationComment, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, hasDelayedNotification, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapListPrice, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscription, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
@@ -2,9 +2,11 @@ import { StripeConfig } from "./config.mjs";
2
2
  import { DECLINE_REASONS, mapDecline } from "./decline.mjs";
3
3
  import { expanded, idOf } from "./expandable.mjs";
4
4
  import { StripeEventHandler } from "./handler.mjs";
5
- import { getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.mjs";
5
+ import { cancellationComment, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price } from "./mapper.mjs";
6
+ import { mapListPrice, mapSubscription } from "./subscription.mjs";
7
+ import { DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, hasDelayedNotification } from "./payment-method.mjs";
6
8
  import { StripeThinEventHandler } from "./thin-handler.mjs";
7
9
  import { cancellationDetailsSchema, createCheckoutSessionSchema, returnUrlSchema } from "./schema.mjs";
8
10
  import { METADATA_KEYS } from "./metadata.mjs";
9
11
  import { addPendingCheckout, getPendingCheckouts, removePendingCheckout } from "./pending-checkout.mjs";
10
- export { DECLINE_REASONS, METADATA_KEYS, StripeConfig, StripeEventHandler, StripeThinEventHandler, addPendingCheckout, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
12
+ export { DECLINE_REASONS, DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, METADATA_KEYS, StripeConfig, StripeEventHandler, StripeThinEventHandler, addPendingCheckout, cancellationComment, cancellationDetailsSchema, createCheckoutSessionSchema, expanded, getBeginCheckoutProperties, getPendingCheckouts, getPurchaseProperties, hasDelayedNotification, idOf, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDecline, mapDisputeStatus, mapInvoice, mapLineItem, mapListPrice, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscription, mapSubscriptionStatus, mapTime, minorUnits, money, price, removePendingCheckout, returnUrlSchema };
@@ -228,10 +228,28 @@ function getBeginCheckoutProperties(p) {
228
228
  };
229
229
  }
230
230
  /**
231
+ * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores
232
+ * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a
233
+ * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes
234
+ * back out.
235
+ */
236
+ function cancellationComment({ comment, resubscribeIntent }) {
237
+ return `${resubscribeIntent ? `[${resubscribeIntent}]` : ""}${comment ?? ""}`;
238
+ }
239
+ function parseCancellationComment(comment) {
240
+ const [, intent, rest] = comment && /^\[(\w+)\](.*)$/s.exec(comment) || [];
241
+ if (!intent || !require_subscription_index.RESUBSCRIBE_INTENTS.includes(intent)) return { comment };
242
+ return {
243
+ comment: rest || null,
244
+ resubscribeIntent: intent
245
+ };
246
+ }
247
+ /**
231
248
  * Stripe's `cancellation_details` in the shared shape. The object is present on every
232
249
  * subscription with all three fields null, so an active one maps to null rather than an empty
233
250
  * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their
234
- * behalf), the payment reasons are Stripe ending it.
251
+ * behalf), the payment reasons are Stripe ending it. A comment written by
252
+ * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.
235
253
  */
236
254
  function mapCancellationDetails(subscription) {
237
255
  const details = subscription.cancellation_details;
@@ -240,7 +258,7 @@ function mapCancellationDetails(subscription) {
240
258
  initiator: details.reason === "payment_failed" || details.reason === "payment_disputed" ? "system" : "user",
241
259
  reason: details.reason ?? null,
242
260
  feedback: details.feedback ? require_subscription_index.CANCELLATION_FEEDBACKS.includes(details.feedback) ? details.feedback : "other" : null,
243
- comment: details.comment ?? null
261
+ ...parseCancellationComment(details.comment ?? null)
244
262
  };
245
263
  }
246
264
  //#endregion
@@ -250,6 +268,7 @@ Object.defineProperty(exports, "ZERO_DECIMAL_CURRENCIES", {
250
268
  return _shware_utils.ZERO_DECIMAL_CURRENCIES;
251
269
  }
252
270
  });
271
+ exports.cancellationComment = cancellationComment;
253
272
  exports.getBeginCheckoutProperties = getBeginCheckoutProperties;
254
273
  exports.getPurchaseProperties = getPurchaseProperties;
255
274
  exports.mapCancellationDetails = mapCancellationDetails;
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.cjs","names":["DISPUTE_STATUSES","stripeMinorUnits","CANCELLATION_FEEDBACKS"],"sources":["../../src/stripe/mapper.ts"],"sourcesContent":["import { stripeMinorUnits } from '@shware/utils';\nimport type { Stripe } from 'stripe';\nimport {\n CANCELLATION_FEEDBACKS,\n type CancellationDetails,\n type CancellationFeedback,\n DISPUTE_STATUSES,\n type DisputeStatus,\n type PaymentType,\n type RefundStatus,\n type SubscriptionStatus,\n} from '../subscription/index';\n\nexport function mapTime<T extends number | null>(\n stripeTimestampSeconds: T\n): T extends number ? string : null {\n if (!stripeTimestampSeconds) return null as T extends number ? string : null;\n return new Date(stripeTimestampSeconds * 1000).toISOString() as T extends number ? string : null;\n}\n\nexport type Price = {\n id: string;\n type: Stripe.Price.Type;\n active: boolean;\n billing_scheme: Stripe.Price.BillingScheme;\n currency: Stripe.Price['currency'];\n unit_amount: number | null;\n unit_amount_decimal: string | null;\n recurring: Stripe.Price.Recurring | null;\n};\n\nexport function mapPrice(price: Stripe.Price): Price {\n return {\n id: price.id,\n type: price.type,\n active: price.active,\n billing_scheme: price.billing_scheme,\n currency: price.currency,\n unit_amount: price.unit_amount,\n unit_amount_decimal: price.unit_amount_decimal?.toString() ?? null,\n recurring: price.recurring,\n };\n}\n\nexport interface LineItem {\n id: string;\n currency: string;\n quantity: number | null;\n description: string | null;\n amount_tax: number;\n amount_total: number;\n amount_subtotal: number;\n amount_discount: number;\n price: Price | null;\n}\n\nexport function mapLineItem(item: Stripe.LineItem): LineItem {\n return {\n id: item.price\n ? typeof item.price.product === 'string'\n ? item.price.product\n : item.price.product.id\n : item.id,\n currency: item.currency,\n quantity: item.quantity,\n description: item.description,\n amount_tax: item.amount_tax,\n amount_total: item.amount_total,\n amount_subtotal: item.amount_subtotal,\n amount_discount: item.amount_discount,\n price: item.price ? mapPrice(item.price) : null,\n };\n}\n\n// Re-exported under our own name so consumers need not reach into stripe's namespace.\n// Open-ended since stripe 22.4: a status the API adds later is not a type error.\nexport type PaymentStatus = Stripe.Checkout.Session.PaymentStatus;\n\n// Declared rather than inferred from mapCheckoutSession: an inferred shape is\n// structural, so a consumer's declaration emit has to spell out payment_status\n// through stripe's internal module path — TS2883 in any package that reaches\n// stripe transitively instead of depending on it (see line_items/price too).\nexport interface CheckoutSession {\n id: string;\n url: string | null;\n coupon: string | undefined;\n livemode: boolean;\n expires_at: number;\n payment_status: PaymentStatus;\n currency: string | null;\n amount_total: number | null;\n line_items: LineItem[] | undefined;\n}\n\nexport function mapCheckoutSession(session: Stripe.Checkout.Session): CheckoutSession {\n let coupon: string | undefined = undefined;\n if (Array.isArray(session.discounts) && session.discounts.length !== 0) {\n const discount = session.discounts[0];\n if (discount.coupon && typeof discount.coupon === 'object') {\n coupon = discount.coupon.id;\n } else if (typeof discount.coupon === 'string') {\n coupon = discount.coupon;\n } else {\n coupon = undefined;\n }\n }\n\n return {\n id: session.id,\n url: session.url,\n coupon,\n livemode: session.livemode,\n expires_at: session.expires_at,\n payment_status: session.payment_status,\n currency: session.currency,\n amount_total: session.amount_total,\n line_items: session.line_items?.data.map(mapLineItem),\n };\n}\n\nexport function mapInvoice(i: Stripe.Invoice) {\n return {\n id: i.id,\n number: i.number,\n total: i.total,\n subtotal: i.subtotal,\n amount_due: i.amount_due,\n amount_paid: i.amount_paid,\n amount_remaining: i.amount_remaining,\n currency: i.currency,\n billing_reason: i.billing_reason,\n hosted_invoice_url: i.hosted_invoice_url,\n invoice_pdf: i.invoice_pdf,\n receipt_number: i.receipt_number,\n status: i.status,\n created: i.created,\n period_start: i.period_start,\n period_end: i.period_end,\n };\n}\n\nexport function mapPaymentIntent(intent: Stripe.PaymentIntent) {\n return {\n id: intent.id,\n amount: intent.amount,\n amount_capturable: intent.amount_capturable,\n amount_received: intent.amount_received,\n currency: intent.currency,\n client_secret: intent.client_secret,\n description: intent.description,\n status: intent.status,\n created: intent.created,\n };\n}\n\nexport function mapCharge(charge: Stripe.Charge) {\n return {\n id: charge.id,\n description: charge.description,\n currency: charge.currency,\n amount: charge.amount,\n amount_captured: charge.amount_captured,\n amount_refunded: charge.amount_refunded,\n receipt_email: charge.receipt_email,\n receipt_number: charge.receipt_number,\n receipt_url: charge.receipt_url,\n status: charge.status,\n created: charge.created,\n payment_intent:\n charge.payment_intent && typeof charge.payment_intent === 'object'\n ? mapPaymentIntent(charge.payment_intent)\n : undefined,\n };\n}\n\nexport type ProductPrice = {\n id: string;\n type: Stripe.Price.Type;\n unit_amount: number;\n currency: Stripe.Price['currency'];\n product: {\n id: Stripe.Product['id'];\n name: Stripe.Product['name'];\n description: Stripe.Product['description'];\n livemode: Stripe.Product['livemode'];\n };\n};\n\nexport function mapSubscriptionStatus(status: Stripe.Subscription.Status): SubscriptionStatus {\n switch (status) {\n case 'active':\n return 'ACTIVE';\n case 'canceled':\n return 'CANCELED';\n case 'incomplete':\n return 'INCOMPLETE';\n case 'incomplete_expired':\n return 'INCOMPLETE_EXPIRED';\n case 'past_due':\n return 'PAST_DUE';\n case 'paused':\n return 'PAUSED';\n case 'trialing':\n return 'TRIALING';\n case 'unpaid':\n return 'UNPAID';\n default: {\n console.error(`Invalid stripe status: ${String(status)}`);\n throw new Error(`Invalid stripe status: ${String(status)}`);\n }\n }\n}\n\n/**\n * What a paid subscription invoice was for, by its `billing_reason`. Only `subscription_cycle` is\n * a renewal: a plan change invoices its proration mid-period (`subscription_update`), and counting\n * that as a renewal would inflate the renewal rate by every upgrade.\n */\nexport function mapPaymentType(\n invoice: Pick<Stripe.Invoice, 'id' | 'billing_reason'>\n): Exclude<PaymentType, 'ONE_TIME'> {\n switch (invoice.billing_reason) {\n // Exactly once per subscription: its first invoice (after a trial, the first paid one is a cycle).\n case 'subscription_create':\n return 'SUBSCRIPTION_START';\n case 'subscription_cycle':\n return 'SUBSCRIPTION_RENEWAL';\n case 'subscription_update':\n return 'SUBSCRIPTION_UPDATE';\n // An invoice made by hand on the subscription (dashboard or API): off-cycle money, not a renewal.\n case 'manual':\n return 'SUBSCRIPTION_UPDATE';\n default:\n // subscription_threshold (metered billing) and the rest are not reasons flat-priced\n // subscriptions produce. Off-cycle is the safe reading: it leaves renewals alone.\n console.warn(`Invoice ${invoice.id} has billing_reason ${invoice.billing_reason}`);\n return 'SUBSCRIPTION_UPDATE';\n }\n}\n\n/** `refund.status` is an untyped string in stripe-node; these are the documented values. */\nconst REFUND_STATUS: Partial<Record<string, RefundStatus>> = {\n pending: 'PENDING',\n requires_action: 'PENDING',\n succeeded: 'SUCCEEDED',\n failed: 'FAILED',\n canceled: 'CANCELED',\n};\n\n/** A Stripe refund's status; a refund that has none yet is pending. Throws on an unknown one. */\nexport function mapRefundStatus(refund: Pick<Stripe.Refund, 'id' | 'status'>): RefundStatus {\n const status = REFUND_STATUS[refund.status ?? 'pending'];\n if (!status) throw new Error(`Unknown refund status ${refund.status}: ${refund.id}`);\n return status;\n}\n\n/** A Stripe dispute's status, upper-cased into {@link DISPUTE_STATUSES}. Throws on an unknown one. */\nexport function mapDisputeStatus(dispute: Pick<Stripe.Dispute, 'id' | 'status'>): DisputeStatus {\n const status = dispute.status.toUpperCase();\n const known = DISPUTE_STATUSES.find((item) => item === status);\n if (!known) throw new Error(`Unknown dispute status ${dispute.status}: ${dispute.id}`);\n return known;\n}\n\nexport { ZERO_DECIMAL_CURRENCIES } from '@shware/utils';\n\n/** How many Stripe API amount units make one unit of `currency` (1, 100 or 1000). */\nexport function minorUnits(currency: string) {\n return stripeMinorUnits(currency);\n}\n\nexport function price(value: number, currency: string) {\n return value / minorUnits(currency);\n}\n\nexport type Money = { amount: number; currency: string };\n\n/** A Stripe amount in minor units (cents, yen, fils) as an amount in `currency` itself. */\nexport function money(value: number, currency: string): Money {\n return { amount: price(value, currency), currency };\n}\n\nexport interface Item {\n item_id: string;\n item_name: string;\n affiliation?: 'Google Store' | (string & {});\n coupon?: string;\n discount?: number;\n index?: number;\n item_brand?: string;\n item_category?: string;\n item_category2?: string;\n item_category3?: string;\n item_category4?: string;\n item_category5?: string;\n item_list_id?: string;\n item_list_name?: string;\n item_variant?: string;\n location_id?: string;\n price?: number;\n quantity?: number;\n}\n\nexport interface PurchaseProperties {\n currency: string;\n value: number;\n transaction_id: string;\n coupon?: string;\n shipping?: number;\n tax?: number;\n items?: Item[];\n}\n\nexport interface BeginCheckoutProperties {\n currency: string;\n value: number;\n coupon?: string;\n items: Item[];\n}\n\n// Google Ads rejects conversion transaction IDs longer than 64 characters.\n// https://support.google.com/google-ads/answer/6386790\nconst GOOGLE_ADS_TRANSACTION_ID_MAX = 64;\n\n/**\n * GA4 purchase events feed Google Ads conversions, whose transaction IDs are\n * capped at 64 characters — but Stripe checkout session ids exceed that:\n * around 2021 they grew from ~32 chars (`cs_live_` + 24 random chars) to ~66\n * (`cs_live_a` + 57), with no announcement. Stripe treats id length/format as\n * opaque and backwards-compatible, guaranteeing only that ids never exceed\n * 255 chars, so they may grow again at any time.\n *\n * Dropping the fixed `cs_live_`/`cs_test_` prefix brings the id under 64\n * while staying reversible: prepend the prefix (livemode tells which) to\n * reconstruct the full session id for Stripe dashboard lookups. The payment\n * intent id (~27 chars) is not a substitute — it only exists on\n * `mode: 'payment'` sessions (subscription-mode sessions carry it on the\n * invoice instead), and its short length is just as unguaranteed as the\n * session id's was.\n *\n * Must stay deterministic: GA4/Google Ads dedupe purchases by\n * transaction_id, and the same session may be tracked from multiple paths.\n */\nfunction toTransactionId(sessionId: string): string {\n const stripped = sessionId.replace(/^cs_(live|test)_/, '');\n if (stripped.length > GOOGLE_ADS_TRANSACTION_ID_MAX) {\n console.warn(\n `transaction_id still exceeds ${GOOGLE_ADS_TRANSACTION_ID_MAX} chars after stripping ` +\n `the prefix — Stripe may have lengthened session ids again; truncating: ${sessionId}`\n );\n }\n return stripped.slice(0, GOOGLE_ADS_TRANSACTION_ID_MAX);\n}\n\nexport function getPurchaseProperties(session: CheckoutSession): PurchaseProperties {\n let value: number;\n let currency: string;\n if (!session.amount_total || !session.currency) {\n value = session.line_items?.reduce((acc, item) => acc + item.amount_total, 0) ?? 0;\n currency = session.line_items?.[0]?.currency ?? 'usd';\n } else {\n value = session.amount_total;\n currency = session.currency;\n }\n\n return {\n transaction_id: toTransactionId(session.id),\n value: price(value, currency),\n currency: currency.toUpperCase(),\n coupon: session.coupon,\n items: session.line_items?.map((item, index) => ({\n index,\n item_id: item.id,\n item_name: item.description ?? '',\n price: price(item.amount_total, item.currency),\n quantity: item.quantity ?? 1,\n discount: price(item.amount_discount, item.currency),\n })),\n };\n}\n\nexport function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties {\n return {\n currency: p.currency.toUpperCase(),\n value: price(p.unit_amount, p.currency),\n items: [\n {\n item_id: p.product.id,\n item_name: p.product.name,\n price: price(p.unit_amount, p.currency),\n },\n ],\n };\n}\n\n/**\n * Stripe's `cancellation_details` in the shared shape. The object is present on every\n * subscription with all three fields null, so an active one maps to null rather than an empty\n * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their\n * behalf), the payment reasons are Stripe ending it.\n */\nexport function mapCancellationDetails(\n subscription: Pick<Stripe.Subscription, 'cancellation_details'>\n): CancellationDetails | null {\n const details = subscription.cancellation_details;\n if (!details || (!details.reason && !details.feedback && !details.comment)) return null;\n return {\n initiator:\n details.reason === 'payment_failed' || details.reason === 'payment_disputed'\n ? 'system'\n : 'user',\n reason: details.reason ?? null,\n // Stripe's feedback vocabulary is ours; anything it adds later is reported as \"other\".\n feedback: details.feedback\n ? (CANCELLATION_FEEDBACKS as readonly string[]).includes(details.feedback)\n ? (details.feedback as CancellationFeedback)\n : 'other'\n : null,\n comment: details.comment ?? null,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mapper.cjs","names":["DISPUTE_STATUSES","stripeMinorUnits","RESUBSCRIBE_INTENTS","CANCELLATION_FEEDBACKS"],"sources":["../../src/stripe/mapper.ts"],"sourcesContent":["import { stripeMinorUnits } from '@shware/utils';\nimport type { Stripe } from 'stripe';\nimport {\n CANCELLATION_FEEDBACKS,\n type CancellationDetails,\n type CancellationFeedback,\n DISPUTE_STATUSES,\n type DisputeStatus,\n type PaymentType,\n RESUBSCRIBE_INTENTS,\n type RefundStatus,\n type ResubscribeIntent,\n type SubscriptionStatus,\n} from '../subscription/index';\n\nexport function mapTime<T extends number | null>(\n stripeTimestampSeconds: T\n): T extends number ? string : null {\n if (!stripeTimestampSeconds) return null as T extends number ? string : null;\n return new Date(stripeTimestampSeconds * 1000).toISOString() as T extends number ? string : null;\n}\n\nexport type Price = {\n id: string;\n type: Stripe.Price.Type;\n active: boolean;\n billing_scheme: Stripe.Price.BillingScheme;\n currency: Stripe.Price['currency'];\n unit_amount: number | null;\n unit_amount_decimal: string | null;\n recurring: Stripe.Price.Recurring | null;\n};\n\nexport function mapPrice(price: Stripe.Price): Price {\n return {\n id: price.id,\n type: price.type,\n active: price.active,\n billing_scheme: price.billing_scheme,\n currency: price.currency,\n unit_amount: price.unit_amount,\n unit_amount_decimal: price.unit_amount_decimal?.toString() ?? null,\n recurring: price.recurring,\n };\n}\n\nexport interface LineItem {\n id: string;\n currency: string;\n quantity: number | null;\n description: string | null;\n amount_tax: number;\n amount_total: number;\n amount_subtotal: number;\n amount_discount: number;\n price: Price | null;\n}\n\nexport function mapLineItem(item: Stripe.LineItem): LineItem {\n return {\n id: item.price\n ? typeof item.price.product === 'string'\n ? item.price.product\n : item.price.product.id\n : item.id,\n currency: item.currency,\n quantity: item.quantity,\n description: item.description,\n amount_tax: item.amount_tax,\n amount_total: item.amount_total,\n amount_subtotal: item.amount_subtotal,\n amount_discount: item.amount_discount,\n price: item.price ? mapPrice(item.price) : null,\n };\n}\n\n// Re-exported under our own name so consumers need not reach into stripe's namespace.\n// Open-ended since stripe 22.4: a status the API adds later is not a type error.\nexport type PaymentStatus = Stripe.Checkout.Session.PaymentStatus;\n\n// Declared rather than inferred from mapCheckoutSession: an inferred shape is\n// structural, so a consumer's declaration emit has to spell out payment_status\n// through stripe's internal module path — TS2883 in any package that reaches\n// stripe transitively instead of depending on it (see line_items/price too).\nexport interface CheckoutSession {\n id: string;\n url: string | null;\n coupon: string | undefined;\n livemode: boolean;\n expires_at: number;\n payment_status: PaymentStatus;\n currency: string | null;\n amount_total: number | null;\n line_items: LineItem[] | undefined;\n}\n\nexport function mapCheckoutSession(session: Stripe.Checkout.Session): CheckoutSession {\n let coupon: string | undefined = undefined;\n if (Array.isArray(session.discounts) && session.discounts.length !== 0) {\n const discount = session.discounts[0];\n if (discount.coupon && typeof discount.coupon === 'object') {\n coupon = discount.coupon.id;\n } else if (typeof discount.coupon === 'string') {\n coupon = discount.coupon;\n } else {\n coupon = undefined;\n }\n }\n\n return {\n id: session.id,\n url: session.url,\n coupon,\n livemode: session.livemode,\n expires_at: session.expires_at,\n payment_status: session.payment_status,\n currency: session.currency,\n amount_total: session.amount_total,\n line_items: session.line_items?.data.map(mapLineItem),\n };\n}\n\nexport function mapInvoice(i: Stripe.Invoice) {\n return {\n id: i.id,\n number: i.number,\n total: i.total,\n subtotal: i.subtotal,\n amount_due: i.amount_due,\n amount_paid: i.amount_paid,\n amount_remaining: i.amount_remaining,\n currency: i.currency,\n billing_reason: i.billing_reason,\n hosted_invoice_url: i.hosted_invoice_url,\n invoice_pdf: i.invoice_pdf,\n receipt_number: i.receipt_number,\n status: i.status,\n created: i.created,\n period_start: i.period_start,\n period_end: i.period_end,\n };\n}\n\nexport function mapPaymentIntent(intent: Stripe.PaymentIntent) {\n return {\n id: intent.id,\n amount: intent.amount,\n amount_capturable: intent.amount_capturable,\n amount_received: intent.amount_received,\n currency: intent.currency,\n client_secret: intent.client_secret,\n description: intent.description,\n status: intent.status,\n created: intent.created,\n };\n}\n\nexport function mapCharge(charge: Stripe.Charge) {\n return {\n id: charge.id,\n description: charge.description,\n currency: charge.currency,\n amount: charge.amount,\n amount_captured: charge.amount_captured,\n amount_refunded: charge.amount_refunded,\n receipt_email: charge.receipt_email,\n receipt_number: charge.receipt_number,\n receipt_url: charge.receipt_url,\n status: charge.status,\n created: charge.created,\n payment_intent:\n charge.payment_intent && typeof charge.payment_intent === 'object'\n ? mapPaymentIntent(charge.payment_intent)\n : undefined,\n };\n}\n\nexport type ProductPrice = {\n id: string;\n type: Stripe.Price.Type;\n unit_amount: number;\n currency: Stripe.Price['currency'];\n product: {\n id: Stripe.Product['id'];\n name: Stripe.Product['name'];\n description: Stripe.Product['description'];\n livemode: Stripe.Product['livemode'];\n };\n};\n\nexport function mapSubscriptionStatus(status: Stripe.Subscription.Status): SubscriptionStatus {\n switch (status) {\n case 'active':\n return 'ACTIVE';\n case 'canceled':\n return 'CANCELED';\n case 'incomplete':\n return 'INCOMPLETE';\n case 'incomplete_expired':\n return 'INCOMPLETE_EXPIRED';\n case 'past_due':\n return 'PAST_DUE';\n case 'paused':\n return 'PAUSED';\n case 'trialing':\n return 'TRIALING';\n case 'unpaid':\n return 'UNPAID';\n default: {\n console.error(`Invalid stripe status: ${String(status)}`);\n throw new Error(`Invalid stripe status: ${String(status)}`);\n }\n }\n}\n\n/**\n * What a paid subscription invoice was for, by its `billing_reason`. Only `subscription_cycle` is\n * a renewal: a plan change invoices its proration mid-period (`subscription_update`), and counting\n * that as a renewal would inflate the renewal rate by every upgrade.\n */\nexport function mapPaymentType(\n invoice: Pick<Stripe.Invoice, 'id' | 'billing_reason'>\n): Exclude<PaymentType, 'ONE_TIME'> {\n switch (invoice.billing_reason) {\n // Exactly once per subscription: its first invoice (after a trial, the first paid one is a cycle).\n case 'subscription_create':\n return 'SUBSCRIPTION_START';\n case 'subscription_cycle':\n return 'SUBSCRIPTION_RENEWAL';\n case 'subscription_update':\n return 'SUBSCRIPTION_UPDATE';\n // An invoice made by hand on the subscription (dashboard or API): off-cycle money, not a renewal.\n case 'manual':\n return 'SUBSCRIPTION_UPDATE';\n default:\n // subscription_threshold (metered billing) and the rest are not reasons flat-priced\n // subscriptions produce. Off-cycle is the safe reading: it leaves renewals alone.\n console.warn(`Invoice ${invoice.id} has billing_reason ${invoice.billing_reason}`);\n return 'SUBSCRIPTION_UPDATE';\n }\n}\n\n/** `refund.status` is an untyped string in stripe-node; these are the documented values. */\nconst REFUND_STATUS: Partial<Record<string, RefundStatus>> = {\n pending: 'PENDING',\n requires_action: 'PENDING',\n succeeded: 'SUCCEEDED',\n failed: 'FAILED',\n canceled: 'CANCELED',\n};\n\n/** A Stripe refund's status; a refund that has none yet is pending. Throws on an unknown one. */\nexport function mapRefundStatus(refund: Pick<Stripe.Refund, 'id' | 'status'>): RefundStatus {\n const status = REFUND_STATUS[refund.status ?? 'pending'];\n if (!status) throw new Error(`Unknown refund status ${refund.status}: ${refund.id}`);\n return status;\n}\n\n/** A Stripe dispute's status, upper-cased into {@link DISPUTE_STATUSES}. Throws on an unknown one. */\nexport function mapDisputeStatus(dispute: Pick<Stripe.Dispute, 'id' | 'status'>): DisputeStatus {\n const status = dispute.status.toUpperCase();\n const known = DISPUTE_STATUSES.find((item) => item === status);\n if (!known) throw new Error(`Unknown dispute status ${dispute.status}: ${dispute.id}`);\n return known;\n}\n\nexport { ZERO_DECIMAL_CURRENCIES } from '@shware/utils';\n\n/** How many Stripe API amount units make one unit of `currency` (1, 100 or 1000). */\nexport function minorUnits(currency: string) {\n return stripeMinorUnits(currency);\n}\n\nexport function price(value: number, currency: string) {\n return value / minorUnits(currency);\n}\n\nexport type Money = { amount: number; currency: string };\n\n/** A Stripe amount in minor units (cents, yen, fils) as an amount in `currency` itself. */\nexport function money(value: number, currency: string): Money {\n return { amount: price(value, currency), currency };\n}\n\nexport interface Item {\n item_id: string;\n item_name: string;\n affiliation?: 'Google Store' | (string & {});\n coupon?: string;\n discount?: number;\n index?: number;\n item_brand?: string;\n item_category?: string;\n item_category2?: string;\n item_category3?: string;\n item_category4?: string;\n item_category5?: string;\n item_list_id?: string;\n item_list_name?: string;\n item_variant?: string;\n location_id?: string;\n price?: number;\n quantity?: number;\n}\n\nexport interface PurchaseProperties {\n currency: string;\n value: number;\n transaction_id: string;\n coupon?: string;\n shipping?: number;\n tax?: number;\n items?: Item[];\n}\n\nexport interface BeginCheckoutProperties {\n currency: string;\n value: number;\n coupon?: string;\n items: Item[];\n}\n\n// Google Ads rejects conversion transaction IDs longer than 64 characters.\n// https://support.google.com/google-ads/answer/6386790\nconst GOOGLE_ADS_TRANSACTION_ID_MAX = 64;\n\n/**\n * GA4 purchase events feed Google Ads conversions, whose transaction IDs are\n * capped at 64 characters — but Stripe checkout session ids exceed that:\n * around 2021 they grew from ~32 chars (`cs_live_` + 24 random chars) to ~66\n * (`cs_live_a` + 57), with no announcement. Stripe treats id length/format as\n * opaque and backwards-compatible, guaranteeing only that ids never exceed\n * 255 chars, so they may grow again at any time.\n *\n * Dropping the fixed `cs_live_`/`cs_test_` prefix brings the id under 64\n * while staying reversible: prepend the prefix (livemode tells which) to\n * reconstruct the full session id for Stripe dashboard lookups. The payment\n * intent id (~27 chars) is not a substitute — it only exists on\n * `mode: 'payment'` sessions (subscription-mode sessions carry it on the\n * invoice instead), and its short length is just as unguaranteed as the\n * session id's was.\n *\n * Must stay deterministic: GA4/Google Ads dedupe purchases by\n * transaction_id, and the same session may be tracked from multiple paths.\n */\nfunction toTransactionId(sessionId: string): string {\n const stripped = sessionId.replace(/^cs_(live|test)_/, '');\n if (stripped.length > GOOGLE_ADS_TRANSACTION_ID_MAX) {\n console.warn(\n `transaction_id still exceeds ${GOOGLE_ADS_TRANSACTION_ID_MAX} chars after stripping ` +\n `the prefix — Stripe may have lengthened session ids again; truncating: ${sessionId}`\n );\n }\n return stripped.slice(0, GOOGLE_ADS_TRANSACTION_ID_MAX);\n}\n\nexport function getPurchaseProperties(session: CheckoutSession): PurchaseProperties {\n let value: number;\n let currency: string;\n if (!session.amount_total || !session.currency) {\n value = session.line_items?.reduce((acc, item) => acc + item.amount_total, 0) ?? 0;\n currency = session.line_items?.[0]?.currency ?? 'usd';\n } else {\n value = session.amount_total;\n currency = session.currency;\n }\n\n return {\n transaction_id: toTransactionId(session.id),\n value: price(value, currency),\n currency: currency.toUpperCase(),\n coupon: session.coupon,\n items: session.line_items?.map((item, index) => ({\n index,\n item_id: item.id,\n item_name: item.description ?? '',\n price: price(item.amount_total, item.currency),\n quantity: item.quantity ?? 1,\n discount: price(item.amount_discount, item.currency),\n })),\n };\n}\n\nexport function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties {\n return {\n currency: p.currency.toUpperCase(),\n value: price(p.unit_amount, p.currency),\n items: [\n {\n item_id: p.product.id,\n item_name: p.product.name,\n price: price(p.unit_amount, p.currency),\n },\n ],\n };\n}\n\n/**\n * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores\n * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a\n * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes\n * back out.\n */\nexport function cancellationComment({\n comment,\n resubscribeIntent,\n}: Pick<CancellationDetails, 'comment' | 'resubscribeIntent'>): string {\n return `${resubscribeIntent ? `[${resubscribeIntent}]` : ''}${comment ?? ''}`;\n}\n\nfunction parseCancellationComment(\n comment: string | null\n): Pick<CancellationDetails, 'comment' | 'resubscribeIntent'> {\n const [, intent, rest] = (comment && /^\\[(\\w+)\\](.*)$/s.exec(comment)) || [];\n if (!intent || !(RESUBSCRIBE_INTENTS as readonly string[]).includes(intent)) return { comment };\n return { comment: rest || null, resubscribeIntent: intent as ResubscribeIntent };\n}\n\n/**\n * Stripe's `cancellation_details` in the shared shape. The object is present on every\n * subscription with all three fields null, so an active one maps to null rather than an empty\n * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their\n * behalf), the payment reasons are Stripe ending it. A comment written by\n * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.\n */\nexport function mapCancellationDetails(\n subscription: Pick<Stripe.Subscription, 'cancellation_details'>\n): CancellationDetails | null {\n const details = subscription.cancellation_details;\n if (!details || (!details.reason && !details.feedback && !details.comment)) return null;\n return {\n initiator:\n details.reason === 'payment_failed' || details.reason === 'payment_disputed'\n ? 'system'\n : 'user',\n reason: details.reason ?? null,\n // Stripe's feedback vocabulary is ours; anything it adds later is reported as \"other\".\n feedback: details.feedback\n ? (CANCELLATION_FEEDBACKS as readonly string[]).includes(details.feedback)\n ? (details.feedback as CancellationFeedback)\n : 'other'\n : null,\n ...parseCancellationComment(details.comment ?? null),\n };\n}\n"],"mappings":"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"}
@@ -161,11 +161,19 @@ export interface BeginCheckoutProperties {
161
161
  }
162
162
  export declare function getPurchaseProperties(session: CheckoutSession): PurchaseProperties;
163
163
  export declare function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties;
164
+ /**
165
+ * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores
166
+ * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a
167
+ * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes
168
+ * back out.
169
+ */
170
+ export declare function cancellationComment({ comment, resubscribeIntent }: Pick<CancellationDetails, 'comment' | 'resubscribeIntent'>): string;
164
171
  /**
165
172
  * Stripe's `cancellation_details` in the shared shape. The object is present on every
166
173
  * subscription with all three fields null, so an active one maps to null rather than an empty
167
174
  * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their
168
- * behalf), the payment reasons are Stripe ending it.
175
+ * behalf), the payment reasons are Stripe ending it. A comment written by
176
+ * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.
169
177
  */
170
178
  export declare function mapCancellationDetails(subscription: Pick<Stripe.Subscription, 'cancellation_details'>): CancellationDetails | null;
171
179
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/stripe/mapper.ts"],"mappings":";;;;wBAagB,QAAQ,yBACtB,wBAAwB,IACvB;YAKS;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,gBAAgB,OAAO,MAAM;EAC7B,UAAU,OAAO;EACjB;EACA;EACA,WAAW,OAAO,MAAM;;wBAGV,SAAS,OAAO,OAAO,QAAQ;iBAa9B;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO;;wBAGO,YAAY,MAAM,OAAO,WAAW;YAoBxC,gBAAgB,OAAO,SAAS,QAAQ;iBAMnC;EACf;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB;EACA;EACA,YAAY;;wBAGE,mBAAmB,SAAS,OAAO,SAAS,UAAU;wBA0BtD,WAAW,GAAG,OAAO;EAEjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAc,OAAA,QAAA;EACd;EACA;EACA;EACA,QAAM,OAAA,QAAA;EACN;EACA;EACA;;wBAIY,iBAAiB,QAAQ,OAAO;EAE5C;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,cAAA;EACN;;wBAIY,UAAU,QAAQ,OAAO;EAErC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,OAAA;EACN;EACA;;;;;;;;;;;;YAOQ;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,UAAU,OAAO;EACjB;IACE,IAAI,OAAO;IACX,MAAM,OAAO;IACb,aAAa,OAAO;IACpB,UAAU,OAAO;;;wBAIL,sBAAsB,QAAQ,OAAO,aAAa,SAAS;;;;;;wBA8B3D,eACd,SAAS,KAAK,OAAO,oCACpB,QAAQ;;wBA8BK,gBAAgB,QAAQ,KAAK,OAAO,2BAA2B;;wBAO/D,iBAAiB,SAAS,KAAK,OAAO,4BAA4B;;wBAUlE,WAAW;wBAIX,MAAM,eAAe;YAIzB;EAAU;EAAgB;;;wBAGtB,MAAM,eAAe,mBAAmB;iBAIvC;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;iBAGO;EACf;EACA;EACA;EACA,OAAO;;wBAqCO,sBAAsB,SAAS,kBAAkB;wBA2BjD,2BAA2B,GAAG,eAAe;;;;;;;wBAoB7C,uBACd,cAAc,KAAK,OAAO,wCACzB"}
1
+ {"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/stripe/mapper.ts"],"mappings":";;;;wBAegB,QAAQ,yBACtB,wBAAwB,IACvB;YAKS;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,gBAAgB,OAAO,MAAM;EAC7B,UAAU,OAAO;EACjB;EACA;EACA,WAAW,OAAO,MAAM;;wBAGV,SAAS,OAAO,OAAO,QAAQ;iBAa9B;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO;;wBAGO,YAAY,MAAM,OAAO,WAAW;YAoBxC,gBAAgB,OAAO,SAAS,QAAQ;iBAMnC;EACf;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB;EACA;EACA,YAAY;;wBAGE,mBAAmB,SAAS,OAAO,SAAS,UAAU;wBA0BtD,WAAW,GAAG,OAAO;EAEjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAc,OAAA,QAAA;EACd;EACA;EACA;EACA,QAAM,OAAA,QAAA;EACN;EACA;EACA;;wBAIY,iBAAiB,QAAQ,OAAO;EAE5C;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,cAAA;EACN;;wBAIY,UAAU,QAAQ,OAAO;EAErC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,OAAA;EACN;EACA;;;;;;;;;;;;YAOQ;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,UAAU,OAAO;EACjB;IACE,IAAI,OAAO;IACX,MAAM,OAAO;IACb,aAAa,OAAO;IACpB,UAAU,OAAO;;;wBAIL,sBAAsB,QAAQ,OAAO,aAAa,SAAS;;;;;;wBA8B3D,eACd,SAAS,KAAK,OAAO,oCACpB,QAAQ;;wBA8BK,gBAAgB,QAAQ,KAAK,OAAO,2BAA2B;;wBAO/D,iBAAiB,SAAS,KAAK,OAAO,4BAA4B;;wBAUlE,WAAW;wBAIX,MAAM,eAAe;YAIzB;EAAU;EAAgB;;;wBAGtB,MAAM,eAAe,mBAAmB;iBAIvC;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;iBAGO;EACf;EACA;EACA;EACA,OAAO;;wBAqCO,sBAAsB,SAAS,kBAAkB;wBA2BjD,2BAA2B,GAAG,eAAe;;;;;;;wBAoB7C,sBACd,SACA,qBACC,KAAK;;;;;;;;wBAmBQ,uBACd,cAAc,KAAK,OAAO,wCACzB"}
@@ -161,11 +161,19 @@ export interface BeginCheckoutProperties {
161
161
  }
162
162
  export declare function getPurchaseProperties(session: CheckoutSession): PurchaseProperties;
163
163
  export declare function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties;
164
+ /**
165
+ * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores
166
+ * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a
167
+ * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes
168
+ * back out.
169
+ */
170
+ export declare function cancellationComment({ comment, resubscribeIntent }: Pick<CancellationDetails, 'comment' | 'resubscribeIntent'>): string;
164
171
  /**
165
172
  * Stripe's `cancellation_details` in the shared shape. The object is present on every
166
173
  * subscription with all three fields null, so an active one maps to null rather than an empty
167
174
  * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their
168
- * behalf), the payment reasons are Stripe ending it.
175
+ * behalf), the payment reasons are Stripe ending it. A comment written by
176
+ * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.
169
177
  */
170
178
  export declare function mapCancellationDetails(subscription: Pick<Stripe.Subscription, 'cancellation_details'>): CancellationDetails | null;
171
179
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/stripe/mapper.ts"],"mappings":";;;;wBAagB,QAAQ,yBACtB,wBAAwB,IACvB;YAKS;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,gBAAgB,OAAO,MAAM;EAC7B,UAAU,OAAO;EACjB;EACA;EACA,WAAW,OAAO,MAAM;;wBAGV,SAAS,OAAO,OAAO,QAAQ;iBAa9B;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO;;wBAGO,YAAY,MAAM,OAAO,WAAW;YAoBxC,gBAAgB,OAAO,SAAS,QAAQ;iBAMnC;EACf;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB;EACA;EACA,YAAY;;wBAGE,mBAAmB,SAAS,OAAO,SAAS,UAAU;wBA0BtD,WAAW,GAAG,OAAO;EAEjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAc,OAAA,QAAA;EACd;EACA;EACA;EACA,QAAM,OAAA,QAAA;EACN;EACA;EACA;;wBAIY,iBAAiB,QAAQ,OAAO;EAE5C;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,cAAA;EACN;;wBAIY,UAAU,QAAQ,OAAO;EAErC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,OAAA;EACN;EACA;;;;;;;;;;;;YAOQ;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,UAAU,OAAO;EACjB;IACE,IAAI,OAAO;IACX,MAAM,OAAO;IACb,aAAa,OAAO;IACpB,UAAU,OAAO;;;wBAIL,sBAAsB,QAAQ,OAAO,aAAa,SAAS;;;;;;wBA8B3D,eACd,SAAS,KAAK,OAAO,oCACpB,QAAQ;;wBA8BK,gBAAgB,QAAQ,KAAK,OAAO,2BAA2B;;wBAO/D,iBAAiB,SAAS,KAAK,OAAO,4BAA4B;;wBAUlE,WAAW;wBAIX,MAAM,eAAe;YAIzB;EAAU;EAAgB;;;wBAGtB,MAAM,eAAe,mBAAmB;iBAIvC;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;iBAGO;EACf;EACA;EACA;EACA,OAAO;;wBAqCO,sBAAsB,SAAS,kBAAkB;wBA2BjD,2BAA2B,GAAG,eAAe;;;;;;;wBAoB7C,uBACd,cAAc,KAAK,OAAO,wCACzB"}
1
+ {"version":3,"file":"mapper.d.mts","names":[],"sources":["../../src/stripe/mapper.ts"],"mappings":";;;;wBAegB,QAAQ,yBACtB,wBAAwB,IACvB;YAKS;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,gBAAgB,OAAO,MAAM;EAC7B,UAAU,OAAO;EACjB;EACA;EACA,WAAW,OAAO,MAAM;;wBAGV,SAAS,OAAO,OAAO,QAAQ;iBAa9B;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO;;wBAGO,YAAY,MAAM,OAAO,WAAW;YAoBxC,gBAAgB,OAAO,SAAS,QAAQ;iBAMnC;EACf;EACA;EACA;EACA;EACA;EACA,gBAAgB;EAChB;EACA;EACA,YAAY;;wBAGE,mBAAmB,SAAS,OAAO,SAAS,UAAU;wBA0BtD,WAAW,GAAG,OAAO;EAEjC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,gBAAc,OAAA,QAAA;EACd;EACA;EACA;EACA,QAAM,OAAA,QAAA;EACN;EACA;EACA;;wBAIY,iBAAiB,QAAQ,OAAO;EAE5C;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,cAAA;EACN;;wBAIY,UAAU,QAAQ,OAAO;EAErC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAM,OAAA,OAAA;EACN;EACA;;;;;;;;;;;;YAOQ;EACV;EACA,MAAM,OAAO,MAAM;EACnB;EACA,UAAU,OAAO;EACjB;IACE,IAAI,OAAO;IACX,MAAM,OAAO;IACb,aAAa,OAAO;IACpB,UAAU,OAAO;;;wBAIL,sBAAsB,QAAQ,OAAO,aAAa,SAAS;;;;;;wBA8B3D,eACd,SAAS,KAAK,OAAO,oCACpB,QAAQ;;wBA8BK,gBAAgB,QAAQ,KAAK,OAAO,2BAA2B;;wBAO/D,iBAAiB,SAAS,KAAK,OAAO,4BAA4B;;wBAUlE,WAAW;wBAIX,MAAM,eAAe;YAIzB;EAAU;EAAgB;;;wBAGtB,MAAM,eAAe,mBAAmB;iBAIvC;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;;iBAGO;EACf;EACA;EACA;EACA,OAAO;;wBAqCO,sBAAsB,SAAS,kBAAkB;wBA2BjD,2BAA2B,GAAG,eAAe;;;;;;;wBAoB7C,sBACd,SACA,qBACC,KAAK;;;;;;;;wBAmBQ,uBACd,cAAc,KAAK,OAAO,wCACzB"}
@@ -1,4 +1,4 @@
1
- import { CANCELLATION_FEEDBACKS, DISPUTE_STATUSES } from "../subscription/index.mjs";
1
+ import { CANCELLATION_FEEDBACKS, DISPUTE_STATUSES, RESUBSCRIBE_INTENTS } from "../subscription/index.mjs";
2
2
  import { ZERO_DECIMAL_CURRENCIES, stripeMinorUnits } from "@shware/utils";
3
3
  //#region src/stripe/mapper.ts
4
4
  function mapTime(stripeTimestampSeconds) {
@@ -227,10 +227,28 @@ function getBeginCheckoutProperties(p) {
227
227
  };
228
228
  }
229
229
  /**
230
+ * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores
231
+ * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a
232
+ * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes
233
+ * back out.
234
+ */
235
+ function cancellationComment({ comment, resubscribeIntent }) {
236
+ return `${resubscribeIntent ? `[${resubscribeIntent}]` : ""}${comment ?? ""}`;
237
+ }
238
+ function parseCancellationComment(comment) {
239
+ const [, intent, rest] = comment && /^\[(\w+)\](.*)$/s.exec(comment) || [];
240
+ if (!intent || !RESUBSCRIBE_INTENTS.includes(intent)) return { comment };
241
+ return {
242
+ comment: rest || null,
243
+ resubscribeIntent: intent
244
+ };
245
+ }
246
+ /**
230
247
  * Stripe's `cancellation_details` in the shared shape. The object is present on every
231
248
  * subscription with all three fields null, so an active one maps to null rather than an empty
232
249
  * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their
233
- * behalf), the payment reasons are Stripe ending it.
250
+ * behalf), the payment reasons are Stripe ending it. A comment written by
251
+ * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.
234
252
  */
235
253
  function mapCancellationDetails(subscription) {
236
254
  const details = subscription.cancellation_details;
@@ -239,10 +257,10 @@ function mapCancellationDetails(subscription) {
239
257
  initiator: details.reason === "payment_failed" || details.reason === "payment_disputed" ? "system" : "user",
240
258
  reason: details.reason ?? null,
241
259
  feedback: details.feedback ? CANCELLATION_FEEDBACKS.includes(details.feedback) ? details.feedback : "other" : null,
242
- comment: details.comment ?? null
260
+ ...parseCancellationComment(details.comment ?? null)
243
261
  };
244
262
  }
245
263
  //#endregion
246
- export { ZERO_DECIMAL_CURRENCIES, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price };
264
+ export { ZERO_DECIMAL_CURRENCIES, cancellationComment, getBeginCheckoutProperties, getPurchaseProperties, mapCancellationDetails, mapCharge, mapCheckoutSession, mapDisputeStatus, mapInvoice, mapLineItem, mapPaymentIntent, mapPaymentType, mapPrice, mapRefundStatus, mapSubscriptionStatus, mapTime, minorUnits, money, price };
247
265
 
248
266
  //# sourceMappingURL=mapper.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/stripe/mapper.ts"],"sourcesContent":["import { stripeMinorUnits } from '@shware/utils';\nimport type { Stripe } from 'stripe';\nimport {\n CANCELLATION_FEEDBACKS,\n type CancellationDetails,\n type CancellationFeedback,\n DISPUTE_STATUSES,\n type DisputeStatus,\n type PaymentType,\n type RefundStatus,\n type SubscriptionStatus,\n} from '../subscription/index';\n\nexport function mapTime<T extends number | null>(\n stripeTimestampSeconds: T\n): T extends number ? string : null {\n if (!stripeTimestampSeconds) return null as T extends number ? string : null;\n return new Date(stripeTimestampSeconds * 1000).toISOString() as T extends number ? string : null;\n}\n\nexport type Price = {\n id: string;\n type: Stripe.Price.Type;\n active: boolean;\n billing_scheme: Stripe.Price.BillingScheme;\n currency: Stripe.Price['currency'];\n unit_amount: number | null;\n unit_amount_decimal: string | null;\n recurring: Stripe.Price.Recurring | null;\n};\n\nexport function mapPrice(price: Stripe.Price): Price {\n return {\n id: price.id,\n type: price.type,\n active: price.active,\n billing_scheme: price.billing_scheme,\n currency: price.currency,\n unit_amount: price.unit_amount,\n unit_amount_decimal: price.unit_amount_decimal?.toString() ?? null,\n recurring: price.recurring,\n };\n}\n\nexport interface LineItem {\n id: string;\n currency: string;\n quantity: number | null;\n description: string | null;\n amount_tax: number;\n amount_total: number;\n amount_subtotal: number;\n amount_discount: number;\n price: Price | null;\n}\n\nexport function mapLineItem(item: Stripe.LineItem): LineItem {\n return {\n id: item.price\n ? typeof item.price.product === 'string'\n ? item.price.product\n : item.price.product.id\n : item.id,\n currency: item.currency,\n quantity: item.quantity,\n description: item.description,\n amount_tax: item.amount_tax,\n amount_total: item.amount_total,\n amount_subtotal: item.amount_subtotal,\n amount_discount: item.amount_discount,\n price: item.price ? mapPrice(item.price) : null,\n };\n}\n\n// Re-exported under our own name so consumers need not reach into stripe's namespace.\n// Open-ended since stripe 22.4: a status the API adds later is not a type error.\nexport type PaymentStatus = Stripe.Checkout.Session.PaymentStatus;\n\n// Declared rather than inferred from mapCheckoutSession: an inferred shape is\n// structural, so a consumer's declaration emit has to spell out payment_status\n// through stripe's internal module path — TS2883 in any package that reaches\n// stripe transitively instead of depending on it (see line_items/price too).\nexport interface CheckoutSession {\n id: string;\n url: string | null;\n coupon: string | undefined;\n livemode: boolean;\n expires_at: number;\n payment_status: PaymentStatus;\n currency: string | null;\n amount_total: number | null;\n line_items: LineItem[] | undefined;\n}\n\nexport function mapCheckoutSession(session: Stripe.Checkout.Session): CheckoutSession {\n let coupon: string | undefined = undefined;\n if (Array.isArray(session.discounts) && session.discounts.length !== 0) {\n const discount = session.discounts[0];\n if (discount.coupon && typeof discount.coupon === 'object') {\n coupon = discount.coupon.id;\n } else if (typeof discount.coupon === 'string') {\n coupon = discount.coupon;\n } else {\n coupon = undefined;\n }\n }\n\n return {\n id: session.id,\n url: session.url,\n coupon,\n livemode: session.livemode,\n expires_at: session.expires_at,\n payment_status: session.payment_status,\n currency: session.currency,\n amount_total: session.amount_total,\n line_items: session.line_items?.data.map(mapLineItem),\n };\n}\n\nexport function mapInvoice(i: Stripe.Invoice) {\n return {\n id: i.id,\n number: i.number,\n total: i.total,\n subtotal: i.subtotal,\n amount_due: i.amount_due,\n amount_paid: i.amount_paid,\n amount_remaining: i.amount_remaining,\n currency: i.currency,\n billing_reason: i.billing_reason,\n hosted_invoice_url: i.hosted_invoice_url,\n invoice_pdf: i.invoice_pdf,\n receipt_number: i.receipt_number,\n status: i.status,\n created: i.created,\n period_start: i.period_start,\n period_end: i.period_end,\n };\n}\n\nexport function mapPaymentIntent(intent: Stripe.PaymentIntent) {\n return {\n id: intent.id,\n amount: intent.amount,\n amount_capturable: intent.amount_capturable,\n amount_received: intent.amount_received,\n currency: intent.currency,\n client_secret: intent.client_secret,\n description: intent.description,\n status: intent.status,\n created: intent.created,\n };\n}\n\nexport function mapCharge(charge: Stripe.Charge) {\n return {\n id: charge.id,\n description: charge.description,\n currency: charge.currency,\n amount: charge.amount,\n amount_captured: charge.amount_captured,\n amount_refunded: charge.amount_refunded,\n receipt_email: charge.receipt_email,\n receipt_number: charge.receipt_number,\n receipt_url: charge.receipt_url,\n status: charge.status,\n created: charge.created,\n payment_intent:\n charge.payment_intent && typeof charge.payment_intent === 'object'\n ? mapPaymentIntent(charge.payment_intent)\n : undefined,\n };\n}\n\nexport type ProductPrice = {\n id: string;\n type: Stripe.Price.Type;\n unit_amount: number;\n currency: Stripe.Price['currency'];\n product: {\n id: Stripe.Product['id'];\n name: Stripe.Product['name'];\n description: Stripe.Product['description'];\n livemode: Stripe.Product['livemode'];\n };\n};\n\nexport function mapSubscriptionStatus(status: Stripe.Subscription.Status): SubscriptionStatus {\n switch (status) {\n case 'active':\n return 'ACTIVE';\n case 'canceled':\n return 'CANCELED';\n case 'incomplete':\n return 'INCOMPLETE';\n case 'incomplete_expired':\n return 'INCOMPLETE_EXPIRED';\n case 'past_due':\n return 'PAST_DUE';\n case 'paused':\n return 'PAUSED';\n case 'trialing':\n return 'TRIALING';\n case 'unpaid':\n return 'UNPAID';\n default: {\n console.error(`Invalid stripe status: ${String(status)}`);\n throw new Error(`Invalid stripe status: ${String(status)}`);\n }\n }\n}\n\n/**\n * What a paid subscription invoice was for, by its `billing_reason`. Only `subscription_cycle` is\n * a renewal: a plan change invoices its proration mid-period (`subscription_update`), and counting\n * that as a renewal would inflate the renewal rate by every upgrade.\n */\nexport function mapPaymentType(\n invoice: Pick<Stripe.Invoice, 'id' | 'billing_reason'>\n): Exclude<PaymentType, 'ONE_TIME'> {\n switch (invoice.billing_reason) {\n // Exactly once per subscription: its first invoice (after a trial, the first paid one is a cycle).\n case 'subscription_create':\n return 'SUBSCRIPTION_START';\n case 'subscription_cycle':\n return 'SUBSCRIPTION_RENEWAL';\n case 'subscription_update':\n return 'SUBSCRIPTION_UPDATE';\n // An invoice made by hand on the subscription (dashboard or API): off-cycle money, not a renewal.\n case 'manual':\n return 'SUBSCRIPTION_UPDATE';\n default:\n // subscription_threshold (metered billing) and the rest are not reasons flat-priced\n // subscriptions produce. Off-cycle is the safe reading: it leaves renewals alone.\n console.warn(`Invoice ${invoice.id} has billing_reason ${invoice.billing_reason}`);\n return 'SUBSCRIPTION_UPDATE';\n }\n}\n\n/** `refund.status` is an untyped string in stripe-node; these are the documented values. */\nconst REFUND_STATUS: Partial<Record<string, RefundStatus>> = {\n pending: 'PENDING',\n requires_action: 'PENDING',\n succeeded: 'SUCCEEDED',\n failed: 'FAILED',\n canceled: 'CANCELED',\n};\n\n/** A Stripe refund's status; a refund that has none yet is pending. Throws on an unknown one. */\nexport function mapRefundStatus(refund: Pick<Stripe.Refund, 'id' | 'status'>): RefundStatus {\n const status = REFUND_STATUS[refund.status ?? 'pending'];\n if (!status) throw new Error(`Unknown refund status ${refund.status}: ${refund.id}`);\n return status;\n}\n\n/** A Stripe dispute's status, upper-cased into {@link DISPUTE_STATUSES}. Throws on an unknown one. */\nexport function mapDisputeStatus(dispute: Pick<Stripe.Dispute, 'id' | 'status'>): DisputeStatus {\n const status = dispute.status.toUpperCase();\n const known = DISPUTE_STATUSES.find((item) => item === status);\n if (!known) throw new Error(`Unknown dispute status ${dispute.status}: ${dispute.id}`);\n return known;\n}\n\nexport { ZERO_DECIMAL_CURRENCIES } from '@shware/utils';\n\n/** How many Stripe API amount units make one unit of `currency` (1, 100 or 1000). */\nexport function minorUnits(currency: string) {\n return stripeMinorUnits(currency);\n}\n\nexport function price(value: number, currency: string) {\n return value / minorUnits(currency);\n}\n\nexport type Money = { amount: number; currency: string };\n\n/** A Stripe amount in minor units (cents, yen, fils) as an amount in `currency` itself. */\nexport function money(value: number, currency: string): Money {\n return { amount: price(value, currency), currency };\n}\n\nexport interface Item {\n item_id: string;\n item_name: string;\n affiliation?: 'Google Store' | (string & {});\n coupon?: string;\n discount?: number;\n index?: number;\n item_brand?: string;\n item_category?: string;\n item_category2?: string;\n item_category3?: string;\n item_category4?: string;\n item_category5?: string;\n item_list_id?: string;\n item_list_name?: string;\n item_variant?: string;\n location_id?: string;\n price?: number;\n quantity?: number;\n}\n\nexport interface PurchaseProperties {\n currency: string;\n value: number;\n transaction_id: string;\n coupon?: string;\n shipping?: number;\n tax?: number;\n items?: Item[];\n}\n\nexport interface BeginCheckoutProperties {\n currency: string;\n value: number;\n coupon?: string;\n items: Item[];\n}\n\n// Google Ads rejects conversion transaction IDs longer than 64 characters.\n// https://support.google.com/google-ads/answer/6386790\nconst GOOGLE_ADS_TRANSACTION_ID_MAX = 64;\n\n/**\n * GA4 purchase events feed Google Ads conversions, whose transaction IDs are\n * capped at 64 characters — but Stripe checkout session ids exceed that:\n * around 2021 they grew from ~32 chars (`cs_live_` + 24 random chars) to ~66\n * (`cs_live_a` + 57), with no announcement. Stripe treats id length/format as\n * opaque and backwards-compatible, guaranteeing only that ids never exceed\n * 255 chars, so they may grow again at any time.\n *\n * Dropping the fixed `cs_live_`/`cs_test_` prefix brings the id under 64\n * while staying reversible: prepend the prefix (livemode tells which) to\n * reconstruct the full session id for Stripe dashboard lookups. The payment\n * intent id (~27 chars) is not a substitute — it only exists on\n * `mode: 'payment'` sessions (subscription-mode sessions carry it on the\n * invoice instead), and its short length is just as unguaranteed as the\n * session id's was.\n *\n * Must stay deterministic: GA4/Google Ads dedupe purchases by\n * transaction_id, and the same session may be tracked from multiple paths.\n */\nfunction toTransactionId(sessionId: string): string {\n const stripped = sessionId.replace(/^cs_(live|test)_/, '');\n if (stripped.length > GOOGLE_ADS_TRANSACTION_ID_MAX) {\n console.warn(\n `transaction_id still exceeds ${GOOGLE_ADS_TRANSACTION_ID_MAX} chars after stripping ` +\n `the prefix — Stripe may have lengthened session ids again; truncating: ${sessionId}`\n );\n }\n return stripped.slice(0, GOOGLE_ADS_TRANSACTION_ID_MAX);\n}\n\nexport function getPurchaseProperties(session: CheckoutSession): PurchaseProperties {\n let value: number;\n let currency: string;\n if (!session.amount_total || !session.currency) {\n value = session.line_items?.reduce((acc, item) => acc + item.amount_total, 0) ?? 0;\n currency = session.line_items?.[0]?.currency ?? 'usd';\n } else {\n value = session.amount_total;\n currency = session.currency;\n }\n\n return {\n transaction_id: toTransactionId(session.id),\n value: price(value, currency),\n currency: currency.toUpperCase(),\n coupon: session.coupon,\n items: session.line_items?.map((item, index) => ({\n index,\n item_id: item.id,\n item_name: item.description ?? '',\n price: price(item.amount_total, item.currency),\n quantity: item.quantity ?? 1,\n discount: price(item.amount_discount, item.currency),\n })),\n };\n}\n\nexport function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties {\n return {\n currency: p.currency.toUpperCase(),\n value: price(p.unit_amount, p.currency),\n items: [\n {\n item_id: p.product.id,\n item_name: p.product.name,\n price: price(p.unit_amount, p.currency),\n },\n ],\n };\n}\n\n/**\n * Stripe's `cancellation_details` in the shared shape. The object is present on every\n * subscription with all three fields null, so an active one maps to null rather than an empty\n * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their\n * behalf), the payment reasons are Stripe ending it.\n */\nexport function mapCancellationDetails(\n subscription: Pick<Stripe.Subscription, 'cancellation_details'>\n): CancellationDetails | null {\n const details = subscription.cancellation_details;\n if (!details || (!details.reason && !details.feedback && !details.comment)) return null;\n return {\n initiator:\n details.reason === 'payment_failed' || details.reason === 'payment_disputed'\n ? 'system'\n : 'user',\n reason: details.reason ?? null,\n // Stripe's feedback vocabulary is ours; anything it adds later is reported as \"other\".\n feedback: details.feedback\n ? (CANCELLATION_FEEDBACKS as readonly string[]).includes(details.feedback)\n ? (details.feedback as CancellationFeedback)\n : 'other'\n : null,\n comment: details.comment ?? null,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mapper.mjs","names":[],"sources":["../../src/stripe/mapper.ts"],"sourcesContent":["import { stripeMinorUnits } from '@shware/utils';\nimport type { Stripe } from 'stripe';\nimport {\n CANCELLATION_FEEDBACKS,\n type CancellationDetails,\n type CancellationFeedback,\n DISPUTE_STATUSES,\n type DisputeStatus,\n type PaymentType,\n RESUBSCRIBE_INTENTS,\n type RefundStatus,\n type ResubscribeIntent,\n type SubscriptionStatus,\n} from '../subscription/index';\n\nexport function mapTime<T extends number | null>(\n stripeTimestampSeconds: T\n): T extends number ? string : null {\n if (!stripeTimestampSeconds) return null as T extends number ? string : null;\n return new Date(stripeTimestampSeconds * 1000).toISOString() as T extends number ? string : null;\n}\n\nexport type Price = {\n id: string;\n type: Stripe.Price.Type;\n active: boolean;\n billing_scheme: Stripe.Price.BillingScheme;\n currency: Stripe.Price['currency'];\n unit_amount: number | null;\n unit_amount_decimal: string | null;\n recurring: Stripe.Price.Recurring | null;\n};\n\nexport function mapPrice(price: Stripe.Price): Price {\n return {\n id: price.id,\n type: price.type,\n active: price.active,\n billing_scheme: price.billing_scheme,\n currency: price.currency,\n unit_amount: price.unit_amount,\n unit_amount_decimal: price.unit_amount_decimal?.toString() ?? null,\n recurring: price.recurring,\n };\n}\n\nexport interface LineItem {\n id: string;\n currency: string;\n quantity: number | null;\n description: string | null;\n amount_tax: number;\n amount_total: number;\n amount_subtotal: number;\n amount_discount: number;\n price: Price | null;\n}\n\nexport function mapLineItem(item: Stripe.LineItem): LineItem {\n return {\n id: item.price\n ? typeof item.price.product === 'string'\n ? item.price.product\n : item.price.product.id\n : item.id,\n currency: item.currency,\n quantity: item.quantity,\n description: item.description,\n amount_tax: item.amount_tax,\n amount_total: item.amount_total,\n amount_subtotal: item.amount_subtotal,\n amount_discount: item.amount_discount,\n price: item.price ? mapPrice(item.price) : null,\n };\n}\n\n// Re-exported under our own name so consumers need not reach into stripe's namespace.\n// Open-ended since stripe 22.4: a status the API adds later is not a type error.\nexport type PaymentStatus = Stripe.Checkout.Session.PaymentStatus;\n\n// Declared rather than inferred from mapCheckoutSession: an inferred shape is\n// structural, so a consumer's declaration emit has to spell out payment_status\n// through stripe's internal module path — TS2883 in any package that reaches\n// stripe transitively instead of depending on it (see line_items/price too).\nexport interface CheckoutSession {\n id: string;\n url: string | null;\n coupon: string | undefined;\n livemode: boolean;\n expires_at: number;\n payment_status: PaymentStatus;\n currency: string | null;\n amount_total: number | null;\n line_items: LineItem[] | undefined;\n}\n\nexport function mapCheckoutSession(session: Stripe.Checkout.Session): CheckoutSession {\n let coupon: string | undefined = undefined;\n if (Array.isArray(session.discounts) && session.discounts.length !== 0) {\n const discount = session.discounts[0];\n if (discount.coupon && typeof discount.coupon === 'object') {\n coupon = discount.coupon.id;\n } else if (typeof discount.coupon === 'string') {\n coupon = discount.coupon;\n } else {\n coupon = undefined;\n }\n }\n\n return {\n id: session.id,\n url: session.url,\n coupon,\n livemode: session.livemode,\n expires_at: session.expires_at,\n payment_status: session.payment_status,\n currency: session.currency,\n amount_total: session.amount_total,\n line_items: session.line_items?.data.map(mapLineItem),\n };\n}\n\nexport function mapInvoice(i: Stripe.Invoice) {\n return {\n id: i.id,\n number: i.number,\n total: i.total,\n subtotal: i.subtotal,\n amount_due: i.amount_due,\n amount_paid: i.amount_paid,\n amount_remaining: i.amount_remaining,\n currency: i.currency,\n billing_reason: i.billing_reason,\n hosted_invoice_url: i.hosted_invoice_url,\n invoice_pdf: i.invoice_pdf,\n receipt_number: i.receipt_number,\n status: i.status,\n created: i.created,\n period_start: i.period_start,\n period_end: i.period_end,\n };\n}\n\nexport function mapPaymentIntent(intent: Stripe.PaymentIntent) {\n return {\n id: intent.id,\n amount: intent.amount,\n amount_capturable: intent.amount_capturable,\n amount_received: intent.amount_received,\n currency: intent.currency,\n client_secret: intent.client_secret,\n description: intent.description,\n status: intent.status,\n created: intent.created,\n };\n}\n\nexport function mapCharge(charge: Stripe.Charge) {\n return {\n id: charge.id,\n description: charge.description,\n currency: charge.currency,\n amount: charge.amount,\n amount_captured: charge.amount_captured,\n amount_refunded: charge.amount_refunded,\n receipt_email: charge.receipt_email,\n receipt_number: charge.receipt_number,\n receipt_url: charge.receipt_url,\n status: charge.status,\n created: charge.created,\n payment_intent:\n charge.payment_intent && typeof charge.payment_intent === 'object'\n ? mapPaymentIntent(charge.payment_intent)\n : undefined,\n };\n}\n\nexport type ProductPrice = {\n id: string;\n type: Stripe.Price.Type;\n unit_amount: number;\n currency: Stripe.Price['currency'];\n product: {\n id: Stripe.Product['id'];\n name: Stripe.Product['name'];\n description: Stripe.Product['description'];\n livemode: Stripe.Product['livemode'];\n };\n};\n\nexport function mapSubscriptionStatus(status: Stripe.Subscription.Status): SubscriptionStatus {\n switch (status) {\n case 'active':\n return 'ACTIVE';\n case 'canceled':\n return 'CANCELED';\n case 'incomplete':\n return 'INCOMPLETE';\n case 'incomplete_expired':\n return 'INCOMPLETE_EXPIRED';\n case 'past_due':\n return 'PAST_DUE';\n case 'paused':\n return 'PAUSED';\n case 'trialing':\n return 'TRIALING';\n case 'unpaid':\n return 'UNPAID';\n default: {\n console.error(`Invalid stripe status: ${String(status)}`);\n throw new Error(`Invalid stripe status: ${String(status)}`);\n }\n }\n}\n\n/**\n * What a paid subscription invoice was for, by its `billing_reason`. Only `subscription_cycle` is\n * a renewal: a plan change invoices its proration mid-period (`subscription_update`), and counting\n * that as a renewal would inflate the renewal rate by every upgrade.\n */\nexport function mapPaymentType(\n invoice: Pick<Stripe.Invoice, 'id' | 'billing_reason'>\n): Exclude<PaymentType, 'ONE_TIME'> {\n switch (invoice.billing_reason) {\n // Exactly once per subscription: its first invoice (after a trial, the first paid one is a cycle).\n case 'subscription_create':\n return 'SUBSCRIPTION_START';\n case 'subscription_cycle':\n return 'SUBSCRIPTION_RENEWAL';\n case 'subscription_update':\n return 'SUBSCRIPTION_UPDATE';\n // An invoice made by hand on the subscription (dashboard or API): off-cycle money, not a renewal.\n case 'manual':\n return 'SUBSCRIPTION_UPDATE';\n default:\n // subscription_threshold (metered billing) and the rest are not reasons flat-priced\n // subscriptions produce. Off-cycle is the safe reading: it leaves renewals alone.\n console.warn(`Invoice ${invoice.id} has billing_reason ${invoice.billing_reason}`);\n return 'SUBSCRIPTION_UPDATE';\n }\n}\n\n/** `refund.status` is an untyped string in stripe-node; these are the documented values. */\nconst REFUND_STATUS: Partial<Record<string, RefundStatus>> = {\n pending: 'PENDING',\n requires_action: 'PENDING',\n succeeded: 'SUCCEEDED',\n failed: 'FAILED',\n canceled: 'CANCELED',\n};\n\n/** A Stripe refund's status; a refund that has none yet is pending. Throws on an unknown one. */\nexport function mapRefundStatus(refund: Pick<Stripe.Refund, 'id' | 'status'>): RefundStatus {\n const status = REFUND_STATUS[refund.status ?? 'pending'];\n if (!status) throw new Error(`Unknown refund status ${refund.status}: ${refund.id}`);\n return status;\n}\n\n/** A Stripe dispute's status, upper-cased into {@link DISPUTE_STATUSES}. Throws on an unknown one. */\nexport function mapDisputeStatus(dispute: Pick<Stripe.Dispute, 'id' | 'status'>): DisputeStatus {\n const status = dispute.status.toUpperCase();\n const known = DISPUTE_STATUSES.find((item) => item === status);\n if (!known) throw new Error(`Unknown dispute status ${dispute.status}: ${dispute.id}`);\n return known;\n}\n\nexport { ZERO_DECIMAL_CURRENCIES } from '@shware/utils';\n\n/** How many Stripe API amount units make one unit of `currency` (1, 100 or 1000). */\nexport function minorUnits(currency: string) {\n return stripeMinorUnits(currency);\n}\n\nexport function price(value: number, currency: string) {\n return value / minorUnits(currency);\n}\n\nexport type Money = { amount: number; currency: string };\n\n/** A Stripe amount in minor units (cents, yen, fils) as an amount in `currency` itself. */\nexport function money(value: number, currency: string): Money {\n return { amount: price(value, currency), currency };\n}\n\nexport interface Item {\n item_id: string;\n item_name: string;\n affiliation?: 'Google Store' | (string & {});\n coupon?: string;\n discount?: number;\n index?: number;\n item_brand?: string;\n item_category?: string;\n item_category2?: string;\n item_category3?: string;\n item_category4?: string;\n item_category5?: string;\n item_list_id?: string;\n item_list_name?: string;\n item_variant?: string;\n location_id?: string;\n price?: number;\n quantity?: number;\n}\n\nexport interface PurchaseProperties {\n currency: string;\n value: number;\n transaction_id: string;\n coupon?: string;\n shipping?: number;\n tax?: number;\n items?: Item[];\n}\n\nexport interface BeginCheckoutProperties {\n currency: string;\n value: number;\n coupon?: string;\n items: Item[];\n}\n\n// Google Ads rejects conversion transaction IDs longer than 64 characters.\n// https://support.google.com/google-ads/answer/6386790\nconst GOOGLE_ADS_TRANSACTION_ID_MAX = 64;\n\n/**\n * GA4 purchase events feed Google Ads conversions, whose transaction IDs are\n * capped at 64 characters — but Stripe checkout session ids exceed that:\n * around 2021 they grew from ~32 chars (`cs_live_` + 24 random chars) to ~66\n * (`cs_live_a` + 57), with no announcement. Stripe treats id length/format as\n * opaque and backwards-compatible, guaranteeing only that ids never exceed\n * 255 chars, so they may grow again at any time.\n *\n * Dropping the fixed `cs_live_`/`cs_test_` prefix brings the id under 64\n * while staying reversible: prepend the prefix (livemode tells which) to\n * reconstruct the full session id for Stripe dashboard lookups. The payment\n * intent id (~27 chars) is not a substitute — it only exists on\n * `mode: 'payment'` sessions (subscription-mode sessions carry it on the\n * invoice instead), and its short length is just as unguaranteed as the\n * session id's was.\n *\n * Must stay deterministic: GA4/Google Ads dedupe purchases by\n * transaction_id, and the same session may be tracked from multiple paths.\n */\nfunction toTransactionId(sessionId: string): string {\n const stripped = sessionId.replace(/^cs_(live|test)_/, '');\n if (stripped.length > GOOGLE_ADS_TRANSACTION_ID_MAX) {\n console.warn(\n `transaction_id still exceeds ${GOOGLE_ADS_TRANSACTION_ID_MAX} chars after stripping ` +\n `the prefix — Stripe may have lengthened session ids again; truncating: ${sessionId}`\n );\n }\n return stripped.slice(0, GOOGLE_ADS_TRANSACTION_ID_MAX);\n}\n\nexport function getPurchaseProperties(session: CheckoutSession): PurchaseProperties {\n let value: number;\n let currency: string;\n if (!session.amount_total || !session.currency) {\n value = session.line_items?.reduce((acc, item) => acc + item.amount_total, 0) ?? 0;\n currency = session.line_items?.[0]?.currency ?? 'usd';\n } else {\n value = session.amount_total;\n currency = session.currency;\n }\n\n return {\n transaction_id: toTransactionId(session.id),\n value: price(value, currency),\n currency: currency.toUpperCase(),\n coupon: session.coupon,\n items: session.line_items?.map((item, index) => ({\n index,\n item_id: item.id,\n item_name: item.description ?? '',\n price: price(item.amount_total, item.currency),\n quantity: item.quantity ?? 1,\n discount: price(item.amount_discount, item.currency),\n })),\n };\n}\n\nexport function getBeginCheckoutProperties(p: ProductPrice): BeginCheckoutProperties {\n return {\n currency: p.currency.toUpperCase(),\n value: price(p.unit_amount, p.currency),\n items: [\n {\n item_id: p.product.id,\n item_name: p.product.name,\n price: price(p.unit_amount, p.currency),\n },\n ],\n };\n}\n\n/**\n * The `cancellation_details.comment` to send Stripe for a cancel dialog's answers. Stripe stores\n * reason, feedback and comment, one field short of `resubscribeIntent`, so the intent travels as a\n * `[maybe]` / `[no]` / `[yes]` prefix on the comment, which {@link mapCancellationDetails} takes\n * back out.\n */\nexport function cancellationComment({\n comment,\n resubscribeIntent,\n}: Pick<CancellationDetails, 'comment' | 'resubscribeIntent'>): string {\n return `${resubscribeIntent ? `[${resubscribeIntent}]` : ''}${comment ?? ''}`;\n}\n\nfunction parseCancellationComment(\n comment: string | null\n): Pick<CancellationDetails, 'comment' | 'resubscribeIntent'> {\n const [, intent, rest] = (comment && /^\\[(\\w+)\\](.*)$/s.exec(comment)) || [];\n if (!intent || !(RESUBSCRIBE_INTENTS as readonly string[]).includes(intent)) return { comment };\n return { comment: rest || null, resubscribeIntent: intent as ResubscribeIntent };\n}\n\n/**\n * Stripe's `cancellation_details` in the shared shape. The object is present on every\n * subscription with all three fields null, so an active one maps to null rather than an empty\n * record. `reason` says who acted: `cancellation_requested` is the customer (or us on their\n * behalf), the payment reasons are Stripe ending it. A comment written by\n * {@link cancellationComment} is split back into the comment and `resubscribeIntent`.\n */\nexport function mapCancellationDetails(\n subscription: Pick<Stripe.Subscription, 'cancellation_details'>\n): CancellationDetails | null {\n const details = subscription.cancellation_details;\n if (!details || (!details.reason && !details.feedback && !details.comment)) return null;\n return {\n initiator:\n details.reason === 'payment_failed' || details.reason === 'payment_disputed'\n ? 'system'\n : 'user',\n reason: details.reason ?? null,\n // Stripe's feedback vocabulary is ours; anything it adds later is reported as \"other\".\n feedback: details.feedback\n ? (CANCELLATION_FEEDBACKS as readonly string[]).includes(details.feedback)\n ? (details.feedback as CancellationFeedback)\n : 'other'\n : null,\n ...parseCancellationComment(details.comment ?? null),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/stripe/payment-method.ts
3
+ /**
4
+ * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's
5
+ * "payment methods with delayed notification" (https://docs.stripe.com/payments/payment-methods#payment-notification).
6
+ * A charge to one sits in `processing` until the bank answers, and can still fail then.
7
+ *
8
+ * Only types that can be saved and charged again (a subscription's or customer's default) are
9
+ * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)
10
+ * also notify late, but never come back as a saved payment method. A deny-list, so instant methods
11
+ * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be
12
+ * added here.
13
+ */
14
+ const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = /* @__PURE__ */ new Set([
15
+ "sepa_debit",
16
+ "us_bank_account",
17
+ "bacs_debit",
18
+ "au_becs_debit",
19
+ "acss_debit",
20
+ "nz_bank_account"
21
+ ]);
22
+ /**
23
+ * Whether a charge to this payment method type is only known to have succeeded days later. Flows
24
+ * that need the money now (a Stripe pending update, which supports no such method and expires
25
+ * after ~23 hours) should refuse it up front rather than report a late failure.
26
+ */
27
+ function hasDelayedNotification(type) {
28
+ return DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES.has(type);
29
+ }
30
+ //#endregion
31
+ exports.DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES;
32
+ exports.hasDelayedNotification = hasDelayedNotification;
33
+
34
+ //# sourceMappingURL=payment-method.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-method.cjs","names":[],"sources":["../../src/stripe/payment-method.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\n\n/**\n * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's\n * \"payment methods with delayed notification\" (https://docs.stripe.com/payments/payment-methods#payment-notification).\n * A charge to one sits in `processing` until the bank answers, and can still fail then.\n *\n * Only types that can be saved and charged again (a subscription's or customer's default) are\n * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)\n * also notify late, but never come back as a saved payment method. A deny-list, so instant methods\n * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be\n * added here.\n */\nexport const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES: ReadonlySet<Stripe.PaymentMethod.Type> =\n new Set([\n 'sepa_debit',\n 'us_bank_account',\n 'bacs_debit',\n 'au_becs_debit',\n 'acss_debit',\n 'nz_bank_account',\n ]);\n\n/**\n * Whether a charge to this payment method type is only known to have succeeded days later. Flows\n * that need the money now (a Stripe pending update, which supports no such method and expires\n * after ~23 hours) should refuse it up front rather than report a late failure.\n */\nexport function hasDelayedNotification(type: Stripe.PaymentMethod.Type): boolean {\n return DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES.has(type);\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,4DACX,IAAI,IAAI;CACN;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOH,SAAgB,uBAAuB,MAA0C;CAC/E,OAAO,0CAA0C,IAAI,IAAI;AAC3D"}
@@ -0,0 +1,22 @@
1
+ import { Stripe } from "stripe";
2
+ //#region src/stripe/payment-method.d.ts
3
+ /**
4
+ * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's
5
+ * "payment methods with delayed notification" (https://docs.stripe.com/payments/payment-methods#payment-notification).
6
+ * A charge to one sits in `processing` until the bank answers, and can still fail then.
7
+ *
8
+ * Only types that can be saved and charged again (a subscription's or customer's default) are
9
+ * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)
10
+ * also notify late, but never come back as a saved payment method. A deny-list, so instant methods
11
+ * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be
12
+ * added here.
13
+ */
14
+ export declare const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES: ReadonlySet<Stripe.PaymentMethod.Type>;
15
+ /**
16
+ * Whether a charge to this payment method type is only known to have succeeded days later. Flows
17
+ * that need the money now (a Stripe pending update, which supports no such method and expires
18
+ * after ~23 hours) should refuse it up front rather than report a late failure.
19
+ */
20
+ export declare function hasDelayedNotification(type: Stripe.PaymentMethod.Type): boolean;
21
+ //#endregion
22
+ //# sourceMappingURL=payment-method.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-method.d.cts","names":[],"sources":["../../src/stripe/payment-method.ts"],"mappings":";;;;;;;;;;;;;qBAaa,2CAA2C,YAAY,OAAO,cAAc;;;;;;wBAezE,uBAAuB,MAAM,OAAO,cAAc"}
@@ -0,0 +1,22 @@
1
+ import { Stripe } from "stripe";
2
+ //#region src/stripe/payment-method.d.ts
3
+ /**
4
+ * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's
5
+ * "payment methods with delayed notification" (https://docs.stripe.com/payments/payment-methods#payment-notification).
6
+ * A charge to one sits in `processing` until the bank answers, and can still fail then.
7
+ *
8
+ * Only types that can be saved and charged again (a subscription's or customer's default) are
9
+ * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)
10
+ * also notify late, but never come back as a saved payment method. A deny-list, so instant methods
11
+ * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be
12
+ * added here.
13
+ */
14
+ export declare const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES: ReadonlySet<Stripe.PaymentMethod.Type>;
15
+ /**
16
+ * Whether a charge to this payment method type is only known to have succeeded days later. Flows
17
+ * that need the money now (a Stripe pending update, which supports no such method and expires
18
+ * after ~23 hours) should refuse it up front rather than report a late failure.
19
+ */
20
+ export declare function hasDelayedNotification(type: Stripe.PaymentMethod.Type): boolean;
21
+ //#endregion
22
+ //# sourceMappingURL=payment-method.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-method.d.mts","names":[],"sources":["../../src/stripe/payment-method.ts"],"mappings":";;;;;;;;;;;;;qBAaa,2CAA2C,YAAY,OAAO,cAAc;;;;;;wBAezE,uBAAuB,MAAM,OAAO,cAAc"}
@@ -0,0 +1,32 @@
1
+ //#region src/stripe/payment-method.ts
2
+ /**
3
+ * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's
4
+ * "payment methods with delayed notification" (https://docs.stripe.com/payments/payment-methods#payment-notification).
5
+ * A charge to one sits in `processing` until the bank answers, and can still fail then.
6
+ *
7
+ * Only types that can be saved and charged again (a subscription's or customer's default) are
8
+ * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)
9
+ * also notify late, but never come back as a saved payment method. A deny-list, so instant methods
10
+ * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be
11
+ * added here.
12
+ */
13
+ const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = /* @__PURE__ */ new Set([
14
+ "sepa_debit",
15
+ "us_bank_account",
16
+ "bacs_debit",
17
+ "au_becs_debit",
18
+ "acss_debit",
19
+ "nz_bank_account"
20
+ ]);
21
+ /**
22
+ * Whether a charge to this payment method type is only known to have succeeded days later. Flows
23
+ * that need the money now (a Stripe pending update, which supports no such method and expires
24
+ * after ~23 hours) should refuse it up front rather than report a late failure.
25
+ */
26
+ function hasDelayedNotification(type) {
27
+ return DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES.has(type);
28
+ }
29
+ //#endregion
30
+ export { DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES, hasDelayedNotification };
31
+
32
+ //# sourceMappingURL=payment-method.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment-method.mjs","names":[],"sources":["../../src/stripe/payment-method.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\n\n/**\n * Reusable payment methods whose outcome arrives days after the charge is attempted: Stripe's\n * \"payment methods with delayed notification\" (https://docs.stripe.com/payments/payment-methods#payment-notification).\n * A charge to one sits in `processing` until the bank answers, and can still fail then.\n *\n * Only types that can be saved and charged again (a subscription's or customer's default) are\n * listed: the bank debits. One-off vouchers and transfers (Boleto, OXXO, Konbini, bank transfers)\n * also notify late, but never come back as a saved payment method. A deny-list, so instant methods\n * (cards, wallets, Link, PayPal, ...) need no entry; a delayed type Stripe adds later has to be\n * added here.\n */\nexport const DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES: ReadonlySet<Stripe.PaymentMethod.Type> =\n new Set([\n 'sepa_debit',\n 'us_bank_account',\n 'bacs_debit',\n 'au_becs_debit',\n 'acss_debit',\n 'nz_bank_account',\n ]);\n\n/**\n * Whether a charge to this payment method type is only known to have succeeded days later. Flows\n * that need the money now (a Stripe pending update, which supports no such method and expires\n * after ~23 hours) should refuse it up front rather than report a late failure.\n */\nexport function hasDelayedNotification(type: Stripe.PaymentMethod.Type): boolean {\n return DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES.has(type);\n}\n"],"mappings":";;;;;;;;;;;;AAaA,MAAa,4DACX,IAAI,IAAI;CACN;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOH,SAAgB,uBAAuB,MAA0C;CAC/E,OAAO,0CAA0C,IAAI,IAAI;AAC3D"}
@@ -0,0 +1,35 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_stripe_mapper = require("./mapper.cjs");
3
+ //#region src/stripe/subscription.ts
4
+ /**
5
+ * The plan's list price, which is what "$X / month" promises. The latest invoice is deliberately
6
+ * not used: after an upgrade it is the prorated difference, and after a coupon the discounted
7
+ * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or
8
+ * custom-amount price, which has no flat unit amount.
9
+ */
10
+ function mapListPrice({ items }) {
11
+ const item = items.data.at(0);
12
+ const price = item?.price;
13
+ if (!price || price.unit_amount === null) return null;
14
+ return require_stripe_mapper.money(price.unit_amount * (item.quantity ?? 1), price.currency);
15
+ }
16
+ function mapSubscription(sub) {
17
+ const periodEnd = sub.items.data.at(0)?.current_period_end ?? null;
18
+ return {
19
+ status: require_stripe_mapper.mapSubscriptionStatus(sub.status),
20
+ autoRenewEnabled: sub.status !== "canceled" && !sub.cancel_at_period_end && sub.cancel_at === null,
21
+ price: mapListPrice(sub),
22
+ startedAt: require_stripe_mapper.mapTime(sub.start_date),
23
+ createdAt: require_stripe_mapper.mapTime(sub.created),
24
+ renewalAt: sub.status === "paused" ? null : require_stripe_mapper.mapTime(periodEnd),
25
+ trialStart: require_stripe_mapper.mapTime(sub.trial_start),
26
+ trialEnd: require_stripe_mapper.mapTime(sub.trial_end),
27
+ canceledAt: require_stripe_mapper.mapTime(sub.canceled_at),
28
+ cancellationDetails: require_stripe_mapper.mapCancellationDetails(sub)
29
+ };
30
+ }
31
+ //#endregion
32
+ exports.mapListPrice = mapListPrice;
33
+ exports.mapSubscription = mapSubscription;
34
+
35
+ //# sourceMappingURL=subscription.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.cjs","names":["money","mapSubscriptionStatus","mapTime","mapCancellationDetails"],"sources":["../../src/stripe/subscription.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\nimport type { CancellationDetails, SubscriptionStatus } from '../subscription/index';\nimport {\n type Money,\n mapCancellationDetails,\n mapSubscriptionStatus,\n mapTime,\n money,\n} from './mapper';\n\n/**\n * A Stripe subscription in the platform-neutral shape a subscription row stores. What it leaves out\n * is the app's: who the user is and which plan the product means (both from the subscription's\n * metadata and the app's config), the row id, and when the row was written.\n */\nexport interface StripeSubscriptionState {\n status: SubscriptionStatus;\n autoRenewEnabled: boolean;\n /** see {@link mapListPrice}; null for a price without a flat amount */\n price: Money | null;\n startedAt: string;\n createdAt: string;\n /** null while paused: a paused subscription has no date it comes back on */\n renewalAt: string | null;\n trialStart: string | null;\n trialEnd: string | null;\n canceledAt: string | null;\n cancellationDetails: CancellationDetails | null;\n}\n\n/**\n * The plan's list price, which is what \"$X / month\" promises. The latest invoice is deliberately\n * not used: after an upgrade it is the prorated difference, and after a coupon the discounted\n * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or\n * custom-amount price, which has no flat unit amount.\n */\nexport function mapListPrice({ items }: Pick<Stripe.Subscription, 'items'>): Money | null {\n const item = items.data.at(0);\n const price = item?.price;\n if (!price || price.unit_amount === null) return null;\n return money(price.unit_amount * (item.quantity ?? 1), price.currency);\n}\n\nexport function mapSubscription(sub: Stripe.Subscription): StripeSubscriptionState {\n const periodEnd = sub.items.data.at(0)?.current_period_end ?? null;\n return {\n status: mapSubscriptionStatus(sub.status),\n autoRenewEnabled:\n sub.status !== 'canceled' && !sub.cancel_at_period_end && sub.cancel_at === null,\n price: mapListPrice(sub),\n startedAt: mapTime(sub.start_date),\n createdAt: mapTime(sub.created),\n // A paused subscription (trial ended without a card, failed payment, or an explicit pause;\n // `status_details.paused` says which) stays paused until it is resumed, so the period end it\n // still reports is not a renewal.\n renewalAt: sub.status === 'paused' ? null : mapTime(periodEnd),\n trialStart: mapTime(sub.trial_start),\n trialEnd: mapTime(sub.trial_end),\n canceledAt: mapTime(sub.canceled_at),\n cancellationDetails: mapCancellationDetails(sub),\n };\n}\n"],"mappings":";;;;;;;;;AAoCA,SAAgB,aAAa,EAAE,SAA2D;CACxF,MAAM,OAAO,MAAM,KAAK,GAAG,CAAC;CAC5B,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,SAAS,MAAM,gBAAgB,MAAM,OAAO;CACjD,OAAOA,sBAAAA,MAAM,MAAM,eAAe,KAAK,YAAY,IAAI,MAAM,QAAQ;AACvE;AAEA,SAAgB,gBAAgB,KAAmD;CACjF,MAAM,YAAY,IAAI,MAAM,KAAK,GAAG,CAAC,CAAC,EAAE,sBAAsB;CAC9D,OAAO;EACL,QAAQC,sBAAAA,sBAAsB,IAAI,MAAM;EACxC,kBACE,IAAI,WAAW,cAAc,CAAC,IAAI,wBAAwB,IAAI,cAAc;EAC9E,OAAO,aAAa,GAAG;EACvB,WAAWC,sBAAAA,QAAQ,IAAI,UAAU;EACjC,WAAWA,sBAAAA,QAAQ,IAAI,OAAO;EAI9B,WAAW,IAAI,WAAW,WAAW,OAAOA,sBAAAA,QAAQ,SAAS;EAC7D,YAAYA,sBAAAA,QAAQ,IAAI,WAAW;EACnC,UAAUA,sBAAAA,QAAQ,IAAI,SAAS;EAC/B,YAAYA,sBAAAA,QAAQ,IAAI,WAAW;EACnC,qBAAqBC,sBAAAA,uBAAuB,GAAG;CACjD;AACF"}
@@ -0,0 +1,33 @@
1
+ import { CancellationDetails, SubscriptionStatus } from "../subscription/index.cjs";
2
+ import { Money } from "./mapper.cjs";
3
+ import { Stripe } from "stripe";
4
+ //#region src/stripe/subscription.d.ts
5
+ /**
6
+ * A Stripe subscription in the platform-neutral shape a subscription row stores. What it leaves out
7
+ * is the app's: who the user is and which plan the product means (both from the subscription's
8
+ * metadata and the app's config), the row id, and when the row was written.
9
+ */
10
+ export interface StripeSubscriptionState {
11
+ status: SubscriptionStatus;
12
+ autoRenewEnabled: boolean;
13
+ /** see {@link mapListPrice}; null for a price without a flat amount */
14
+ price: Money | null;
15
+ startedAt: string;
16
+ createdAt: string;
17
+ /** null while paused: a paused subscription has no date it comes back on */
18
+ renewalAt: string | null;
19
+ trialStart: string | null;
20
+ trialEnd: string | null;
21
+ canceledAt: string | null;
22
+ cancellationDetails: CancellationDetails | null;
23
+ }
24
+ /**
25
+ * The plan's list price, which is what "$X / month" promises. The latest invoice is deliberately
26
+ * not used: after an upgrade it is the prorated difference, and after a coupon the discounted
27
+ * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or
28
+ * custom-amount price, which has no flat unit amount.
29
+ */
30
+ export declare function mapListPrice({ items }: Pick<Stripe.Subscription, 'items'>): Money | null;
31
+ export declare function mapSubscription(sub: Stripe.Subscription): StripeSubscriptionState;
32
+ //#endregion
33
+ //# sourceMappingURL=subscription.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.d.cts","names":[],"sources":["../../src/stripe/subscription.ts"],"mappings":";;;;;;;;;iBAeiB;EACf,QAAQ;EACR;;EAEA,OAAO;EACP;EACA;;EAEA;EACA;EACA;EACA;EACA,qBAAqB;;;;;;;;wBASP,eAAe,SAAS,KAAK,OAAO,yBAAyB;wBAO7D,gBAAgB,KAAK,OAAO,eAAe"}
@@ -0,0 +1,33 @@
1
+ import { CancellationDetails, SubscriptionStatus } from "../subscription/index.mjs";
2
+ import { Money } from "./mapper.mjs";
3
+ import { Stripe } from "stripe";
4
+ //#region src/stripe/subscription.d.ts
5
+ /**
6
+ * A Stripe subscription in the platform-neutral shape a subscription row stores. What it leaves out
7
+ * is the app's: who the user is and which plan the product means (both from the subscription's
8
+ * metadata and the app's config), the row id, and when the row was written.
9
+ */
10
+ export interface StripeSubscriptionState {
11
+ status: SubscriptionStatus;
12
+ autoRenewEnabled: boolean;
13
+ /** see {@link mapListPrice}; null for a price without a flat amount */
14
+ price: Money | null;
15
+ startedAt: string;
16
+ createdAt: string;
17
+ /** null while paused: a paused subscription has no date it comes back on */
18
+ renewalAt: string | null;
19
+ trialStart: string | null;
20
+ trialEnd: string | null;
21
+ canceledAt: string | null;
22
+ cancellationDetails: CancellationDetails | null;
23
+ }
24
+ /**
25
+ * The plan's list price, which is what "$X / month" promises. The latest invoice is deliberately
26
+ * not used: after an upgrade it is the prorated difference, and after a coupon the discounted
27
+ * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or
28
+ * custom-amount price, which has no flat unit amount.
29
+ */
30
+ export declare function mapListPrice({ items }: Pick<Stripe.Subscription, 'items'>): Money | null;
31
+ export declare function mapSubscription(sub: Stripe.Subscription): StripeSubscriptionState;
32
+ //#endregion
33
+ //# sourceMappingURL=subscription.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.d.mts","names":[],"sources":["../../src/stripe/subscription.ts"],"mappings":";;;;;;;;;iBAeiB;EACf,QAAQ;EACR;;EAEA,OAAO;EACP;EACA;;EAEA;EACA;EACA;EACA;EACA,qBAAqB;;;;;;;;wBASP,eAAe,SAAS,KAAK,OAAO,yBAAyB;wBAO7D,gBAAgB,KAAK,OAAO,eAAe"}
@@ -0,0 +1,33 @@
1
+ import { mapCancellationDetails, mapSubscriptionStatus, mapTime, money } from "./mapper.mjs";
2
+ //#region src/stripe/subscription.ts
3
+ /**
4
+ * The plan's list price, which is what "$X / month" promises. The latest invoice is deliberately
5
+ * not used: after an upgrade it is the prorated difference, and after a coupon the discounted
6
+ * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or
7
+ * custom-amount price, which has no flat unit amount.
8
+ */
9
+ function mapListPrice({ items }) {
10
+ const item = items.data.at(0);
11
+ const price = item?.price;
12
+ if (!price || price.unit_amount === null) return null;
13
+ return money(price.unit_amount * (item.quantity ?? 1), price.currency);
14
+ }
15
+ function mapSubscription(sub) {
16
+ const periodEnd = sub.items.data.at(0)?.current_period_end ?? null;
17
+ return {
18
+ status: mapSubscriptionStatus(sub.status),
19
+ autoRenewEnabled: sub.status !== "canceled" && !sub.cancel_at_period_end && sub.cancel_at === null,
20
+ price: mapListPrice(sub),
21
+ startedAt: mapTime(sub.start_date),
22
+ createdAt: mapTime(sub.created),
23
+ renewalAt: sub.status === "paused" ? null : mapTime(periodEnd),
24
+ trialStart: mapTime(sub.trial_start),
25
+ trialEnd: mapTime(sub.trial_end),
26
+ canceledAt: mapTime(sub.canceled_at),
27
+ cancellationDetails: mapCancellationDetails(sub)
28
+ };
29
+ }
30
+ //#endregion
31
+ export { mapListPrice, mapSubscription };
32
+
33
+ //# sourceMappingURL=subscription.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription.mjs","names":[],"sources":["../../src/stripe/subscription.ts"],"sourcesContent":["import type { Stripe } from 'stripe';\nimport type { CancellationDetails, SubscriptionStatus } from '../subscription/index';\nimport {\n type Money,\n mapCancellationDetails,\n mapSubscriptionStatus,\n mapTime,\n money,\n} from './mapper';\n\n/**\n * A Stripe subscription in the platform-neutral shape a subscription row stores. What it leaves out\n * is the app's: who the user is and which plan the product means (both from the subscription's\n * metadata and the app's config), the row id, and when the row was written.\n */\nexport interface StripeSubscriptionState {\n status: SubscriptionStatus;\n autoRenewEnabled: boolean;\n /** see {@link mapListPrice}; null for a price without a flat amount */\n price: Money | null;\n startedAt: string;\n createdAt: string;\n /** null while paused: a paused subscription has no date it comes back on */\n renewalAt: string | null;\n trialStart: string | null;\n trialEnd: string | null;\n canceledAt: string | null;\n cancellationDetails: CancellationDetails | null;\n}\n\n/**\n * The plan's list price, which is what \"$X / month\" promises. The latest invoice is deliberately\n * not used: after an upgrade it is the prorated difference, and after a coupon the discounted\n * amount, both of which belong in the invoice history rather than on the plan. Null for a tiered or\n * custom-amount price, which has no flat unit amount.\n */\nexport function mapListPrice({ items }: Pick<Stripe.Subscription, 'items'>): Money | null {\n const item = items.data.at(0);\n const price = item?.price;\n if (!price || price.unit_amount === null) return null;\n return money(price.unit_amount * (item.quantity ?? 1), price.currency);\n}\n\nexport function mapSubscription(sub: Stripe.Subscription): StripeSubscriptionState {\n const periodEnd = sub.items.data.at(0)?.current_period_end ?? null;\n return {\n status: mapSubscriptionStatus(sub.status),\n autoRenewEnabled:\n sub.status !== 'canceled' && !sub.cancel_at_period_end && sub.cancel_at === null,\n price: mapListPrice(sub),\n startedAt: mapTime(sub.start_date),\n createdAt: mapTime(sub.created),\n // A paused subscription (trial ended without a card, failed payment, or an explicit pause;\n // `status_details.paused` says which) stays paused until it is resumed, so the period end it\n // still reports is not a renewal.\n renewalAt: sub.status === 'paused' ? null : mapTime(periodEnd),\n trialStart: mapTime(sub.trial_start),\n trialEnd: mapTime(sub.trial_end),\n canceledAt: mapTime(sub.canceled_at),\n cancellationDetails: mapCancellationDetails(sub),\n };\n}\n"],"mappings":";;;;;;;;AAoCA,SAAgB,aAAa,EAAE,SAA2D;CACxF,MAAM,OAAO,MAAM,KAAK,GAAG,CAAC;CAC5B,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,SAAS,MAAM,gBAAgB,MAAM,OAAO;CACjD,OAAO,MAAM,MAAM,eAAe,KAAK,YAAY,IAAI,MAAM,QAAQ;AACvE;AAEA,SAAgB,gBAAgB,KAAmD;CACjF,MAAM,YAAY,IAAI,MAAM,KAAK,GAAG,CAAC,CAAC,EAAE,sBAAsB;CAC9D,OAAO;EACL,QAAQ,sBAAsB,IAAI,MAAM;EACxC,kBACE,IAAI,WAAW,cAAc,CAAC,IAAI,wBAAwB,IAAI,cAAc;EAC9E,OAAO,aAAa,GAAG;EACvB,WAAW,QAAQ,IAAI,UAAU;EACjC,WAAW,QAAQ,IAAI,OAAO;EAI9B,WAAW,IAAI,WAAW,WAAW,OAAO,QAAQ,SAAS;EAC7D,YAAY,QAAQ,IAAI,WAAW;EACnC,UAAU,QAAQ,IAAI,SAAS;EAC/B,YAAY,QAAQ,IAAI,WAAW;EACnC,qBAAqB,uBAAuB,GAAG;CACjD;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/purchase",
3
- "version": "7.3.0",
3
+ "version": "7.5.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",