@usethrottle/quotes 0.4.1 → 0.6.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.
package/dist/index.d.cts CHANGED
@@ -16,6 +16,28 @@ type PaymentTerms = {
16
16
  };
17
17
  balanceNetN: number;
18
18
  };
19
+ /** A subscription line's terms (Throttle creates one subscription per line when the quote is paid). */
20
+ interface QuoteItemRecurring {
21
+ plan: string;
22
+ planName: string | null;
23
+ interval: string;
24
+ intervalUnit: string;
25
+ intervalCount: number;
26
+ /** Set on an installment plan: exactly this many payments. */
27
+ totalPayments: number | null;
28
+ /** 0 when there is no free trial. A free-trial line charges nothing at acceptance. */
29
+ trialDays: number;
30
+ /**
31
+ * Custom installment schedule: payments that differ in amount (per unit, minor
32
+ * units) and in the gap before each one. Present only on a schedule line, where
33
+ * `totalPayments` equals its length. Row 0 has no gap.
34
+ */
35
+ schedule?: Array<{
36
+ amount: number;
37
+ intervalUnit: string | null;
38
+ intervalCount: number | null;
39
+ }>;
40
+ }
19
41
  interface QuoteItem {
20
42
  id: string;
21
43
  type: string;
@@ -35,6 +57,8 @@ interface QuoteItem {
35
57
  qtyEditable: boolean;
36
58
  imageUrl: string | null;
37
59
  position: number;
60
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
61
+ recurring: QuoteItemRecurring | null;
38
62
  }
39
63
  interface QuoteRevision {
40
64
  id: string;
@@ -261,6 +285,25 @@ declare function scaleLineCharges(item: {
261
285
  taxAmount: number;
262
286
  discountAmount: number;
263
287
  };
288
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
289
+ declare function isFreeTrialLine(item: {
290
+ recurring?: {
291
+ trialDays?: number | null;
292
+ } | null;
293
+ }): boolean;
294
+ /**
295
+ * What one line charges at acceptance, at `quantity`. MUST match
296
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
297
+ */
298
+ declare function lineDueAtAcceptance(item: {
299
+ unitPrice: number;
300
+ quantity: number;
301
+ taxAmount: number;
302
+ discountAmount: number;
303
+ recurring?: {
304
+ trialDays?: number | null;
305
+ } | null;
306
+ }, quantity: number): number;
264
307
  /** Effective total for a selection, using the same math the server will apply. */
265
308
  declare function computeSelectionTotal(items: Array<{
266
309
  id: string;
@@ -269,6 +312,9 @@ declare function computeSelectionTotal(items: Array<{
269
312
  taxAmount: number;
270
313
  discountAmount: number;
271
314
  optional: boolean;
315
+ recurring?: {
316
+ trialDays?: number | null;
317
+ } | null;
272
318
  }>, selections: Record<string, {
273
319
  selected: boolean;
274
320
  quantity: number;
@@ -483,4 +529,4 @@ interface QuoteRequestFormProps {
483
529
  */
484
530
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
485
531
 
486
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
532
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, type QuoteItemRecurring, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, isFreeTrialLine, lineDueAtAcceptance, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.d.ts CHANGED
@@ -16,6 +16,28 @@ type PaymentTerms = {
16
16
  };
17
17
  balanceNetN: number;
18
18
  };
19
+ /** A subscription line's terms (Throttle creates one subscription per line when the quote is paid). */
20
+ interface QuoteItemRecurring {
21
+ plan: string;
22
+ planName: string | null;
23
+ interval: string;
24
+ intervalUnit: string;
25
+ intervalCount: number;
26
+ /** Set on an installment plan: exactly this many payments. */
27
+ totalPayments: number | null;
28
+ /** 0 when there is no free trial. A free-trial line charges nothing at acceptance. */
29
+ trialDays: number;
30
+ /**
31
+ * Custom installment schedule: payments that differ in amount (per unit, minor
32
+ * units) and in the gap before each one. Present only on a schedule line, where
33
+ * `totalPayments` equals its length. Row 0 has no gap.
34
+ */
35
+ schedule?: Array<{
36
+ amount: number;
37
+ intervalUnit: string | null;
38
+ intervalCount: number | null;
39
+ }>;
40
+ }
19
41
  interface QuoteItem {
20
42
  id: string;
21
43
  type: string;
@@ -35,6 +57,8 @@ interface QuoteItem {
35
57
  qtyEditable: boolean;
36
58
  imageUrl: string | null;
37
59
  position: number;
60
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
61
+ recurring: QuoteItemRecurring | null;
38
62
  }
39
63
  interface QuoteRevision {
40
64
  id: string;
@@ -261,6 +285,25 @@ declare function scaleLineCharges(item: {
261
285
  taxAmount: number;
262
286
  discountAmount: number;
263
287
  };
288
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
289
+ declare function isFreeTrialLine(item: {
290
+ recurring?: {
291
+ trialDays?: number | null;
292
+ } | null;
293
+ }): boolean;
294
+ /**
295
+ * What one line charges at acceptance, at `quantity`. MUST match
296
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
297
+ */
298
+ declare function lineDueAtAcceptance(item: {
299
+ unitPrice: number;
300
+ quantity: number;
301
+ taxAmount: number;
302
+ discountAmount: number;
303
+ recurring?: {
304
+ trialDays?: number | null;
305
+ } | null;
306
+ }, quantity: number): number;
264
307
  /** Effective total for a selection, using the same math the server will apply. */
265
308
  declare function computeSelectionTotal(items: Array<{
266
309
  id: string;
@@ -269,6 +312,9 @@ declare function computeSelectionTotal(items: Array<{
269
312
  taxAmount: number;
270
313
  discountAmount: number;
271
314
  optional: boolean;
315
+ recurring?: {
316
+ trialDays?: number | null;
317
+ } | null;
272
318
  }>, selections: Record<string, {
273
319
  selected: boolean;
274
320
  quantity: number;
@@ -483,4 +529,4 @@ interface QuoteRequestFormProps {
483
529
  */
484
530
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
485
531
 
486
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
532
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, type QuoteItemRecurring, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, isFreeTrialLine, lineDueAtAcceptance, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.js CHANGED
@@ -164,14 +164,20 @@ function scaleLineCharges(item, quantity) {
164
164
  discountAmount: Math.round(item.discountAmount * ratio)
165
165
  };
166
166
  }
167
+ function isFreeTrialLine(item) {
168
+ return (item.recurring?.trialDays ?? 0) > 0;
169
+ }
170
+ function lineDueAtAcceptance(item, quantity) {
171
+ const { taxAmount, discountAmount } = scaleLineCharges(item, quantity);
172
+ return (isFreeTrialLine(item) ? 0 : item.unitPrice * quantity) + taxAmount - discountAmount;
173
+ }
167
174
  function computeSelectionTotal(items, selections, shippingTotal = 0) {
168
175
  let total = shippingTotal;
169
176
  for (const item of items) {
170
177
  const state = selections[item.id];
171
178
  if (item.optional && state && !state.selected) continue;
172
179
  const qty = state?.quantity ?? item.quantity;
173
- const { taxAmount, discountAmount } = scaleLineCharges(item, qty);
174
- total += item.unitPrice * qty + taxAmount - discountAmount;
180
+ total += lineDueAtAcceptance(item, qty);
175
181
  }
176
182
  return total;
177
183
  }
@@ -522,6 +528,54 @@ function useQuoteCart(options = {}) {
522
528
 
523
529
  // src/QuoteView.tsx
524
530
  import { useState as useState2 } from "react";
531
+
532
+ // ../core-cadence/src/cadence.ts
533
+ var INTERVAL_UNITS = ["hour", "day", "week", "month"];
534
+ var NAMED_INTERVALS = ["weekly", "biweekly", "monthly", "quarterly", "yearly"];
535
+ var INTERVAL_VALUES = [...NAMED_INTERVALS, "custom"];
536
+ var HOUR_MS = 36e5;
537
+ var DAY_MS = 24 * HOUR_MS;
538
+ var WEEK_MS = 7 * DAY_MS;
539
+ function isNamedInterval(value) {
540
+ return typeof value === "string" && NAMED_INTERVALS.includes(value);
541
+ }
542
+ function isIntervalUnit(value) {
543
+ return typeof value === "string" && INTERVAL_UNITS.includes(value);
544
+ }
545
+
546
+ // ../core-cadence/src/schedule.ts
547
+ function scheduleUnitTotal(schedule) {
548
+ return schedule.reduce((sum, row2) => sum + row2.amount, 0);
549
+ }
550
+
551
+ // ../core-cadence/src/labels.ts
552
+ var NAMED_NOUN = {
553
+ weekly: "week",
554
+ biweekly: "2 weeks",
555
+ monthly: "month",
556
+ quarterly: "quarter",
557
+ yearly: "year"
558
+ };
559
+ function cadenceNoun(cadence) {
560
+ if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];
561
+ if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {
562
+ return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;
563
+ }
564
+ return cadence.interval;
565
+ }
566
+ function recurringTermsLabel(terms, price, money) {
567
+ const quantity = price.quantity ?? 1;
568
+ if (terms.schedule && terms.schedule.length > 0) {
569
+ return `${money(scheduleUnitTotal(terms.schedule) * quantity)} total \xB7 ${terms.schedule.length} payments`;
570
+ }
571
+ const each = money(price.unitPrice * quantity);
572
+ const noun = cadenceNoun(terms);
573
+ const trialDays = terms.trialDays ?? 0;
574
+ if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? "" : "s"}, then ${each} / ${noun}`;
575
+ return `${each} / ${noun}` + (terms.totalPayments != null ? ` \xB7 ${terms.totalPayments} payments` : "");
576
+ }
577
+
578
+ // src/QuoteView.tsx
525
579
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
526
580
  var row = { display: "flex", justifyContent: "space-between", gap: 16, padding: "8px 0" };
527
581
  var muted = { color: "#666", fontSize: 13 };
@@ -541,6 +595,7 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
541
595
  const ordered = quote.status === "ordered";
542
596
  const accepted = quote.status === "accepted";
543
597
  const canAct = quote.status === "ready";
598
+ const recurringKept = rev ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true))) : [];
544
599
  const handleResume = async () => {
545
600
  const { checkoutUrl } = await q.resumeCheckout();
546
601
  if (onCheckout) onCheckout(checkoutUrl);
@@ -599,7 +654,11 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
599
654
  " ",
600
655
  /* @__PURE__ */ jsx("span", { style: muted, children: "optional" })
601
656
  ] }) : item.name,
602
- item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null
657
+ item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null,
658
+ item.recurring ? /* @__PURE__ */ jsxs("div", { style: muted, children: [
659
+ "Subscription \xB7 ",
660
+ recurringTermsLabel(item.recurring, { unitPrice: item.unitPrice, quantity: state.quantity }, (m) => formatMoney(m, currency))
661
+ ] }) : null
603
662
  ] }),
604
663
  /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(item.unitPrice, currency) }),
605
664
  /* @__PURE__ */ jsx("td", { align: "right", children: item.qtyEditable && canAct ? /* @__PURE__ */ jsx(
@@ -614,18 +673,23 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
614
673
  style: { width: 64 }
615
674
  }
616
675
  ) : state.quantity }),
617
- /* @__PURE__ */ jsx("td", { align: "right", children: included ? formatMoney(
618
- item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,
619
- currency
620
- ) : formatMoney(0, currency) })
676
+ /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency) })
621
677
  ] }, item.id);
622
678
  }) })
623
679
  ] }) : null,
624
680
  rev ? /* @__PURE__ */ jsxs("section", { children: [
625
681
  /* @__PURE__ */ jsxs("div", { style: row, children: [
626
- /* @__PURE__ */ jsx("strong", { children: "Total" }),
682
+ /* @__PURE__ */ jsx("strong", { children: recurringKept.length > 0 ? "Due at acceptance" : "Total" }),
627
683
  /* @__PURE__ */ jsx("strong", { children: formatMoney(q.total, currency) })
628
684
  ] }),
685
+ recurringKept.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
686
+ /* @__PURE__ */ jsx("p", { style: muted, children: "Recurring after" }),
687
+ recurringKept.map((i) => /* @__PURE__ */ jsxs("p", { style: muted, children: [
688
+ i.name,
689
+ ": ",
690
+ recurringTermsLabel(i.recurring, { unitPrice: i.unitPrice, quantity: q.selections[i.id]?.quantity ?? i.quantity }, (m) => formatMoney(m, currency))
691
+ ] }, i.id))
692
+ ] }) : null,
629
693
  /* @__PURE__ */ jsx("p", { style: muted, children: termsLabel(rev.paymentTerms, rev.depositAmount, currency) }),
630
694
  rev.leadTime ? /* @__PURE__ */ jsxs("p", { style: muted, children: [
631
695
  "Lead time: ",
@@ -898,6 +962,8 @@ export {
898
962
  createQuoteCart,
899
963
  createQuotesClient,
900
964
  formatMoney,
965
+ isFreeTrialLine,
966
+ lineDueAtAcceptance,
901
967
  scaleLineCharges,
902
968
  useQuote,
903
969
  useQuoteCart,