import { eq } from "drizzle-orm"; import type { NodePgDatabase } from "drizzle-orm/node-postgres"; import * as schema from "./schema/index.js"; type Db = NodePgDatabase; export interface ActivationState { firstScanCompletedAt: Date | null; fifthItemConfirmedAt: Date | null; firstRecipeRecommendationViewedAt: Date | null; firstRecipeSavedOrStartedAt: Date | null; activatedAt: Date | null; firstCookingSessionCompletedAt: Date | null; inventoryUpdatedAfterCookingAt: Date | null; valueConfirmedAt: Date | null; } function milestoneColumn(milestone: keyof ActivationState) { return schema.households[milestone] as (typeof schema.households)["firstScanCompletedAt"]; } const activationMilestones: Array = [ "firstScanCompletedAt", "fifthItemConfirmedAt", "firstRecipeRecommendationViewedAt", "firstRecipeSavedOrStartedAt", "activatedAt", "firstCookingSessionCompletedAt", "inventoryUpdatedAfterCookingAt", "valueConfirmedAt", ]; function isActivationMilestone(m: string): m is keyof ActivationState { return activationMilestones.includes(m as keyof ActivationState); } /** * Markera en milstolpe och utvärdera aktiveringsstatus. * Endast den första bekräftelsen sparas; anrop är idempotenta. */ export async function markMilestone( db: Db, householdId: string, milestone: keyof ActivationState, ): Promise<{ wasNew: boolean; activatedNow: boolean; valueConfirmedNow: boolean }> { if (!isActivationMilestone(milestone)) { throw new Error(`Unknown activation milestone: ${milestone}`); } const [before] = await db .select({ milestoneAt: milestoneColumn(milestone), firstScanCompletedAt: schema.households.firstScanCompletedAt, fifthItemConfirmedAt: schema.households.fifthItemConfirmedAt, firstRecipeRecommendationViewedAt: schema.households.firstRecipeRecommendationViewedAt, firstRecipeSavedOrStartedAt: schema.households.firstRecipeSavedOrStartedAt, activatedAt: schema.households.activatedAt, firstCookingSessionCompletedAt: schema.households.firstCookingSessionCompletedAt, inventoryUpdatedAfterCookingAt: schema.households.inventoryUpdatedAfterCookingAt, valueConfirmedAt: schema.households.valueConfirmedAt, }) .from(schema.households) .where(eq(schema.households.id, householdId)); if (!before) { throw new Error(`Household not found: ${householdId}`); } const wasNew = before.milestoneAt == null; const wasActivated = before.activatedAt != null; const wasValueConfirmed = before.valueConfirmedAt != null; if (!wasNew) { return { wasNew: false, activatedNow: false, valueConfirmedNow: false }; } const isActivatedNow = before.firstScanCompletedAt != null && before.fifthItemConfirmedAt != null && before.firstRecipeRecommendationViewedAt != null && (before.firstRecipeSavedOrStartedAt != null || milestone === "firstRecipeSavedOrStartedAt"); const activatedNow = !wasActivated && isActivatedNow; const isValueConfirmedNow = isActivatedNow && (before.firstCookingSessionCompletedAt != null || milestone === "firstCookingSessionCompletedAt") && (before.inventoryUpdatedAfterCookingAt != null || milestone === "inventoryUpdatedAfterCookingAt"); const valueConfirmedNow = !wasValueConfirmed && isValueConfirmedNow; const updates: Partial = { [milestone]: new Date(), updatedAt: new Date(), }; if (activatedNow) { updates.activatedAt = new Date(); } if (valueConfirmedNow) { updates.valueConfirmedAt = new Date(); } await db.update(schema.households).set(updates).where(eq(schema.households.id, householdId)); return { wasNew, activatedNow, valueConfirmedNow }; } export async function getActivationState(db: Db, householdId: string): Promise { const [row] = await db .select({ firstScanCompletedAt: schema.households.firstScanCompletedAt, fifthItemConfirmedAt: schema.households.fifthItemConfirmedAt, firstRecipeRecommendationViewedAt: schema.households.firstRecipeRecommendationViewedAt, firstRecipeSavedOrStartedAt: schema.households.firstRecipeSavedOrStartedAt, activatedAt: schema.households.activatedAt, firstCookingSessionCompletedAt: schema.households.firstCookingSessionCompletedAt, inventoryUpdatedAfterCookingAt: schema.households.inventoryUpdatedAfterCookingAt, valueConfirmedAt: schema.households.valueConfirmedAt, }) .from(schema.households) .where(eq(schema.households.id, householdId)); return ( row ?? { firstScanCompletedAt: null, fifthItemConfirmedAt: null, firstRecipeRecommendationViewedAt: null, firstRecipeSavedOrStartedAt: null, activatedAt: null, firstCookingSessionCompletedAt: null, inventoryUpdatedAfterCookingAt: null, valueConfirmedAt: null, } ); }