@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/README.md +18 -0
- package/dist/index.cjs +76 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +74 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
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 ?
|
|
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,
|