bundle-offers-app/app/lib/orders.server.ts
metatroncubeswdev ba7a7c89a2 Rename app to Offerlyst: Bundles & BOGO; add App Store icon and brand assets
Icon rebuilt as a full-bleed 1200x1200 square (Shopify rounds corners itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:03:32 -04:00

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
// (DISCOUNT_TITLE, "Offerlyst bundles"), 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 };
}