bundle-offers-app/app/lib/templates.ts
metatroncubeswdev 365e45406a Bundle Offers Shopify app: discount function, admin, storefront, deploy stack
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>
2026-10-13 21:35:58 -04:00

88 lines
6.0 KiB
TypeScript

// Festival templates and calendar (N03). Lunar-calendar dates are approximate: the UI asks the merchant to
// check them against their local calendar before publishing.
export interface Festival {
key: string;
name: string;
/** main day, YYYY-MM-DD */
date: string;
approximate: boolean;
/** days the offer starts before / ends after the main day */
leadDays: number;
tailDays: number;
}
export const FESTIVALS: Festival[] = [
{ key: "navaratri-2026", name: "Navaratri", date: "2026-10-11", approximate: true, leadDays: 7, tailDays: 9 },
{ key: "diwali-2026", name: "Diwali", date: "2026-11-08", approximate: true, leadDays: 21, tailDays: 1 },
{ key: "black-friday-2026", name: "Black Friday", date: "2026-11-27", approximate: false, leadDays: 0, tailDays: 3 },
{ key: "christmas-2026", name: "Christmas", date: "2026-12-25", approximate: false, leadDays: 21, tailDays: 1 },
{ key: "pongal-2027", name: "Pongal / Makar Sankranti", date: "2027-01-14", approximate: true, leadDays: 10, tailDays: 3 },
{ key: "lunar-new-year-2027", name: "Lunar New Year", date: "2027-02-06", approximate: true, leadDays: 14, tailDays: 7 },
{ key: "eid-al-fitr-2027", name: "Eid al-Fitr", date: "2027-03-10", approximate: true, leadDays: 14, tailDays: 3 },
{ key: "holi-2027", name: "Holi", date: "2027-03-22", approximate: true, leadDays: 10, tailDays: 1 },
{ key: "eid-al-adha-2027", name: "Eid al-Adha", date: "2027-05-17", approximate: true, leadDays: 10, tailDays: 3 },
{ key: "navaratri-2027", name: "Navaratri", date: "2027-09-30", approximate: true, leadDays: 7, tailDays: 9 },
{ key: "diwali-2027", name: "Diwali", date: "2027-10-29", approximate: true, leadDays: 21, tailDays: 1 },
];
export interface OfferTemplate {
key: string;
festival: string; // Festival.name or "Everyday"
name: string;
badge: string;
description: string;
type: "fixed" | "mix" | "bxgy";
discountType: "percent" | "amount" | "fixed_price";
discountValue: number;
mixQty?: number;
buyQty?: number;
getQty?: number;
getPercent?: number;
hint: string;
}
export const TEMPLATES: OfferTemplate[] = [
{ key: "diwali-puja-kit", festival: "Diwali", name: "Diwali Puja Kit", badge: "Diwali Special", type: "fixed", discountType: "percent", discountValue: 15,
description: "Everything you need for Lakshmi Puja, in one bundle.", hint: "Add the diya, camphor, kumkum and incense products with their quantities." },
{ key: "diwali-sweets", festival: "Diwali", name: "Any 3 Diwali Sweets", badge: "Festival offer", type: "mix", discountType: "fixed_price", discountValue: 20, mixQty: 3,
description: "Mix and match your favourite mithai.", hint: "Add your sweets collection." },
{ key: "navaratri-golu", festival: "Navaratri", name: "Navaratri Golu Set", badge: "Navaratri", type: "fixed", discountType: "amount", discountValue: 10,
description: "A ready-made golu display set.", hint: "Add the dolls and steps that make up the set." },
{ key: "garlands-b2g1", festival: "Navaratri", name: "Buy 2 Garlands, Get 1 Free", badge: "Festival offer", type: "bxgy", discountType: "percent", discountValue: 0, buyQty: 2, getQty: 1, getPercent: 100,
description: "Fresh garlands for every day of the festival.", hint: "Use the same garland collection for Buy and Get." },
{ key: "pongal-essentials", festival: "Pongal / Makar Sankranti", name: "Pongal Essentials", badge: "Pongal", type: "fixed", discountType: "percent", discountValue: 10,
description: "Rice, jaggery, ghee and more for Pongal.", hint: "Add the essentials and quantities." },
{ key: "eid-gift-box", festival: "Eid al-Fitr", name: "Eid Gift Box", badge: "Eid Mubarak", type: "mix", discountType: "percent", discountValue: 15, mixQty: 4,
description: "Pick any 4 treats for a gift box.", hint: "Add dates, sweets and dry fruit collections." },
{ key: "holi-colours", festival: "Holi", name: "Holi Colours Pack", badge: "Happy Holi", type: "mix", discountType: "fixed_price", discountValue: 15, mixQty: 5,
description: "Any 5 colour packs.", hint: "Add your gulal / colour collection." },
{ key: "lny-gift", festival: "Lunar New Year", name: "Lunar New Year Gift Set", badge: "New Year", type: "fixed", discountType: "percent", discountValue: 12,
description: "A festive gift set.", hint: "Add the products of the gift set." },
{ key: "christmas-hamper", festival: "Christmas", name: "Christmas Hamper", badge: "Christmas", type: "fixed", discountType: "fixed_price", discountValue: 49,
description: "A hamper of seasonal favourites.", hint: "Add the hamper contents." },
{ key: "black-friday-bogo", festival: "Black Friday", name: "Black Friday: Buy 1 Get 1 50% Off", badge: "Black Friday", type: "bxgy", discountType: "percent", discountValue: 0, buyQty: 1, getQty: 1, getPercent: 50,
description: "Our biggest deal of the year.", hint: "Pick the collections that are part of the sale." },
{ key: "everyday-mix", festival: "Everyday", name: "Any 3 for a bundle price", badge: "Bundle & Save", type: "mix", discountType: "fixed_price", discountValue: 25, mixQty: 3,
description: "Mix and match any 3.", hint: "Add the products or collections that count." },
];
export function festivalByName(name: string) {
const today = new Date().toISOString().slice(0, 10);
return FESTIVALS.filter((f) => f.name === name && f.date >= today).sort((a, b) => a.date.localeCompare(b.date))[0];
}
/** Offer dates for a festival, as shop-local "YYYY-MM-DDTHH:mm" strings. */
export function festivalWindow(f: Festival) {
const day = new Date(`${f.date}T00:00:00Z`);
const start = new Date(day.getTime() - f.leadDays * 86400000);
const end = new Date(day.getTime() + f.tailDays * 86400000);
return { start: `${start.toISOString().slice(0, 10)}T00:00`, end: `${end.toISOString().slice(0, 10)}T23:59` };
}
export function upcomingFestivals(now = new Date(), days = 365) {
const today = now.toISOString().slice(0, 10);
const until = new Date(now.getTime() + days * 86400000).toISOString().slice(0, 10);
return FESTIVALS.filter((f) => f.date >= today && f.date <= until).sort((a, b) => a.date.localeCompare(b.date));
}