Implement ST-203 budget logic and tests

This commit is contained in:
Janpeter Visser 2026-04-18 19:00:24 +02:00
parent 2c9344a94f
commit 07afbc3213
11 changed files with 1057 additions and 9 deletions

View file

@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import {
BUDGET_FORMULA_VERSION,
deriveBudgetSnapshot,
deriveDailyBudget,
deriveEnergyLevel,
formatEnergyLevelLabel,
} from "./budget";
describe("deriveEnergyLevel", () => {
it("maps scores 1 and 2 to zeer_laag", () => {
expect(deriveEnergyLevel(1)).toBe("zeer_laag");
expect(deriveEnergyLevel(2)).toBe("zeer_laag");
});
it("maps scores 3 and 4 to laag", () => {
expect(deriveEnergyLevel(3)).toBe("laag");
expect(deriveEnergyLevel(4)).toBe("laag");
});
it("maps scores 5 and 6 to midden", () => {
expect(deriveEnergyLevel(5)).toBe("midden");
expect(deriveEnergyLevel(6)).toBe("midden");
});
it("maps scores 7 and 8 to redelijk", () => {
expect(deriveEnergyLevel(7)).toBe("redelijk");
expect(deriveEnergyLevel(8)).toBe("redelijk");
});
it("maps scores 9 and 10 to hoog", () => {
expect(deriveEnergyLevel(9)).toBe("hoog");
expect(deriveEnergyLevel(10)).toBe("hoog");
});
});
describe("deriveDailyBudget", () => {
it("keeps v1 daily budget equal to the energy score", () => {
expect(deriveDailyBudget(1)).toBe(1);
expect(deriveDailyBudget(5)).toBe(5);
expect(deriveDailyBudget(10)).toBe(10);
});
});
describe("deriveBudgetSnapshot", () => {
it("returns a stable snapshot with formula version", () => {
expect(deriveBudgetSnapshot(6)).toEqual({
energyLevel: "midden",
dailyBudget: 6,
budgetFormulaVersion: BUDGET_FORMULA_VERSION,
});
});
});
describe("formatEnergyLevelLabel", () => {
it("returns readable Dutch labels", () => {
expect(formatEnergyLevelLabel("zeer_laag")).toBe("Zeer laag");
expect(formatEnergyLevelLabel("redelijk")).toBe("Redelijk");
});
});

56
lib/check-in/budget.ts Normal file
View file

@ -0,0 +1,56 @@
import type { EnergyLevel } from "@/lib/check-in/types";
export const BUDGET_FORMULA_VERSION = 1;
export type BudgetSnapshot = {
energyLevel: EnergyLevel;
dailyBudget: number;
budgetFormulaVersion: number;
};
export function deriveEnergyLevel(energyScore: number): EnergyLevel {
if (energyScore <= 2) {
return "zeer_laag";
}
if (energyScore <= 4) {
return "laag";
}
if (energyScore <= 6) {
return "midden";
}
if (energyScore <= 8) {
return "redelijk";
}
return "hoog";
}
export function deriveDailyBudget(energyScore: number): number {
return energyScore;
}
export function deriveBudgetSnapshot(energyScore: number): BudgetSnapshot {
return {
energyLevel: deriveEnergyLevel(energyScore),
dailyBudget: deriveDailyBudget(energyScore),
budgetFormulaVersion: BUDGET_FORMULA_VERSION,
};
}
export function formatEnergyLevelLabel(energyLevel: EnergyLevel): string {
switch (energyLevel) {
case "zeer_laag":
return "Zeer laag";
case "laag":
return "Laag";
case "midden":
return "Midden";
case "redelijk":
return "Redelijk";
case "hoog":
return "Hoog";
}
}

View file

@ -1,7 +1,9 @@
import { getAuthenticatedUser } from "@/lib/auth/session";
import { deriveBudgetSnapshot } from "@/lib/check-in/budget";
import { ensureProfileBundleForCurrentUser } from "@/lib/profile/service";
import { createClient } from "@/lib/supabase/server";
import type {
EnergyLevel,
MorningCheckInRecord,
MorningCheckInStatus,
MorningCheckInSubmission,
@ -16,6 +18,9 @@ type MorningCheckInRow = {
check_in_date: string;
energy_score: number;
sleep_quality: SleepQuality;
energy_level: EnergyLevel;
daily_budget: number;
budget_formula_version: number;
created_at: string;
updated_at: string;
};
@ -25,10 +30,13 @@ type MorningCheckInInsert = {
check_in_date: string;
energy_score: number;
sleep_quality: SleepQuality;
energy_level: EnergyLevel;
daily_budget: number;
budget_formula_version: number;
};
const MORNING_CHECK_IN_COLUMNS =
"id, user_id, check_in_date, energy_score, sleep_quality, created_at, updated_at";
"id, user_id, check_in_date, energy_score, sleep_quality, energy_level, daily_budget, budget_formula_version, created_at, updated_at";
function mapMorningCheckInRow(row: MorningCheckInRow): MorningCheckInRecord {
return {
@ -37,6 +45,9 @@ function mapMorningCheckInRow(row: MorningCheckInRow): MorningCheckInRecord {
checkInDate: row.check_in_date,
energyScore: row.energy_score,
sleepQuality: row.sleep_quality,
energyLevel: row.energy_level,
dailyBudget: row.daily_budget,
budgetFormulaVersion: row.budget_formula_version,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
@ -122,11 +133,15 @@ export async function upsertTodayCheckInForCurrentUser(
}
const checkInDate = getLocalDateForTimezone(profileBundle.profile.timezone);
const budgetSnapshot = deriveBudgetSnapshot(submission.energyScore);
const payload: MorningCheckInInsert = {
user_id: user.id,
check_in_date: checkInDate,
energy_score: submission.energyScore,
sleep_quality: submission.sleepQuality,
energy_level: budgetSnapshot.energyLevel,
daily_budget: budgetSnapshot.dailyBudget,
budget_formula_version: budgetSnapshot.budgetFormulaVersion,
};
const supabase = await createClient();

View file

@ -1,4 +1,5 @@
export type SleepQuality = "goed" | "matig" | "slecht";
export type EnergyLevel = "zeer_laag" | "laag" | "midden" | "redelijk" | "hoog";
export type MorningCheckInRecord = {
id: string;
@ -6,6 +7,9 @@ export type MorningCheckInRecord = {
checkInDate: string;
energyScore: number;
sleepQuality: SleepQuality;
energyLevel: EnergyLevel;
dailyBudget: number;
budgetFormulaVersion: number;
createdAt: string;
updatedAt: string;
};