import { selectButtonText } from "../lib/locale"; import type { ButtonTextMap } from "../lib/locale"; export type ActivityRequest = { shop: string; orderId: string; orderNumber?: string; locale?: string; }; export type ActivityConfig = { activityId: number; apiAppId: string; apiSecret: string; statusApiUrl: string; pushApiUrl: string; statusSuccessCode?: number; statusActiveRequired?: boolean; pushSuccessCode?: number; redirectUrl: string; buttonText: ButtonTextMap; shop?: string; }; export type OrderDetails = { id: string; legacyId: string; orderNumber?: string; email?: string; customerId?: string; currency: string; shopDomain: string; }; export type StatusResponse = { code?: number; data?: { status?: boolean; trigger?: string[]; }; msg?: string; }; export type PushResponse = { code?: number; data?: unknown; msg?: string; }; export type ThirdPartyResult = { httpOk: boolean; status: number; body: TBody; requestBody: string; responseBody: string; }; export type PushPayload = { activity_id: number; event_type: "order_end"; created_at: number; user_id: string; email: string; event_id: string; properties: { shop: string; currency: string; order_id: string; order_number: string; }; }; export type LogEntry = { shop: string; orderId: string; customerId?: string; email?: string; activityId: string; eventType: string; eventId: string; status: "success" | "fail"; requestBody?: string; responseBody?: string; errorMessage?: string; }; export type IdempotencyHit = { eventId: string; redirectUrl: string; }; export type ActivityDeps = { nowMs: () => number; uuid: () => string; getOrder: (shop: string, orderId: string) => Promise; getConfig: (shop: string) => Promise; findSuccess: (shop: string, orderId: string, activityId: number) => Promise; recordSuccess: (entry: { shop: string; orderId: string; activityId: number; eventType: "order_end"; eventId: string; redirectUrl: string; buttonText: string; }) => Promise; log: (entry: LogEntry) => Promise; callStatus: (config: ActivityConfig) => Promise>; callPush: (config: ActivityConfig, payload: PushPayload) => Promise>; }; export type ActivityResponse = | { success: true; redirectUrl: string; buttonText: string; } | { success: false; }; const ORDER_END_EVENT = "order_end" as const; export async function runActivityCheck(request: ActivityRequest, deps: ActivityDeps): Promise { const normalizedOrderId = normalizeOrderId(request.orderId); let order: OrderDetails | undefined; let config: ActivityConfig | undefined; let eventId = ""; try { order = await deps.getOrder(request.shop, normalizedOrderId); config = await deps.getConfig(request.shop); const buttonText = selectButtonText(request.locale, config.buttonText); const idempotency = await deps.findSuccess(request.shop, normalizedOrderId, config.activityId); if (idempotency) { return { success: true, redirectUrl: config.redirectUrl, buttonText, }; } if (!order.email) { await deps.log(failureLog(request.shop, order, config, eventId, "Order email is missing")); return { success: false }; } if (!order.customerId) { await deps.log(failureLog(request.shop, order, config, eventId, "Order customer id is missing")); return { success: false }; } const statusResult = await deps.callStatus(config); if (!isActiveStatus(statusResult, config)) { await deps.log( failureLog( request.shop, order, config, eventId, "Activity status API did not return active status", statusResult.requestBody, statusResult.responseBody, ), ); return { success: false }; } eventId = deps.uuid(); const pushPayload: PushPayload = { activity_id: config.activityId, event_type: ORDER_END_EVENT, created_at: deps.nowMs(), user_id: order.customerId, email: order.email, event_id: eventId, properties: { shop: config.shop || order.shopDomain, currency: order.currency, order_id: order.legacyId, order_number: order.orderNumber || request.orderNumber || order.legacyId, }, }; const pushResult = await deps.callPush(config, pushPayload); if (!isSuccessfulPush(pushResult, config)) { await deps.log( failureLog( request.shop, order, config, eventId, "Activity push API failed", pushResult.requestBody, pushResult.responseBody, ), ); return { success: false }; } await deps.recordSuccess({ shop: request.shop, orderId: normalizedOrderId, activityId: config.activityId, eventType: ORDER_END_EVENT, eventId, redirectUrl: config.redirectUrl, buttonText, }); await deps.log({ shop: request.shop, orderId: normalizedOrderId, customerId: order.customerId, email: order.email, activityId: config.activityId.toString(), eventType: ORDER_END_EVENT, eventId, status: "success", requestBody: pushResult.requestBody, responseBody: pushResult.responseBody, }); return { success: true, redirectUrl: config.redirectUrl, buttonText, }; } catch (error) { await safeLog(deps, failureLog(request.shop, order, config, eventId, toErrorMessage(error))); return { success: false }; } } export function normalizeOrderId(orderId: string): string { if (orderId.startsWith("gid://shopify/Order/")) { return orderId; } return `gid://shopify/Order/${orderId.replace(/[^0-9]/g, "")}`; } export function extractLegacyId(orderId: string): string { return orderId.split("/").pop() || orderId; } function isActiveStatus(result: ThirdPartyResult, config: ActivityConfig): boolean { if (!result.httpOk || result.body.code !== (config.statusSuccessCode ?? 0)) { return false; } if (config.statusActiveRequired === false) { return true; } return result.body.data?.status === true; } function isSuccessfulPush(result: ThirdPartyResult, config: ActivityConfig): boolean { return result.httpOk && result.body.code === (config.pushSuccessCode ?? 0); } function failureLog( shop: string, order: OrderDetails | undefined, config: ActivityConfig | undefined, eventId: string, errorMessage: string, requestBody?: string, responseBody?: string, ): LogEntry { return { shop, orderId: order?.id || "", customerId: order?.customerId, email: order?.email, activityId: config?.activityId.toString() || "", eventType: ORDER_END_EVENT, eventId, status: "fail", requestBody, responseBody, errorMessage, }; } async function safeLog(deps: ActivityDeps, entry: LogEntry): Promise { try { await deps.log(entry); } catch { // Logging must not break Thank You Page behavior. } } function toErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }