feat(stock): keep shop stock in sync with ApiCore, write sales back
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m40s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m40s
ApiCore now owns the remaining stock per batch; the shop sells from it and reports every paid sale and every return back. - Outbox apicore_stock_events, written in the same transaction as the status change: paid -> SALE, cancelled-after-payment / refunded return -> RETURN onto the batches of the sale, reopened paid order -> SALE. Whether a return is due is decided by the open sales in the outbox, so unpaid orders never book and nothing is booked twice. - Delivery right after the response (next/server after()) plus CronJob wabenhain-apicore-stock every 10 minutes with 1/5/15/60 min backoff; ApiCore deduplicates by event id. Rejected bookings stop and show up on the dashboard. - Reconcile: shop stock = ApiCore remaining - unconfirmed bookings - reservations of unpaid orders, in a serializable transaction like the checkout. Triggered after bookings, by the signed ApiCore webhook stock.changed (ids only, numbers are fetched) and by the sync button. - Manufactured goods (candles, propolis products, ...): one shop product per ApiCore article via Product.apicoreProductId, drafts created by the sync, existing products linkable in the product form; new category SONSTIGES. Stock field is read-only for linked products. - OrderItem.apicoreAllocations keeps which lots were sold (recalls). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
641afa1ae4
commit
93b589ff04
39 files changed
+1400
-37
No files matched your search
@@ -80,6 +80,18 @@ const db = {
|
||||
|
||||
vi.mock("@/lib/prisma", () => ({ prisma: db }));
|
||||
|
||||
// Bestandsabgleich mit ApiCore: hier nur, DASS die bezahlte Bestellung als
|
||||
// Verkauf in die Warteschlange geht; die Buchungslogik selbst testet
|
||||
// lib/apicore-stock.
|
||||
const recordOrderSale = vi.fn(async () => 0);
|
||||
const scheduleApiCoreStockSync = vi.fn();
|
||||
vi.mock("@/lib/apicore-stock/stock-events", () => ({
|
||||
recordOrderSale: (...args: unknown[]) => recordOrderSale(...(args as [])),
|
||||
}));
|
||||
vi.mock("@/lib/apicore-stock/schedule", () => ({
|
||||
scheduleApiCoreStockSync: (...args: unknown[]) => scheduleApiCoreStockSync(...(args as [])),
|
||||
}));
|
||||
|
||||
const { POST } = await import("./route");
|
||||
|
||||
const ORDER_ID = "order_1";
|
||||
|
||||
@@ -11,6 +11,8 @@ import { invoiceRelativePath, storeInvoicePdf } from "@/lib/invoice-storage";
|
||||
import { formatInvoiceNumber } from "@/lib/invoice-number";
|
||||
import { isLocale, defaultLocale } from "@/i18n/config";
|
||||
import type Stripe from "stripe";
|
||||
import { recordOrderSale } from "@/lib/apicore-stock/stock-events";
|
||||
import { scheduleApiCoreStockSync } from "@/lib/apicore-stock/schedule";
|
||||
|
||||
/**
|
||||
* Erzeugt das Rechnungs-PDF, legt es nach Jahr und Kunde sortiert ab und
|
||||
@@ -323,6 +325,12 @@ async function fulfillOrder(session: Stripe.Checkout.Session): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// Bezahlt = verkauft: die Ware in ApiCore abbuchen (Warteschlange, in
|
||||
// derselben Transaktion wie der Statuswechsel). recordOrderSale bucht
|
||||
// je Position hoechstens einen offenen Verkauf, ein erneut zugestellter
|
||||
// Webhook legt also nichts doppelt an.
|
||||
await recordOrderSale(tx, orderId);
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
@@ -330,6 +338,8 @@ async function fulfillOrder(session: Stripe.Checkout.Session): Promise<void> {
|
||||
// (z.B. bereits storniert) — nichts weiter zu tun.
|
||||
if (!order) return;
|
||||
|
||||
scheduleApiCoreStockSync(order.items.map((item) => item.productId));
|
||||
|
||||
// Abgleich zwischen dem, was wir berechnet haben, und dem, was Stripe
|
||||
// tatsaechlich eingezogen hat. Beide Betraege stammen aus derselben Quelle
|
||||
// (Positionspreise aus der Datenbank, Versand aus der Mengenstaffel, Rabatt
|
||||
|
||||
Reference in new issue
Block a user