import { bigint, boolean, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; import { createdAt, subscriptionPlanEnum, subscriptionProviderEnum, subscriptionStatusEnum, updatedAt, } 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").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(), plan: subscriptionPlanEnum("plan").notNull(), originalTransactionId: text("original_transaction_id"), status: subscriptionStatusEnum("status").notNull(), purchasedAt: timestamp("purchased_at", { withTimezone: true }), expiresAt: timestamp("expires_at", { withTimezone: true }), gracePeriodExpiresAt: timestamp("grace_period_expires_at", { withTimezone: true }), canceledAt: timestamp("canceled_at", { withTimezone: true }), lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }), createdAt: createdAt(), updatedAt: updatedAt(), }, (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`), ], ); /** Händelselogg: köp, förnyelse, grace, churn (spec §47). */ export const subscriptionEvents = pgTable( "subscription_events", { id: uuid("id").primaryKey().defaultRandom(), subscriptionId: uuid("subscription_id").references(() => subscriptions.id, { 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), index("subscription_events_pseudonym_idx").on(t.pseudonym), ], ); /** Råa store-transaktioner för revision (spec §47). */ export const storeTransactions = pgTable( "store_transactions", { id: uuid("id").primaryKey().defaultRandom(), provider: subscriptionProviderEnum("provider").notNull(), 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), index("store_transactions_pseudonym_idx").on(t.pseudonym), ], ); /** App Store Server Notifications / Play RTDN – tas emot rått, processas av worker. */ export const storeNotifications = pgTable( "store_notifications", { id: uuid("id").primaryKey().defaultRandom(), provider: subscriptionProviderEnum("provider").notNull(), notificationType: text("notification_type"), rawPayload: jsonb("raw_payload").notNull(), signatureVerified: boolean("signature_verified").notNull().default(false), processed: boolean("processed").notNull().default(false), processedAt: timestamp("processed_at", { withTimezone: true }), error: text("error"), createdAt: createdAt(), }, (t) => [index("store_notifications_pending_idx").on(t.processed, t.createdAt)], ); /** Trial-spårning: en trial per användare (spec §46). */ export const trials = pgTable("trials", { userId: uuid("user_id") .primaryKey() .references(() => users.id, { onDelete: "cascade" }), startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(), endsAt: timestamp("ends_at", { withTimezone: true }).notNull(), }); /** Månatlig AI-användning för fair use / gratiskvot (spec §45–46). */ export const aiUsageCounters = pgTable( "ai_usage_counters", { userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), /** Format YYYY-MM. */ month: text("month").notNull(), aiScans: integer("ai_scans").notNull().default(0), aiTokensIn: integer("ai_tokens_in").notNull().default(0), aiTokensOut: integer("ai_tokens_out").notNull().default(0), aiCostUsdMicrocents: bigint("ai_cost_usd_microcents", { mode: "number" }).notNull().default(0), updatedAt: updatedAt(), }, (t) => [uniqueIndex("ai_usage_user_month_unique").on(t.userId, t.month)], );