feat(gdpr): fullständig raderingsflöde med pseudonym-retention, residual-test och hushållshantering
- Implementerar eraseUser() i packages/database/src/gdpr-erasure.ts som raderar/anonymiserar/pseudonymiserar alla personliga tabeller. - DELETE /v1/me orkestrerar nu plan + transaktion + lagringsrensning. - Enpersonshushåll raderas automatiskt; publika recept anonymiseras; draft/private-recept raderas. - Finansiella poster (subscriptions/store_transactions/subscription_events) behålls under retention med slumpmässig pseudonym från gdpr_retention_pseudonyms. - audit_logs/domain_events anonymiseras; kvitton i kvarvarande hushåll strippas på bild och OCR-text. - Lägger till permanent residual-test (me.residual.test.ts) med schema-diff-assertion mot information_schema. - Uppdaterar docs/23 och docs/29 med implementerad mekanism. - Migration 0021 för gdpr_retention_pseudonyms och nullable cooking_sessions.started_by_user_id.
This commit is contained in:
@@ -20,15 +20,18 @@ import {
|
||||
} from "./_shared.js";
|
||||
import { households } from "./households.js";
|
||||
import { users } from "./users.js";
|
||||
import { gdprRetentionPseudonyms } from "./gdpr.js";
|
||||
|
||||
/** Backend är source of truth för Premium (spec §47, §61.14). */
|
||||
export const subscriptions = pgTable(
|
||||
"subscriptions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
/** Pseudonym used after GDPR deletion while financial records are retained. */
|
||||
pseudonym: uuid("pseudonym").references(() => gdprRetentionPseudonyms.pseudonym, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }),
|
||||
provider: subscriptionProviderEnum("provider").notNull(),
|
||||
productId: text("product_id").notNull(),
|
||||
@@ -45,6 +48,7 @@ export const subscriptions = pgTable(
|
||||
},
|
||||
(t) => [
|
||||
index("subscriptions_user_idx").on(t.userId),
|
||||
index("subscriptions_pseudonym_idx").on(t.pseudonym),
|
||||
uniqueIndex("subscriptions_original_tx_unique")
|
||||
.on(t.provider, t.originalTransactionId)
|
||||
.where(sql`${t.originalTransactionId} IS NOT NULL`),
|
||||
@@ -60,11 +64,18 @@ export const subscriptionEvents = pgTable(
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
/** Pseudonym used after GDPR deletion while records are retained. */
|
||||
pseudonym: uuid("pseudonym").references(() => gdprRetentionPseudonyms.pseudonym, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
eventType: text("event_type").notNull(),
|
||||
payload: jsonb("payload"),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [index("subscription_events_sub_idx").on(t.subscriptionId, t.createdAt)],
|
||||
(t) => [
|
||||
index("subscription_events_sub_idx").on(t.subscriptionId, t.createdAt),
|
||||
index("subscription_events_pseudonym_idx").on(t.pseudonym),
|
||||
],
|
||||
);
|
||||
|
||||
/** Råa store-transaktioner för revision (spec §47). */
|
||||
@@ -76,12 +87,19 @@ export const storeTransactions = pgTable(
|
||||
transactionId: text("transaction_id").notNull(),
|
||||
originalTransactionId: text("original_transaction_id"),
|
||||
userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
/** Pseudonym used after GDPR deletion while records are retained. */
|
||||
pseudonym: uuid("pseudonym").references(() => gdprRetentionPseudonyms.pseudonym, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
productId: text("product_id"),
|
||||
rawPayload: jsonb("raw_payload").notNull(),
|
||||
processedAt: timestamp("processed_at", { withTimezone: true }),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [uniqueIndex("store_transactions_unique").on(t.provider, t.transactionId)],
|
||||
(t) => [
|
||||
uniqueIndex("store_transactions_unique").on(t.provider, t.transactionId),
|
||||
index("store_transactions_pseudonym_idx").on(t.pseudonym),
|
||||
],
|
||||
);
|
||||
|
||||
/** App Store Server Notifications / Play RTDN – tas emot rått, processas av worker. */
|
||||
|
||||
Reference in New Issue
Block a user