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
@@ -0,0 +1,63 @@
|
||||
-- Bestandsabgleich mit ApiCore (ApiCore/docs/superpowers/specs/2026-10-02-bestandsabgleich-design.md):
|
||||
-- ApiCore fuehrt den Restbestand je Charge; Wabenhain meldet bezahlte
|
||||
-- Verkaeufe und Rueckbuchungen ueber eine Warteschlange und gleicht seinen
|
||||
-- Shop-Bestand daran ab. Rein additiv.
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ProductCategory" ADD VALUE 'SONSTIGES';
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ApiCoreStockEventKind" AS ENUM ('SALE', 'RETURN');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "products" ADD COLUMN "apicoreProductId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "order_items" ADD COLUMN "apicoreAllocations" JSONB;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "apicore_stock_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "ApiCoreStockEventKind" NOT NULL,
|
||||
"orderItemId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"apicoreBottlingId" TEXT,
|
||||
"apicoreProductId" TEXT,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"reversesEventId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"rejectedAt" TIMESTAMP(3),
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastError" TEXT,
|
||||
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "apicore_stock_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "products_apicoreProductId_key" ON "products"("apicoreProductId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "apicore_stock_events_sentAt_rejectedAt_nextAttemptAt_idx" ON "apicore_stock_events"("sentAt", "rejectedAt", "nextAttemptAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "apicore_stock_events_orderItemId_idx" ON "apicore_stock_events"("orderItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "apicore_stock_events_productId_idx" ON "apicore_stock_events"("productId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "apicore_stock_events" ADD CONSTRAINT "apicore_stock_events_orderItemId_fkey" FOREIGN KEY ("orderItemId") REFERENCES "order_items"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "apicore_stock_events" ADD CONSTRAINT "apicore_stock_events_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "apicore_stock_events" ADD CONSTRAINT "apicore_stock_events_reversesEventId_fkey" FOREIGN KEY ("reversesEventId") REFERENCES "apicore_stock_events"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Genau ein ApiCore-Ziel je Buchung.
|
||||
ALTER TABLE "apicore_stock_events" ADD CONSTRAINT "apicore_stock_events_exactly_one_target"
|
||||
CHECK (("apicoreBottlingId" IS NULL) <> ("apicoreProductId" IS NULL));
|
||||
ALTER TABLE "apicore_stock_events" ADD CONSTRAINT "apicore_stock_events_quantity_positive"
|
||||
CHECK ("quantity" > 0);
|
||||
Reference in new issue
Block a user