fix(worker): S2 proactive tips logic, i18n parity, hardened test DB reset
- processProactiveTips now iterates all expiring items sorted by urgency and emits the first candidate with a recipe that clears 7-day dedup. - Add pt-PT proactive-tip templates; remove en-GB duplicate; parity test asserts against shared-types SUPPORTED_LANGUAGE_TAGS. - Serialize DB test setup via turbo dependencies; seed truncates only *_test DBs, guarded by both --test flag and DB name suffix.
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"build": "tsup src/index.ts --format esm --target node22 --sourcemap --clean",
|
"build": "tsup src/index.ts --format esm --target node22 --sourcemap --clean",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "pnpm --filter=@app/database run db:test-setup && vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"eval": "tsx src/eval/run.ts",
|
"eval": "tsx src/eval/run.ts",
|
||||||
"eval:scan": "tsx src/eval/scan-eval.ts"
|
"eval:scan": "tsx src/eval/scan-eval.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,19 +36,16 @@ export interface ProcessProactiveTipsOptions {
|
|||||||
nowUtc?: Date;
|
nowUtc?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
function consentGranted(
|
function consentGranted(consents: Array<{ kind: string; status: string }>, kind: string): boolean {
|
||||||
consents: Array<{ kind: string; status: string }>,
|
|
||||||
kind: string,
|
|
||||||
): boolean {
|
|
||||||
return consents.some((c) => c.kind === kind && c.status === "granted");
|
return consents.some((c) => c.kind === kind && c.status === "granted");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hitta en utgångsnära vara för hushållet (≤2 dagar kvar). */
|
/** Hitta alla utgångsnära varor för hushållet (≤2 dagar kvar), sorterade efter brådska. */
|
||||||
async function findExpiringItem(
|
async function findExpiringItems(
|
||||||
ctx: WorkerContext,
|
ctx: WorkerContext,
|
||||||
householdId: string,
|
householdId: string,
|
||||||
now: Date,
|
now: Date,
|
||||||
): Promise<ExpiringItem | null> {
|
): Promise<ExpiringItem[]> {
|
||||||
const rows = await ctx.db
|
const rows = await ctx.db
|
||||||
.select({
|
.select({
|
||||||
item: schema.inventoryItems,
|
item: schema.inventoryItems,
|
||||||
@@ -96,7 +93,7 @@ async function findExpiringItem(
|
|||||||
|
|
||||||
// Prioritera kortast tid kvar.
|
// Prioritera kortast tid kvar.
|
||||||
expiring.sort((a, b) => (a.daysLeft ?? 99) - (b.daysLeft ?? 99));
|
expiring.sort((a, b) => (a.daysLeft ?? 99) - (b.daysLeft ?? 99));
|
||||||
return expiring[0] ?? null;
|
return expiring;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hitta ett lämpligt recept som använder ingrediensen och inte lagats nyligen. */
|
/** Hitta ett lämpligt recept som använder ingrediensen och inte lagats nyligen. */
|
||||||
@@ -215,9 +212,7 @@ export async function processProactiveTips(
|
|||||||
const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||||
const sevenDaysAgo = new Date(now.getTime() - SEVEN_DAYS_MS);
|
const sevenDaysAgo = new Date(now.getTime() - SEVEN_DAYS_MS);
|
||||||
|
|
||||||
const households = await ctx.db
|
const households = await ctx.db.select({ id: schema.households.id }).from(schema.households);
|
||||||
.select({ id: schema.households.id })
|
|
||||||
.from(schema.households);
|
|
||||||
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
|
|
||||||
@@ -227,13 +222,9 @@ export async function processProactiveTips(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hitta en vara att rädda.
|
// Hämta alla utgångsnära varor sorterade efter brådska.
|
||||||
const item = await findExpiringItem(ctx, household.id, now);
|
const items = await findExpiringItems(ctx, household.id, now);
|
||||||
if (!item) continue;
|
if (items.length === 0) continue;
|
||||||
|
|
||||||
// Hitta ett recept som passar.
|
|
||||||
const recipe = await findRecipeForIngredient(ctx, household.id, item.canonicalIngredientId, now);
|
|
||||||
if (!recipe) continue;
|
|
||||||
|
|
||||||
// Välj högst en berättigad mottagare per hushåll per dag.
|
// Välj högst en berättigad mottagare per hushåll per dag.
|
||||||
const members = await ctx.db
|
const members = await ctx.db
|
||||||
@@ -253,42 +244,63 @@ export async function processProactiveTips(
|
|||||||
}
|
}
|
||||||
if (!recipientId) continue;
|
if (!recipientId) continue;
|
||||||
|
|
||||||
// Ingen upprepning av samma vara+recept inom 7 dagar.
|
// Försök varje kandidat i brådskande ordning tills en kvalificerar.
|
||||||
if (await isDuplicateTip(ctx, recipientId, item.canonicalIngredientId, recipe.recipeId, sevenDaysAgo)) {
|
let emitted = false;
|
||||||
continue;
|
for (const item of items) {
|
||||||
}
|
const recipe = await findRecipeForIngredient(
|
||||||
|
ctx,
|
||||||
|
household.id,
|
||||||
|
item.canonicalIngredientId,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
if (!recipe) continue;
|
||||||
|
|
||||||
const locale = await getLocaleContext(ctx, recipientId);
|
// Ingen upprepning av samma vara+recept inom 7 dagar.
|
||||||
const rendered = renderProactiveTip(
|
if (
|
||||||
"expiring_ingredient",
|
await isDuplicateTip(
|
||||||
locale,
|
ctx,
|
||||||
{
|
recipientId,
|
||||||
|
item.canonicalIngredientId,
|
||||||
|
recipe.recipeId,
|
||||||
|
sevenDaysAgo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = await getLocaleContext(ctx, recipientId);
|
||||||
|
const rendered = renderProactiveTip("expiring_ingredient", locale, {
|
||||||
ingredientName: item.displayName,
|
ingredientName: item.displayName,
|
||||||
recipeTitle: recipe.titleSv,
|
recipeTitle: recipe.titleSv,
|
||||||
recipeId: recipe.recipeId,
|
recipeId: recipe.recipeId,
|
||||||
},
|
});
|
||||||
);
|
if (!rendered) continue;
|
||||||
if (!rendered) continue;
|
|
||||||
|
|
||||||
await ctx.db.insert(schema.notifications).values({
|
await ctx.db.insert(schema.notifications).values({
|
||||||
userId: recipientId,
|
userId: recipientId,
|
||||||
type: "proactive_tip",
|
type: "proactive_tip",
|
||||||
titleSv: rendered.title,
|
titleSv: rendered.title,
|
||||||
bodySv: rendered.body,
|
bodySv: rendered.body,
|
||||||
data: {
|
data: {
|
||||||
itemId: item.itemId,
|
itemId: item.itemId,
|
||||||
recipeId: recipe.recipeId,
|
recipeId: recipe.recipeId,
|
||||||
ingredientName: item.displayName,
|
ingredientName: item.displayName,
|
||||||
canonicalIngredientId: item.canonicalIngredientId,
|
canonicalIngredientId: item.canonicalIngredientId,
|
||||||
dedupKey: `${item.canonicalIngredientId ?? ""}:${recipe.recipeId}`,
|
dedupKey: `${item.canonicalIngredientId ?? ""}:${recipe.recipeId}`,
|
||||||
},
|
},
|
||||||
templateKey: rendered.templateKey,
|
templateKey: rendered.templateKey,
|
||||||
variables: rendered.variables,
|
variables: rendered.variables,
|
||||||
locale: locale.languageTag,
|
locale: locale.languageTag,
|
||||||
scheduledFor: now,
|
scheduledFor: now,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
});
|
});
|
||||||
created++;
|
emitted = true;
|
||||||
|
created++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Om ingen kandidat kvalificerade går vi vidare till nästa hushåll.
|
||||||
|
if (!emitted) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return created;
|
return created;
|
||||||
|
|||||||
@@ -22,132 +22,108 @@ export interface RenderedProactiveTip {
|
|||||||
variables: ProactiveTipTemplateVariables;
|
variables: ProactiveTipTemplateVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMPLATES: Record<
|
export const TEMPLATES: Record<
|
||||||
ProactiveTipVariant,
|
ProactiveTipVariant,
|
||||||
Record<string, { title: string; body: string }>
|
Record<string, { title: string; body: string }>
|
||||||
> = {
|
> = {
|
||||||
expiring_ingredient: {
|
expiring_ingredient: {
|
||||||
"sv-SE": {
|
"sv-SE": {
|
||||||
title: "Rädda {ingredientName}",
|
title: "Rädda {ingredientName}",
|
||||||
body:
|
body: "{ingredientName} börjar bli slapp — här är ett recept du brukar gilla som använder den.",
|
||||||
"{ingredientName} börjar bli slapp — här är ett recept du brukar gilla som använder den.",
|
|
||||||
},
|
},
|
||||||
"en-US": {
|
"en-US": {
|
||||||
title: "Save the {ingredientName}",
|
title: "Save the {ingredientName}",
|
||||||
body:
|
body: "Your {ingredientName} is getting soft — here's a recipe you usually like that uses it.",
|
||||||
"Your {ingredientName} is getting soft — here's a recipe you usually like that uses it.",
|
|
||||||
},
|
},
|
||||||
"en-GB": {
|
"pt-PT": {
|
||||||
title: "Save the {ingredientName}",
|
title: "Salva {ingredientName}",
|
||||||
body:
|
body: "O teu {ingredientName} está a ficar mole — aqui está uma receita que normalmente gostas e que o usa.",
|
||||||
"Your {ingredientName} is getting soft — here's a recipe you usually like that uses it.",
|
|
||||||
},
|
},
|
||||||
"de-DE": {
|
"de-DE": {
|
||||||
title: "Rette {ingredientName}",
|
title: "Rette {ingredientName}",
|
||||||
body:
|
body: "Dein {ingredientName} wird weich — hier ist ein Rezept, das du normalerweise magst und das es verwendet.",
|
||||||
"Dein {ingredientName} wird weich — hier ist ein Rezept, das du normalerweise magst und das es verwendet.",
|
|
||||||
},
|
},
|
||||||
"fr-FR": {
|
"fr-FR": {
|
||||||
title: "Sauve {ingredientName}",
|
title: "Sauve {ingredientName}",
|
||||||
body:
|
body: "Ton {ingredientName} commence à ramollir — voici une recette que tu aimes habituellement et qui l'utilise.",
|
||||||
"Ton {ingredientName} commence à ramollir — voici une recette que tu aimes habituellement et qui l'utilise.",
|
|
||||||
},
|
},
|
||||||
"es-ES": {
|
"es-ES": {
|
||||||
title: "Salva {ingredientName}",
|
title: "Salva {ingredientName}",
|
||||||
body:
|
body: "Tu {ingredientName} se está poniendo blanda — aquí tienes una receta que sueles gustarte y que la usa.",
|
||||||
"Tu {ingredientName} se está poniendo blanda — aquí tienes una receta que sueles gustarte y que la usa.",
|
|
||||||
},
|
},
|
||||||
"it-IT": {
|
"it-IT": {
|
||||||
title: "Salva {ingredientName}",
|
title: "Salva {ingredientName}",
|
||||||
body:
|
body: "Il tuo {ingredientName} sta diventando molle — ecco una ricetta che di solito ti piace e che la usa.",
|
||||||
"Il tuo {ingredientName} sta diventando molle — ecco una ricetta che di solito ti piace e che la usa.",
|
|
||||||
},
|
},
|
||||||
"pl-PL": {
|
"pl-PL": {
|
||||||
title: "Uratuj {ingredientName}",
|
title: "Uratuj {ingredientName}",
|
||||||
body:
|
body: "Twoje {ingredientName} robi się miękkie — oto przepis, który zwykle lubisz i który go używa.",
|
||||||
"Twoje {ingredientName} robi się miękkie — oto przepis, który zwykle lubisz i który go używa.",
|
|
||||||
},
|
},
|
||||||
"nl-NL": {
|
"nl-NL": {
|
||||||
title: "Red de {ingredientName}",
|
title: "Red de {ingredientName}",
|
||||||
body:
|
body: "Je {ingredientName} wordt zacht — hier is een recept dat je normaal gesproken lekker vindt en dat het gebruikt.",
|
||||||
"Je {ingredientName} wordt zacht — hier is een recept dat je normaal gesproken lekker vindt en dat het gebruikt.",
|
|
||||||
},
|
},
|
||||||
"fi-FI": {
|
"fi-FI": {
|
||||||
title: "Pelasta {ingredientName}",
|
title: "Pelasta {ingredientName}",
|
||||||
body:
|
body: "{ingredientName} alkaa pehmentyä — tässä on resepti, josta yleensä pidät ja jossa sitä käytetään.",
|
||||||
"{ingredientName} alkaa pehmentyä — tässä on resepti, josta yleensä pidät ja jossa sitä käytetään.",
|
|
||||||
},
|
},
|
||||||
"da-DK": {
|
"da-DK": {
|
||||||
title: "Red {ingredientName}",
|
title: "Red {ingredientName}",
|
||||||
body:
|
body: "Din {ingredientName} begynder at blive slap — her er en opskrift, du plejer at kunne lide, og som bruger den.",
|
||||||
"Din {ingredientName} begynder at blive slap — her er en opskrift, du plejer at kunne lide, og som bruger den.",
|
|
||||||
},
|
},
|
||||||
"nb-NO": {
|
"nb-NO": {
|
||||||
title: "Redd {ingredientName}",
|
title: "Redd {ingredientName}",
|
||||||
body:
|
body: "Din {ingredientName} begynner å bli slapp — her er en oppskrift du pleier å like og som bruker den.",
|
||||||
"Din {ingredientName} begynner å bli slapp — her er en oppskrift du pleier å like og som bruker den.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
behavior_mirror: {
|
behavior_mirror: {
|
||||||
"sv-SE": {
|
"sv-SE": {
|
||||||
title: "Söndagsmiddag?",
|
title: "Söndagsmiddag?",
|
||||||
body:
|
body: "Ni brukar ofta laga {recipeTitle} på {dayName} — vill du planera den här veckan?",
|
||||||
"Ni brukar ofta laga {recipeTitle} på {dayName} — vill du planera den här veckan?",
|
|
||||||
},
|
},
|
||||||
"en-US": {
|
"en-US": {
|
||||||
title: "Sunday dinner?",
|
title: "Sunday dinner?",
|
||||||
body:
|
body: "You often cook {recipeTitle} on {dayName} — want to plan it for this week?",
|
||||||
"You often cook {recipeTitle} on {dayName} — want to plan it for this week?",
|
|
||||||
},
|
},
|
||||||
"en-GB": {
|
"pt-PT": {
|
||||||
title: "Sunday dinner?",
|
title: "Jantar de domingo?",
|
||||||
body:
|
body: "Costumas cozinhar {recipeTitle} no {dayName} — queres planear para esta semana?",
|
||||||
"You often cook {recipeTitle} on {dayName} — want to plan it for this week?",
|
|
||||||
},
|
},
|
||||||
"de-DE": {
|
"de-DE": {
|
||||||
title: "Sonntagsessen?",
|
title: "Sonntagsessen?",
|
||||||
body:
|
body: "Ihr kocht {recipeTitle} oft am {dayName} — wollt ihr es diese Woche planen?",
|
||||||
"Ihr kocht {recipeTitle} oft am {dayName} — wollt ihr es diese Woche planen?",
|
|
||||||
},
|
},
|
||||||
"fr-FR": {
|
"fr-FR": {
|
||||||
title: "Dîner dominical ?",
|
title: "Dîner dominical ?",
|
||||||
body:
|
body: "Vous cuisinez souvent {recipeTitle} le {dayName} — envie de la planifier cette semaine ?",
|
||||||
"Vous cuisinez souvent {recipeTitle} le {dayName} — envie de la planifier cette semaine ?",
|
|
||||||
},
|
},
|
||||||
"es-ES": {
|
"es-ES": {
|
||||||
title: "¿Cena del domingo?",
|
title: "¿Cena del domingo?",
|
||||||
body:
|
body: "Soleis cocinar {recipeTitle} los {dayName} — ¿queréis planearla para esta semana?",
|
||||||
"Soleis cocinar {recipeTitle} los {dayName} — ¿queréis planearla para esta semana?",
|
|
||||||
},
|
},
|
||||||
"it-IT": {
|
"it-IT": {
|
||||||
title: "Cena della domenica?",
|
title: "Cena della domenica?",
|
||||||
body:
|
body: "Cuocete spesso {recipeTitle} di {dayName} — volete pianificarla per questa settimana?",
|
||||||
"Cuocete spesso {recipeTitle} di {dayName} — volete pianificarla per questa settimana?",
|
|
||||||
},
|
},
|
||||||
"pl-PL": {
|
"pl-PL": {
|
||||||
title: "Kolacja niedzielna?",
|
title: "Kolacja niedzielna?",
|
||||||
body:
|
body: "Często gotujesz {recipeTitle} w {dayName} — chcesz zaplanować to na ten tydzień?",
|
||||||
"Często gotujesz {recipeTitle} w {dayName} — chcesz zaplanować to na ten tydzień?",
|
|
||||||
},
|
},
|
||||||
"nl-NL": {
|
"nl-NL": {
|
||||||
title: "Zondagsdiner?",
|
title: "Zondagsdiner?",
|
||||||
body:
|
body: "Jullie koken {recipeTitle} vaak op {dayName} — willen jullie het deze week plannen?",
|
||||||
"Jullie koken {recipeTitle} vaak op {dayName} — willen jullie het deze week plannen?",
|
|
||||||
},
|
},
|
||||||
"fi-FI": {
|
"fi-FI": {
|
||||||
title: "Sunnuntaiillallinen?",
|
title: "Sunnuntaiillallinen?",
|
||||||
body:
|
body: "Teette usein {recipeTitle} {dayName} — haluatteko suunnitella sen tälle viikolle?",
|
||||||
"Teette usein {recipeTitle} {dayName} — haluatteko suunnitella sen tälle viikolle?",
|
|
||||||
},
|
},
|
||||||
"da-DK": {
|
"da-DK": {
|
||||||
title: "Søndagsmiddag?",
|
title: "Søndagsmiddag?",
|
||||||
body:
|
body: "I laver ofte {recipeTitle} om {dayName} — vil I planlægge den denne uge?",
|
||||||
"I laver ofte {recipeTitle} om {dayName} — vil I planlægge den denne uge?",
|
|
||||||
},
|
},
|
||||||
"nb-NO": {
|
"nb-NO": {
|
||||||
title: "Søndagsmiddag?",
|
title: "Søndagsmiddag?",
|
||||||
body:
|
body: "Dere lager ofte {recipeTitle} på {dayName} — vil dere planlegge den denne uken?",
|
||||||
"Dere lager ofte {recipeTitle} på {dayName} — vil dere planlegge den denne uken?",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -163,10 +139,7 @@ function resolveLocale(languageTag: string): string {
|
|||||||
return match ?? FALLBACK_LOCALE;
|
return match ?? FALLBACK_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
function interpolate(
|
function interpolate(template: string, variables: ProactiveTipTemplateVariables): string {
|
||||||
template: string,
|
|
||||||
variables: ProactiveTipTemplateVariables,
|
|
||||||
): string {
|
|
||||||
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
||||||
const value = variables[key as keyof ProactiveTipTemplateVariables];
|
const value = variables[key as keyof ProactiveTipTemplateVariables];
|
||||||
return value === undefined ? "" : String(value);
|
return value === undefined ? "" : String(value);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { TEMPLATES } from "../src/templates/proactive-tips.js";
|
||||||
|
import { SUPPORTED_LANGUAGE_TAGS } from "@app/shared-types";
|
||||||
|
|
||||||
|
describe("proactive tip i18n parity", () => {
|
||||||
|
const canonical = new Set<string>(SUPPORTED_LANGUAGE_TAGS);
|
||||||
|
|
||||||
|
for (const [variant, locales] of Object.entries(TEMPLATES)) {
|
||||||
|
it(`${variant} täcker exakt de kanoniska språken utan dubbletter`, () => {
|
||||||
|
const keys = Object.keys(locales);
|
||||||
|
const uniqueKeys = new Set(keys);
|
||||||
|
|
||||||
|
expect(keys).toHaveLength(SUPPORTED_LANGUAGE_TAGS.length);
|
||||||
|
expect(uniqueKeys.size).toBe(keys.length);
|
||||||
|
|
||||||
|
for (const tag of SUPPORTED_LANGUAGE_TAGS) {
|
||||||
|
expect(keys).toContain(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
expect(canonical.has(key)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "pnpm run db:test-setup && vitest run --passWithNoTests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "tsx src/migrate.ts",
|
"db:migrate": "tsx src/migrate.ts",
|
||||||
"db:seed": "tsx src/seed/run.ts",
|
"db:seed": "tsx src/seed/run.ts",
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ if (!connectionString) {
|
|||||||
throw new Error("Sätt DATABASE_URL innan seed körs.");
|
throw new Error("Sätt DATABASE_URL innan seed körs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertTestDatabaseName(url: string): void {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const dbName = parsed.pathname.replace(/^\//, "");
|
||||||
|
if (!/_test$/.test(dbName)) {
|
||||||
|
throw new Error(
|
||||||
|
`[seed] Vägrar nollställa databas "${dbName}". Truncation kräver att databasnamnet slutar på _test.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Näring, allergener och kostnad för recepten beräknas HÄR, deterministiskt,
|
* Näring, allergener och kostnad för recepten beräknas HÄR, deterministiskt,
|
||||||
* ur ingredienserna (spec §61.1–2) – aldrig hårdkodade och aldrig från AI.
|
* ur ingredienserna (spec §61.1–2) – aldrig hårdkodade och aldrig från AI.
|
||||||
@@ -48,6 +58,23 @@ async function main() {
|
|||||||
const { db, pool } = createDatabase(connectionString);
|
const { db, pool } = createDatabase(connectionString);
|
||||||
console.log("[seed] Startar …");
|
console.log("[seed] Startar …");
|
||||||
|
|
||||||
|
// Testläge: nollställ allt användardata så att upprepade körningar blir deterministiska.
|
||||||
|
// Dubbelgrind: (a) --test / SEED_TARGET=test, OCH (b) URL:en pekar på en *_test-databas.
|
||||||
|
if (isTestTarget) {
|
||||||
|
assertTestDatabaseName(connectionString!);
|
||||||
|
console.log("[seed] Nollställer testdatabasen …");
|
||||||
|
const tables = await pool.query<{ tablename: string }>(
|
||||||
|
`SELECT tablename FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND tablename NOT LIKE 'pg_%'
|
||||||
|
AND tablename NOT LIKE 'drizzle_%'`,
|
||||||
|
);
|
||||||
|
for (const { tablename } of tables.rows) {
|
||||||
|
await pool.query(`TRUNCATE TABLE "${tablename}" CASCADE`);
|
||||||
|
}
|
||||||
|
console.log(`[seed] Nollställde ${tables.rows.length} tabeller.`);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Kanoniska ingredienser
|
// 1. Kanoniska ingredienser
|
||||||
for (const ing of SEED_INGREDIENTS) {
|
for (const ing of SEED_INGREDIENTS) {
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "pnpm --filter=@app/database run db:test-setup && vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@app/ai-contracts": "workspace:*",
|
"@app/ai-contracts": "workspace:*",
|
||||||
|
|||||||
@@ -31,6 +31,22 @@ export interface LocaleContext {
|
|||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kanoniska UI-språk (BCP 47) som alla notismallar måste täcka. */
|
||||||
|
export const SUPPORTED_LANGUAGE_TAGS = [
|
||||||
|
"sv-SE",
|
||||||
|
"en-US",
|
||||||
|
"de-DE",
|
||||||
|
"fr-FR",
|
||||||
|
"es-ES",
|
||||||
|
"it-IT",
|
||||||
|
"pl-PL",
|
||||||
|
"nl-NL",
|
||||||
|
"fi-FI",
|
||||||
|
"da-DK",
|
||||||
|
"nb-NO",
|
||||||
|
"pt-PT",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const DEFAULT_LOCALE_PREFERENCES: UserLocalePreferences = {
|
export const DEFAULT_LOCALE_PREFERENCES: UserLocalePreferences = {
|
||||||
languageTag: "sv-SE",
|
languageTag: "sv-SE",
|
||||||
regionCode: "SE",
|
regionCode: "SE",
|
||||||
|
|||||||
+1
-16
@@ -17,22 +17,7 @@
|
|||||||
"env": ["TEST_DATABASE_URL"]
|
"env": ["TEST_DATABASE_URL"]
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": ["^typecheck"],
|
"dependsOn": ["^typecheck", "@app/database#db:test-setup"],
|
||||||
"outputs": [],
|
|
||||||
"env": ["TEST_DATABASE_URL"]
|
|
||||||
},
|
|
||||||
"@app/api#test": {
|
|
||||||
"dependsOn": ["@app/database#db:test-setup"],
|
|
||||||
"outputs": [],
|
|
||||||
"env": ["TEST_DATABASE_URL"]
|
|
||||||
},
|
|
||||||
"@app/database#test": {
|
|
||||||
"dependsOn": ["@app/database#db:test-setup"],
|
|
||||||
"outputs": [],
|
|
||||||
"env": ["TEST_DATABASE_URL"]
|
|
||||||
},
|
|
||||||
"@app/worker#test": {
|
|
||||||
"dependsOn": ["@app/database#db:test-setup"],
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"env": ["TEST_DATABASE_URL"]
|
"env": ["TEST_DATABASE_URL"]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user