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
@@ -212,6 +212,7 @@ enum ProductCategory {
|
||||
HONIG
|
||||
KERZEN
|
||||
PROPOLIS
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
model Product {
|
||||
@@ -235,6 +236,12 @@ model Product {
|
||||
/// bleiben ("waldhonig-2026"), darf sich also nicht nach einer fremden ID
|
||||
/// richten.
|
||||
apicoreBottlingId String? @unique
|
||||
/// ApiCore-ID des ARTIKELS fuer hergestellte Waren (Kerzen, Propolis-
|
||||
/// Produkte, ...): deren Bestand liegt in ApiCore je Herstellungscharge und
|
||||
/// wird als Summe gefuehrt, ein Shop-Produkt je Artikel. Honig nutzt
|
||||
/// stattdessen apicoreBottlingId. Hoechstens eines von beiden ist gesetzt;
|
||||
/// ist eines gesetzt, kommt der Bestand aus ApiCore (lib/apicore-stock).
|
||||
apicoreProductId String? @unique
|
||||
/// Nur fuer Honig-Produkte gesetzt (Kerzen/Propolis sind keiner Ernte/Charge zugeordnet)
|
||||
harvestId String?
|
||||
batchId String?
|
||||
@@ -280,6 +287,7 @@ model Product {
|
||||
inventory Inventory?
|
||||
orderItems OrderItem[]
|
||||
reviews Review[]
|
||||
apicoreStockEvents ApiCoreStockEvent[]
|
||||
wishlistItems WishlistItem[]
|
||||
stockNotifications StockNotification[]
|
||||
|
||||
@@ -525,9 +533,16 @@ model OrderItem {
|
||||
/// feststellbar ist, welcher Satz gegolten haette.
|
||||
vatRatePercent Int?
|
||||
|
||||
/// Aus welchen ApiCore-Chargen die verkaufte Ware stammt, wie ApiCore sie
|
||||
/// beim Verbuchen zugeordnet hat: [{ lotNumber, quantity }]. Fuer
|
||||
/// Rueckverfolgung und Rueckruf; null, solange der Verkauf noch nicht
|
||||
/// bestaetigt ist oder das Produkt nicht mit ApiCore verknuepft ist.
|
||||
apicoreAllocations Json?
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||
batch Batch? @relation(fields: [batchId], references: [id], onDelete: Restrict)
|
||||
apicoreStockEvents ApiCoreStockEvent[]
|
||||
|
||||
/// Wird bei jedem Laden der Positionen einer Bestellung (Konto, Admin,
|
||||
/// Rechnung) ueber die order.items-Relation abgefragt.
|
||||
@@ -551,6 +566,55 @@ model OrderItem {
|
||||
@@map("order_items")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bestandsabgleich mit ApiCore
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum ApiCoreStockEventKind {
|
||||
SALE
|
||||
RETURN
|
||||
}
|
||||
|
||||
/// Warteschlange der Bestandsbuchungen an ApiCore (lib/apicore-stock).
|
||||
///
|
||||
/// Geschrieben in DERSELBEN Transaktion wie die ausloesende Statusaenderung
|
||||
/// (bezahlt, storniert, erstattet), gesendet danach — sofort und per
|
||||
/// CronJob nachgereicht. So geht keine Buchung verloren, wenn ApiCore gerade
|
||||
/// nicht erreichbar ist. Die id geht als externalId an ApiCore; ApiCore bucht
|
||||
/// jede id nur einmal, Wiederholungen sind also gefahrlos.
|
||||
model ApiCoreStockEvent {
|
||||
id String @id @default(cuid())
|
||||
kind ApiCoreStockEventKind
|
||||
orderItemId String
|
||||
/// Wabenhain-Produkt, dessen Shop-Bestand von dieser Buchung abhaengt.
|
||||
productId String
|
||||
/// Ziel in ApiCore: eine Abfuellung (Honig) ODER ein Artikel (FIFO ueber
|
||||
/// dessen Herstellungschargen).
|
||||
apicoreBottlingId String?
|
||||
apicoreProductId String?
|
||||
quantity Int
|
||||
/// Bei RETURN: der Verkauf, der zurueckgebucht wird.
|
||||
reversesEventId String?
|
||||
createdAt DateTime @default(now())
|
||||
/// Von ApiCore bestaetigt (neu gebucht oder schon bekannt).
|
||||
sentAt DateTime?
|
||||
/// Von ApiCore abgelehnt — wird nicht wiederholt, das Dashboard zeigt es.
|
||||
rejectedAt DateTime?
|
||||
attempts Int @default(0)
|
||||
lastError String?
|
||||
nextAttemptAt DateTime @default(now())
|
||||
|
||||
orderItem OrderItem @relation(fields: [orderItemId], references: [id], onDelete: Restrict)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
|
||||
reverses ApiCoreStockEvent? @relation("StockEventReversal", fields: [reversesEventId], references: [id], onDelete: Restrict)
|
||||
reversedBy ApiCoreStockEvent[] @relation("StockEventReversal")
|
||||
|
||||
@@index([sentAt, rejectedAt, nextAttemptAt])
|
||||
@@index([orderItemId])
|
||||
@@index([productId])
|
||||
@@map("apicore_stock_events")
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
SUCCEEDED
|
||||
|
||||
Reference in new issue
Block a user