139 lines
4.8 KiB
TypeScript
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,
|
|
}
|
|
);
|
|
}
|