@usethrottle/quotes 0.4.0 → 0.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.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ThrottleError } from '@usethrottle/errors';
1
+ import { ThrottleError, ThrottleErrorArgs } from '@usethrottle/errors';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -16,6 +16,18 @@ 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
+ }
19
31
  interface QuoteItem {
20
32
  id: string;
21
33
  type: string;
@@ -35,6 +47,8 @@ interface QuoteItem {
35
47
  qtyEditable: boolean;
36
48
  imageUrl: string | null;
37
49
  position: number;
50
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
51
+ recurring: QuoteItemRecurring | null;
38
52
  }
39
53
  interface QuoteRevision {
40
54
  id: string;
@@ -261,6 +275,25 @@ declare function scaleLineCharges(item: {
261
275
  taxAmount: number;
262
276
  discountAmount: number;
263
277
  };
278
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
279
+ declare function isFreeTrialLine(item: {
280
+ recurring?: {
281
+ trialDays?: number | null;
282
+ } | null;
283
+ }): boolean;
284
+ /**
285
+ * What one line charges at acceptance, at `quantity`. MUST match
286
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
287
+ */
288
+ declare function lineDueAtAcceptance(item: {
289
+ unitPrice: number;
290
+ quantity: number;
291
+ taxAmount: number;
292
+ discountAmount: number;
293
+ recurring?: {
294
+ trialDays?: number | null;
295
+ } | null;
296
+ }, quantity: number): number;
264
297
  /** Effective total for a selection, using the same math the server will apply. */
265
298
  declare function computeSelectionTotal(items: Array<{
266
299
  id: string;
@@ -269,6 +302,9 @@ declare function computeSelectionTotal(items: Array<{
269
302
  taxAmount: number;
270
303
  discountAmount: number;
271
304
  optional: boolean;
305
+ recurring?: {
306
+ trialDays?: number | null;
307
+ } | null;
272
308
  }>, selections: Record<string, {
273
309
  selected: boolean;
274
310
  quantity: number;
@@ -292,6 +328,7 @@ declare class ThrottleQuotesError extends ThrottleError {
292
328
  message: string;
293
329
  statusCode: number;
294
330
  details?: unknown;
331
+ responseHeaders?: ThrottleErrorArgs['responseHeaders'];
295
332
  });
296
333
  /** The offer moved on — reload and show the buyer the current revision. */
297
334
  get isSuperseded(): boolean;
@@ -482,4 +519,4 @@ interface QuoteRequestFormProps {
482
519
  */
483
520
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
484
521
 
485
- 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 };
522
+ 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
@@ -1,4 +1,4 @@
1
- import { ThrottleError } from '@usethrottle/errors';
1
+ import { ThrottleError, ThrottleErrorArgs } from '@usethrottle/errors';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ReactNode } from 'react';
4
4
 
@@ -16,6 +16,18 @@ 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
+ }
19
31
  interface QuoteItem {
20
32
  id: string;
21
33
  type: string;
@@ -35,6 +47,8 @@ interface QuoteItem {
35
47
  qtyEditable: boolean;
36
48
  imageUrl: string | null;
37
49
  position: number;
50
+ /** Subscription terms, or null for a one-time line. `unitPrice` is the per-period price per seat. */
51
+ recurring: QuoteItemRecurring | null;
38
52
  }
39
53
  interface QuoteRevision {
40
54
  id: string;
@@ -261,6 +275,25 @@ declare function scaleLineCharges(item: {
261
275
  taxAmount: number;
262
276
  discountAmount: number;
263
277
  };
278
+ /** A subscription line with a free trial: nothing is charged for it at acceptance. */
279
+ declare function isFreeTrialLine(item: {
280
+ recurring?: {
281
+ trialDays?: number | null;
282
+ } | null;
283
+ }): boolean;
284
+ /**
285
+ * What one line charges at acceptance, at `quantity`. MUST match
286
+ * `lineDueAtAcceptance` in @commerce/quotes (shared test vectors).
287
+ */
288
+ declare function lineDueAtAcceptance(item: {
289
+ unitPrice: number;
290
+ quantity: number;
291
+ taxAmount: number;
292
+ discountAmount: number;
293
+ recurring?: {
294
+ trialDays?: number | null;
295
+ } | null;
296
+ }, quantity: number): number;
264
297
  /** Effective total for a selection, using the same math the server will apply. */
265
298
  declare function computeSelectionTotal(items: Array<{
266
299
  id: string;
@@ -269,6 +302,9 @@ declare function computeSelectionTotal(items: Array<{
269
302
  taxAmount: number;
270
303
  discountAmount: number;
271
304
  optional: boolean;
305
+ recurring?: {
306
+ trialDays?: number | null;
307
+ } | null;
272
308
  }>, selections: Record<string, {
273
309
  selected: boolean;
274
310
  quantity: number;
@@ -292,6 +328,7 @@ declare class ThrottleQuotesError extends ThrottleError {
292
328
  message: string;
293
329
  statusCode: number;
294
330
  details?: unknown;
331
+ responseHeaders?: ThrottleErrorArgs['responseHeaders'];
295
332
  });
296
333
  /** The offer moved on — reload and show the buyer the current revision. */
297
334
  get isSuperseded(): boolean;
@@ -482,4 +519,4 @@ interface QuoteRequestFormProps {
482
519
  */
483
520
  declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
484
521
 
485
- 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 };
522
+ 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
@@ -48,7 +48,8 @@ var QuotesClient = class {
48
48
  code: json.error?.code ?? "unknown_error",
49
49
  message: json.error?.message ?? `HTTP ${res.status}`,
50
50
  statusCode: res.status,
51
- details: json.error?.details
51
+ details: json.error?.details,
52
+ responseHeaders: res.headers
52
53
  });
53
54
  }
54
55
  return json.data;
@@ -163,14 +164,20 @@ function scaleLineCharges(item, quantity) {
163
164
  discountAmount: Math.round(item.discountAmount * ratio)
164
165
  };
165
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
+ }
166
174
  function computeSelectionTotal(items, selections, shippingTotal = 0) {
167
175
  let total = shippingTotal;
168
176
  for (const item of items) {
169
177
  const state = selections[item.id];
170
178
  if (item.optional && state && !state.selected) continue;
171
179
  const qty = state?.quantity ?? item.quantity;
172
- const { taxAmount, discountAmount } = scaleLineCharges(item, qty);
173
- total += item.unitPrice * qty + taxAmount - discountAmount;
180
+ total += lineDueAtAcceptance(item, qty);
174
181
  }
175
182
  return total;
176
183
  }
@@ -521,6 +528,44 @@ function useQuoteCart(options = {}) {
521
528
 
522
529
  // src/QuoteView.tsx
523
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/labels.ts
547
+ var NAMED_NOUN = {
548
+ weekly: "week",
549
+ biweekly: "2 weeks",
550
+ monthly: "month",
551
+ quarterly: "quarter",
552
+ yearly: "year"
553
+ };
554
+ function cadenceNoun(cadence) {
555
+ if (isNamedInterval(cadence.interval)) return NAMED_NOUN[cadence.interval];
556
+ if (isIntervalUnit(cadence.intervalUnit) && cadence.intervalCount != null) {
557
+ return cadence.intervalCount === 1 ? cadence.intervalUnit : `${cadence.intervalCount} ${cadence.intervalUnit}s`;
558
+ }
559
+ return cadence.interval;
560
+ }
561
+ function recurringTermsLabel(terms, each) {
562
+ const noun = cadenceNoun(terms);
563
+ const trialDays = terms.trialDays ?? 0;
564
+ if (trialDays > 0) return `Free for ${trialDays} day${trialDays === 1 ? "" : "s"}, then ${each} / ${noun}`;
565
+ return `${each} / ${noun}` + (terms.totalPayments != null ? ` \xB7 ${terms.totalPayments} payments` : "");
566
+ }
567
+
568
+ // src/QuoteView.tsx
524
569
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
525
570
  var row = { display: "flex", justifyContent: "space-between", gap: 16, padding: "8px 0" };
526
571
  var muted = { color: "#666", fontSize: 13 };
@@ -540,6 +585,7 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
540
585
  const ordered = quote.status === "ordered";
541
586
  const accepted = quote.status === "accepted";
542
587
  const canAct = quote.status === "ready";
588
+ const recurringKept = rev ? rev.items.filter((i) => i.recurring && (!i.optional || (q.selections[i.id]?.selected ?? true))) : [];
543
589
  const handleResume = async () => {
544
590
  const { checkoutUrl } = await q.resumeCheckout();
545
591
  if (onCheckout) onCheckout(checkoutUrl);
@@ -598,7 +644,11 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
598
644
  " ",
599
645
  /* @__PURE__ */ jsx("span", { style: muted, children: "optional" })
600
646
  ] }) : item.name,
601
- item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null
647
+ item.description ? /* @__PURE__ */ jsx("div", { style: muted, children: item.description }) : null,
648
+ item.recurring ? /* @__PURE__ */ jsxs("div", { style: muted, children: [
649
+ "Subscription \xB7 ",
650
+ recurringTermsLabel(item.recurring, formatMoney(item.unitPrice * state.quantity, currency))
651
+ ] }) : null
602
652
  ] }),
603
653
  /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(item.unitPrice, currency) }),
604
654
  /* @__PURE__ */ jsx("td", { align: "right", children: item.qtyEditable && canAct ? /* @__PURE__ */ jsx(
@@ -613,18 +663,23 @@ function QuoteView({ token, options, onCheckout, loadingFallback, className }) {
613
663
  style: { width: 64 }
614
664
  }
615
665
  ) : state.quantity }),
616
- /* @__PURE__ */ jsx("td", { align: "right", children: included ? formatMoney(
617
- item.unitPrice * state.quantity + item.taxAmount - item.discountAmount,
618
- currency
619
- ) : formatMoney(0, currency) })
666
+ /* @__PURE__ */ jsx("td", { align: "right", children: formatMoney(included ? lineDueAtAcceptance(item, state.quantity) : 0, currency) })
620
667
  ] }, item.id);
621
668
  }) })
622
669
  ] }) : null,
623
670
  rev ? /* @__PURE__ */ jsxs("section", { children: [
624
671
  /* @__PURE__ */ jsxs("div", { style: row, children: [
625
- /* @__PURE__ */ jsx("strong", { children: "Total" }),
672
+ /* @__PURE__ */ jsx("strong", { children: recurringKept.length > 0 ? "Due at acceptance" : "Total" }),
626
673
  /* @__PURE__ */ jsx("strong", { children: formatMoney(q.total, currency) })
627
674
  ] }),
675
+ recurringKept.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
676
+ /* @__PURE__ */ jsx("p", { style: muted, children: "Recurring after" }),
677
+ recurringKept.map((i) => /* @__PURE__ */ jsxs("p", { style: muted, children: [
678
+ i.name,
679
+ ": ",
680
+ recurringTermsLabel(i.recurring, formatMoney(i.unitPrice * (q.selections[i.id]?.quantity ?? i.quantity), currency))
681
+ ] }, i.id))
682
+ ] }) : null,
628
683
  /* @__PURE__ */ jsx("p", { style: muted, children: termsLabel(rev.paymentTerms, rev.depositAmount, currency) }),
629
684
  rev.leadTime ? /* @__PURE__ */ jsxs("p", { style: muted, children: [
630
685
  "Lead time: ",
@@ -897,6 +952,8 @@ export {
897
952
  createQuoteCart,
898
953
  createQuotesClient,
899
954
  formatMoney,
955
+ isFreeTrialLine,
956
+ lineDueAtAcceptance,
900
957
  scaleLineCharges,
901
958
  useQuote,
902
959
  useQuoteCart,