import type { EventType, Unit } from "@app/shared-types"; /** * Typade payloads för domänhändelser (spec §55). * Events skrivs i outbox-tabellen (domain_events) i samma transaktion som * affärsdatan och konsumeras asynkront av worker (memory, analytics, training). */ export interface EventPayloadMap { PRODUCT_ADDED: { inventoryItemId: string; canonicalIngredientId?: string | null; quantity: number; unit: Unit; source: string; }; PRODUCT_UPDATED: { inventoryItemId: string; changes: Record }; PRODUCT_CONSUMED: { inventoryItemId: string; quantity: number; unit: Unit; refType?: string | null; }; PRODUCT_DISCARDED: { inventoryItemId: string; quantity: number; unit: Unit; valueMinor?: number | null; reason?: string | null; }; RECIPE_COOKED: { recipeId: string; portions: number; mealBoxPortions: number }; RECIPE_RATED: { recipeId: string; stars: number; feedbackTags: string[] }; RECIPE_CREATED: { recipeId: string; sourceType: string }; RECIPE_FORKED: { recipeId: string; forkedFromRecipeId: string }; MEAL_LOGGED: { mealId: string; mealType: string; kcal: number; source: string }; MEAL_PHOTO_ANALYZED: { scanJobId: string; matched: boolean; kcalMostLikely?: number | null }; HOUSEHOLD_MEMBER_ADDED: { householdId: string; newUserId: string; role: string }; SUBSCRIPTION_STARTED: { subscriptionId: string; plan: string; provider: string }; SUBSCRIPTION_CHANGED: { subscriptionId: string; status: string }; AI_CORRECTED: { scanJobId?: string | null; taskType: string; field: string }; MEMORY_UPDATED: { memoryItemId: string; kind: string; origin: string }; SHOPPING_COMPLETED: { shoppingListId: string; itemsAdded: number }; WEEK_PLAN_UPDATED: { weekPlanId: string; reason?: string | null }; MEAL_BOX_CREATED: { mealBoxId: string; portions: number }; MEAL_BOX_UPDATED: { mealBoxId: string; addedPortions: number; totalPortions: number }; MEAL_BOX_CONSUMED: { mealBoxId: string; portions: number }; MEAL_BOX_DISCARDED: { mealBoxId: string; portions: number; source: string }; MEAL_REMOVED: { mealId: string; recipeId: string; source: string }; COOKING_SESSION_UNDONE: { cookingSessionId: string; recipeId: string; reversedTransactions: number; }; } /** Kontroll i kompileringstid att kartan täcker alla EventType. */ type AssertCoversAll = EventType extends keyof EventPayloadMap ? true : never; const _coversAll: AssertCoversAll = true; void _coversAll; export interface NewDomainEvent { type: T; payload: T extends keyof EventPayloadMap ? EventPayloadMap[T] : never; userId?: string | undefined; householdId?: string | undefined; correlationId?: string | undefined; } /** Hjälpare med full typinferens: makeEvent("RECIPE_COOKED", {...}). */ export function makeEvent( type: T, payload: T extends keyof EventPayloadMap ? EventPayloadMap[T] : never, scope: { userId?: string; householdId?: string; correlationId?: string } = {}, ): NewDomainEvent { return { type, payload, ...scope }; }