fix: Ernte-Abos reservieren das naechste Fenster nach laufenden Abos
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m10s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m38s
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m14s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 12m1s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m10s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m38s
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m14s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 12m1s
Bedarf je Erntefenster = max(geplante Glaeser, laufende Abos x Glaeser), und nur das naechste Fenster je Angebot. Vorher hielten alle kuenftigen Fenster ihre geplante Menge sofort fest, und ohne Planung blieben neue Ernte-Abos unreserviert. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c4b0212004
commit
68fe297da6
2 files changed
+63
-10
No files matched your search
@@ -35,14 +35,21 @@ function productRow(product: FakeProduct) {
|
||||
|
||||
function fakeDb(data: {
|
||||
quarterly?: { offer: { honeyJars: number } }[];
|
||||
windows?: { offerId: string; startsOn: Date; endsOn: Date; plannedJars: number }[];
|
||||
windows?: { offerId: string; startsOn: Date; endsOn: Date; plannedJars: number; offer?: { honeyJars: number } }[];
|
||||
deliveries?: { jars: number; dueAt: Date; subscription: { offerId: string } }[];
|
||||
/** Laufende Abos je Ernte-Angebot (groupBy). */
|
||||
ernteActive?: { offerId: string; count: number }[];
|
||||
products?: FakeProduct[];
|
||||
inventory?: { productId: string; quantity: number }[];
|
||||
}) {
|
||||
return {
|
||||
sponsorshipSubscription: { findMany: vi.fn(async () => data.quarterly ?? []) },
|
||||
harvestWindow: { findMany: vi.fn(async () => data.windows ?? []) },
|
||||
sponsorshipSubscription: {
|
||||
findMany: vi.fn(async () => data.quarterly ?? []),
|
||||
groupBy: vi.fn(async () => (data.ernteActive ?? []).map((row) => ({ offerId: row.offerId, _count: { _all: row.count } }))),
|
||||
},
|
||||
harvestWindow: {
|
||||
findMany: vi.fn(async () => (data.windows ?? []).map((window) => ({ offer: { honeyJars: 3 }, ...window }))),
|
||||
},
|
||||
subscriptionDelivery: { findMany: vi.fn(async () => data.deliveries ?? []) },
|
||||
product: { findMany: vi.fn(async () => (data.products ?? []).map(productRow)) },
|
||||
inventory: {
|
||||
@@ -82,6 +89,28 @@ describe("subscriptionReservedJars", () => {
|
||||
expect(await subscriptionReservedJars(NOW, db as unknown as Db)).toBe(2 + 2 + 3 + (10 - 4));
|
||||
});
|
||||
|
||||
test("Erntefenster: laufende Abos reservieren mindestens ihren Bedarf, auch ohne Planung", async () => {
|
||||
const db = fakeDb({
|
||||
windows: [
|
||||
{ offerId: "ernte", startsOn: new Date("2026-10-01T00:00:00.000Z"), endsOn: new Date("2026-10-31T00:00:00.000Z"), plannedJars: 0 },
|
||||
],
|
||||
ernteActive: [{ offerId: "ernte", count: 4 }],
|
||||
});
|
||||
|
||||
expect(await subscriptionReservedJars(NOW, db as unknown as Db)).toBe(4 * 3);
|
||||
});
|
||||
|
||||
test("Erntefenster: nur das naechste Fenster je Angebot zaehlt", async () => {
|
||||
const db = fakeDb({
|
||||
windows: [
|
||||
{ offerId: "ernte", startsOn: new Date("2026-10-01T00:00:00.000Z"), endsOn: new Date("2026-10-31T00:00:00.000Z"), plannedJars: 6 },
|
||||
{ offerId: "ernte", startsOn: new Date("2027-06-01T00:00:00.000Z"), endsOn: new Date("2027-06-30T00:00:00.000Z"), plannedJars: 30 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(await subscriptionReservedJars(NOW, db as unknown as Db)).toBe(6);
|
||||
});
|
||||
|
||||
test("ein ueberliefertes Erntefenster reserviert nichts mehr (nie negativ)", async () => {
|
||||
const db = fakeDb({
|
||||
windows: [
|
||||
|
||||
@@ -60,29 +60,53 @@ export async function subscriptionReservedJars(now: Date = new Date(), db: Db =
|
||||
const quarterlyJars = quarterly.reduce((sum, subscription) => sum + subscription.offer.honeyJars, 0);
|
||||
|
||||
const today = startOfUtcDay(now);
|
||||
const windows = await db.harvestWindow.findMany({
|
||||
const allWindows = await db.harvestWindow.findMany({
|
||||
where: { endsOn: { gte: today }, offer: { cycle: "ERNTE" } },
|
||||
select: { offerId: true, startsOn: true, endsOn: true, plannedJars: true },
|
||||
select: { offerId: true, startsOn: true, endsOn: true, plannedJars: true, offer: { select: { honeyJars: true } } },
|
||||
orderBy: { startsOn: "asc" },
|
||||
});
|
||||
if (windows.length === 0) return quarterlyJars;
|
||||
// Nur das jeweils NAECHSTE Fenster je Angebot zaehlt: jedes Abo bekommt
|
||||
// hoechstens eine Lieferung je Fenster, und spaetere Fenster werden aus
|
||||
// spaeterer Ernte bedient. Alle offenen Fenster zu reservieren, hielte heute
|
||||
// Honig fuer eine Ernte fest, die es noch gar nicht gibt.
|
||||
const nextByOffer = new Map<string, (typeof allWindows)[number]>();
|
||||
for (const window of allWindows) if (!nextByOffer.has(window.offerId)) nextByOffer.set(window.offerId, window);
|
||||
if (nextByOffer.size === 0) return quarterlyJars;
|
||||
|
||||
// Bedarf des Fensters: was der Inhaber geplant hat, mindestens aber, was
|
||||
// die laufenden Abos dieses Angebots im Fenster beanspruchen koennen. So
|
||||
// bleibt kein neues Abo ohne Honig, nur weil die Planung noch nicht
|
||||
// nachgezogen wurde.
|
||||
const ernteSubscriptions = await db.sponsorshipSubscription.groupBy({
|
||||
by: ["offerId"],
|
||||
where: {
|
||||
status: { in: [...RESERVING_STATUSES] },
|
||||
NOT: { status: "IM_VERZUG", payments: { some: { reversedAt: { not: null } } } },
|
||||
offerId: { in: [...nextByOffer.keys()] },
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
const activeByOffer = new Map(ernteSubscriptions.map((group) => [group.offerId, group._count._all]));
|
||||
|
||||
// Alle in den offenen Fenstern bereits angelegten Lieferungen in einer
|
||||
// Abfrage; die Zuordnung zum Fenster geschieht hier im Speicher.
|
||||
const earliest = new Date(Math.min(...windows.map((window) => window.startsOn.getTime())));
|
||||
const nextWindows = [...nextByOffer.values()];
|
||||
const earliest = new Date(Math.min(...nextWindows.map((window) => window.startsOn.getTime())));
|
||||
const deliveries = await db.subscriptionDelivery.findMany({
|
||||
where: {
|
||||
status: "ANGELEGT",
|
||||
subscription: { offerId: { in: [...new Set(windows.map((window) => window.offerId))] } },
|
||||
subscription: { offerId: { in: [...nextByOffer.keys()] } },
|
||||
dueAt: { gte: earliest },
|
||||
},
|
||||
select: { jars: true, dueAt: true, subscription: { select: { offerId: true } } },
|
||||
});
|
||||
const harvestJars = windows.reduce((sum, window) => {
|
||||
const harvestJars = nextWindows.reduce((sum, window) => {
|
||||
const until = endOfUtcDay(window.endsOn);
|
||||
const delivered = deliveries
|
||||
.filter((d) => d.subscription.offerId === window.offerId && d.dueAt >= window.startsOn && d.dueAt <= until)
|
||||
.reduce((jars, d) => jars + d.jars, 0);
|
||||
return sum + Math.max(0, window.plannedJars - delivered);
|
||||
const needed = Math.max(window.plannedJars, (activeByOffer.get(window.offerId) ?? 0) * window.offer.honeyJars);
|
||||
return sum + Math.max(0, needed - delivered);
|
||||
}, 0);
|
||||
return quarterlyJars + harvestJars;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user