Initial scaffold: Shopify recesso (withdrawal) compliance app
- Remix (TypeScript) + Polaris, official Shopify app template - Prisma multi-tenant schema (Settings, ExclusionRule, WithdrawalRequest, AuditLog, WebhookEvent) on Postgres - Mandatory GDPR compliance webhooks (data_request, redact, shop/redact) + HMAC handlers - API version pinned 2026-04, scopes read_orders/read_products - Fly deploy config; two-env strategy (custom now, public later) - Dev setup: shopify.web.toml + Vite allowedHosts for tunnels - Docs: PLAN.md, ANALISI-REQUISITI-LEGALI.md (Art. 54-bis compliance) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
This commit is contained in:
133
app/prisma/migrations/20260706135931_init/migration.sql
Normal file
133
app/prisma/migrations/20260706135931_init/migration.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExclusionScope" AS ENUM ('PRODUCT', 'COLLECTION', 'TAG', 'ALL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExclusionReason" AS ENUM ('CUSTOM', 'PERISHABLE', 'HYGIENE', 'OTHER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WithdrawalChannel" AS ENUM ('GUEST', 'ACCOUNT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WithdrawalStatus" AS ENUM ('RECEIVED', 'ACKNOWLEDGED', 'GOODS_PENDING', 'CLOSED', 'REJECTED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"state" TEXT NOT NULL,
|
||||
"isOnline" BOOLEAN NOT NULL DEFAULT false,
|
||||
"scope" TEXT,
|
||||
"expires" TIMESTAMP(3),
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"userId" BIGINT,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"email" TEXT,
|
||||
"accountOwner" BOOLEAN NOT NULL DEFAULT false,
|
||||
"locale" TEXT,
|
||||
"collaborator" BOOLEAN DEFAULT false,
|
||||
"emailVerified" BOOLEAN DEFAULT false,
|
||||
"refreshToken" TEXT,
|
||||
"refreshTokenExpires" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Settings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"buttonLabel" TEXT NOT NULL DEFAULT 'Recedere dal contratto qui',
|
||||
"confirmLabel" TEXT NOT NULL DEFAULT 'Conferma recesso',
|
||||
"brandPrimaryColor" TEXT,
|
||||
"returnAddress" TEXT,
|
||||
"defaultWindowDays" INTEGER NOT NULL DEFAULT 14,
|
||||
"withdrawalInfoText" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Settings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ExclusionRule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"scope" "ExclusionScope" NOT NULL,
|
||||
"targetId" TEXT,
|
||||
"reason" "ExclusionReason" NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ExclusionRule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WithdrawalRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"orderName" TEXT,
|
||||
"customerName" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"statementText" TEXT NOT NULL,
|
||||
"transmittedAt" TIMESTAMP(3) NOT NULL,
|
||||
"locale" TEXT,
|
||||
"channel" "WithdrawalChannel" NOT NULL,
|
||||
"productType" TEXT,
|
||||
"status" "WithdrawalStatus" NOT NULL DEFAULT 'RECEIVED',
|
||||
"receiptSentAt" TIMESTAMP(3),
|
||||
"computedDeadline" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WithdrawalRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"event" TEXT NOT NULL,
|
||||
"payloadHash" TEXT,
|
||||
"detail" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL,
|
||||
"topic" TEXT NOT NULL,
|
||||
"webhookId" TEXT,
|
||||
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"processed" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "WebhookEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Settings_shop_key" ON "Settings"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Settings_shop_idx" ON "Settings"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ExclusionRule_shop_idx" ON "ExclusionRule"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WithdrawalRequest_shop_idx" ON "WithdrawalRequest"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WithdrawalRequest_shop_orderId_idx" ON "WithdrawalRequest"("shop", "orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_shop_idx" ON "AuditLog"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebhookEvent_webhookId_key" ON "WebhookEvent"("webhookId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WebhookEvent_shop_idx" ON "WebhookEvent"("shop");
|
||||
3
app/prisma/migrations/migration_lock.toml
Normal file
3
app/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
147
app/prisma/schema.prisma
Normal file
147
app/prisma/schema.prisma
Normal file
@@ -0,0 +1,147 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session — used by @shopify/shopify-app-session-storage-prisma.
|
||||
// DO NOT change the shape of this model (the session storage depends on it).
|
||||
// ---------------------------------------------------------------------------
|
||||
model Session {
|
||||
id String @id
|
||||
shop String
|
||||
state String
|
||||
isOnline Boolean @default(false)
|
||||
scope String?
|
||||
expires DateTime?
|
||||
accessToken String
|
||||
userId BigInt?
|
||||
firstName String?
|
||||
lastName String?
|
||||
email String?
|
||||
accountOwner Boolean @default(false)
|
||||
locale String?
|
||||
collaborator Boolean? @default(false)
|
||||
emailVerified Boolean? @default(false)
|
||||
refreshToken String?
|
||||
refreshTokenExpires DateTime?
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multi-tenant app models. Every tenant-scoped row carries `shop` + an index
|
||||
// on it (public-grade isolation from day 1, even with only 2-3 custom stores).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Per-shop merchant configuration for the withdrawal button/flow.
|
||||
model Settings {
|
||||
id String @id @default(cuid())
|
||||
shop String @unique
|
||||
buttonLabel String @default("Recedere dal contratto qui")
|
||||
confirmLabel String @default("Conferma recesso")
|
||||
brandPrimaryColor String?
|
||||
returnAddress String?
|
||||
defaultWindowDays Int @default(14)
|
||||
withdrawalInfoText String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([shop])
|
||||
}
|
||||
|
||||
// Art. 59 exclusions (made-to-order, perishable, sealed-for-hygiene, etc.).
|
||||
model ExclusionRule {
|
||||
id String @id @default(cuid())
|
||||
shop String
|
||||
scope ExclusionScope
|
||||
targetId String?
|
||||
reason ExclusionReason
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([shop])
|
||||
}
|
||||
|
||||
// A withdrawal (recesso) statement submitted by a consumer.
|
||||
model WithdrawalRequest {
|
||||
id String @id @default(cuid())
|
||||
shop String
|
||||
orderId String
|
||||
orderName String?
|
||||
customerName String
|
||||
email String
|
||||
statementText String
|
||||
transmittedAt DateTime // legal timestamp of transmission (Art. 54-bis)
|
||||
locale String?
|
||||
channel WithdrawalChannel
|
||||
productType String?
|
||||
status WithdrawalStatus @default(RECEIVED)
|
||||
receiptSentAt DateTime?
|
||||
computedDeadline DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([shop])
|
||||
@@index([shop, orderId])
|
||||
}
|
||||
|
||||
// Append-only audit trail (burden of proof — Art. 54-bis / R6). No updatedAt.
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
shop String
|
||||
event String
|
||||
payloadHash String?
|
||||
detail String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([shop])
|
||||
}
|
||||
|
||||
// Webhook idempotency ledger.
|
||||
model WebhookEvent {
|
||||
id String @id @default(cuid())
|
||||
shop String
|
||||
topic String
|
||||
webhookId String? @unique
|
||||
receivedAt DateTime @default(now())
|
||||
processed Boolean @default(false)
|
||||
|
||||
@@index([shop])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum ExclusionScope {
|
||||
PRODUCT
|
||||
COLLECTION
|
||||
TAG
|
||||
ALL
|
||||
}
|
||||
|
||||
enum ExclusionReason {
|
||||
CUSTOM
|
||||
PERISHABLE
|
||||
HYGIENE
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum WithdrawalChannel {
|
||||
GUEST
|
||||
ACCOUNT
|
||||
}
|
||||
|
||||
enum WithdrawalStatus {
|
||||
RECEIVED
|
||||
ACKNOWLEDGED
|
||||
GOODS_PENDING
|
||||
CLOSED
|
||||
REJECTED
|
||||
}
|
||||
Reference in New Issue
Block a user