import db from "../db.server"; // Shopify's mandatory GDPR webhook payload shapes (customers/data_request, // customers/redact). Only the fields we actually use are declared. export interface CustomerWebhookPayload { customer?: { id?: number; email?: string | null; phone?: string | null; }; } function customerContactMatch(customer: CustomerWebhookPayload["customer"]) { const email = customer?.email ?? undefined; const phone = customer?.phone ?? undefined; const or: Array<{ customerEmail?: string; customerPhone?: string }> = []; if (email) or.push({ customerEmail: email }); if (phone) or.push({ customerPhone: phone }); return or; } // customers/data_request: compile every Booking record this shop holds for // the requesting customer. Shopify doesn't accept a data payload back over // the webhook itself — the merchant is expected to supply it to the // customer directly (email/support ticket), so we log a structured record // here for the merchant to retrieve. A self-serve export (e.g. emailed to // the shop owner) is a Phase 9+ notification-system enhancement. export async function compileCustomerData( shopDomain: string, customer: CustomerWebhookPayload["customer"], ) { const or = customerContactMatch(customer); if (or.length === 0) return []; return db.booking.findMany({ where: { shopDomain, OR: or }, select: { id: true, orderId: true, orderName: true, method: true, slotStart: true, slotEnd: true, status: true, customerEmail: true, customerPhone: true, createdAt: true, }, }); } // customers/redact: anonymize this customer's PII on any Booking rows for // this shop. Bookings themselves are kept (needed for revenue/utilization // history on the dashboard) — only the customer-identifying fields go. export async function redactCustomerData( shopDomain: string, customer: CustomerWebhookPayload["customer"], ) { const or = customerContactMatch(customer); if (or.length === 0) return { count: 0 }; return db.booking.updateMany({ where: { shopDomain, OR: or }, data: { customerEmail: null, customerPhone: null }, }); } // shop/redact: erase every shopDomain-scoped row, 48h after uninstall. // Booking has no cascade from Location (kept deliberately restrictive so an // active shop can't accidentally cascade-delete booking history by deleting // a Location), so it must be deleted before Location; deleting Location // cascades SlotTemplate/SlotOverride/BlackoutDate/Zone/Rate per schema.prisma. // GeocodeCache is intentionally excluded — it's a shared, address-keyed // cache with no shopDomain, not shop-owned data. export async function purgeShopData(shopDomain: string) { await db.$transaction([ db.booking.deleteMany({ where: { shopDomain } }), db.location.deleteMany({ where: { shopDomain } }), db.shop.deleteMany({ where: { shopDomain } }), db.session.deleteMany({ where: { shop: shopDomain } }), ]); }