Skip to content

Commit 1c63ed9

Browse files
committed
Fix stripe webhook idempotency bug and add dynamic shipping brackets functionality
1 parent 1fc2129 commit 1c63ed9

File tree

6 files changed

+194
-98
lines changed

6 files changed

+194
-98
lines changed

app/admin/settings/page.tsx

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,22 +123,28 @@ export default async function AdminSettings() {
123123
<section id="section-shipping" className="scroll-mt-32">
124124
<SettingsForm action={updateShippingSettings} title="Shipping Rate Configuration" iconName="truck">
125125
<p className="text-[11px] text-luxury-subtext leading-relaxed">
126-
Configure the flat-rate shipping prices shown to customers at checkout. Stripe collects the customer address — Shippo is only called by admin at fulfillment.
126+
Configure the weight-based shipping prices shown to customers at checkout. Brackets are evaluated from lowest weight to highest. Set `max_lb` to 999 for the catch-all bracket.
127127
</p>
128128

129-
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
129+
<div className="grid grid-cols-1 gap-8">
130130
<div className="p-6 bg-white/5 rounded-xl border border-white/5 space-y-4">
131131
<div className="flex items-center gap-3">
132132
<Package className="w-4 h-4 text-white" />
133-
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">Standard</p>
134-
</div>
135-
<div className="relative">
136-
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-luxury-subtext text-sm">$</span>
137-
<input name="standard_rate" type="number" step="0.01" defaultValue={shipping.standard_rate ?? '7.99'}
138-
className="w-full bg-[#0B0B0D] border border-white/10 rounded-md pl-8 pr-4 py-3 text-sm text-white focus:border-gold/50 outline-none transition-all" />
133+
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">US Domestic Standard</p>
139134
</div>
140-
<input name="standard_label" type="text" defaultValue={shipping.standard_label ?? 'Standard Shipping (5-10 Business Days)'}
135+
<input name="standard_label" type="text" defaultValue={shipping.standard_label ?? 'USPS Ground Advantage (3-5 Days)'}
141136
className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-sm text-white focus:border-gold/50 outline-none transition-all" />
137+
<div className="space-y-1.5">
138+
<label className="text-[9px] uppercase tracking-luxury text-luxury-subtext font-medium">Weight Brackets (JSON) Default: [ {`{"max_lb": 0.5, "rate": 4.99}`} ... ]</label>
139+
<textarea name="weight_brackets" defaultValue={JSON.stringify(shipping.weight_brackets || [
140+
{ max_lb: 0.5, rate: 4.99 },
141+
{ max_lb: 1, rate: 6.99 },
142+
{ max_lb: 2, rate: 8.99 },
143+
{ max_lb: 5, rate: 12.99 },
144+
{ max_lb: 999, rate: 15.99 }
145+
], null, 2)}
146+
rows={5} className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-xs font-mono text-emerald-400 focus:border-gold/50 outline-none transition-all" />
147+
</div>
142148
<div className="space-y-1.5">
143149
<label className="text-[9px] uppercase tracking-luxury text-luxury-subtext font-medium">Free Shipping Threshold</label>
144150
<input name="free_shipping_threshold" type="number" step="0.01" defaultValue={shipping.free_shipping_threshold ?? '100'}
@@ -149,22 +155,60 @@ export default async function AdminSettings() {
149155
<div className="p-6 bg-white/5 rounded-xl border border-white/5 space-y-4">
150156
<div className="flex items-center gap-3">
151157
<Truck className="w-4 h-4 text-gold" />
152-
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">Express</p>
153-
</div>
154-
<div className="relative">
155-
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-luxury-subtext text-sm">$</span>
156-
<input name="express_rate" type="number" step="0.01" defaultValue={shipping.express_rate ?? '19.99'}
157-
className="w-full bg-[#0B0B0D] border border-white/10 rounded-md pl-8 pr-4 py-3 text-sm text-white focus:border-gold/50 outline-none transition-all" />
158+
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">US Domestic Express</p>
158159
</div>
159-
<input name="express_label" type="text" defaultValue={shipping.express_label ?? 'Express Shipping'}
160+
<input name="express_label" type="text" defaultValue={shipping.express_label ?? 'USPS Priority Mail (1-3 Days)'}
160161
className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-sm text-white focus:border-gold/50 outline-none transition-all" />
162+
<div className="space-y-1.5">
163+
<label className="text-[9px] uppercase tracking-luxury text-luxury-subtext font-medium">Weight Brackets (JSON)</label>
164+
<textarea name="express_weight_brackets" defaultValue={JSON.stringify(shipping.express_weight_brackets || [
165+
{ max_lb: 1, rate: 9.99 },
166+
{ max_lb: 3, rate: 14.99 },
167+
{ max_lb: 999, rate: 19.99 }
168+
], null, 2)}
169+
rows={5} className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-xs font-mono text-emerald-400 focus:border-gold/50 outline-none transition-all" />
170+
</div>
161171
</div>
172+
173+
<div className="p-6 bg-white/5 rounded-xl border border-white/5 space-y-4">
174+
<div className="flex items-center gap-3">
175+
<Globe className="w-4 h-4 text-white" />
176+
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">International Standard</p>
177+
</div>
178+
<div className="space-y-1.5">
179+
<label className="text-[9px] uppercase tracking-luxury text-luxury-subtext font-medium">International Std Brackets (JSON)</label>
180+
<textarea name="intl_weight_brackets" defaultValue={JSON.stringify(shipping.intl_weight_brackets || [
181+
{ max_lb: 1, rate: 19.99 },
182+
{ max_lb: 3, rate: 29.99 },
183+
{ max_lb: 5, rate: 39.99 },
184+
{ max_lb: 999, rate: 59.99 }
185+
], null, 2)}
186+
rows={5} className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-xs font-mono text-emerald-400 focus:border-gold/50 outline-none transition-all" />
187+
</div>
188+
</div>
189+
190+
<div className="p-6 bg-white/5 rounded-xl border border-white/5 space-y-4">
191+
<div className="flex items-center gap-3">
192+
<Globe className="w-4 h-4 text-gold" />
193+
<p className="text-[11px] uppercase tracking-luxury font-bold text-white">International Express</p>
194+
</div>
195+
<div className="space-y-1.5">
196+
<label className="text-[9px] uppercase tracking-luxury text-luxury-subtext font-medium">International Express Brackets (JSON)</label>
197+
<textarea name="intl_express_weight_brackets" defaultValue={JSON.stringify(shipping.intl_express_weight_brackets || [
198+
{ max_lb: 1, rate: 49.99 },
199+
{ max_lb: 3, rate: 69.99 },
200+
{ max_lb: 999, rate: 89.99 }
201+
], null, 2)}
202+
rows={5} className="w-full bg-[#0B0B0D] border border-white/10 rounded-md px-4 py-3 text-xs font-mono text-emerald-400 focus:border-gold/50 outline-none transition-all" />
203+
</div>
204+
</div>
205+
162206
</div>
163207

164-
<div className="flex items-start gap-3 p-5 bg-emerald-950/20 border border-emerald-500/20 rounded-xl">
208+
<div className="flex items-start gap-3 p-5 bg-emerald-950/20 border border-emerald-500/20 rounded-xl mt-6">
165209
<DollarSign className="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0" />
166210
<p className="text-[11px] text-emerald-400/80 leading-relaxed">
167-
Live Shippo rates are active. The flat rates above are used as fallback if carrier services are unreachable.
211+
Shipping rates are now fully dynamic and calculated live based on cart weight + location when requested at checkout. Admin can adjust these JSON boundaries anytime.
168212
</p>
169213
</div>
170214
</SettingsForm>

app/api/checkout/route.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,30 @@ export async function POST(req: Request) {
9696
const stdRate = calculateShippingRate(totalWeightLb, subtotal, cfg, "standard");
9797
const expRate = calculateShippingRate(totalWeightLb, subtotal, cfg, "express");
9898

99-
// International flat rate (recovers cost for heavy cross-border shipments)
100-
const intlBrackets = cfg.intl_weight_brackets || [
101-
{"max_lb": 1, "rate": 19.99},
102-
{"max_lb": 3, "rate": 29.99},
103-
{"max_lb": 5, "rate": 39.99},
104-
{"max_lb": 10, "rate": 59.99},
105-
{"max_lb": 999, "rate": 99.99}
106-
];
107-
99+
// International Standard Evaluation
100+
let intlBrackets = cfg.intl_weight_brackets;
101+
if (!intlBrackets || intlBrackets.length === 0) {
102+
intlBrackets = [
103+
{ max_lb: 1, rate: 19.99 },
104+
{ max_lb: 3, rate: 29.99 },
105+
{ max_lb: 5, rate: 39.99 },
106+
{ max_lb: 999, rate: 59.99 }
107+
];
108+
}
108109
const matchingIntlBracket = intlBrackets.find((b: any) => totalWeightLb <= b.max_lb) || intlBrackets[intlBrackets.length - 1];
109110
const intlStandardCost = matchingIntlBracket ? parseFloat(matchingIntlBracket.rate) : 19.99;
110111

111-
// International Express is evaluated dynamically as standard bracket + $30 premium
112-
const intlExpressCost = intlStandardCost + 30.00;
112+
// International Express Evaluation
113+
let intlExpressBrackets = cfg.intl_express_weight_brackets;
114+
if (!intlExpressBrackets || intlExpressBrackets.length === 0) {
115+
intlExpressBrackets = [
116+
{ max_lb: 1, rate: 49.99 },
117+
{ max_lb: 3, rate: 69.99 },
118+
{ max_lb: 999, rate: 89.99 }
119+
]
120+
}
121+
const matchingIntlExpressBracket = intlExpressBrackets.find((b: any) => totalWeightLb <= b.max_lb) || intlExpressBrackets[intlExpressBrackets.length - 1];
122+
const intlExpressCost = matchingIntlExpressBracket ? parseFloat(matchingIntlExpressBracket.rate) : 49.99;
113123

114124
const shippingOptions: Stripe.Checkout.SessionCreateParams.ShippingOption[] = [
115125
// Option 1: US Domestic Standard (or FREE)
@@ -120,10 +130,10 @@ export async function POST(req: Request) {
120130
amount: isFree ? 0 : Math.round(stdRate.cost * 100),
121131
currency: "usd",
122132
},
123-
display_name: isFree ? "Free Standard Shipping 🎁" : "Standard Shipping (US)",
133+
display_name: isFree ? "Free Standard Shipping 🎁" : `${stdRate.name} (US)`,
124134
delivery_estimate: {
125-
minimum: { unit: "business_day", value: 5 },
126-
maximum: { unit: "business_day", value: 10 },
135+
minimum: { unit: "business_day", value: stdRate.minDays },
136+
maximum: { unit: "business_day", value: stdRate.maxDays },
127137
},
128138
},
129139
},
@@ -135,10 +145,10 @@ export async function POST(req: Request) {
135145
amount: Math.round(expRate.cost * 100),
136146
currency: "usd",
137147
},
138-
display_name: "Express Shipping (US) ⚡",
148+
display_name: `${expRate.name} (US)`,
139149
delivery_estimate: {
140-
minimum: { unit: "business_day", value: 2 },
141-
maximum: { unit: "business_day", value: 4 },
150+
minimum: { unit: "business_day", value: expRate.minDays },
151+
maximum: { unit: "business_day", value: expRate.maxDays },
142152
},
143153
},
144154
},
@@ -150,25 +160,25 @@ export async function POST(req: Request) {
150160
amount: Math.round(intlStandardCost * 100),
151161
currency: "usd",
152162
},
153-
display_name: "International Standard Shipping 🌍",
163+
display_name: "USPS Priority Mail International 🌍",
154164
delivery_estimate: {
155-
minimum: { unit: "business_day", value: 10 },
156-
maximum: { unit: "business_day", value: 21 },
165+
minimum: { unit: "business_day", value: 6 },
166+
maximum: { unit: "business_day", value: 10 },
157167
},
158168
},
159169
},
160-
// Option 4: International Express (DHL / Priority)
170+
// Option 4: International Express
161171
{
162172
shipping_rate_data: {
163173
type: "fixed_amount",
164174
fixed_amount: {
165175
amount: Math.round(intlExpressCost * 100),
166176
currency: "usd",
167177
},
168-
display_name: "International Express (DHL) 🚀",
178+
display_name: "USPS Priority Mail Express International 🚀",
169179
delivery_estimate: {
170180
minimum: { unit: "business_day", value: 3 },
171-
maximum: { unit: "business_day", value: 7 },
181+
maximum: { unit: "business_day", value: 5 },
172182
},
173183
},
174184
},

app/api/shipping-settings/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ export async function POST(req: Request) {
5050
// Get standard rate from brackets
5151
const ship = calculateShippingRate(totalWeightLb, subtotal, cfg, 'standard');
5252
standard_rate = ship.cost;
53+
const exprShip = calculateShippingRate(totalWeightLb, subtotal, cfg, 'express');
54+
express_rate = exprShip.cost;
5355
}
5456

5557
const settings = {
5658
standard_rate: standard_rate.toString(),
5759
express_rate: express_rate.toString(),
58-
standard_label: cfg.standard_label ?? "Standard Shipping",
59-
express_label: cfg.express_label ?? "Express Shipping",
60+
standard_label: cfg.standard_label ?? "USPS Ground Advantage (3-5 Days)",
61+
express_label: cfg.express_label ?? "USPS Priority Mail (1-3 Days)",
6062
free_shipping_threshold: cfg.free_shipping_threshold ?? "100",
6163
};
6264

0 commit comments

Comments
 (0)