@usethrottle/quotes 0.1.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 +150 -0
- package/dist/index.cjs +736 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +354 -0
- package/dist/index.d.ts +354 -0
- package/dist/index.js +700 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +101 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +25 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +76 -0
- package/dist/server.js.map +1 -0
- package/package.json +63 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { ThrottleError } from '@usethrottle/errors';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
type BuyerQuoteStatus = 'in_progress' | 'ready' | 'being_revised' | 'accepted' | 'ordered' | 'declined' | 'expired';
|
|
6
|
+
type PaymentTerms = {
|
|
7
|
+
mode: 'pay_in_full';
|
|
8
|
+
} | {
|
|
9
|
+
mode: 'net_terms';
|
|
10
|
+
netN: number;
|
|
11
|
+
} | {
|
|
12
|
+
mode: 'deposit_balance';
|
|
13
|
+
deposit: {
|
|
14
|
+
type: 'percent' | 'fixed';
|
|
15
|
+
value: number;
|
|
16
|
+
};
|
|
17
|
+
balanceNetN: number;
|
|
18
|
+
};
|
|
19
|
+
interface QuoteItem {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
sku: string | null;
|
|
23
|
+
name: string;
|
|
24
|
+
description: string | null;
|
|
25
|
+
unitPrice: number;
|
|
26
|
+
quantity: number;
|
|
27
|
+
subtotal: number;
|
|
28
|
+
taxAmount: number;
|
|
29
|
+
discountAmount: number;
|
|
30
|
+
total: number;
|
|
31
|
+
/** Buyer may deselect this line. */
|
|
32
|
+
optional: boolean;
|
|
33
|
+
optionalSelected: boolean;
|
|
34
|
+
/** Buyer may change the quantity. Reps lock volume-priced lines. */
|
|
35
|
+
qtyEditable: boolean;
|
|
36
|
+
imageUrl: string | null;
|
|
37
|
+
position: number;
|
|
38
|
+
}
|
|
39
|
+
interface QuoteRevision {
|
|
40
|
+
id: string;
|
|
41
|
+
revisionNumber: number;
|
|
42
|
+
issuedAt: string | null;
|
|
43
|
+
expiresAt: string | null;
|
|
44
|
+
subtotal: number;
|
|
45
|
+
taxTotal: number;
|
|
46
|
+
discountTotal: number;
|
|
47
|
+
shippingTotal: number;
|
|
48
|
+
total: number;
|
|
49
|
+
shippingMode: 'none' | 'flat' | 'checkout_calculated' | 'free';
|
|
50
|
+
shippingFlatAmount: number | null;
|
|
51
|
+
paymentTerms: PaymentTerms;
|
|
52
|
+
/** Frozen at issue time for deposit_balance terms. */
|
|
53
|
+
depositAmount: number | null;
|
|
54
|
+
buyerNote: string | null;
|
|
55
|
+
changeSummary: string | null;
|
|
56
|
+
leadTime: string | null;
|
|
57
|
+
items: QuoteItem[];
|
|
58
|
+
}
|
|
59
|
+
interface QuoteComment {
|
|
60
|
+
id: string;
|
|
61
|
+
authorType: 'buyer' | 'merchant' | 'system';
|
|
62
|
+
authorName: string | null;
|
|
63
|
+
body: string;
|
|
64
|
+
createdAt: string;
|
|
65
|
+
}
|
|
66
|
+
interface QuoteBranding {
|
|
67
|
+
displayName?: string | null;
|
|
68
|
+
logoUrl?: string | null;
|
|
69
|
+
brandColor?: string | null;
|
|
70
|
+
}
|
|
71
|
+
interface BuyerQuote {
|
|
72
|
+
quoteNumber: string;
|
|
73
|
+
status: BuyerQuoteStatus;
|
|
74
|
+
buyerReference: string | null;
|
|
75
|
+
currency: string;
|
|
76
|
+
expiresAt: string | null;
|
|
77
|
+
currentRevision: QuoteRevision | null;
|
|
78
|
+
revisionHistory: QuoteRevision[];
|
|
79
|
+
comments: QuoteComment[];
|
|
80
|
+
/** Present only once the quote has converted. */
|
|
81
|
+
orderId?: string | null;
|
|
82
|
+
branding?: QuoteBranding | null;
|
|
83
|
+
}
|
|
84
|
+
/** What the buyer chose: which optional lines they kept, at what quantities. */
|
|
85
|
+
interface ItemSelection {
|
|
86
|
+
itemId: string;
|
|
87
|
+
selected?: boolean;
|
|
88
|
+
quantity?: number;
|
|
89
|
+
}
|
|
90
|
+
interface AcceptQuoteInput {
|
|
91
|
+
revisionId: string;
|
|
92
|
+
acceptedByName: string;
|
|
93
|
+
acceptedByEmail: string;
|
|
94
|
+
buyerReference?: string | null;
|
|
95
|
+
itemSelections?: ItemSelection[];
|
|
96
|
+
}
|
|
97
|
+
interface AcceptQuoteResult {
|
|
98
|
+
quote: BuyerQuote;
|
|
99
|
+
/** Hosted checkout URL — redirect the buyer here immediately. */
|
|
100
|
+
checkoutUrl: string;
|
|
101
|
+
}
|
|
102
|
+
interface RequestFormConfig {
|
|
103
|
+
title: string | null;
|
|
104
|
+
introText: string | null;
|
|
105
|
+
successMessage: string | null;
|
|
106
|
+
redirectUrl: string | null;
|
|
107
|
+
fields: {
|
|
108
|
+
companyName?: boolean;
|
|
109
|
+
phone?: boolean;
|
|
110
|
+
buyerReference?: boolean;
|
|
111
|
+
requireTurnstile?: boolean;
|
|
112
|
+
};
|
|
113
|
+
branding?: QuoteBranding | null;
|
|
114
|
+
}
|
|
115
|
+
interface SubmitQuoteRequestInput {
|
|
116
|
+
email: string;
|
|
117
|
+
message: string;
|
|
118
|
+
name?: string | null;
|
|
119
|
+
companyName?: string | null;
|
|
120
|
+
phone?: string | null;
|
|
121
|
+
buyerReference?: string | null;
|
|
122
|
+
/** Honeypot — leave empty. The component wires this for you. */
|
|
123
|
+
website?: string | null;
|
|
124
|
+
/** Client mount time in ms; the API enforces a minimum fill duration. */
|
|
125
|
+
formLoadedAt?: number;
|
|
126
|
+
turnstileToken?: string;
|
|
127
|
+
}
|
|
128
|
+
interface ThrottleEnvelope<T> {
|
|
129
|
+
data?: T;
|
|
130
|
+
error?: {
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
details?: unknown;
|
|
134
|
+
};
|
|
135
|
+
meta?: {
|
|
136
|
+
requestId?: string;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
interface QuotesClientOptions {
|
|
140
|
+
/**
|
|
141
|
+
* Where the public quote-link routes live. Defaults to Throttle's API.
|
|
142
|
+
* Point this at your own origin when proxying through `createQuoteProxyHandler`
|
|
143
|
+
* so the browser never talks to a third-party host.
|
|
144
|
+
*/
|
|
145
|
+
baseUrl?: string;
|
|
146
|
+
fetch?: typeof globalThis.fetch;
|
|
147
|
+
timeoutMs?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Headless client for the public quote surfaces. Everything is authorized by
|
|
152
|
+
* the capability token in the URL — there is no API key here, and there must
|
|
153
|
+
* not be: this runs in the buyer's browser.
|
|
154
|
+
*/
|
|
155
|
+
declare class QuotesClient {
|
|
156
|
+
private readonly baseUrl;
|
|
157
|
+
private readonly fetchImpl;
|
|
158
|
+
private readonly timeoutMs;
|
|
159
|
+
constructor(opts?: QuotesClientOptions);
|
|
160
|
+
private request;
|
|
161
|
+
/** Fetch the quote a buyer link points at. 404s for unknown/rotated tokens. */
|
|
162
|
+
getQuote(token: string): Promise<BuyerQuote>;
|
|
163
|
+
/**
|
|
164
|
+
* Tell the merchant the buyer opened the quote. Fire-and-forget: the server
|
|
165
|
+
* throttles the resulting webhook to one per revision per 6h, and a failed
|
|
166
|
+
* beacon must never break the page.
|
|
167
|
+
*/
|
|
168
|
+
recordView(token: string): Promise<void>;
|
|
169
|
+
addComment(token: string, input: {
|
|
170
|
+
body: string;
|
|
171
|
+
authorName?: string;
|
|
172
|
+
authorEmail?: string;
|
|
173
|
+
}): Promise<QuoteComment>;
|
|
174
|
+
requestRevision(token: string, input?: {
|
|
175
|
+
message?: string;
|
|
176
|
+
authorName?: string;
|
|
177
|
+
}): Promise<BuyerQuote>;
|
|
178
|
+
decline(token: string, input?: {
|
|
179
|
+
reason?: string;
|
|
180
|
+
authorName?: string;
|
|
181
|
+
}): Promise<BuyerQuote>;
|
|
182
|
+
/**
|
|
183
|
+
* Accept and get the checkout URL to send the buyer to.
|
|
184
|
+
*
|
|
185
|
+
* Re-accepting the same revision is idempotent and replays the same URL.
|
|
186
|
+
* Accepting a superseded revision throws `revision_superseded` carrying
|
|
187
|
+
* `currentRevisionId`; an expired offer throws `quote_expired`.
|
|
188
|
+
*/
|
|
189
|
+
accept(token: string, input: AcceptQuoteInput): Promise<AcceptQuoteResult>;
|
|
190
|
+
/** URL of the PDF for the revision the buyer is looking at (server 302s to signed storage). */
|
|
191
|
+
pdfUrl(token: string): string;
|
|
192
|
+
getRequestForm(formToken: string): Promise<RequestFormConfig>;
|
|
193
|
+
/**
|
|
194
|
+
* Submit an RFQ. The response is deliberately thin (`{ submitted: true }`) —
|
|
195
|
+
* anonymous callers learn no internal identifiers. A submission caught by
|
|
196
|
+
* the honeypot returns the same shape without creating anything, so a bot
|
|
197
|
+
* cannot tell it was detected; treat success as "accepted", not "created".
|
|
198
|
+
*/
|
|
199
|
+
submitRequest(formToken: string, input: SubmitQuoteRequestInput): Promise<{
|
|
200
|
+
submitted: boolean;
|
|
201
|
+
}>;
|
|
202
|
+
}
|
|
203
|
+
declare function createQuotesClient(opts?: QuotesClientOptions): QuotesClient;
|
|
204
|
+
/** Minor units → a display string, e.g. (104000, 'USD') → "$1,040.00". */
|
|
205
|
+
declare function formatMoney(minorUnits: number, currency: string, locale?: string): string;
|
|
206
|
+
/**
|
|
207
|
+
* Scale a line's quoted tax/discount to an edited quantity.
|
|
208
|
+
*
|
|
209
|
+
* MUST match `scaleLineCharges` in @commerce/quotes — the server recomputes
|
|
210
|
+
* the accepted total with this rule, and any divergence shows the buyer one
|
|
211
|
+
* total and charges another.
|
|
212
|
+
*/
|
|
213
|
+
declare function scaleLineCharges(item: {
|
|
214
|
+
quantity: number;
|
|
215
|
+
taxAmount: number;
|
|
216
|
+
discountAmount: number;
|
|
217
|
+
}, quantity: number): {
|
|
218
|
+
taxAmount: number;
|
|
219
|
+
discountAmount: number;
|
|
220
|
+
};
|
|
221
|
+
/** Effective total for a selection, using the same math the server will apply. */
|
|
222
|
+
declare function computeSelectionTotal(items: Array<{
|
|
223
|
+
id: string;
|
|
224
|
+
unitPrice: number;
|
|
225
|
+
quantity: number;
|
|
226
|
+
taxAmount: number;
|
|
227
|
+
discountAmount: number;
|
|
228
|
+
optional: boolean;
|
|
229
|
+
}>, selections: Record<string, {
|
|
230
|
+
selected: boolean;
|
|
231
|
+
quantity: number;
|
|
232
|
+
}>, shippingTotal?: number): number;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Every failure from the quote-link routes surfaces as this. The `code` is the
|
|
236
|
+
* API's own error code, so callers can branch on the ones that matter to a
|
|
237
|
+
* buyer flow rather than parsing messages:
|
|
238
|
+
*
|
|
239
|
+
* revision_superseded — a newer revision was issued; `details.currentRevisionId`
|
|
240
|
+
* tells you what to reload
|
|
241
|
+
* quote_expired — the offer lapsed (HTTP 410)
|
|
242
|
+
* invalid_item_selection — locked-line edit, non-optional deselect, or unknown item
|
|
243
|
+
* accept_in_progress — a concurrent accept won; retry shortly
|
|
244
|
+
* not_found — unknown/rotated token, or a draft/archived quote
|
|
245
|
+
*/
|
|
246
|
+
declare class ThrottleQuotesError extends ThrottleError {
|
|
247
|
+
constructor(args: {
|
|
248
|
+
code: string;
|
|
249
|
+
message: string;
|
|
250
|
+
statusCode: number;
|
|
251
|
+
details?: unknown;
|
|
252
|
+
});
|
|
253
|
+
/** The offer moved on — reload and show the buyer the current revision. */
|
|
254
|
+
get isSuperseded(): boolean;
|
|
255
|
+
get isExpired(): boolean;
|
|
256
|
+
/** Revision the server considers current, when it told us (spec §8.1). */
|
|
257
|
+
get currentRevisionId(): string | undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface UseQuoteResult {
|
|
261
|
+
quote: BuyerQuote | null;
|
|
262
|
+
loading: boolean;
|
|
263
|
+
error: ThrottleQuotesError | null;
|
|
264
|
+
/** Re-fetch — call after a `revision_superseded` to show the current offer. */
|
|
265
|
+
refresh: () => Promise<void>;
|
|
266
|
+
/** Buyer's current line choices, keyed by item id. */
|
|
267
|
+
selections: Record<string, {
|
|
268
|
+
selected: boolean;
|
|
269
|
+
quantity: number;
|
|
270
|
+
}>;
|
|
271
|
+
setSelected: (itemId: string, selected: boolean) => void;
|
|
272
|
+
setQuantity: (itemId: string, quantity: number) => void;
|
|
273
|
+
/** Live total for the current selections, using the server's own math. */
|
|
274
|
+
total: number;
|
|
275
|
+
accept: (input: Omit<AcceptQuoteInput, 'revisionId' | 'itemSelections'>) => Promise<{
|
|
276
|
+
checkoutUrl: string;
|
|
277
|
+
}>;
|
|
278
|
+
requestRevision: (message: string, authorName?: string) => Promise<void>;
|
|
279
|
+
decline: (reason?: string, authorName?: string) => Promise<void>;
|
|
280
|
+
addComment: (body: string, authorName?: string) => Promise<void>;
|
|
281
|
+
pdfUrl: string;
|
|
282
|
+
submitting: boolean;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Everything the buyer quote page needs.
|
|
286
|
+
*
|
|
287
|
+
* Fires the view beacon once per mount, keeps line selections in local state,
|
|
288
|
+
* and recomputes the total with the same rule the server applies at
|
|
289
|
+
* acceptance — so the number the buyer sees is the number they are charged.
|
|
290
|
+
*/
|
|
291
|
+
declare function useQuote(token: string, options?: QuotesClientOptions): UseQuoteResult;
|
|
292
|
+
interface UseQuoteRequestFormResult {
|
|
293
|
+
config: RequestFormConfig | null;
|
|
294
|
+
loading: boolean;
|
|
295
|
+
error: ThrottleQuotesError | null;
|
|
296
|
+
submitting: boolean;
|
|
297
|
+
submitted: boolean;
|
|
298
|
+
submit: (input: SubmitQuoteRequestInput) => Promise<void>;
|
|
299
|
+
/** Mount time; pass through so the API's minimum-fill-time check passes. */
|
|
300
|
+
formLoadedAt: number;
|
|
301
|
+
}
|
|
302
|
+
/** Loads a hosted RFQ form's config and submits it, with bot fields handled. */
|
|
303
|
+
declare function useQuoteRequestForm(formToken: string, options?: QuotesClientOptions): UseQuoteRequestFormResult;
|
|
304
|
+
|
|
305
|
+
interface QuoteViewProps {
|
|
306
|
+
/** The `qtl_…` token from the buyer's link. */
|
|
307
|
+
token: string;
|
|
308
|
+
options?: QuotesClientOptions;
|
|
309
|
+
/**
|
|
310
|
+
* Where to send the buyer after acceptance. Defaults to assigning
|
|
311
|
+
* `window.location.href`, which is what the hosted page does.
|
|
312
|
+
*/
|
|
313
|
+
onCheckout?: (checkoutUrl: string) => void;
|
|
314
|
+
/** Rendered instead of the built-in markup while loading. */
|
|
315
|
+
loadingFallback?: ReactNode;
|
|
316
|
+
className?: string;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Drop-in buyer quote page: line items with optional toggles and quantity
|
|
320
|
+
* editing, live totals, revision history, comment thread, and the
|
|
321
|
+
* accept / request-changes / decline actions.
|
|
322
|
+
*
|
|
323
|
+
* Intentionally unstyled beyond a readable default — it is meant to be
|
|
324
|
+
* restyled or replaced. `useQuote` carries all the behaviour if you want to
|
|
325
|
+
* build your own presentation.
|
|
326
|
+
*/
|
|
327
|
+
declare function QuoteView({ token, options, onCheckout, loadingFallback, className }: QuoteViewProps): react_jsx_runtime.JSX.Element;
|
|
328
|
+
|
|
329
|
+
interface QuoteRequestFormProps {
|
|
330
|
+
/** The `qrf_…` token from the merchant's hosted form URL. */
|
|
331
|
+
formToken: string;
|
|
332
|
+
options?: QuotesClientOptions;
|
|
333
|
+
/**
|
|
334
|
+
* Turnstile token, when the form requires it. Render the widget yourself and
|
|
335
|
+
* pass its token through — this package deliberately pulls in no Cloudflare
|
|
336
|
+
* script. If `config.fields.requireTurnstile` is set and this is empty, the
|
|
337
|
+
* API fails the submission closed.
|
|
338
|
+
*/
|
|
339
|
+
turnstileToken?: string;
|
|
340
|
+
onSubmitted?: () => void;
|
|
341
|
+
loadingFallback?: ReactNode;
|
|
342
|
+
className?: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Hosted RFQ intake form.
|
|
346
|
+
*
|
|
347
|
+
* Bot protection is wired in for you: the honeypot field and the mount-time
|
|
348
|
+
* anchor that feeds the API's minimum-fill-time check. A honeypot hit returns
|
|
349
|
+
* a success-shaped response and creates nothing, so `onSubmitted` fires either
|
|
350
|
+
* way — by design, a bot must not learn it was caught.
|
|
351
|
+
*/
|
|
352
|
+
declare function QuoteRequestForm({ formToken, options, turnstileToken, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
|
|
353
|
+
|
|
354
|
+
export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteRequestForm };
|