feat: Patenschafts-Honig automatisch waehlen (nie cremig, neueste Ernte)
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m18s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m32s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m41s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 20m38s
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m18s
Docker Image bauen und veroeffentlichen / test (push) Successful in 5m32s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 3m41s
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 20m38s
Pakete, Abo-Reservierung und Abo-Lieferungen nehmen immer den aktiven, nicht cremigen Honig der neuesten Ernte mit genug freiem Bestand, bei gleichem Jahrgang den mit dem meisten Bestand. Feste Honig-Zuordnung im Admin entfaellt (Spalte bleibt, wird nicht mehr genutzt). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
3ddf0bfa73
commit
1d88bac52e
30 files changed
+1152
-394
No files matched your search
@@ -3,6 +3,7 @@ import { personalizationSchema, startDateRange, toIsoDate } from "@/lib/certific
|
||||
import { checkCertificateFits } from "@/lib/certificate/layout";
|
||||
import { CURRENT_TEMPLATE_VERSION } from "@/lib/certificate/templates";
|
||||
import type { SponsorshipOfferView } from "@/lib/sponsorship/offers";
|
||||
import { pickHoneyProduct, withTakenStock, type HoneyCandidate } from "@/lib/sponsorship/honey-selection";
|
||||
import type { Locale } from "@/i18n/config";
|
||||
|
||||
/**
|
||||
@@ -13,12 +14,20 @@ import type { Locale } from "@/i18n/config";
|
||||
* - MAIN: die Patenschaft (Traegerprodukt, Regelsteuersatz), traegt die
|
||||
* Personalisierung. Keine Bestandsreservierung — eine Urkunde ist nie
|
||||
* ausverkauft.
|
||||
* - HONEY: der Honiganteil als Position des verknuepften Honigprodukts zum
|
||||
* - HONEY: der Honiganteil als Position eines Honigprodukts zum
|
||||
* Honig-Steuersatz. Dadurch reserviert der Checkout den Honig wie jedes
|
||||
* andere Glas, die Rechnung zeigt die Steueraufteilung, und die
|
||||
* ApiCore-Abbuchung bei Zahlung (recordOrderSale) bucht den Honig ohne
|
||||
* Sonderweg aus dem Bestand.
|
||||
*
|
||||
* Welcher Honig das ist, entscheidet die automatische Wahl im Moment der
|
||||
* Bestellung (honey-selection.ts: nie cremig, neueste Ernte, genug freier
|
||||
* Bestand). Mehrere Pakete in einem Warenkorb werden nacheinander gewaehlt,
|
||||
* jedes gegen den Bestand, den die einzeln gekauften Glaeser und die Pakete
|
||||
* davor uebrig lassen — reicht der erste Honig nicht fuer alle, bekommt das
|
||||
* naechste Paket den naechsten. Das ist einfacher als ein Paket auf zwei
|
||||
* Sorten aufzuteilen, und jedes Paket bleibt sortenrein.
|
||||
*
|
||||
* Bewusst als reine Funktion ohne Datenbankzugriff: der Checkout laedt alles
|
||||
* vorher, und diese Regeln lassen sich ohne Datenbank testen.
|
||||
*/
|
||||
@@ -76,6 +85,11 @@ export interface ExpandInput {
|
||||
products: ReadonlyMap<string, CheckoutProduct>;
|
||||
/** Angebote nach Traegerprodukt-ID. */
|
||||
offers: ReadonlyMap<string, SponsorshipOfferView>;
|
||||
/**
|
||||
* Honig-Kandidaten mit FREIEM Bestand (honey-candidates.ts). Die Produkte
|
||||
* der gewaehlten Kandidaten muessen in `products` stehen.
|
||||
*/
|
||||
honeyCandidates: readonly HoneyCandidate[];
|
||||
vatRateFor: (product: CheckoutProduct) => number;
|
||||
locale: Locale;
|
||||
now: Date;
|
||||
@@ -90,12 +104,31 @@ export function startDateWindow(now: Date): { min: string; max: string } {
|
||||
return startDateRange(toIsoDate(now));
|
||||
}
|
||||
|
||||
/**
|
||||
* Glaeser, die der Warenkorb schon einzeln kauft, je Produkt. Sie gehen vom
|
||||
* freien Bestand ab, bevor ein Paket seinen Honig waehlt — sonst fiele ein
|
||||
* Paket auf denselben Honig, und die Bestandspruefung im Checkout lehnte
|
||||
* die ganze Bestellung ab, obwohl ein anderer Honig gereicht haette.
|
||||
*/
|
||||
function individuallyTakenJars(input: ExpandInput): Map<string, number> {
|
||||
const taken = new Map<string, number>();
|
||||
for (const cartItem of input.cartItems) {
|
||||
const product = input.products.get(cartItem.productId);
|
||||
if (!product || product.category === "PATENSCHAFT") continue;
|
||||
taken.set(product.id, (taken.get(product.id) ?? 0) + cartItem.quantity);
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
export function expandCheckoutItems(input: ExpandInput): ExpandResult {
|
||||
const items: OrderItemDraft[] = [];
|
||||
const reserve: ReservationLine[] = [];
|
||||
let physicalQuantity = 0;
|
||||
let hasSponsorship = false;
|
||||
const window = startDateWindow(input.now);
|
||||
// Verplante Glaeser je Honig: erst die einzeln gekauften, dann wachsend mit
|
||||
// jedem Paket (lokaler Zaehler dieser einen Berechnung).
|
||||
const takenJars = individuallyTakenJars(input);
|
||||
|
||||
for (const cartItem of input.cartItems) {
|
||||
const product = input.products.get(cartItem.productId);
|
||||
@@ -169,10 +202,12 @@ export function expandCheckoutItems(input: ExpandInput): ExpandResult {
|
||||
});
|
||||
|
||||
if (offer.honeyJars > 0) {
|
||||
const honey = offer.honeyProduct ? input.products.get(offer.honeyProduct.id) : undefined;
|
||||
const picked = pickHoneyProduct(withTakenStock(input.honeyCandidates, takenJars), offer.honeyJars);
|
||||
const honey = picked ? input.products.get(picked.id) : undefined;
|
||||
if (!honey || !honey.isActive) {
|
||||
return { ok: false, error: "Diese Patenschaft ist derzeit nicht bestellbar." };
|
||||
return { ok: false, error: "Für dieses Paket ist gerade nicht genug Honig da." };
|
||||
}
|
||||
takenJars.set(honey.id, (takenJars.get(honey.id) ?? 0) + offer.honeyJars);
|
||||
items.push({
|
||||
productId: honey.id,
|
||||
batchId: honey.batchId,
|
||||
|
||||
Reference in new issue
Block a user