Files
Wabenhain/src/lib/apicore-stock/dashboard.ts
T
Nick SzaunigandClaude Opus 5.5 93b589ff04
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m40s
feat(stock): keep shop stock in sync with ApiCore, write sales back
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]>
2026-10-05 21:16:54 +02:00

29 lines
1.2 KiB
TypeScript

import { prisma } from "@/lib/prisma";
import { OVERDUE_AFTER_MS } from "./stock-math";
export interface ApiCoreStockIssues {
/** Seit ueber 15 Minuten nicht bei ApiCore angekommen. */
overdue: number;
/** Von ApiCore abgelehnt — braucht einen Menschen. */
rejected: number;
/** Aktive Produkte, deren Bestand noch von Hand gepflegt wird. */
unlinked: number;
}
/**
* Stoerungen des Bestandsabgleichs fuer das Admin-Dashboard. Ohne
* ApiCore-Konfiguration gibt es nichts zu melden.
*/
export async function countApiCoreStockIssues(now: Date = new Date()): Promise<ApiCoreStockIssues> {
const configured = Boolean(process.env.APICORE_API_URL?.trim() && process.env.APICORE_API_KEY?.trim());
if (!configured) return { overdue: 0, rejected: 0, unlinked: 0 };
const [overdue, rejected, unlinked] = await Promise.all([
prisma.apiCoreStockEvent.count({
where: { sentAt: null, rejectedAt: null, createdAt: { lt: new Date(now.getTime() - OVERDUE_AFTER_MS) } },
}),
prisma.apiCoreStockEvent.count({ where: { rejectedAt: { not: null } } }),
prisma.product.count({ where: { isActive: true, apicoreBottlingId: null, apicoreProductId: null } }),
]);
return { overdue, rejected, unlinked };
}