Files
Cibello-app/packages/database/src/activation.ts
T
Sven (AAMOS AI) c32a7e33c7
CI / Typecheck, test & build (push) Failing after 2s
ci: trigga på master + formatfix inför Gitea Actions
2026-08-13 17:25:14 +07:00

139 lines
4.8 KiB
TypeScript

import { eq } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import * as schema from "./schema/index.js";
type Db = NodePgDatabase<typeof schema>;
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<keyof ActivationState> = [
"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<typeof schema.households.$inferInsert> = {
[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<ActivationState> {
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,
}
);
}