111 lines
6.5 KiB
JavaScript
111 lines
6.5 KiB
JavaScript
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(' ')
|
||
}
|