From da8be8c637ab7af3622336a2c17e64803e6d79c9 Mon Sep 17 00:00:00 2001 From: konouzdz19 Date: Tue, 8 Sep 2026 02:34:46 +0100 Subject: [PATCH 1/5] feat(tracking): 4-tier Meta conversion tracking with D1 idempotency protection --- cod-astro/theme01/src/core/api/types.ts | 1 + cod-astro/theme01/src/pages/thank-you.astro | 6 ++- cod-astro/theme01/src/theme/config/store.ts | 1 + cod-client-astro/locales/ar/settings.json | 10 ++-- cod-client-astro/locales/en/settings.json | 12 +++-- cod-client-astro/locales/fr/settings.json | 10 ++-- .../settings/components/TrackingSettings.tsx | 6 ++- .../src/features/settings/types.ts | 4 +- .../endpoints/orders/status-transitions.ts | 9 ++-- cod-server/src/endpoints/stores/handlers.ts | 2 +- cod-server/src/endpoints/stores/routes.ts | 2 +- cod-server/src/openapi/schemas/store.ts | 4 +- cod-server/src/workflows/capi-helpers.ts | 15 +++++- cod-server/src/workflows/capi.ts | 50 +++++++++++++++++-- cod-shared/db/schema.ts | 2 +- cod-shared/queries/pixel-config.ts | 2 +- 16 files changed, 106 insertions(+), 30 deletions(-) diff --git a/cod-astro/theme01/src/core/api/types.ts b/cod-astro/theme01/src/core/api/types.ts index 377ac36..a574d9d 100644 --- a/cod-astro/theme01/src/core/api/types.ts +++ b/cod-astro/theme01/src/core/api/types.ts @@ -36,6 +36,7 @@ export interface StoreConfig { otpEnabled: boolean; status: "active" | "inactive"; pixelId?: string | null; + conversionEvent?: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead" | null; } export interface ShippingRates { diff --git a/cod-astro/theme01/src/pages/thank-you.astro b/cod-astro/theme01/src/pages/thank-you.astro index 577ed63..4b4bc4d 100644 --- a/cod-astro/theme01/src/pages/thank-you.astro +++ b/cod-astro/theme01/src/pages/thank-you.astro @@ -17,6 +17,7 @@ if (!orderNumber) { } const pixelId = config.pixelId ?? null; +const conversionEvent = config.conversionEvent === "Purchase" ? "Purchase" : "Lead"; const totalNum = total ? parseInt(total) : null; const steps = [ @@ -113,11 +114,12 @@ const steps = [ (function() { if (typeof fbq !== 'function') return; var eventId = ${JSON.stringify(orderId)}; - // No orderId means no server Lead mirror to deduplicate against — firing + // No orderId means no server mirror to deduplicate against — firing // with a different id would double-count the conversion. Skip instead. if (!eventId) return; var total = ${JSON.stringify(totalNum)}; - fbq('track', 'Lead', total ? { value: total, currency: 'DZD' } : {}, { eventID: eventId }); + var eventName = ${JSON.stringify(conversionEvent)}; + fbq('track', eventName, total ? { value: total, currency: 'DZD' } : {}, { eventID: eventId }); })(); `} /> )} diff --git a/cod-astro/theme01/src/theme/config/store.ts b/cod-astro/theme01/src/theme/config/store.ts index 4d5d61f..0ab8198 100644 --- a/cod-astro/theme01/src/theme/config/store.ts +++ b/cod-astro/theme01/src/theme/config/store.ts @@ -36,6 +36,7 @@ export const DEFAULT_CONFIG: StoreConfig = { otpEnabled: false, status: "active", pixelId: null, + conversionEvent: null, }; export interface StoreContext { diff --git a/cod-client-astro/locales/ar/settings.json b/cod-client-astro/locales/ar/settings.json index beea17f..8de7903 100644 --- a/cod-client-astro/locales/ar/settings.json +++ b/cod-client-astro/locales/ar/settings.json @@ -55,10 +55,14 @@ "tracking_ad_account_hint": "تسمية خاصة بك لحساب الإعلانات على Meta — للمرجعية فقط", "tracking_ad_account_placeholder": "مثال: متجري — الحساب الرئيسي", "tracking_event_label": "حدث التحويل", + "tracking_event_purchase_instant_label": "Purchase فوري — عند تسجيل الطلب (موصى به لإعلانات فيسبوك)", + "tracking_event_purchase_instant_hint": "يُطلق حدث الشراء فوراً عند إتمام الطلب مع المبلغ ومعرف إلغاء الازدواجية. ضروري لتحسين خوارزمية إعلانات Meta في الوقت الفعلي.", + "tracking_event_purchase_confirmed_label": "Purchase — عند التأكيد الهاتفي (مؤكد)", + "tracking_event_purchase_confirmed_hint": "يُطلق حدث الشراء فقط عندما يؤكد فريقك الطلب هاتفياً مع العميل. يفلتر 100% من الطلبات الوهمية مع البقاء ضمن نافذة التعلم لـ Meta (خلال 24 ساعة).", + "tracking_event_purchase_delivered_label": "Purchase — عند الاستلام الفعلي والدفع (مُسلّم) [نموذج Big Hadj]", + "tracking_event_purchase_delivered_hint": "يُطلق حدث الشراء فقط عند استلام الزبون للطلب ودفع قيمته لمندوب التوصيل. محاسبة مالية دقيقة وصافية (لا تُحتسب المرتجعات).", "tracking_event_lead_label": "Lead — عند تسجيل الطلب", - "tracking_event_lead_hint": "يُطلق لحظة تقديم الطلب. إشارة أسرع لإعلاناتك، لكن الطلبات غير المؤكدة تُحتسب ضمنه.", - "tracking_event_purchase_label": "Purchase — عند التسليم المؤكد", - "tracking_event_purchase_hint": "يُطلق فقط عند تسليم الطلب ودفع قيمته. إشارة أبطأ، لكنها تطابق الإيرادات الحقيقية.", + "tracking_event_lead_hint": "يُطلق حدث Lead فور تسجيل الطلب. اختر هذا الخيار إذا كانت حملتك الإعلانية على Meta محسّنة لجمع بيانات العملاء (Leads).", "tracking_test_mode_label": "الوضع التجريبي", "tracking_test_mode_hint": "يرسل أحداث API التحويلات إلى مسار الاختبار لدى Meta بدلاً من قياس الإنتاج", "tracking_test_mode_warning": "الوضع التجريبي مفعّل — الأحداث لا تُحتسب في القياس الفعلي. عطّله قبل الانطلاق.", diff --git a/cod-client-astro/locales/en/settings.json b/cod-client-astro/locales/en/settings.json index d3ad8d5..808f1e7 100644 --- a/cod-client-astro/locales/en/settings.json +++ b/cod-client-astro/locales/en/settings.json @@ -55,10 +55,14 @@ "tracking_ad_account_hint": "Your own label for the Meta ad account — for reference only", "tracking_ad_account_placeholder": "e.g. My Store — Main Account", "tracking_event_label": "Conversion Event", - "tracking_event_lead_label": "Lead — at order placement", - "tracking_event_lead_hint": "Fires the moment an order is placed. Faster signal for your ads, but unconfirmed orders are included.", - "tracking_event_purchase_label": "Purchase — at confirmed delivery", - "tracking_event_purchase_hint": "Fires only when the order is delivered and paid. Slower signal, but matches real revenue.", + "tracking_event_purchase_instant_label": "Instant Purchase — at checkout (Recommended for Meta Ads)", + "tracking_event_purchase_instant_hint": "Fires Purchase immediately upon checkout with order total and deduplication ID. Essential for Facebook Ads real-time conversion optimization.", + "tracking_event_purchase_confirmed_label": "Purchase — on phone confirmation (confirmed)", + "tracking_event_purchase_confirmed_hint": "Fires Purchase only when your call center confirms the order with the customer. Filters out 100% of fake leads while staying within Meta's 24h attribution window.", + "tracking_event_purchase_delivered_label": "Purchase — on delivery (delivered) [Big Hadj COD]", + "tracking_event_purchase_delivered_hint": "Fires Purchase only when the package is delivered and cash is collected. Strict net accounting (zero returns counted).", + "tracking_event_lead_label": "Lead — at checkout", + "tracking_event_lead_hint": "Fires a Lead event immediately upon order placement. Select this if your Meta ad campaign optimizes for Leads instead of Purchases.", "tracking_test_mode_label": "Test Mode", "tracking_test_mode_hint": "Sends Conversions API events to Meta's test stream instead of production measurement", "tracking_test_mode_warning": "Test mode is on — events are not counted for real measurement. Turn it off before going live.", diff --git a/cod-client-astro/locales/fr/settings.json b/cod-client-astro/locales/fr/settings.json index 443e004..36731fe 100644 --- a/cod-client-astro/locales/fr/settings.json +++ b/cod-client-astro/locales/fr/settings.json @@ -55,10 +55,14 @@ "tracking_ad_account_hint": "Votre propre libellé pour le compte publicitaire Meta — à titre de référence", "tracking_ad_account_placeholder": "ex: Ma Boutique — Compte principal", "tracking_event_label": "Événement de conversion", + "tracking_event_purchase_instant_label": "Purchase instantané — dès la commande (Recommandé pour Meta Ads)", + "tracking_event_purchase_instant_hint": "Déclenche l'événement Purchase dès la commande avec le montant du panier et l'identifiant dédoublonné. Essentiel pour l'optimisation publicitaire en temps réel.", + "tracking_event_purchase_confirmed_label": "Purchase — à la confirmation téléphonique (confirmé)", + "tracking_event_purchase_confirmed_hint": "Déclenche l'événement Purchase uniquement lorsque votre équipe confirme la commande par téléphone. Élimine 100% des faux numéros tout en restant dans la fenêtre de 24h de Meta.", + "tracking_event_purchase_delivered_label": "Purchase — à la livraison payée (livré) [Modèle COD Big Hadj]", + "tracking_event_purchase_delivered_hint": "Déclenche l'événement Purchase uniquement lorsque le colis est livré et payé. Comptabilité nette stricte (aucun retour comptabilisé).", "tracking_event_lead_label": "Lead — dès la commande", - "tracking_event_lead_hint": "Déclenché dès qu'une commande est passée. Signal plus rapide pour vos publicités, mais les commandes non confirmées sont incluses.", - "tracking_event_purchase_label": "Purchase — à la livraison confirmée", - "tracking_event_purchase_hint": "Déclenché uniquement lorsque la commande est livrée et payée. Signal plus lent, mais conforme au revenu réel.", + "tracking_event_lead_hint": "Déclenche l'événement Lead dès la commande. Choisissez cette option si votre campagne Meta Ads optimise pour les Prospects (Leads).", "tracking_test_mode_label": "Mode test", "tracking_test_mode_hint": "Envoie les événements de l'API Conversions vers le flux de test de Meta au lieu de la production", "tracking_test_mode_warning": "Le mode test est activé — les événements ne comptent pas pour la mesure réelle. Désactivez-le avant de lancer.", diff --git a/cod-client-astro/src/features/settings/components/TrackingSettings.tsx b/cod-client-astro/src/features/settings/components/TrackingSettings.tsx index b6c057f..4706f07 100644 --- a/cod-client-astro/src/features/settings/components/TrackingSettings.tsx +++ b/cod-client-astro/src/features/settings/components/TrackingSettings.tsx @@ -5,11 +5,13 @@ import { useT } from "@/i18n/react"; import { getPixelConfig, savePixelConfig } from "@/features/settings/api"; import { FieldRow, SettingsSection } from "@/features/settings/components/SettingsSection"; -type ConversionEvent = "Lead" | "Purchase"; +type ConversionEvent = "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; const EVENT_OPTIONS: { value: ConversionEvent; labelKey: string; hintKey: string }[] = [ + { value: "Purchase", labelKey: "store.tracking_event_purchase_instant_label", hintKey: "store.tracking_event_purchase_instant_hint" }, + { value: "Purchase_Confirmed", labelKey: "store.tracking_event_purchase_confirmed_label", hintKey: "store.tracking_event_purchase_confirmed_hint" }, + { value: "Purchase_Delivered", labelKey: "store.tracking_event_purchase_delivered_label", hintKey: "store.tracking_event_purchase_delivered_hint" }, { value: "Lead", labelKey: "store.tracking_event_lead_label", hintKey: "store.tracking_event_lead_hint" }, - { value: "Purchase", labelKey: "store.tracking_event_purchase_label", hintKey: "store.tracking_event_purchase_hint" }, ]; export function TrackingSettings() { diff --git a/cod-client-astro/src/features/settings/types.ts b/cod-client-astro/src/features/settings/types.ts index 7213275..40d6091 100644 --- a/cod-client-astro/src/features/settings/types.ts +++ b/cod-client-astro/src/features/settings/types.ts @@ -57,7 +57,7 @@ export interface PixelConfig { adAccountName: string | null; accessTokenMasked: string; testEventCode: string | null; - conversionEvent: "Lead" | "Purchase"; + conversionEvent: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; testMode: boolean; enabled: boolean; createdAt: string; @@ -69,7 +69,7 @@ export interface SavePixelConfigData { adAccountName?: string | null; accessToken?: string; testEventCode?: string | null; - conversionEvent: "Lead" | "Purchase"; + conversionEvent: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; testMode?: boolean; enabled?: boolean; } diff --git a/cod-server/src/endpoints/orders/status-transitions.ts b/cod-server/src/endpoints/orders/status-transitions.ts index 2c228cb..c64cb18 100644 --- a/cod-server/src/endpoints/orders/status-transitions.ts +++ b/cod-server/src/endpoints/orders/status-transitions.ts @@ -14,7 +14,7 @@ import * as validation from "./validation"; import { logActivity, ACTIONS } from "@/lib/activity"; import { NotFoundError, BusinessLogicError, ValidationError } from "@/lib/errors/classes"; import { ERROR_CODES, ERROR_CATEGORIES } from "../../../../cod-shared/errors/codes"; -import { shouldTriggerCapiPurchase } from "@/workflows/capi-helpers"; +import { shouldTriggerCapiPurchase, shouldTriggerCapiConfirmed } from "@/workflows/capi-helpers"; /** * PATCH /orders/:id/status @@ -78,7 +78,10 @@ export async function updateStatus(c: Context) { // Fire CAPI Purchase Workflow — never blocks the status response. // waitUntil: the runtime cancels un-awaited promises after the response, // which would silently drop the workflow creation. - if (shouldTriggerCapiPurchase(validated.status, order.wilayaId)) { + const isDeliveredTrigger = shouldTriggerCapiPurchase(validated.status, order.wilayaId); + const isConfirmedTrigger = shouldTriggerCapiConfirmed(validated.status); + + if (isDeliveredTrigger || isConfirmedTrigger) { if (!c.env.CAPI_WORKFLOW) { // Binding absent — worker was provisioned before CAPI_WORKFLOW was added. // Re-provision the client to activate the binding. @@ -86,7 +89,7 @@ export async function updateStatus(c: Context) { } else { c.executionCtx.waitUntil( c.env.CAPI_WORKFLOW.create({ - id: `capi-${orderId}-Purchase`, + id: `capi-${orderId}-Purchase-${validated.status}`, params: { orderId, eventName: "Purchase", diff --git a/cod-server/src/endpoints/stores/handlers.ts b/cod-server/src/endpoints/stores/handlers.ts index 36f3b72..d5d2a73 100644 --- a/cod-server/src/endpoints/stores/handlers.ts +++ b/cod-server/src/endpoints/stores/handlers.ts @@ -45,7 +45,7 @@ const pixelConfigSchema = z.object({ adAccountName: z.string().max(200).nullable().optional(), accessToken: z.string().default(""), testEventCode: z.string().nullable().optional(), - conversionEvent: z.enum(["Lead", "Purchase"]), + conversionEvent: z.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"]), testMode: z.boolean().optional(), enabled: z.boolean().optional(), }); diff --git a/cod-server/src/endpoints/stores/routes.ts b/cod-server/src/endpoints/stores/routes.ts index 4b04c6f..4cd10ef 100644 --- a/cod-server/src/endpoints/stores/routes.ts +++ b/cod-server/src/endpoints/stores/routes.ts @@ -55,7 +55,7 @@ const savePixelBodySchema = z.object({ "Meta access token. Empty string keeps the previously stored token (the token is never sent back to the client).", }), testEventCode: z.string().nullable().optional(), - conversionEvent: z.enum(["Lead", "Purchase"]), + conversionEvent: z.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"]), testMode: z.boolean().optional(), enabled: z.boolean().optional(), }); diff --git a/cod-server/src/openapi/schemas/store.ts b/cod-server/src/openapi/schemas/store.ts index 0c43ac3..c3cec52 100644 --- a/cod-server/src/openapi/schemas/store.ts +++ b/cod-server/src/openapi/schemas/store.ts @@ -70,9 +70,9 @@ export const StorePixelConfigSchema = z testEventCode: z.string().nullable().openapi({ description: "Meta test event code — used only while Test Mode is on. Set to null in production.", }), - conversionEvent: z.enum(["Lead", "Purchase"]).openapi({ + conversionEvent: z.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"]).openapi({ description: - "Merchant-chosen conversion event: 'Lead' fires at order placement (deduplicated with the browser pixel), 'Purchase' fires at confirmed delivery.", + "Merchant-chosen conversion event: 'Purchase' fires immediately at checkout, 'Purchase_Confirmed' fires on order confirmation, 'Purchase_Delivered' fires on confirmed delivery, and 'Lead' fires at checkout.", }), testMode: z.boolean().openapi({ description: "When true, Conversions API events carry test_event_code to Meta's test stream.", diff --git a/cod-server/src/workflows/capi-helpers.ts b/cod-server/src/workflows/capi-helpers.ts index 5ea93a7..cd7bb10 100644 --- a/cod-server/src/workflows/capi-helpers.ts +++ b/cod-server/src/workflows/capi-helpers.ts @@ -13,6 +13,10 @@ const LONG_HAUL_WILAYA_IDS = new Set([ * Determines whether to trigger CodCapiWorkflow for a given status transition. * Call this after updateOrderStatus() resolves. */ +export function shouldTriggerCapiConfirmed(newStatus: string): boolean { + return newStatus === "confirmed"; +} + export function shouldTriggerCapiPurchase( newStatus: string, wilayaId: number | null | undefined, @@ -25,7 +29,7 @@ export function shouldTriggerCapiPurchase( export interface CapiDispatchConfig { enabled: boolean; accessToken: string; - conversionEvent: "Lead" | "Purchase"; + conversionEvent: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; testMode: boolean; testEventCode: string | null; } @@ -59,7 +63,14 @@ export function resolveCapiDispatch( message: "No CAPI access token — configure it in Settings → Tracking", }; } - if (config.conversionEvent !== eventName) { + const allowed = + (eventName === "Purchase" && + (config.conversionEvent === "Purchase" || + config.conversionEvent === "Purchase_Confirmed" || + config.conversionEvent === "Purchase_Delivered")) || + (eventName === "Lead" && config.conversionEvent === "Lead"); + + if (!allowed) { return { send: false, reason: "conversion-event-mismatch", diff --git a/cod-server/src/workflows/capi.ts b/cod-server/src/workflows/capi.ts index 7cd2910..8814731 100644 --- a/cod-server/src/workflows/capi.ts +++ b/cod-server/src/workflows/capi.ts @@ -22,8 +22,8 @@ import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloud import { NonRetryableError } from "cloudflare:workflows"; import type { Env } from "@/types/env"; import { getDb } from "@/db"; -import { orders, communes, orderProducts, stores } from "@/db/schema"; -import { eq } from "drizzle-orm"; +import { orders, communes, orderProducts, stores, capiEventLog } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; import { getPixelConfig } from "../../../cod-shared/queries/pixel-config"; import { sendCapiEvent, type CapiResult } from "@/lib/capi"; import { resolveCapiDispatch } from "./capi-helpers"; @@ -50,7 +50,7 @@ function splitName(customerName: string): { firstName?: string; lastName?: strin export class CodCapiWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { - const { orderId, eventName, triggeredAt, eventSourceUrl } = event.payload; + const { orderId, eventName, triggeredAt, eventSourceUrl, triggerStatus } = event.payload; // Step 1 — fetch fresh data from D1 (never rely on stale params) const data = await step.do("fetch-order-and-config", async () => { @@ -95,15 +95,59 @@ export class CodCapiWorkflow extends WorkflowEntrypoint { const pixelConfig = await getPixelConfig(db, storeRow.id); + let alreadySent = false; + let triggerMismatch = false; + + if (eventName === "Purchase") { + const configMode = pixelConfig?.conversionEvent ?? "Purchase"; + if (configMode === "Purchase" && triggerStatus !== "order_created") { + triggerMismatch = true; + } else if (configMode === "Purchase_Confirmed" && triggerStatus !== "confirmed") { + triggerMismatch = true; + } else if ( + configMode === "Purchase_Delivered" && + triggerStatus !== "delivered" && + triggerStatus !== "out_for_delivery" + ) { + triggerMismatch = true; + } + + const existingPurchase = await db + .select({ id: capiEventLog.id }) + .from(capiEventLog) + .where( + and( + eq(capiEventLog.orderId, orderId), + eq(capiEventLog.eventName, "Purchase"), + eq(capiEventLog.status, "sent") + ) + ) + .get(); + + if (existingPurchase) { + alreadySent = true; + } + } + return { order, cityName: communeRow?.name ?? null, postalCode: communeRow?.postalCode ?? null, contentIds: [...new Set(productRows.map((r) => r.productId))], pixelConfig, + alreadySent, + triggerMismatch, }; }); + if (data.triggerMismatch) { + return { skipped: true, reason: "trigger_mismatch" }; + } + + if (data.alreadySent) { + return { skipped: true, reason: "purchase_already_sent" }; + } + // Step 2 — gate: merchant must have chosen this event, with a token, tracking on const dispatch = resolveCapiDispatch(data.pixelConfig, eventName); if (!dispatch.send) { diff --git a/cod-shared/db/schema.ts b/cod-shared/db/schema.ts index b484607..a6233b0 100644 --- a/cod-shared/db/schema.ts +++ b/cod-shared/db/schema.ts @@ -1206,7 +1206,7 @@ export const storePixelConfig = sqliteTable("store_pixel_config", { /** Meta test event code — used during integration testing only. Set to null in production. */ testEventCode: text("test_event_code"), /** Which CAPI event the merchant optimizes for — chosen explicitly in the dashboard, never defaulted by the UI. */ - conversionEvent: text("conversion_event", { enum: ["Lead", "Purchase"] }).notNull().default("Purchase"), + conversionEvent: text("conversion_event", { enum: ["Lead", "Purchase", "Purchase_Confirmed", "Purchase_Delivered"] }).notNull().default("Purchase"), /** When true, CAPI events carry test_event_code to Meta's test stream instead of production measurement. */ testMode: integer("test_mode", { mode: "boolean" }).notNull().default(false), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), diff --git a/cod-shared/queries/pixel-config.ts b/cod-shared/queries/pixel-config.ts index 2d6fec1..7b8f42c 100644 --- a/cod-shared/queries/pixel-config.ts +++ b/cod-shared/queries/pixel-config.ts @@ -2,7 +2,7 @@ import type { AppDb } from "../db/client"; import { storePixelConfig } from "../db/schema"; import { eq } from "drizzle-orm"; -export type ConversionEvent = "Lead" | "Purchase"; +export type ConversionEvent = "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; export async function getPixelConfig(db: AppDb, storeId: string) { return db From 68cb2b236ea2302c83ccd32abede05964692abdf Mon Sep 17 00:00:00 2001 From: konouzdz19 Date: Tue, 8 Sep 2026 03:34:34 +0100 Subject: [PATCH 2/5] fix(pixel): make storeId optional in getPixelConfig --- cod-shared/queries/pixel-config.ts | 11 +++++++++-- package-lock.json | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cod-shared/queries/pixel-config.ts b/cod-shared/queries/pixel-config.ts index 7b8f42c..a519724 100644 --- a/cod-shared/queries/pixel-config.ts +++ b/cod-shared/queries/pixel-config.ts @@ -4,11 +4,18 @@ import { eq } from "drizzle-orm"; export type ConversionEvent = "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead"; -export async function getPixelConfig(db: AppDb, storeId: string) { +export async function getPixelConfig(db: AppDb, storeId?: string) { + if (storeId) { + return db + .select() + .from(storePixelConfig) + .where(eq(storePixelConfig.storeId, storeId)) + .get(); + } return db .select() .from(storePixelConfig) - .where(eq(storePixelConfig.storeId, storeId)) + .limit(1) .get(); } diff --git a/package-lock.json b/package-lock.json index 1041ed1..50a20bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -636,6 +636,7 @@ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" From b7c9da47d8756ae40b038addc4909196115c8d42 Mon Sep 17 00:00:00 2001 From: konouzdz19 Date: Tue, 8 Sep 2026 22:41:28 +0100 Subject: [PATCH 3/5] feat(tracking): address upstream review improvements for PR #103 - Introduce shared canonical conversion model & resolver (resolveConversionForStage) - Decouple merchant phone confirmation from carrier delivery logistics - Add D1 atomic idempotency claim (0022_capi_event_claim.sql) on (order_id, stage, event_name) - Implement redundant browser Pixel + CAPI at checkout with matching event ID (order.id) - Expose conversionEvent in store context (getStoreConfig) - Standardize CAPI workflow IDs (getCapiWorkflowId) across checkout, transitions, and webhooks - Align delivery mode descriptions across en/fr/ar locales - Add Zod runtime payload validation with NonRetryableError and 7-day cutoff guard in CAPI workflow - Fix test suite mocks and Vitest Cloudflare runtime stubs - Revert unrelated package-lock.json modification --- cod-astro/theme01/src/pages/thank-you.astro | 14 +- cod-client-astro/locales/ar/settings.json | 4 +- cod-client-astro/locales/en/settings.json | 4 +- cod-client-astro/locales/fr/settings.json | 4 +- .../db/migrations/0022_capi_event_claim.sql | 5 + .../src/endpoints/orders/orders.test.ts | 3 + .../src/endpoints/orders/routes.test.ts | 3 + .../endpoints/orders/status-transitions.ts | 7 +- .../endpoints/store/capi-lead-trigger.test.ts | 11 +- cod-server/src/endpoints/store/handlers.ts | 78 ++++-- .../src/endpoints/store/otp-gate.test.ts | 3 + cod-server/src/endpoints/webhooks/handlers.ts | 22 +- .../src/endpoints/webhooks/webhooks.test.ts | 3 + .../webhooks/yalidine-webhook.e2e.test.ts | 3 + cod-server/src/lib/capi-log.ts | 4 +- .../src/test-utils/cloudflare-workers.ts | 25 +- .../src/test-utils/cloudflare-workflows.ts | 9 + cod-server/src/workflows/capi-helpers.test.ts | 59 ++++- cod-server/src/workflows/capi-helpers.ts | 56 +++-- cod-server/src/workflows/capi.test.ts | 122 ++++++++++ cod-server/src/workflows/capi.ts | 227 +++++++++++------- .../src/workflows/conversion-model.test.ts | 141 +++++++++++ cod-server/src/workflows/conversion-model.ts | 90 +++++++ cod-server/vitest.config.ts | 1 + cod-shared/db/schema.ts | 30 ++- cod-shared/queries/store.ts | 7 +- package-lock.json | 1 - 27 files changed, 773 insertions(+), 163 deletions(-) create mode 100644 cod-server/src/db/migrations/0022_capi_event_claim.sql create mode 100644 cod-server/src/test-utils/cloudflare-workflows.ts create mode 100644 cod-server/src/workflows/capi.test.ts create mode 100644 cod-server/src/workflows/conversion-model.test.ts create mode 100644 cod-server/src/workflows/conversion-model.ts diff --git a/cod-astro/theme01/src/pages/thank-you.astro b/cod-astro/theme01/src/pages/thank-you.astro index 4b4bc4d..9b6088a 100644 --- a/cod-astro/theme01/src/pages/thank-you.astro +++ b/cod-astro/theme01/src/pages/thank-you.astro @@ -17,7 +17,15 @@ if (!orderNumber) { } const pixelId = config.pixelId ?? null; -const conversionEvent = config.conversionEvent === "Purchase" ? "Purchase" : "Lead"; +// At checkout stage, only "Purchase" (instant mode) and "Lead" (lead mode) fire on the thank-you page. +// "Purchase_Confirmed" and "Purchase_Delivered" deliberately fire downstream via server CAPI only. +const rawMode = config.conversionEvent ?? "Purchase"; +const checkoutEvent = + rawMode === "Purchase" + ? "Purchase" + : rawMode === "Lead" + ? "Lead" + : null; const totalNum = total ? parseInt(total) : null; const steps = [ @@ -109,7 +117,7 @@ const steps = [ -{pixelId && ( +{pixelId && checkoutEvent && (