Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m24s
Abgeschlossene Eintraege wandern automatisch in einen Tab "Archiv", damit die aktiven Listen nur zeigen, was noch zu tun ist: - Bestellungen: versendet/abgeholt/storniert, keine offene Retoure, seit einem Monat nicht bearbeitet - Retouren: angenommen/abgelehnt/erstattet, seit einem Monat nicht bearbeitet - Produkte: seit einem Monat Bestand 0 (nur Admin-Sicht, Shop unveraendert) Die Automatik laeuft beim Oeffnen der jeweiligen Liste (hoechstens alle 5 Minuten) und holt Eintraege zurueck, sobald sie wieder offen sind. Zusaetzlich "Archivieren"/"Wiederherstellen" per Hand; von Hand archivierte Eintraege fasst die Automatik nicht an. Alle Verschiebungen landen im Audit-Log. Neue Felder archivedAt/archivedManually samt Index an orders, return_requests und products (Migration 20260925010000_archive_fields). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import {
|
|
ARCHIVE_AFTER_DAYS,
|
|
archiveCutoff,
|
|
orderArchiveWhere,
|
|
orderUnarchiveWhere,
|
|
productArchiveWhere,
|
|
productUnarchiveWhere,
|
|
returnArchiveWhere,
|
|
returnUnarchiveWhere,
|
|
runAutoArchive,
|
|
type ArchiveDb,
|
|
} from "./archive";
|
|
|
|
const NOW = new Date("2026-09-25T12:00:00Z");
|
|
|
|
function fakeDb(counts: { archived: number; restored: number }) {
|
|
const model = () => ({
|
|
updateMany: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ count: counts.archived })
|
|
.mockResolvedValueOnce({ count: counts.restored }),
|
|
});
|
|
return { order: model(), returnRequest: model(), product: model() };
|
|
}
|
|
|
|
describe("archiveCutoff", () => {
|
|
it("liegt genau einen Monat (30 Tage) vor jetzt", () => {
|
|
expect(ARCHIVE_AFTER_DAYS).toBe(30);
|
|
expect(archiveCutoff(NOW).toISOString()).toBe("2026-08-26T12:00:00.000Z");
|
|
});
|
|
});
|
|
|
|
describe("orderArchiveWhere", () => {
|
|
const where = orderArchiveWhere(archiveCutoff(NOW));
|
|
|
|
it("archiviert nur versendete, abgeholte und stornierte Bestellungen", () => {
|
|
expect(where.status).toEqual({ in: ["VERSENDET", "ABGEHOLT", "STORNIERT"] });
|
|
});
|
|
|
|
it("verlangt, dass die letzte Bearbeitung mehr als einen Monat zurueckliegt", () => {
|
|
expect(where.updatedAt).toEqual({ lte: archiveCutoff(NOW) });
|
|
expect(where.archivedAt).toBeNull();
|
|
});
|
|
|
|
it("laesst Bestellungen mit offener oder frisch bearbeiteter Retoure aktiv", () => {
|
|
expect(where.OR).toEqual([
|
|
{ returnRequest: { is: null } },
|
|
{
|
|
returnRequest: {
|
|
is: { status: { in: ["ANGENOMMEN", "ABGELEHNT", "ERSTATTET"] }, updatedAt: { lte: archiveCutoff(NOW) } },
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("orderUnarchiveWhere", () => {
|
|
it("holt nur automatisch archivierte Bestellungen zurueck, wenn Status oder Retoure wieder offen sind", () => {
|
|
expect(orderUnarchiveWhere()).toEqual({
|
|
archivedAt: { not: null },
|
|
archivedManually: false,
|
|
OR: [
|
|
{ status: { notIn: ["VERSENDET", "ABGEHOLT", "STORNIERT"] } },
|
|
{ returnRequest: { is: { status: "ANGEFRAGT" } } },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("returnArchiveWhere / returnUnarchiveWhere", () => {
|
|
it("archiviert angenommene, abgelehnte und erstattete Retouren nach einem Monat ohne Bearbeitung", () => {
|
|
expect(returnArchiveWhere(archiveCutoff(NOW))).toEqual({
|
|
archivedAt: null,
|
|
status: { in: ["ANGENOMMEN", "ABGELEHNT", "ERSTATTET"] },
|
|
updatedAt: { lte: archiveCutoff(NOW) },
|
|
});
|
|
});
|
|
|
|
it("holt eine automatisch archivierte Retoure zurueck, sobald sie wieder offen ist", () => {
|
|
expect(returnUnarchiveWhere()).toEqual({
|
|
archivedAt: { not: null },
|
|
archivedManually: false,
|
|
status: { notIn: ["ANGENOMMEN", "ABGELEHNT", "ERSTATTET"] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("productArchiveWhere / productUnarchiveWhere", () => {
|
|
it("archiviert Produkte, die seit ueber einem Monat Bestand 0 haben und nicht bearbeitet wurden", () => {
|
|
const cutoff = archiveCutoff(NOW);
|
|
expect(productArchiveWhere(cutoff)).toEqual({
|
|
archivedAt: null,
|
|
updatedAt: { lte: cutoff },
|
|
OR: [{ inventory: { is: null } }, { inventory: { is: { quantity: { lte: 0 }, updatedAt: { lte: cutoff } } } }],
|
|
});
|
|
});
|
|
|
|
it("holt ein automatisch archiviertes Produkt zurueck, sobald wieder Bestand da ist", () => {
|
|
expect(productUnarchiveWhere()).toEqual({
|
|
archivedAt: { not: null },
|
|
archivedManually: false,
|
|
inventory: { is: { quantity: { gt: 0 } } },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("runAutoArchive", () => {
|
|
it("archiviert faellige Eintraege und holt wieder offene zurueck, jeweils nur fuer die angefragte Liste", async () => {
|
|
const db = fakeDb({ archived: 3, restored: 1 });
|
|
|
|
const result = await runAutoArchive(db as unknown as ArchiveDb, "orders", NOW);
|
|
|
|
expect(result).toEqual({ archived: 3, restored: 1 });
|
|
expect(db.order.updateMany).toHaveBeenNthCalledWith(1, {
|
|
where: orderArchiveWhere(archiveCutoff(NOW)),
|
|
data: { archivedAt: NOW, archivedManually: false },
|
|
});
|
|
expect(db.order.updateMany).toHaveBeenNthCalledWith(2, {
|
|
where: orderUnarchiveWhere(),
|
|
data: { archivedAt: null },
|
|
});
|
|
expect(db.product.updateMany).not.toHaveBeenCalled();
|
|
expect(db.returnRequest.updateMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("nutzt fuer Retouren und Produkte die jeweils eigenen Regeln", async () => {
|
|
const db = fakeDb({ archived: 0, restored: 0 });
|
|
|
|
await runAutoArchive(db as unknown as ArchiveDb, "returns", NOW);
|
|
await runAutoArchive(db as unknown as ArchiveDb, "products", NOW);
|
|
|
|
expect(db.returnRequest.updateMany.mock.calls[0][0].where).toEqual(returnArchiveWhere(archiveCutoff(NOW)));
|
|
expect(db.product.updateMany.mock.calls[0][0].where).toEqual(productArchiveWhere(archiveCutoff(NOW)));
|
|
});
|
|
});
|