Rust discount function (one automatic discount evaluating all offers in priority order, parity with the Odoo engine), React Router admin with Polaris, theme app extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
// orders/create -> analytics (F23, N02). The function's candidate message is the offer name, or "A + B" when
|
|
// one line carries two offers; we attribute by name. If Shopify reports only the discount title
|
|
// ("Bundle Offers"), the usage is kept under that title. Verify the exact title on a dev store (README).
|
|
import db from "../db.server";
|
|
|
|
export interface OrderPayload {
|
|
id: number | string;
|
|
name?: string;
|
|
currency?: string;
|
|
total_price?: string;
|
|
discount_applications?: { type?: string; title?: string; description?: string | null }[];
|
|
line_items?: {
|
|
price?: string;
|
|
quantity?: number;
|
|
discount_allocations?: { amount?: string; discount_application_index?: number }[];
|
|
}[];
|
|
}
|
|
|
|
export async function recordOrder(
|
|
shopId: string,
|
|
order: OrderPayload,
|
|
offers: { id: string; name: string }[],
|
|
discountTitle: string,
|
|
) {
|
|
const orderId = String(order.id);
|
|
const apps = order.discount_applications || [];
|
|
const byName = new Map(offers.map((o) => [o.name, o.id]));
|
|
const ours = new Map<number, string[]>(); // application index -> offer names
|
|
apps.forEach((a, i) => {
|
|
const title = (a.title || a.description || "").trim();
|
|
if (!title) return;
|
|
const parts = title.split(" + ").map((s) => s.trim());
|
|
if (parts.every((p) => byName.has(p))) ours.set(i, parts);
|
|
else if (title === discountTitle && a.type !== "discount_code") ours.set(i, [discountTitle]);
|
|
});
|
|
|
|
const perOffer = new Map<string, { discount: number; revenue: number }>();
|
|
for (const li of order.line_items || []) {
|
|
const lineTotal = Number(li.price || 0) * (li.quantity || 0);
|
|
for (const al of li.discount_allocations || []) {
|
|
const names = ours.get(al.discount_application_index ?? -1);
|
|
if (!names) continue;
|
|
const amount = Number(al.amount || 0);
|
|
// a merged line ("A + B"): split evenly, we don't know the per-offer share
|
|
for (const n of names) {
|
|
const e = perOffer.get(n) || { discount: 0, revenue: 0 };
|
|
e.discount += amount / names.length;
|
|
e.revenue += lineTotal / names.length;
|
|
perOffer.set(n, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
const hasBundle = perOffer.size > 0;
|
|
const currency = order.currency || "USD";
|
|
const total = Number(order.total_price || 0);
|
|
const existing = await db.orderStat.findUnique({ where: { shopId_orderId: { shopId, orderId } } });
|
|
if (!existing) {
|
|
await db.orderStat.create({ data: { shopId, orderId, total, hasBundle, currency } });
|
|
}
|
|
for (const [name, v] of perOffer) {
|
|
await db.orderUsage.upsert({
|
|
where: { shopId_orderId_offerName: { shopId, orderId, offerName: name } },
|
|
create: {
|
|
shopId, orderId, orderName: order.name || null, offerId: byName.get(name) || null, offerName: name,
|
|
discount: Math.round(v.discount * 100) / 100, bundleRevenue: Math.round(v.revenue * 100) / 100, orderTotal: total, currency,
|
|
},
|
|
update: {},
|
|
});
|
|
}
|
|
return { hasBundle, isNew: !existing };
|
|
}
|