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
@@ -1,6 +1,7 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { asStringArray } from "@/lib/json-array";
|
||||
import { availableForSaleByProduct } from "@/lib/sponsorship-subscription/reservation";
|
||||
import { loadHoneyCandidates } from "@/lib/sponsorship/honey-candidates";
|
||||
import { pickHoneyProduct, type HoneyCandidate } from "@/lib/sponsorship/honey-selection";
|
||||
import { quotaSubscriptionWhere } from "@/lib/sponsorship-subscription/pending";
|
||||
import type { Locale } from "@/i18n/config";
|
||||
import type {
|
||||
@@ -14,11 +15,17 @@ import type {
|
||||
* Warenkorb und Checkout sie brauchen: lokalisiert, mit Leistungsliste als
|
||||
* Array und mit der Aussage, ob das Angebot gerade bestellbar ist.
|
||||
*
|
||||
* "Bestellbar" haengt am Honig: ein physisches Paket ohne genug Glaeser des
|
||||
* verknuepften Honigprodukts darf nicht verkauft werden — sonst bekaeme die
|
||||
* Kundschaft eine Urkunde ohne den bezahlten Honig. Gezaehlt wird der FREIE
|
||||
* Bestand, also abzueglich der fuer Abos reservierten Glaeser. Bei Abos
|
||||
* zaehlt zusaetzlich das Kontingent (maxActiveSubscriptions).
|
||||
* "Bestellbar" haengt am Honig: ein Angebot mit Glaesern braucht einen Honig,
|
||||
* den die automatische Wahl nehmen darf (nie cremig, neueste Ernte, genug
|
||||
* FREIER Bestand, also abzueglich der fuer Abos reservierten Glaeser —
|
||||
* lib/sponsorship/honey-selection.ts). Gibt es keinen, ist das Angebot nicht
|
||||
* bestellbar — sonst bekaeme die Kundschaft eine Urkunde ohne den bezahlten
|
||||
* Honig. Bei Abos zaehlt zusaetzlich das Kontingent (maxActiveSubscriptions).
|
||||
*
|
||||
* Einen fest verknuepften Honig je Angebot gibt es nicht mehr: die Spalte
|
||||
* SponsorshipOffer.honeyProductId bleibt (nullable) im Schema stehen, um
|
||||
* keine Migration auf der laufenden Produktion zu brauchen, wird aber weder
|
||||
* gelesen noch geschrieben.
|
||||
*/
|
||||
export interface SponsorshipOfferView {
|
||||
id: string;
|
||||
@@ -37,26 +44,26 @@ export interface SponsorshipOfferView {
|
||||
medalTier: MedalTier;
|
||||
/** Traegerprodukt (nur EINMALIG). */
|
||||
productId: string | null;
|
||||
/**
|
||||
* Der Honig, den die automatische Wahl GERADE fuer dieses Angebot nimmt
|
||||
* (nur zur Anzeige und Pruefung; der Checkout waehlt bei der Bestellung neu,
|
||||
* weil mehrere Pakete im Warenkorb gemeinsam reichen muessen).
|
||||
*/
|
||||
honeyProduct: { id: string; name: string; weightGrams: number } | null;
|
||||
/** false = derzeit nicht bestellbar (Honig fehlt, Kontingent voll, Angebot aus). */
|
||||
isAvailable: boolean;
|
||||
}
|
||||
|
||||
const offerInclude = {
|
||||
honeyProduct: { select: { id: true, name: true, weightGrams: true, isActive: true } },
|
||||
} as const;
|
||||
|
||||
async function loadOfferRows(where: { isActive?: boolean; kind?: SponsorshipOfferKind; slug?: string; productId?: { in: string[] } }) {
|
||||
return prisma.sponsorshipOffer.findMany({
|
||||
where,
|
||||
include: offerInclude,
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
type OfferRow = Awaited<ReturnType<typeof loadOfferRows>>[number];
|
||||
|
||||
function localize(row: OfferRow, locale: Locale, isAvailable: boolean): SponsorshipOfferView {
|
||||
function localize(row: OfferRow, locale: Locale, honey: HoneyCandidate | null, isAvailable: boolean): SponsorshipOfferView {
|
||||
const en = locale === "en";
|
||||
const featuresEn = asStringArray(row.featuresEn);
|
||||
return {
|
||||
@@ -74,16 +81,14 @@ function localize(row: OfferRow, locale: Locale, isAvailable: boolean): Sponsors
|
||||
cycle: row.cycle,
|
||||
medalTier: row.medalTier,
|
||||
productId: row.productId,
|
||||
honeyProduct: row.honeyProduct
|
||||
? { id: row.honeyProduct.id, name: row.honeyProduct.name, weightGrams: row.honeyProduct.weightGrams }
|
||||
: null,
|
||||
honeyProduct: honey ? { id: honey.id, name: honey.name, weightGrams: honey.weightGrams } : null,
|
||||
isAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
async function freeJarsByProduct(rows: OfferRow[]): Promise<Map<string, number>> {
|
||||
const ids = [...new Set(rows.map((row) => row.honeyProduct?.id).filter((id): id is string => Boolean(id)))];
|
||||
return availableForSaleByProduct(ids);
|
||||
/** Kandidaten nur laden, wenn ein Angebot ueberhaupt Honig braucht (rein digitale Angebote nicht). */
|
||||
async function honeyCandidatesFor(rows: OfferRow[]): Promise<HoneyCandidate[]> {
|
||||
return rows.some((row) => row.honeyJars > 0) ? loadHoneyCandidates() : [];
|
||||
}
|
||||
|
||||
async function activeSubscriptionCounts(rows: OfferRow[]): Promise<Map<string, number>> {
|
||||
@@ -105,17 +110,14 @@ export function isOfferAvailable(input: {
|
||||
kind: SponsorshipOfferKind;
|
||||
hasCarrierProduct: boolean;
|
||||
honeyJars: number;
|
||||
honeyProductActive: boolean | null;
|
||||
freeJars: number;
|
||||
/** Die automatische Honigwahl hat fuer honeyJars Glaeser einen Honig gefunden. */
|
||||
hasEligibleHoney: boolean;
|
||||
maxActiveSubscriptions: number | null;
|
||||
activeSubscriptions: number;
|
||||
}): boolean {
|
||||
if (!input.isActive) return false;
|
||||
if (input.kind === "EINMALIG" && !input.hasCarrierProduct) return false;
|
||||
if (input.honeyJars > 0) {
|
||||
if (!input.honeyProductActive) return false;
|
||||
if (input.freeJars < input.honeyJars) return false;
|
||||
}
|
||||
if (input.honeyJars > 0 && !input.hasEligibleHoney) return false;
|
||||
if (input.kind === "ABO" && input.maxActiveSubscriptions !== null) {
|
||||
if (input.activeSubscriptions >= input.maxActiveSubscriptions) return false;
|
||||
}
|
||||
@@ -123,23 +125,24 @@ export function isOfferAvailable(input: {
|
||||
}
|
||||
|
||||
async function toViews(rows: OfferRow[], locale: Locale): Promise<SponsorshipOfferView[]> {
|
||||
const [jars, subscriptionCounts] = await Promise.all([freeJarsByProduct(rows), activeSubscriptionCounts(rows)]);
|
||||
return rows.map((row) =>
|
||||
localize(
|
||||
const [candidates, subscriptionCounts] = await Promise.all([honeyCandidatesFor(rows), activeSubscriptionCounts(rows)]);
|
||||
return rows.map((row) => {
|
||||
const honey = row.honeyJars > 0 ? pickHoneyProduct(candidates, row.honeyJars) : null;
|
||||
return localize(
|
||||
row,
|
||||
locale,
|
||||
honey,
|
||||
isOfferAvailable({
|
||||
isActive: row.isActive,
|
||||
kind: row.kind,
|
||||
hasCarrierProduct: row.productId !== null,
|
||||
honeyJars: row.honeyJars,
|
||||
honeyProductActive: row.honeyProduct ? row.honeyProduct.isActive : null,
|
||||
freeJars: row.honeyProduct ? (jars.get(row.honeyProduct.id) ?? 0) : 0,
|
||||
hasEligibleHoney: honey !== null,
|
||||
maxActiveSubscriptions: row.maxActiveSubscriptions,
|
||||
activeSubscriptions: subscriptionCounts.get(row.id) ?? 0,
|
||||
})
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getActiveOffers(locale: Locale, kind?: SponsorshipOfferKind): Promise<SponsorshipOfferView[]> {
|
||||
|
||||
Reference in new issue
Block a user