This commit is contained in:
Bastian Wagner
2026-06-24 15:36:46 +02:00
parent 94ef484108
commit 0abd2eb45c
2 changed files with 188 additions and 4 deletions

View File

@@ -128,6 +128,70 @@ describe('DashboardService', () => {
expect(secondResponse.weeklySuggestions.suggestions).toHaveLength(1);
});
it('loads the existing weekly snapshot when a parallel request already created it', async () => {
const { weekKey } = currentKeys();
const existingSnapshot: WeeklyListSuggestionSnapshotEntity = {
id: 'weekly-existing',
userId: 'user-1',
weekKey,
timezone: 'UTC',
suggestions: [
{
id: 'suggestion-existing',
title: 'Meal Prep',
items: [{ title: 'Rezepte auswaehlen', required: true }],
reason: 'Schon gespeichert.',
},
],
requestPayload: {},
responsePayload: null,
errorMessage: null,
createdAt: new Date(),
updatedAt: new Date(),
};
listsService.listLists.mockResolvedValue([createList('list-1', 'Einkauf')]);
await saveCurrentDailySnapshot(dailySnapshotsRepository, 'user-1');
jest
.spyOn(weeklySnapshotsRepository, 'findOne')
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(existingSnapshot);
jest.spyOn(weeklySnapshotsRepository, 'save').mockRejectedValueOnce({
code: 'ER_DUP_ENTRY',
errno: 1062,
message:
"Duplicate entry 'user-1-2026-W26' for key 'weekly_list_suggestion_snapshots.IDX_test'",
});
mockMistralResponses([
{
choices: [
{
message: {
content: JSON.stringify({
suggestions: [
{
title: 'Meal Prep',
items: [{ title: 'Rezepte auswaehlen', required: true }],
reason: 'Parallel erzeugt.',
},
],
}),
},
},
],
},
]);
const response = await service.getDashboard('user-1');
expect(response.weeklySuggestions.suggestions).toEqual([
expect.objectContaining({
id: 'suggestion-existing',
title: 'Meal Prep',
}),
]);
});
it('keeps dashboard snapshots isolated per user', async () => {
listsService.listLists.mockImplementation((userId: string) =>
Promise.resolve([createList(`${userId}-list`, `Liste ${userId}`)]),
@@ -357,6 +421,26 @@ async function saveCurrentWeeklySnapshot(
});
}
async function saveCurrentDailySnapshot(
repository: InMemoryRepository<DailyDashboardSnapshotEntity>,
userId: string,
): Promise<void> {
const { dateKey } = currentKeys();
await repository.save({
id: `${userId}-daily`,
userId,
dateKey,
timezone: 'UTC',
selectedLists: [],
requestPayload: {},
responsePayload: null,
errorMessage: null,
createdAt: new Date(),
updatedAt: new Date(),
});
}
function currentKeys(): { dateKey: string; weekKey: string } {
const now = new Date();
const year = now.getUTCFullYear();

View File

@@ -176,7 +176,9 @@ export class DashboardService {
try {
const result = await this.selectImportantListsWithAi(visibleLists);
return this.dailySnapshotsRepository.save(
return this.saveOrLoadDailySnapshot(
userId,
dateKey,
this.dailySnapshotsRepository.create({
id: randomUUID(),
userId,
@@ -189,7 +191,9 @@ export class DashboardService {
}),
);
} catch (error) {
return this.dailySnapshotsRepository.save(
return this.saveOrLoadDailySnapshot(
userId,
dateKey,
this.dailySnapshotsRepository.create({
id: randomUUID(),
userId,
@@ -223,7 +227,9 @@ export class DashboardService {
try {
const result = await this.createWeeklySuggestionsWithAi(visibleLists);
return this.weeklySnapshotsRepository.save(
return this.saveOrLoadWeeklySnapshot(
userId,
weekKey,
this.weeklySnapshotsRepository.create({
id: randomUUID(),
userId,
@@ -236,7 +242,9 @@ export class DashboardService {
}),
);
} catch (error) {
return this.weeklySnapshotsRepository.save(
return this.saveOrLoadWeeklySnapshot(
userId,
weekKey,
this.weeklySnapshotsRepository.create({
id: randomUUID(),
userId,
@@ -255,6 +263,70 @@ export class DashboardService {
}
}
private async saveOrLoadDailySnapshot(
userId: string,
dateKey: string,
snapshot: DailyDashboardSnapshotEntity,
): Promise<DailyDashboardSnapshotEntity> {
try {
return await this.dailySnapshotsRepository.save(snapshot);
} catch (error) {
if (!this.isDuplicateSnapshotError(error)) {
throw error;
}
return this.requireDailySnapshot(userId, dateKey);
}
}
private async saveOrLoadWeeklySnapshot(
userId: string,
weekKey: string,
snapshot: WeeklyListSuggestionSnapshotEntity,
): Promise<WeeklyListSuggestionSnapshotEntity> {
try {
return await this.weeklySnapshotsRepository.save(snapshot);
} catch (error) {
if (!this.isDuplicateSnapshotError(error)) {
throw error;
}
return this.requireWeeklySnapshot(userId, weekKey);
}
}
private async requireDailySnapshot(
userId: string,
dateKey: string,
): Promise<DailyDashboardSnapshotEntity> {
const snapshot = await this.dailySnapshotsRepository.findOne({
where: { userId, dateKey },
});
if (!snapshot) {
throw new Error('Daily dashboard snapshot was not found after conflict.');
}
return snapshot;
}
private async requireWeeklySnapshot(
userId: string,
weekKey: string,
): Promise<WeeklyListSuggestionSnapshotEntity> {
const snapshot = await this.weeklySnapshotsRepository.findOne({
where: { userId, weekKey },
});
if (!snapshot) {
throw new Error(
'Weekly list suggestion snapshot was not found after conflict.',
);
}
return snapshot;
}
private async selectImportantListsWithAi(
lists: UserList[],
): Promise<DashboardAiResult<SelectedListSnapshot[]>> {
@@ -871,4 +943,32 @@ export class DashboardService {
private errorResponsePayload(error: unknown): unknown {
return error instanceof DashboardAiCallError ? error.responsePayload : null;
}
private isDuplicateSnapshotError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const candidate = error as {
code?: unknown;
errno?: unknown;
message?: unknown;
driverError?: {
code?: unknown;
errno?: unknown;
message?: unknown;
};
};
const code = candidate.code ?? candidate.driverError?.code;
const errno = candidate.errno ?? candidate.driverError?.errno;
const message = String(
candidate.message ?? candidate.driverError?.message ?? '',
);
return (
code === 'ER_DUP_ENTRY' ||
errno === 1062 ||
message.includes('Duplicate entry')
);
}
}