feat(catalog): persist idempotent bulk operations and retryable deliveries
CI / check (push) Successful in 3m7s
CI / deploy (push) Successful in 1m37s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-13 18:05:03 +02:00
1 parent 76e46d295c
commit 13665e0c3c
53 files changed
+1316 -76

No files matched your search

+59 -29
View File
@@ -8,51 +8,52 @@ import {
undoBulkOffersCommand,
} from "@/features/catalog/server/bulk-offers";
import { catalogFailure } from "@/features/catalog/server/errors";
import { sendCatalogUpdate } from "@/features/catalog/server/sync-status";
import { OperationConflict } from "@/features/operations/model";
import { requirePermission } from "@/lib/admin/guard";
import { logger } from "@/lib/logger";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
export async function previewBulkOffers(input: BulkOfferInput) {
await requirePermission(PERMS.CATALOG_VIEW);
try {
return { ok: true as const, data: await previewBulkOffersCommand(input) };
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
return {
ok: false as const,
error:
error instanceof OperationConflict
? error.message
: catalogFailure(error).message,
};
}
}
export async function applyBulkOffers(
input: BulkOfferInput,
fingerprint: string,
requestKey?: string,
) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
try {
return await withCatalogExport(async () => {
const data = await applyBulkOffersCommand(input, fingerprint, staff.id);
const data = await applyBulkOffersCommand(
input,
fingerprint,
staff.id,
requestKey,
);
if (data.changedCount > 0) {
await logAudit({
userId: staff.id,
action: "catalog_offers_bulk_update",
target: "catalog",
after: {
ids: input.ids,
changes: input.changes,
changedCount: data.changedCount,
},
}).catch((error) =>
logger.error("Catalog bulk update committed; audit write failed", {
module: "catalog",
error,
}),
);
await sendCatalogUpdate();
revalidatePath("/admin/catalog", "layout");
refreshCatalog();
}
return { ok: true as const, data };
});
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
return {
ok: false as const,
error:
error instanceof OperationConflict
? error.message
: catalogFailure(error).message,
};
}
}
@@ -64,21 +65,50 @@ export async function getBulkOfferDestinations() {
data: await listBulkOfferDestinationsCommand(),
};
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
return {
ok: false as const,
error:
error instanceof OperationConflict
? error.message
: catalogFailure(error).message,
};
}
}
export async function undoBulkOffers(historyIds: number[]) {
export async function undoBulkOffers(
historyIds: number[],
requestKey?: string,
) {
const staff = await requirePermission(PERMS.CATALOG_EDIT);
try {
return await withCatalogExport(async () => {
const data = await undoBulkOffersCommand(historyIds, staff.id);
await sendCatalogUpdate();
revalidatePath("/admin/catalog", "layout");
revalidatePath("/admin/logs/audit");
const data = await undoBulkOffersCommand(
historyIds,
staff.id,
requestKey,
);
refreshCatalog();
return { ok: true as const, data };
});
} catch (error) {
return { ok: false as const, error: catalogFailure(error).message };
return {
ok: false as const,
error:
error instanceof OperationConflict
? error.message
: catalogFailure(error).message,
};
}
}
function refreshCatalog() {
try {
revalidatePath("/admin/catalog", "layout");
revalidatePath("/admin/logs/audit");
} catch (error) {
logger.error("Catalog committed; page invalidation failed", {
module: "catalog",
error,
});
}
}