import type { FastifyInstance } from "fastify"; import { and, eq, gte, inArray, sql } from "drizzle-orm"; import { schema } from "@app/database"; import { requireActiveHousehold } from "../lib/helpers.js"; /** Budget & matsvinn (spec §26): vecka/månad, kostnad per måltid, svinnvärde. */ export async function budgetRoutes(app: FastifyInstance) { const auth = { preHandler: [app.authenticate] }; app.get("/v1/budget/summary", auth, async (req) => { const householdId = await requireActiveHousehold(app.db, req.userId); const now = new Date(); // UTC-kalender (samma policy som motorerna): identiska summor oavsett serverns tidszon. const mondayOffset = (now.getUTCDay() + 6) % 7; // 0 = måndag const weekStart = new Date( Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - mondayOffset), ); const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); // Inköpskostnad = summa purchase-transaktioner med värde const [weekPurchases] = await app.db .select({ total: sql`coalesce(sum(${schema.inventoryTransactions.valueMinor}), 0)` }) .from(schema.inventoryTransactions) .where( and( eq(schema.inventoryTransactions.householdId, householdId), eq(schema.inventoryTransactions.type, "purchase"), gte(schema.inventoryTransactions.createdAt, weekStart), ), ); const [monthPurchases] = await app.db .select({ total: sql`coalesce(sum(${schema.inventoryTransactions.valueMinor}), 0)` }) .from(schema.inventoryTransactions) .where( and( eq(schema.inventoryTransactions.householdId, householdId), eq(schema.inventoryTransactions.type, "purchase"), gte(schema.inventoryTransactions.createdAt, monthStart), ), ); // Matsvinnsvärde = summa discard-transaktioner (spec §12, §26) const [weekWaste] = await app.db .select({ total: sql`coalesce(sum(${schema.inventoryTransactions.valueMinor}), 0)`, count: sql`count(*)`, }) .from(schema.inventoryTransactions) .where( and( eq(schema.inventoryTransactions.householdId, householdId), eq(schema.inventoryTransactions.type, "discard"), gte(schema.inventoryTransactions.createdAt, weekStart), ), ); const [monthWaste] = await app.db .select({ total: sql`coalesce(sum(${schema.inventoryTransactions.valueMinor}), 0)`, count: sql`count(*)`, }) .from(schema.inventoryTransactions) .where( and( eq(schema.inventoryTransactions.householdId, householdId), eq(schema.inventoryTransactions.type, "discard"), gte(schema.inventoryTransactions.createdAt, monthStart), ), ); // Kostnad per hemlagad måltid: lagade recept denna månad med kostnadsdata const cooks = await app.db .select({ recipeId: schema.recipeCooks.recipeId, portions: schema.recipeCooks.portionsCooked, }) .from(schema.recipeCooks) .where( and( eq(schema.recipeCooks.householdId, householdId), gte(schema.recipeCooks.cookedAt, monthStart), ), ); let mealCostTotal = 0; let mealPortions = 0; if (cooks.length > 0) { const recipes = await app.db .select({ id: schema.recipes.id, cost: schema.recipes.estimatedCostMinorPerPortion }) .from(schema.recipes) .where(inArray(schema.recipes.id, [...new Set(cooks.map((c) => c.recipeId))])); const costMap = new Map(recipes.map((r) => [r.id, r.cost])); for (const cook of cooks) { const cost = costMap.get(cook.recipeId); if (cost != null) { mealCostTotal += cost * cook.portions; mealPortions += cook.portions; } } } const [household] = await app.db .select({ budget: schema.households.weeklyBudgetMinor, currencyCode: schema.households.currencyCode, }) .from(schema.households) .where(eq(schema.households.id, householdId)) .limit(1); // Alla belopp i minor units (heltal) i hushållets valuta (i18n-spec §20). const currency = household?.currencyCode ?? "SEK"; return { currency, week: { purchasedMinor: Math.round(Number(weekPurchases?.total ?? 0)), wasteMinor: Math.round(Number(weekWaste?.total ?? 0)), wasteCount: Number(weekWaste?.count ?? 0), budgetMinor: household?.budget ?? null, }, month: { purchasedMinor: Math.round(Number(monthPurchases?.total ?? 0)), wasteMinor: Math.round(Number(monthWaste?.total ?? 0)), wasteCount: Number(monthWaste?.count ?? 0), estimatedCostPerPortionMinor: mealPortions > 0 ? Math.round(mealCostTotal / mealPortions) : null, cookedPortions: mealPortions, }, note: "Kostnader bygger på kvitton, angivna priser och schablonpriser – uppskattningar, inte bokföring.", }; }); }