import { berlinYmd, fmtBerlin, fmtDate, fmtYmd, formatCents, PRODUCT_LABEL } from './format' // Abo-Angebote: Texte, Rabattende und Formularpruefung wortgleich aus dem frueheren Admin-Panel des Portals. // "today" ist der Kalendertag in Deutschland (YYYY-MM-DD) und fuer Tests injizierbar. export const OFFER_STATUS = { offen: { tone: 'warn', label: 'offen' }, aktiv: { tone: 'ok', label: 'aktiv' }, abgelaufen: { tone: 'muted', label: 'abgelaufen' }, zurueckgezogen: { tone: 'muted', label: 'zurückgezogen' }, } export const offerStatus = (status) => OFFER_STATUS[status] || { tone: 'muted', label: status } const todayBerlin = () => berlinYmd(new Date()) export function offerPreviewText(price, months, regular, running) { const lead = months === 1 ? (running ? 'Nächster Monat' : 'Erster Monat') : `${running ? 'Nächste' : 'Erste'} ${months} Monate` const amount = price === 0 ? 'kostenlos' : `${months === 1 ? '' : 'je '}${formatCents(price)} statt ${formatCents(regular)}` return `${lead} ${amount}, danach ${formatCents(regular)} pro Monat` } /** Kalendertag + n Monate, Monatsende wie bei Stripe (31.01. + 1 Monat = 28./29.02.) */ export function addMonthsYmd(ymd, n) { const [y, m, d] = ymd.split('-').map(Number) const last = new Date(Date.UTC(y, m + n, 0)).getUTCDate() return new Date(Date.UTC(y, m - 1 + n, Math.min(d, last))).toISOString().slice(0, 10) } /** Rabattende eines noch nicht eingeloesten Angebots (Abschluss heute bis letzter Gueltigkeitstag) */ export function offerDiscountEstimate(months, validUntilYmd, testAccount = false, today = todayBerlin()) { const n = months + (testAccount ? 1 : 0) return { earliest: addMonthsYmd(today, n), latest: addMonthsYmd(validUntilYmd < today ? today : validUntilYmd, n) } } export function discountLine(o, today = todayBerlin()) { if (o.status === 'offen' && o.validUntil) { const { earliest, latest } = offerDiscountEstimate(o.months, String(o.validUntil).slice(0, 10), o.testAccount, today) return earliest === latest ? `Rabatt endet voraussichtlich am ${fmtYmd(earliest)}` : `Rabatt endet voraussichtlich zwischen ${fmtYmd(earliest)} und ${fmtYmd(latest)}` } if (o.status !== 'aktiv' || !o.discount) return '' const d = o.discount if (!d.known) return 'Rabattende unbekannt (Abo nicht gefunden)' const end = d.subscriptionEndsAt || d.endsAt const past = new Date(end) <= new Date() return d.subscriptionEndsAt ? `Rabatt ${past ? 'endete' : 'endet'} mit dem Abo am ${fmtBerlin(end)}` : `Rabatt ${past ? 'endete' : 'endet'} am ${fmtBerlin(end)}` } export const isRunning = (ctx, productKey) => Boolean(ctx?.subscription?.active && ctx.subscription.productKey === productKey) /** * Pruefung und Vorschau des Formulars "Neues Angebot" (wie im frueheren Admin-Panel). * -> { errors, running, saveLabel, previewLines }; Speichern nur ohne errors. */ export function evaluateOfferForm(ctx, f, today = todayBerlin()) { const regular = ctx.products[f.productKey].regularPriceCents const running = isRunning(ctx, f.productKey) const openOffer = ctx.openOffers[f.productKey] const errors = [] if (openOffer) errors.push(`Es gibt schon ein offenes ${PRODUCT_LABEL[f.productKey]}-Angebot (${openOffer.ref}: ${openOffer.text}) – bitte zuerst zurückziehen.`) if (!Number.isInteger(f.monthlyPriceCents) || f.monthlyPriceCents < 0 || f.monthlyPriceCents >= regular) { errors.push(`Angebotspreis bitte zwischen 0,00 € und ${formatCents(regular - 1)} angeben.`) } if (!f.validUntil && !running) errors.push('Bitte „Gültig bis“ angeben.') const previewLines = [] if (!errors.length) { previewLines.push(`${offerPreviewText(f.monthlyPriceCents, f.months, regular, running)} · Ersparnis ${formatCents((regular - f.monthlyPriceCents) * f.months)}`) if (running) { const charges = ctx.subscription.upcomingCharges || [] if (charges.length > f.months) previewLines.push(`Rabatt endet am ${fmtBerlin(charges[f.months])} (nach ${f.months === 1 ? '1 Abbuchung' : `${f.months} Abbuchungen`}).`) } else { previewLines.push(`Angebot gültig bis ${fmtYmd(f.validUntil)}`) const { earliest, latest } = offerDiscountEstimate(f.months, f.validUntil, ctx.customer.testAccount, today) const lead = `Rabatt läuft ${f.months === 1 ? '1 Monat' : `${f.months} Monate`} ab Abschluss und endet voraussichtlich` previewLines.push(`${earliest === latest ? `${lead} am ${fmtYmd(earliest)}.` : `${lead} zwischen ${fmtYmd(earliest)} (Abschluss heute) und ${fmtYmd(latest)} (Abschluss am letzten Tag).`} Bei Beginn nach der Widerrufsfrist gut 2 Wochen später.`) } } previewLines.push(running ? `Laufendes Abo: gilt ab der nächsten Abbuchung${ctx.subscription.currentPeriodEnd ? ` (${fmtDate(ctx.subscription.currentPeriodEnd)})` : ''} und wird sofort in Stripe angewendet.` : 'Noch kein laufendes Abo: Der Coupon wird angelegt und beim Abschluss im Kundenportal angewendet.') if (!running && !ctx.customer.portalAccessEnabled) previewLines.push('Achtung: Kunde hat keinen Portalzugang – er kann erst bestellen, wenn der Zugang freigeschaltet ist.') if (ctx.customer.testAccount) previewLines.push(`Testkonto: Im Checkout bleibt der 0,50-€-Testrabatt, das Angebot wird danach zusätzlich angehängt${running ? '' : ' und gilt deshalb erst ab der zweiten Abbuchung'}.`) return { errors, running, saveLabel: running ? 'Angebot anlegen und aufs laufende Abo anwenden' : 'Angebot anlegen', previewLines } } export function offerConfirm(ctx, f, running) { const text = offerPreviewText(f.monthlyPriceCents, f.months, ctx.products[f.productKey].regularPriceCents, running) return `Angebot für ${ctx.customer.name || 'den Kunden'} anlegen?\n\n${text}${running ? '' : `\nGültig bis ${fmtYmd(f.validUntil)}`}\n\nDer Coupon wird sofort in Stripe angelegt${running ? ' und auf das laufende Abo angewendet' : ''}.` } export function createdMessage(result, notifyCustomer) { const o = result.offer const until = o.scope !== 'laufend' ? `, gültig bis ${fmtDate(o.validUntil)}` : o.discount?.known ? `, Rabatt endet am ${fmtBerlin(o.discount.endsAt)}` : '' return [ `Angebot ${o.ref} für ${o.customerName} angelegt: ${o.text}${until}.`, `Stripe-Coupon ${o.stripeCouponId} ist eingerichtet${o.status === 'aktiv' ? ' und am laufenden Abo aktiv' : ''}.`, notifyCustomer ? (result.mailSent ? `Die E-Mail an ${o.customerEmail} ist raus.` : `E-Mail an den Kunden fehlgeschlagen (${result.mailError})${o.mailSendable ? ', sie lässt sich in der Liste erneut senden' : ''}.`) : '', ].filter(Boolean).join(' ') }