import { prisma } from "@/lib/prisma"; import type { SponsorshipCycle, SubscriptionStatus, DeliveryStatus } from "@/generated/prisma/client"; import { deliveryProgress } from "@/lib/sponsorship-subscription/entitlement"; import { contractReference } from "@/lib/sponsorship-subscription/cancellation"; /** * Abo eines Kontos fuer die Kontoseite und den Kuendigungsknopf. Gefiltert * nach customerId: ein fremdes Abo ist hier schlicht nicht vorhanden (404). */ export interface OwnedSubscriptionView { id: string; status: SubscriptionStatus; offerName: string; cycle: SponsorshipCycle | null; /** Zuletzt eingezogener Monatsbetrag, sonst der Preis des Angebots. */ monthlyCents: number; contractReference: string; beeName: string | null; sponsorshipId: string | null; currentPeriodEnd: Date | null; cancelAt: Date | null; endedAt: Date | null; paidMonths: number; progress: { paid: number; required: number } | null; /** Bezahlte Monate, die wegen der SEPA-Neukunden-Frist noch nicht zaehlen. */ heldPayments: number; deliveries: { id: string; dueAt: Date; jars: number; status: DeliveryStatus }[]; stripeCustomerId: string | null; customerEmail: string; } function beeNameOf(personalization: unknown): string | null { const inner = (personalization as { personalization?: { beeName?: unknown } } | null)?.personalization; return typeof inner?.beeName === "string" ? inner.beeName : null; } export async function getOwnedSubscription(customerId: string, id: string, now: Date = new Date()): Promise { const subscription = await prisma.sponsorshipSubscription.findFirst({ where: { id, customerId }, include: { offer: { select: { name: true, cycle: true, priceCents: true, honeyJars: true } }, sponsorship: { select: { id: true, number: true } }, customer: { select: { email: true, stripeCustomerId: true } }, payments: { orderBy: { paidAt: "asc" }, select: { id: true, paidAt: true, countsFrom: true, reversedAt: true, deliveryId: true, amountCents: true }, }, deliveries: { orderBy: { dueAt: "desc" }, select: { id: true, dueAt: true, jars: true, status: true } }, }, }); if (!subscription) return null; const valid = subscription.payments.filter((p) => p.reversedAt === null); const last = valid[valid.length - 1]; const hasHoney = subscription.offer.honeyJars > 0 && subscription.offer.cycle !== null; return { id: subscription.id, status: subscription.status, offerName: subscription.offer.name, cycle: subscription.offer.cycle, monthlyCents: last?.amountCents ?? subscription.offer.priceCents, contractReference: contractReference(subscription), beeName: beeNameOf(subscription.personalization), sponsorshipId: subscription.sponsorship?.id ?? null, currentPeriodEnd: subscription.currentPeriodEnd, cancelAt: subscription.cancelAt, endedAt: subscription.endedAt, paidMonths: valid.length, progress: hasHoney && subscription.offer.cycle ? deliveryProgress(subscription.offer.cycle, subscription.payments) : null, heldPayments: valid.filter((p) => p.deliveryId === null && p.countsFrom > now).length, deliveries: subscription.deliveries, stripeCustomerId: subscription.customer.stripeCustomerId, customerEmail: subscription.customer.email, }; } /** Kuendigung moeglich? Nur laufende Vertraege; ein gekuendigtes Abo zeigt stattdessen sein Ende. */ export function canCancel(view: Pick): boolean { return (view.status === "AKTIV" || view.status === "IM_VERZUG") && view.cancelAt === null; }