Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
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";
|
||||
|
||||
/** 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" }),
|
||||
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),
|
||||
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" }),
|
||||
eventType: text("event_type").notNull(),
|
||||
payload: jsonb("payload"),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [index("subscription_events_sub_idx").on(t.subscriptionId, t.createdAt)],
|
||||
);
|
||||
|
||||
/** 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" }),
|
||||
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)],
|
||||
);
|
||||
|
||||
/** 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),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
(t) => [uniqueIndex("ai_usage_user_month_unique").on(t.userId, t.month)],
|
||||
);
|
||||
Reference in New Issue
Block a user