@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/README.md +18 -0
- package/dist/index.cjs +68 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +66 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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
|
-
|
|
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 ?
|
|
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,
|