@rovela-ai/sdk 0.6.1 → 0.7.1
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/admin/api/discounts.d.ts +53 -0
- package/dist/admin/api/discounts.d.ts.map +1 -0
- package/dist/admin/api/discounts.js +339 -0
- package/dist/admin/api/discounts.js.map +1 -0
- package/dist/admin/api/index.d.ts +1 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +2 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/orders.js +1 -1
- package/dist/admin/api/orders.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +10 -0
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/components/CustomerAddressPanel.d.ts.map +1 -1
- package/dist/admin/components/CustomerAddressPanel.js +13 -6
- package/dist/admin/components/CustomerAddressPanel.js.map +1 -1
- package/dist/admin/components/DiscountsManager.d.ts +5 -0
- package/dist/admin/components/DiscountsManager.d.ts.map +1 -0
- package/dist/admin/components/DiscountsManager.js +363 -0
- package/dist/admin/components/DiscountsManager.js.map +1 -0
- package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
- package/dist/admin/components/ShippingSettings.js +19 -2
- package/dist/admin/components/ShippingSettings.js.map +1 -1
- package/dist/admin/components/StoreSettings.d.ts.map +1 -1
- package/dist/admin/components/StoreSettings.js +8 -1
- package/dist/admin/components/StoreSettings.js.map +1 -1
- package/dist/admin/components/TaxSettings.d.ts.map +1 -1
- package/dist/admin/components/TaxSettings.js +8 -3
- package/dist/admin/components/TaxSettings.js.map +1 -1
- package/dist/admin/components/index.d.ts +2 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/hooks/index.d.ts +2 -0
- package/dist/admin/hooks/index.d.ts.map +1 -1
- package/dist/admin/hooks/index.js +1 -0
- package/dist/admin/hooks/index.js.map +1 -1
- package/dist/admin/hooks/useAdminDiscounts.d.ts +12 -0
- package/dist/admin/hooks/useAdminDiscounts.d.ts.map +1 -0
- package/dist/admin/hooks/useAdminDiscounts.js +77 -0
- package/dist/admin/hooks/useAdminDiscounts.js.map +1 -0
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/types.d.ts +72 -0
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/auth/components/AddressForm.d.ts.map +1 -1
- package/dist/auth/components/AddressForm.js +22 -7
- package/dist/auth/components/AddressForm.js.map +1 -1
- package/dist/checkout/api/checkout.d.ts.map +1 -1
- package/dist/checkout/api/checkout.js +15 -2
- package/dist/checkout/api/checkout.js.map +1 -1
- package/dist/checkout/api/discount.d.ts +47 -0
- package/dist/checkout/api/discount.d.ts.map +1 -0
- package/dist/checkout/api/discount.js +77 -0
- package/dist/checkout/api/discount.js.map +1 -0
- package/dist/checkout/api/index.d.ts +1 -0
- package/dist/checkout/api/index.d.ts.map +1 -1
- package/dist/checkout/api/index.js +1 -0
- package/dist/checkout/api/index.js.map +1 -1
- package/dist/checkout/api/manual.d.ts.map +1 -1
- package/dist/checkout/api/manual.js +73 -135
- package/dist/checkout/api/manual.js.map +1 -1
- package/dist/checkout/api/shipping.d.ts.map +1 -1
- package/dist/checkout/api/shipping.js +19 -27
- package/dist/checkout/api/shipping.js.map +1 -1
- package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
- package/dist/checkout/components/CheckoutFlow.js +89 -9
- package/dist/checkout/components/CheckoutFlow.js.map +1 -1
- package/dist/checkout/components/OrderSummary.d.ts +1 -1
- package/dist/checkout/components/OrderSummary.d.ts.map +1 -1
- package/dist/checkout/components/OrderSummary.js +3 -3
- package/dist/checkout/components/OrderSummary.js.map +1 -1
- package/dist/checkout/components/ShippingForm.d.ts.map +1 -1
- package/dist/checkout/components/ShippingForm.js +42 -13
- package/dist/checkout/components/ShippingForm.js.map +1 -1
- package/dist/checkout/constants.d.ts +38 -0
- package/dist/checkout/constants.d.ts.map +1 -1
- package/dist/checkout/constants.js +39 -0
- package/dist/checkout/constants.js.map +1 -1
- package/dist/checkout/hooks/useCheckout.d.ts.map +1 -1
- package/dist/checkout/hooks/useCheckout.js +2 -0
- package/dist/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/checkout/server/checkout-customer.d.ts +30 -0
- package/dist/checkout/server/checkout-customer.d.ts.map +1 -0
- package/dist/checkout/server/checkout-customer.js +47 -0
- package/dist/checkout/server/checkout-customer.js.map +1 -0
- package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
- package/dist/checkout/server/create-checkout-session.js +149 -9
- package/dist/checkout/server/create-checkout-session.js.map +1 -1
- package/dist/checkout/server/discounts.d.ts +75 -0
- package/dist/checkout/server/discounts.d.ts.map +1 -0
- package/dist/checkout/server/discounts.js +294 -0
- package/dist/checkout/server/discounts.js.map +1 -0
- package/dist/checkout/server/handle-webhook.js +34 -8
- package/dist/checkout/server/handle-webhook.js.map +1 -1
- package/dist/checkout/server/index.d.ts +4 -0
- package/dist/checkout/server/index.d.ts.map +1 -1
- package/dist/checkout/server/index.js +4 -0
- package/dist/checkout/server/index.js.map +1 -1
- package/dist/checkout/server/order-service.d.ts.map +1 -1
- package/dist/checkout/server/order-service.js +60 -10
- package/dist/checkout/server/order-service.js.map +1 -1
- package/dist/checkout/server/pricing.d.ts +164 -0
- package/dist/checkout/server/pricing.d.ts.map +1 -0
- package/dist/checkout/server/pricing.js +309 -0
- package/dist/checkout/server/pricing.js.map +1 -0
- package/dist/checkout/types.d.ts +8 -0
- package/dist/checkout/types.d.ts.map +1 -1
- package/dist/core/StoreSettingsProvider.d.ts +4 -0
- package/dist/core/StoreSettingsProvider.d.ts.map +1 -1
- package/dist/core/StoreSettingsProvider.js +6 -0
- package/dist/core/StoreSettingsProvider.js.map +1 -1
- package/dist/core/api/settings.d.ts +1 -0
- package/dist/core/api/settings.d.ts.map +1 -1
- package/dist/core/api/settings.js +2 -0
- package/dist/core/api/settings.js.map +1 -1
- package/dist/core/db/queries.d.ts +267 -19
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +297 -0
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +618 -0
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +78 -0
- package/dist/core/db/schema.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rovela/sdk/checkout/server/discounts
|
|
3
|
+
*
|
|
4
|
+
* The native discount EVALUATOR — step 2 of the pricing core.
|
|
5
|
+
*
|
|
6
|
+
* Given the server-priced cart lines and the (trusted) customer context, it
|
|
7
|
+
* decides which discounts apply and how much they take off. It runs
|
|
8
|
+
* identically for every payment path (Stripe, manual methods, future
|
|
9
|
+
* providers) because it operates BEFORE any processor is involved.
|
|
10
|
+
*
|
|
11
|
+
* Model (see schema.ts `discounts`):
|
|
12
|
+
* - `automatic` discounts are always candidates; a `code` discount is a
|
|
13
|
+
* candidate only when the shopper entered its code.
|
|
14
|
+
* - Each discount belongs to a CLASS — product / order / shipping — and
|
|
15
|
+
* carries per-class `combinesWith` flags. A set of discounts can coexist
|
|
16
|
+
* only when every PAIR allows the other's class (Shopify's model, without
|
|
17
|
+
* Shopify's caps).
|
|
18
|
+
* - Resolution is deterministic: the entered code is honored first (the
|
|
19
|
+
* shopper asked for it), then automatic discounts are added greedily in
|
|
20
|
+
* descending amount order while pairwise-compatible. One shipping-class
|
|
21
|
+
* discount at most (a shipping rate can't be discounted twice).
|
|
22
|
+
* - The cumulative product+order discount is clamped to the subtotal.
|
|
23
|
+
*
|
|
24
|
+
* Validation per candidate: enabled → schedule → eligibility (all /
|
|
25
|
+
* logged_in / groups) → scoped minimums (min subtotal / quantity count only
|
|
26
|
+
* the items in scope, like Shopify) → usage limit → once-per-customer.
|
|
27
|
+
* Automatic discounts failing a check are silently skipped; a failing CODE
|
|
28
|
+
* reports a friendly `codeError` for the checkout UI.
|
|
29
|
+
*/
|
|
30
|
+
import { findDiscountByCode, findActiveAutomaticDiscounts, countDiscountRedemptionsForCustomer, } from '../../core/db/queries';
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Helpers
|
|
33
|
+
// =============================================================================
|
|
34
|
+
export function discountClassOf(d) {
|
|
35
|
+
if (d.type === 'free_shipping')
|
|
36
|
+
return 'shipping';
|
|
37
|
+
if (d.type === 'buy_x_get_y')
|
|
38
|
+
return 'product';
|
|
39
|
+
return d.appliesTo === 'order' ? 'order' : 'product';
|
|
40
|
+
}
|
|
41
|
+
function pairCanCombine(a, b) {
|
|
42
|
+
const flagsA = (a.combinesWith ?? {});
|
|
43
|
+
const flagsB = (b.combinesWith ?? {});
|
|
44
|
+
return !!flagsA[discountClassOf(b)] && !!flagsB[discountClassOf(a)];
|
|
45
|
+
}
|
|
46
|
+
/** Lines matched by a products/categories scope. 'order'/'any' matches all. */
|
|
47
|
+
function matchingLines(lines, appliesTo, targetIds, categoryByProductId) {
|
|
48
|
+
if (appliesTo === 'order' || appliesTo === 'any')
|
|
49
|
+
return lines;
|
|
50
|
+
const targets = new Set(targetIds);
|
|
51
|
+
if (targets.size === 0)
|
|
52
|
+
return [];
|
|
53
|
+
if (appliesTo === 'products') {
|
|
54
|
+
return lines.filter((l) => l.productId && targets.has(l.productId));
|
|
55
|
+
}
|
|
56
|
+
// categories
|
|
57
|
+
return lines.filter((l) => {
|
|
58
|
+
const cat = l.productId ? categoryByProductId[l.productId] : null;
|
|
59
|
+
return !!cat && targets.has(cat);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function sumCents(lines) {
|
|
63
|
+
return lines.reduce((sum, l) => sum + l.price * l.quantity, 0);
|
|
64
|
+
}
|
|
65
|
+
function sumQuantity(lines) {
|
|
66
|
+
return lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
67
|
+
}
|
|
68
|
+
function decimalToNumber(v) {
|
|
69
|
+
if (v == null)
|
|
70
|
+
return 0;
|
|
71
|
+
const n = Number.parseFloat(v);
|
|
72
|
+
return Number.isFinite(n) ? n : 0;
|
|
73
|
+
}
|
|
74
|
+
async function validateCandidate(d, ctx) {
|
|
75
|
+
const now = ctx.now ?? new Date();
|
|
76
|
+
if (!d.enabled)
|
|
77
|
+
return 'INVALID_CODE';
|
|
78
|
+
if (d.startsAt && now < d.startsAt)
|
|
79
|
+
return 'EXPIRED';
|
|
80
|
+
if (d.endsAt && now > d.endsAt)
|
|
81
|
+
return 'EXPIRED';
|
|
82
|
+
// Eligibility
|
|
83
|
+
if (d.eligibility === 'logged_in' && !ctx.customerId)
|
|
84
|
+
return 'NOT_ELIGIBLE';
|
|
85
|
+
if (d.eligibility === 'groups') {
|
|
86
|
+
const groups = (d.groupIds ?? []);
|
|
87
|
+
if (!ctx.customerGroupId || !groups.includes(ctx.customerGroupId)) {
|
|
88
|
+
return 'NOT_ELIGIBLE';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Scoped minimums — only in-scope items count (Shopify semantics).
|
|
92
|
+
const catMap = ctx.categoryByProductId ?? {};
|
|
93
|
+
const scopeLines = d.type === 'buy_x_get_y'
|
|
94
|
+
? ctx.lines // BXGY qualification handled by its own config below
|
|
95
|
+
: matchingLines(ctx.lines, d.appliesTo, (d.targetIds ?? []), catMap);
|
|
96
|
+
const minSubtotal = decimalToNumber(d.minSubtotal);
|
|
97
|
+
if (minSubtotal > 0 && sumCents(scopeLines) < Math.round(minSubtotal * 100)) {
|
|
98
|
+
return 'MINIMUM_NOT_MET';
|
|
99
|
+
}
|
|
100
|
+
if (d.minQuantity && sumQuantity(scopeLines) < d.minQuantity) {
|
|
101
|
+
return 'MINIMUM_NOT_MET';
|
|
102
|
+
}
|
|
103
|
+
// Usage limits
|
|
104
|
+
if (d.usageLimit != null && d.usedCount >= d.usageLimit) {
|
|
105
|
+
return 'USAGE_LIMIT_REACHED';
|
|
106
|
+
}
|
|
107
|
+
if (d.oncePerCustomer && (ctx.customerId || ctx.customerEmail)) {
|
|
108
|
+
const used = await countDiscountRedemptionsForCustomer(d.id, {
|
|
109
|
+
customerId: ctx.customerId,
|
|
110
|
+
email: ctx.customerEmail,
|
|
111
|
+
});
|
|
112
|
+
if (used > 0)
|
|
113
|
+
return 'ALREADY_USED';
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Amount computation
|
|
119
|
+
// =============================================================================
|
|
120
|
+
function computeBxgyAmount(d, ctx) {
|
|
121
|
+
const cfg = d.bxgy;
|
|
122
|
+
if (!cfg || cfg.buyQuantity < 1 || cfg.getQuantity < 1)
|
|
123
|
+
return 0;
|
|
124
|
+
const catMap = ctx.categoryByProductId ?? {};
|
|
125
|
+
const buyLines = matchingLines(ctx.lines, cfg.buyAppliesTo, cfg.buyTargetIds ?? [], catMap);
|
|
126
|
+
const getLines = matchingLines(ctx.lines, cfg.getAppliesTo, cfg.getTargetIds ?? [], catMap);
|
|
127
|
+
if (buyLines.length === 0 || getLines.length === 0)
|
|
128
|
+
return 0;
|
|
129
|
+
// Expand get-candidates into single units, cheapest first (the discount
|
|
130
|
+
// always lands on the cheapest qualifying units — Shopify's behavior).
|
|
131
|
+
const getUnits = [];
|
|
132
|
+
for (const l of getLines) {
|
|
133
|
+
for (let i = 0; i < l.quantity; i++)
|
|
134
|
+
getUnits.push(l.price);
|
|
135
|
+
}
|
|
136
|
+
getUnits.sort((a, b) => a - b);
|
|
137
|
+
const buyUnits = sumQuantity(buyLines);
|
|
138
|
+
let applications = Math.floor(buyUnits / cfg.buyQuantity);
|
|
139
|
+
if (cfg.maxUsesPerOrder && cfg.maxUsesPerOrder > 0) {
|
|
140
|
+
applications = Math.min(applications, cfg.maxUsesPerOrder);
|
|
141
|
+
}
|
|
142
|
+
if (applications < 1)
|
|
143
|
+
return 0;
|
|
144
|
+
const unitsToDiscount = Math.min(applications * cfg.getQuantity, getUnits.length);
|
|
145
|
+
const pct = cfg.getType === 'free' ? 100 : Math.min(100, Math.max(0, cfg.getValue));
|
|
146
|
+
let amount = 0;
|
|
147
|
+
for (let i = 0; i < unitsToDiscount; i++) {
|
|
148
|
+
amount += Math.round(getUnits[i] * (pct / 100));
|
|
149
|
+
}
|
|
150
|
+
return amount;
|
|
151
|
+
}
|
|
152
|
+
/** Standalone value of a discount against the cart, in cents. */
|
|
153
|
+
function computeAmountCents(d, ctx) {
|
|
154
|
+
const catMap = ctx.categoryByProductId ?? {};
|
|
155
|
+
const value = decimalToNumber(d.value);
|
|
156
|
+
switch (d.type) {
|
|
157
|
+
case 'free_shipping':
|
|
158
|
+
return 0; // applied on the shipping axis, not the subtotal
|
|
159
|
+
case 'buy_x_get_y':
|
|
160
|
+
return computeBxgyAmount(d, ctx);
|
|
161
|
+
case 'percentage': {
|
|
162
|
+
const pct = Math.min(100, Math.max(0, value));
|
|
163
|
+
const base = d.appliesTo === 'order'
|
|
164
|
+
? ctx.subtotalCents
|
|
165
|
+
: sumCents(matchingLines(ctx.lines, d.appliesTo, (d.targetIds ?? []), catMap));
|
|
166
|
+
return Math.round(base * (pct / 100));
|
|
167
|
+
}
|
|
168
|
+
case 'fixed_amount': {
|
|
169
|
+
const cents = Math.max(0, Math.round(value * 100));
|
|
170
|
+
const base = d.appliesTo === 'order'
|
|
171
|
+
? ctx.subtotalCents
|
|
172
|
+
: sumCents(matchingLines(ctx.lines, d.appliesTo, (d.targetIds ?? []), catMap));
|
|
173
|
+
// A scoped fixed amount requires at least one matching item.
|
|
174
|
+
if (d.appliesTo !== 'order' && base <= 0)
|
|
175
|
+
return 0;
|
|
176
|
+
return Math.min(cents, base);
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Friendly code-error copy
|
|
184
|
+
// =============================================================================
|
|
185
|
+
const CODE_ERROR_MESSAGES = {
|
|
186
|
+
INVALID_CODE: "This code isn't valid.",
|
|
187
|
+
EXPIRED: 'This code has expired.',
|
|
188
|
+
NOT_ELIGIBLE: "This code isn't available on your account.",
|
|
189
|
+
MINIMUM_NOT_MET: "Your order doesn't meet the minimum for this code.",
|
|
190
|
+
USAGE_LIMIT_REACHED: 'This code has reached its usage limit.',
|
|
191
|
+
ALREADY_USED: "You've already used this code.",
|
|
192
|
+
NOT_APPLICABLE: "This code doesn't apply to the items in your cart.",
|
|
193
|
+
};
|
|
194
|
+
function codeError(reason) {
|
|
195
|
+
return { reason, message: CODE_ERROR_MESSAGES[reason] };
|
|
196
|
+
}
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// Evaluation
|
|
199
|
+
// =============================================================================
|
|
200
|
+
export async function evaluateDiscounts(ctx) {
|
|
201
|
+
const result = {
|
|
202
|
+
discountCents: 0,
|
|
203
|
+
freeShipping: false,
|
|
204
|
+
applied: [],
|
|
205
|
+
};
|
|
206
|
+
if (!ctx.lines || ctx.lines.length === 0 || ctx.subtotalCents <= 0) {
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
// ── Candidates ────────────────────────────────────────────────────────────
|
|
210
|
+
let codeDiscount = null;
|
|
211
|
+
if (ctx.code && ctx.code.trim()) {
|
|
212
|
+
codeDiscount = await findDiscountByCode(ctx.code);
|
|
213
|
+
if (!codeDiscount) {
|
|
214
|
+
result.codeError = codeError('INVALID_CODE');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let automatics = [];
|
|
218
|
+
try {
|
|
219
|
+
automatics = await findActiveAutomaticDiscounts();
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
// Discounts must never break checkout — degrade to "no automatics".
|
|
223
|
+
console.error('[Discounts] automatic-discounts lookup failed:', err);
|
|
224
|
+
}
|
|
225
|
+
let codeCandidate = null;
|
|
226
|
+
if (codeDiscount) {
|
|
227
|
+
const rejection = await validateCandidate(codeDiscount, ctx);
|
|
228
|
+
if (rejection) {
|
|
229
|
+
result.codeError = codeError(rejection);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const amountCents = computeAmountCents(codeDiscount, ctx);
|
|
233
|
+
const cls = discountClassOf(codeDiscount);
|
|
234
|
+
if (cls !== 'shipping' && amountCents <= 0) {
|
|
235
|
+
result.codeError = codeError('NOT_APPLICABLE');
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
codeCandidate = { discount: codeDiscount, amountCents, discountClass: cls };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const automaticCandidates = [];
|
|
243
|
+
for (const d of automatics) {
|
|
244
|
+
const rejection = await validateCandidate(d, ctx);
|
|
245
|
+
if (rejection)
|
|
246
|
+
continue;
|
|
247
|
+
const amountCents = computeAmountCents(d, ctx);
|
|
248
|
+
const cls = discountClassOf(d);
|
|
249
|
+
if (cls !== 'shipping' && amountCents <= 0)
|
|
250
|
+
continue;
|
|
251
|
+
automaticCandidates.push({ discount: d, amountCents, discountClass: cls });
|
|
252
|
+
}
|
|
253
|
+
// Highest value first (free-shipping candidates carry 0 and sort last —
|
|
254
|
+
// they compete on their own axis anyway).
|
|
255
|
+
automaticCandidates.sort((a, b) => b.amountCents - a.amountCents);
|
|
256
|
+
// ── Deterministic greedy resolution ──────────────────────────────────────
|
|
257
|
+
// The entered code is honored first; automatics join when every pair in
|
|
258
|
+
// the growing set allows the combination. At most one shipping discount.
|
|
259
|
+
const selected = [];
|
|
260
|
+
const canJoin = (candidate) => {
|
|
261
|
+
if (candidate.discountClass === 'shipping' &&
|
|
262
|
+
selected.some((s) => s.discountClass === 'shipping')) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return selected.every((s) => pairCanCombine(s.discount, candidate.discount));
|
|
266
|
+
};
|
|
267
|
+
if (codeCandidate)
|
|
268
|
+
selected.push(codeCandidate);
|
|
269
|
+
for (const candidate of automaticCandidates) {
|
|
270
|
+
if (canJoin(candidate))
|
|
271
|
+
selected.push(candidate);
|
|
272
|
+
}
|
|
273
|
+
// ── Totals (clamped to the subtotal) ─────────────────────────────────────
|
|
274
|
+
let remaining = ctx.subtotalCents;
|
|
275
|
+
for (const s of selected) {
|
|
276
|
+
const clamped = s.discountClass === 'shipping' ? 0 : Math.min(s.amountCents, remaining);
|
|
277
|
+
remaining -= clamped;
|
|
278
|
+
result.applied.push({
|
|
279
|
+
id: s.discount.id,
|
|
280
|
+
title: s.discount.title,
|
|
281
|
+
code: s.discount.code,
|
|
282
|
+
method: s.discount.method ?? 'code',
|
|
283
|
+
type: s.discount.type,
|
|
284
|
+
discountClass: s.discountClass,
|
|
285
|
+
amountCents: clamped,
|
|
286
|
+
freeShipping: s.discount.type === 'free_shipping',
|
|
287
|
+
});
|
|
288
|
+
if (s.discount.type === 'free_shipping')
|
|
289
|
+
result.freeShipping = true;
|
|
290
|
+
}
|
|
291
|
+
result.discountCents = ctx.subtotalCents - remaining;
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=discounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discounts.js","sourceRoot":"","sources":["../../../src/checkout/server/discounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { getStripe, getAllWebhookSecrets, getConnectedAccountId } from '../stripe/client';
|
|
8
8
|
import { createOrderFromCheckout, extractOrderParamsFromSession, handleRefund, attachReceiptToOrder, } from './order-service';
|
|
9
|
-
import { findSettings } from '../../core/db/queries';
|
|
9
|
+
import { findSettings, recordDiscountRedemption } from '../../core/db/queries';
|
|
10
10
|
import { sendOrderConfirmation, formatPrice, formatDate, formatOrderNumber, } from '../../emails';
|
|
11
11
|
// =============================================================================
|
|
12
12
|
// Main Webhook Handler
|
|
@@ -150,6 +150,28 @@ async function handleCheckoutSessionCompleted(session) {
|
|
|
150
150
|
// Create the order
|
|
151
151
|
const { orderId, isNew } = await createOrderFromCheckout(orderParams);
|
|
152
152
|
console.log('[Webhook] Order', isNew ? 'created' : 'already exists', orderId);
|
|
153
|
+
// Record native-discount redemptions (usage limits + audit). The session
|
|
154
|
+
// builder stamped the applied snapshot in metadata. Best-effort — a
|
|
155
|
+
// redemption failure must never fail the webhook/order.
|
|
156
|
+
if (isNew && session.metadata?.rovelaDiscounts) {
|
|
157
|
+
try {
|
|
158
|
+
const applied = JSON.parse(session.metadata.rovelaDiscounts);
|
|
159
|
+
for (const d of applied) {
|
|
160
|
+
if (!d?.id)
|
|
161
|
+
continue;
|
|
162
|
+
await recordDiscountRedemption({
|
|
163
|
+
discountId: d.id,
|
|
164
|
+
orderId,
|
|
165
|
+
customerId: orderParams.customerId ?? null,
|
|
166
|
+
email: orderParams.email || null,
|
|
167
|
+
amountCents: typeof d.a === 'number' ? d.a : 0,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error('[Webhook] discount redemption recording failed:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
153
175
|
// Attach receipt URL to order (non-blocking, errors are logged)
|
|
154
176
|
let receiptUrl = null;
|
|
155
177
|
if (isNew && orderParams.stripePaymentIntentId) {
|
|
@@ -264,11 +286,15 @@ async function sendOrderConfirmationEmail(orderId, params, session, receiptUrl,
|
|
|
264
286
|
const customerName = session.customer_details?.name ||
|
|
265
287
|
params.email.split('@')[0] ||
|
|
266
288
|
'Customer';
|
|
267
|
-
// Convert order items to email format
|
|
289
|
+
// Convert order items to email format.
|
|
290
|
+
// formatPrice's integer input is AMBIGUOUS (its cents-vs-dollars heuristic
|
|
291
|
+
// breaks at <= 100 cents — a 1.00 amount renders as 100.00). Always hand
|
|
292
|
+
// it an unambiguous decimal STRING.
|
|
293
|
+
const emailAmount = (cents) => (cents / 100).toFixed(2);
|
|
268
294
|
const items = params.items.map((item) => ({
|
|
269
295
|
name: item.name,
|
|
270
296
|
quantity: item.quantity,
|
|
271
|
-
price: formatPrice(item.price, currency),
|
|
297
|
+
price: formatPrice(emailAmount(item.price), currency),
|
|
272
298
|
imageUrl: item.image, // Note: CheckoutLineItem uses 'image' not 'imageUrl'
|
|
273
299
|
}));
|
|
274
300
|
// Convert shipping address to email format
|
|
@@ -296,14 +322,14 @@ async function sendOrderConfirmationEmail(orderId, params, session, receiptUrl,
|
|
|
296
322
|
orderDate: formatDate(new Date()),
|
|
297
323
|
paymentStatus: 'Paid',
|
|
298
324
|
items,
|
|
299
|
-
subtotal: formatPrice(params.subtotal, currency),
|
|
325
|
+
subtotal: formatPrice(emailAmount(params.subtotal), currency),
|
|
300
326
|
discount: params.discountAmount && params.discountAmount > 0
|
|
301
|
-
? `-${formatPrice(params.discountAmount, currency)}`
|
|
327
|
+
? `-${formatPrice(emailAmount(params.discountAmount), currency)}`
|
|
302
328
|
: undefined,
|
|
303
329
|
discountCode: params.discountCode,
|
|
304
|
-
shippingCost: params.shipping > 0 ? formatPrice(params.shipping, currency) : 'Free',
|
|
305
|
-
tax: formatPrice(params.tax, currency),
|
|
306
|
-
total: formatPrice(params.total, currency),
|
|
330
|
+
shippingCost: params.shipping > 0 ? formatPrice(emailAmount(params.shipping), currency) : 'Free',
|
|
331
|
+
tax: formatPrice(emailAmount(params.tax), currency),
|
|
332
|
+
total: formatPrice(emailAmount(params.total), currency),
|
|
307
333
|
shippingAddress,
|
|
308
334
|
orderTrackingUrl,
|
|
309
335
|
receiptUrl: receiptUrl || undefined,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handle-webhook.js","sourceRoot":"","sources":["../../../src/checkout/server/handle-webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACzF,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,EACZ,oBAAoB,GACrB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"handle-webhook.js","sourceRoot":"","sources":["../../../src/checkout/server/handle-webhook.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,10 @@ export { createCheckoutSession, getCheckoutSession, deserializeItemsFromMetadata
|
|
|
7
7
|
export type { CreateCheckoutSessionOptions } from './create-checkout-session';
|
|
8
8
|
export { handleStripeWebhook } from './handle-webhook';
|
|
9
9
|
export { createOrderFromCheckout, extractOrderParamsFromSession, handleRefund, } from './order-service';
|
|
10
|
+
export { buildQuote, repriceItems, resolveShippingSelection, resolveTax, PricingError, } from './pricing';
|
|
11
|
+
export type { Quote, QuoteInput, QuoteItemInput, RepricedItems, ShippingResolution, TaxResolution, } from './pricing';
|
|
12
|
+
export { resolveCheckoutCustomer } from './checkout-customer';
|
|
13
|
+
export type { TrustedCheckoutCustomer } from './checkout-customer';
|
|
10
14
|
export { createCouponWithCode, createBulkPromotionCodes, } from './coupons';
|
|
11
15
|
export type { CreateCouponWithCodeOptions, CreateCouponResult, CreateBulkPromotionCodesOptions, CreateBulkResult, } from './coupons';
|
|
12
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/checkout/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA;AAElC,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAA;AAE7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,GACb,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/checkout/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA;AAElC,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAA;AAE7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,GACb,MAAM,iBAAiB,CAAA;AAIxB,OAAO,EACL,UAAU,EACV,YAAY,EACZ,wBAAwB,EACxB,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAElB,YAAY,EACV,KAAK,EACL,UAAU,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,aAAa,GACd,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAC7D,YAAY,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAElE,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,WAAW,CAAA;AAElB,YAAY,EACV,2BAA2B,EAC3B,kBAAkB,EAClB,+BAA+B,EAC/B,gBAAgB,GACjB,MAAM,WAAW,CAAA"}
|
|
@@ -6,5 +6,9 @@
|
|
|
6
6
|
export { createCheckoutSession, getCheckoutSession, deserializeItemsFromMetadata, } from './create-checkout-session';
|
|
7
7
|
export { handleStripeWebhook } from './handle-webhook';
|
|
8
8
|
export { createOrderFromCheckout, extractOrderParamsFromSession, handleRefund, } from './order-service';
|
|
9
|
+
// Pricing core — the processor-agnostic pipeline that computes an order's
|
|
10
|
+
// final price. Payment processors only collect what this computes.
|
|
11
|
+
export { buildQuote, repriceItems, resolveShippingSelection, resolveTax, PricingError, } from './pricing';
|
|
12
|
+
export { resolveCheckoutCustomer } from './checkout-customer';
|
|
9
13
|
export { createCouponWithCode, createBulkPromotionCodes, } from './coupons';
|
|
10
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checkout/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA;AAIlC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,GACb,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,WAAW,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checkout/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,2BAA2B,CAAA;AAIlC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,YAAY,GACb,MAAM,iBAAiB,CAAA;AAExB,0EAA0E;AAC1E,mEAAmE;AACnE,OAAO,EACL,UAAU,EACV,YAAY,EACZ,wBAAwB,EACxB,UAAU,EACV,YAAY,GACb,MAAM,WAAW,CAAA;AAWlB,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAG7D,OAAO,EACL,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,WAAW,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"order-service.d.ts","sourceRoot":"","sources":["../../../src/checkout/server/order-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAA;AAchC,OAAO,KAAK,EAAE,iBAAiB,EAAoB,MAAM,UAAU,CAAA;AAOnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"order-service.d.ts","sourceRoot":"","sources":["../../../src/checkout/server/order-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAA;AAchC,OAAO,KAAK,EAAE,iBAAiB,EAAoB,MAAM,UAAU,CAAA;AAOnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAgK9C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,EAChC,SAAS,CAAC,EAAE,MAAM,CAAC,QAAQ,EAAE,GAC5B,iBAAiB,CA8MnB;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAChC,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAuCpF;AAMD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+CxB"}
|
|
@@ -88,6 +88,7 @@ export async function createOrderFromCheckout(params) {
|
|
|
88
88
|
discountAmount: params.discountAmount ? centsToDecimal(params.discountAmount) : '0',
|
|
89
89
|
stripePromotionCodeId: params.stripePromotionCodeId,
|
|
90
90
|
total: centsToDecimal(params.total),
|
|
91
|
+
currency: params.currency ? params.currency.toUpperCase().slice(0, 3) : undefined,
|
|
91
92
|
shippingAddress: params.shippingAddress,
|
|
92
93
|
billingAddress: params.billingAddress,
|
|
93
94
|
stripePaymentIntentId: params.stripePaymentIntentId,
|
|
@@ -244,11 +245,16 @@ export function extractOrderParamsFromSession(session, lineItems) {
|
|
|
244
245
|
? parseStripeAddress(session.customer_details.address, session.customer_details.name || undefined)
|
|
245
246
|
: undefined;
|
|
246
247
|
}
|
|
247
|
-
// Extract discount
|
|
248
|
-
//
|
|
249
|
-
|
|
248
|
+
// Extract discount info. NATIVE discounts (our engine) stamp their code +
|
|
249
|
+
// NET amount in metadata — preferred. Legacy Stripe-hosted promo codes fall
|
|
250
|
+
// back to total_details.amount_discount / discounts[].
|
|
251
|
+
const nativeDiscountCode = typeof session.metadata?.rovelaDiscountCode === 'string' &&
|
|
252
|
+
session.metadata.rovelaDiscountCode
|
|
253
|
+
? session.metadata.rovelaDiscountCode
|
|
254
|
+
: undefined;
|
|
255
|
+
const stripeDiscountAmount = session.total_details?.amount_discount || 0;
|
|
250
256
|
const firstDiscount = session.discounts?.[0];
|
|
251
|
-
const
|
|
257
|
+
const legacyDiscountCode = firstDiscount
|
|
252
258
|
? (typeof firstDiscount === 'object' && 'coupon' in firstDiscount
|
|
253
259
|
? firstDiscount.coupon?.name
|
|
254
260
|
: undefined)
|
|
@@ -260,8 +266,44 @@ export function extractOrderParamsFromSession(session, lineItems) {
|
|
|
260
266
|
: undefined)
|
|
261
267
|
: undefined)
|
|
262
268
|
: undefined;
|
|
263
|
-
|
|
264
|
-
//
|
|
269
|
+
const discountCode = nativeDiscountCode ?? legacyDiscountCode;
|
|
270
|
+
// ==========================================================================
|
|
271
|
+
// Authoritative totals (pricing core) — `rovelaTotals` is OUR server-side
|
|
272
|
+
// computation stamped at session creation. When present it is the source
|
|
273
|
+
// of truth for the recorded order; Stripe's amounts are a cross-check.
|
|
274
|
+
// Sessions from the standard flow (shipping chosen on the hosted page) or
|
|
275
|
+
// from older SDK versions don't carry it and use the legacy fallbacks.
|
|
276
|
+
// ==========================================================================
|
|
277
|
+
let quoteTotals = null;
|
|
278
|
+
if (session.metadata?.rovelaTotals) {
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(session.metadata.rovelaTotals);
|
|
281
|
+
if (parsed && typeof parsed === 'object')
|
|
282
|
+
quoteTotals = parsed;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
console.warn('[Order] Unparseable rovelaTotals metadata — using Stripe amounts');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Discount provenance: NATIVE (our engine, stamped in rovelaTotals — the
|
|
289
|
+
// recorded amount is the NET goods discount) vs LEGACY Stripe-hosted promo
|
|
290
|
+
// (allow_promotion_codes; only the Stripe amounts know about it).
|
|
291
|
+
const isNativeDiscount = !!session.metadata?.rovelaDiscounts;
|
|
292
|
+
const legacyStripeDiscount = !isNativeDiscount && stripeDiscountAmount > 0;
|
|
293
|
+
const discountAmount = isNativeDiscount
|
|
294
|
+
? quoteTotals?.discount ?? 0
|
|
295
|
+
: stripeDiscountAmount;
|
|
296
|
+
if (quoteTotals &&
|
|
297
|
+
!legacyStripeDiscount &&
|
|
298
|
+
!isNativeDiscount &&
|
|
299
|
+
typeof quoteTotals.total === 'number' &&
|
|
300
|
+
typeof session.amount_total === 'number' &&
|
|
301
|
+
quoteTotals.total !== session.amount_total) {
|
|
302
|
+
// Should never happen — the session's line items were built from the same
|
|
303
|
+
// quote. Loud log so a drift is investigated, then OUR number is recorded.
|
|
304
|
+
console.error('[Order] QUOTE/STRIPE TOTAL MISMATCH:', 'quote =', quoteTotals.total, 'stripe =', session.amount_total, 'session =', session.id);
|
|
305
|
+
}
|
|
306
|
+
// Legacy metadata fallbacks (pre-rovelaTotals sessions)
|
|
265
307
|
const taxFromMetadata = session.metadata?.taxAmount
|
|
266
308
|
? parseInt(session.metadata.taxAmount, 10)
|
|
267
309
|
: null;
|
|
@@ -282,13 +324,21 @@ export function extractOrderParamsFromSession(session, lineItems) {
|
|
|
282
324
|
sessionId: session.id,
|
|
283
325
|
email: session.customer_details?.email || session.customer_email || '',
|
|
284
326
|
items,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
// Our quote wins; Stripe amounts are the legacy fallback. NOTE: Stripe's
|
|
328
|
+
// amount_subtotal INCLUDES the synthetic Tax/Shipping line items our
|
|
329
|
+
// sessions carry, so the quote subtotal is also the more correct number.
|
|
330
|
+
subtotal: quoteTotals?.subtotal ?? (session.amount_subtotal || 0),
|
|
331
|
+
tax: quoteTotals?.tax ?? taxFromMetadata ?? (session.total_details?.amount_tax || 0),
|
|
332
|
+
shipping: quoteTotals?.shipping ??
|
|
333
|
+
shippingFromMetadata ??
|
|
334
|
+
(session.total_details?.amount_shipping || 0),
|
|
288
335
|
discountCode,
|
|
289
336
|
discountAmount,
|
|
290
337
|
stripePromotionCodeId,
|
|
291
|
-
total:
|
|
338
|
+
total: legacyStripeDiscount
|
|
339
|
+
? session.amount_total || 0 // legacy hosted promo — charged amount wins
|
|
340
|
+
: quoteTotals?.total ?? (session.amount_total || 0),
|
|
341
|
+
currency: session.currency ? session.currency.toUpperCase() : undefined,
|
|
292
342
|
shippingAddress,
|
|
293
343
|
billingAddress,
|
|
294
344
|
stripePaymentIntentId: typeof session.payment_intent === 'string'
|