metatroncubeswdev e6b8b710c4
Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
feat(phase-8): implement GDPR compliance webhook handlers
Replace the customers/data_request, customers/redact, and shop/redact
stubs with real logic in app/services/gdpr.server.ts: compile a
customer's Booking history, anonymize customerEmail/customerPhone on
redact, and purge every shopDomain-scoped row on shop uninstall
(Booking before Location, relying on Location's cascade for
SlotTemplate/SlotOverride/BlackoutDate/Zone/Rate). Covered by a new
tests/integration/gdpr.test.ts against live Postgres to verify the
FK deletion order actually works, not just typechecks.

Still commented out in shopify.app.toml pending Protected customer
data access approval in the Partner Dashboard — unrelated to code
readiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:20:16 -04:00

94 lines
3.8 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import db from "../../app/db.server";
import { compileCustomerData, purgeShopData, redactCustomerData } from "../../app/services/gdpr.server";
const shopDomain = "gdpr-integration-test.myshopify.com";
async function cleanup() {
await db.booking.deleteMany({ where: { shopDomain } });
await db.location.deleteMany({ where: { shopDomain } });
await db.shop.deleteMany({ where: { shopDomain } });
await db.session.deleteMany({ where: { shop: shopDomain } });
}
async function seed() {
await db.shop.create({ data: { shopDomain } });
const location = await db.location.create({
data: { shopDomain, name: "Test Location", address: "", timezone: "America/Toronto" },
});
const zone = await db.zone.create({
data: { shopDomain, locationId: location.id, name: "Local", type: "postal", postalCodes: ["M5V"] },
});
await db.rate.create({
data: { shopDomain, method: "LOCAL_DELIVERY", zoneId: zone.id, name: "Standard", priceCents: 500, keyedBy: "zone" },
});
await db.slotTemplate.create({
data: { shopDomain, locationId: location.id, method: "PICKUP", weekday: 1, startMin: 540, endMin: 600, capacity: 5 },
});
const booking = await db.booking.create({
data: {
shopDomain,
orderId: "gid://shopify/Order/gdpr-1",
locationId: location.id,
method: "PICKUP",
slotStart: new Date("2026-08-25T13:00:00.000Z"),
slotEnd: new Date("2026-08-25T13:30:00.000Z"),
customerEmail: "shopper@example.com",
customerPhone: "+15551234567",
},
});
return { location, zone, booking };
}
describe("GDPR handlers", () => {
beforeEach(cleanup);
afterAll(async () => {
await cleanup();
await db.$disconnect();
});
it("compileCustomerData finds Bookings matching the customer's email", async () => {
await seed();
const results = await compileCustomerData(shopDomain, { email: "shopper@example.com" });
expect(results).toHaveLength(1);
expect(results[0].orderId).toBe("gid://shopify/Order/gdpr-1");
});
it("compileCustomerData finds Bookings matching the customer's phone", async () => {
await seed();
const results = await compileCustomerData(shopDomain, { phone: "+15551234567" });
expect(results).toHaveLength(1);
});
it("compileCustomerData returns nothing for an unrelated customer", async () => {
await seed();
const results = await compileCustomerData(shopDomain, { email: "someone-else@example.com" });
expect(results).toHaveLength(0);
});
it("redactCustomerData anonymizes matching Bookings but keeps the row", async () => {
const { booking } = await seed();
const { count } = await redactCustomerData(shopDomain, { email: "shopper@example.com" });
expect(count).toBe(1);
const after = await db.booking.findUnique({ where: { id: booking.id } });
expect(after).not.toBeNull();
expect(after?.customerEmail).toBeNull();
expect(after?.customerPhone).toBeNull();
expect(after?.orderId).toBe("gid://shopify/Order/gdpr-1"); // booking history preserved
});
it("purgeShopData deletes every shopDomain-scoped row, respecting FK order", async () => {
const { location, zone, booking } = await seed();
await purgeShopData(shopDomain);
expect(await db.booking.findUnique({ where: { id: booking.id } })).toBeNull();
expect(await db.location.findUnique({ where: { id: location.id } })).toBeNull();
expect(await db.zone.findUnique({ where: { id: zone.id } })).toBeNull(); // cascaded from Location
expect(await db.rate.findMany({ where: { shopDomain } })).toHaveLength(0); // cascaded from Zone
expect(await db.slotTemplate.findMany({ where: { shopDomain } })).toHaveLength(0); // cascaded from Location
expect(await db.shop.findUnique({ where: { shopDomain } })).toBeNull();
});
});