Files
tickte-system/src/lib/offerFormat.js
T

111 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(' ')
}