Zusatzspeicher: Plus sofort anteilig, Minus zum Monatsende, Aufgaben für konsoleH
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
887e1e54a0
commit
379cc26dcd
11 files changed
+1531
-27
No files matched your search
@@ -0,0 +1,384 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { config } from '../config.js'
|
||||
import { getDocument, updateDocument } from './appwriteAdmin.js'
|
||||
import { listContractEvents, recordContractEvent, updateContractEvent } from './contractEvents.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { contractAttachmentFor, describeItems, priceIdsOfInvoice, regularStorageLine } from './products.js'
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
ensurePrice,
|
||||
getCurrentSubscription,
|
||||
getStripe,
|
||||
invoiceLines,
|
||||
resolvePriceKeys,
|
||||
subscriptionIdOfInvoice,
|
||||
syncSubscription,
|
||||
} from './stripeBilling.js'
|
||||
|
||||
/**
|
||||
* E-Mail-Zusatzspeicher (Spec 3.4, Bedingungen Fassung 4 § 10): Pakete zu 5 GB fuer 1,00 € je Monat als
|
||||
* zweite Position (Stueckzahl) im Stripe-Abo des Kunden, 0 bis 10 Pakete, in beiden Abos.
|
||||
*
|
||||
* - Plus (Bestellung, "Zahlungspflichtig bestellen"): sofort. Den Rest des laufenden Monats berechnet
|
||||
* Stripe anteilig mit der naechsten Abbuchung (proration_behavior create_prorations). Pakete, die fuer
|
||||
* den laufenden Monat schon bezahlt sind (nach einem Minus im selben Monat), kosten nicht noch einmal:
|
||||
* bis zur bezahlten Menge proration_behavior none, darueber anteilig - notfalls in zwei Schritten
|
||||
* (planStorageChange).
|
||||
* - Minus ("Speichern"): Stueckzahl sofort bei Stripe, ohne Erstattung (none). Die Postfachgrenze in
|
||||
* konsoleH sinkt erst zum Ende des Abrechnungsmonats (Aufgabe fuers Team, dueAt = Periodenende).
|
||||
* - Jede Aenderung ist ein Ereignis `speicher` mit Status offen (= Aufgabe fuers Team in konsoleH); nur ein
|
||||
* Plus traegt Zustimmungsdaten, ein Minus keine (Rechtsrunde 28.09.2026). Eine neue Aenderung ersetzt
|
||||
* die aeltere offene Aufgabe desselben Abos (F21): eine Aufgabe je Abo mit Zielgroesse und Datum.
|
||||
* - Bezahlte Menge je Abrechnungsmonat (storagePaidUnits, storagePaidPeriodEnd): beim Plus gesetzt, sonst
|
||||
* aus der regulaeren Speicher-Zeile jeder bezahlten Rechnung (recordPaidStorage, Webhook invoice.paid).
|
||||
*/
|
||||
|
||||
function httpError(status, message) {
|
||||
const err = new Error(message)
|
||||
err.status = status
|
||||
return err
|
||||
}
|
||||
|
||||
const fmtDate = (iso) =>
|
||||
iso
|
||||
? new Date(iso).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
: '–'
|
||||
// Preis wie in den Bedingungen: "2,00 €" (normales Leerzeichen)
|
||||
const fmtEuro = (cents) => `${(Number(cents || 0) / 100).toFixed(2).replace('.', ',')} €`
|
||||
const toIso = (unixSeconds) => (unixSeconds ? new Date(unixSeconds * 1000).toISOString() : null)
|
||||
|
||||
const unitsOf = (doc) => Number(doc?.storageUnits) || 0
|
||||
const totalGbOf = (units) => config.mailStorage.includedGb + units * config.mailStorage.gbPerUnit
|
||||
const monthlyCentsOf = (units) => units * config.mailStorage.amountCents
|
||||
|
||||
/**
|
||||
* Stripe-Schritte fuer eine Aenderung auf newUnits Pakete: { direction: 'plus' | 'minus' | 'none', steps:
|
||||
* [{ quantity, proration_behavior }] }. Ein Plus bis zur schon bezahlten Menge (paidUnits) laeuft ohne
|
||||
* Verrechnung (none); darueber hinaus anteilig (create_prorations), bei currentUnits < paidUnits in zwei
|
||||
* Schritten. Ein Minus laeuft ohne Erstattung (none). newUnits muss eine ganze Zahl von 0 bis maxUnits sein.
|
||||
*/
|
||||
export function planStorageChange({ currentUnits, paidUnits, newUnits, maxUnits }) {
|
||||
if (!Number.isInteger(newUnits) || newUnits < 0 || newUnits > maxUnits) {
|
||||
throw httpError(400, `Bitte wählen Sie zwischen 0 und ${maxUnits} Paketen Zusatzspeicher.`)
|
||||
}
|
||||
const current = Number(currentUnits) || 0
|
||||
const paid = Number(paidUnits) || 0
|
||||
if (newUnits === current) return { direction: 'none', steps: [] }
|
||||
if (newUnits < current) return { direction: 'minus', steps: [{ quantity: newUnits, proration_behavior: 'none' }] }
|
||||
if (newUnits <= paid) return { direction: 'plus', steps: [{ quantity: newUnits, proration_behavior: 'none' }] }
|
||||
if (current < paid) {
|
||||
return {
|
||||
direction: 'plus',
|
||||
steps: [
|
||||
{ quantity: paid, proration_behavior: 'none' },
|
||||
{ quantity: newUnits, proration_behavior: 'create_prorations' },
|
||||
],
|
||||
}
|
||||
}
|
||||
return { direction: 'plus', steps: [{ quantity: newUnits, proration_behavior: 'create_prorations' }] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuer den laufenden Abrechnungsmonat schon bezahlte Pakete: storagePaidUnits, wenn storagePaidPeriodEnd
|
||||
* der laufende Zeitraum ist (Appwrite gibt Datumswerte mit +00:00 zurueck, der Spiegel schreibt Z: gleicher
|
||||
* Zeitpunkt zaehlt), sonst die aktuelle Menge (die regulaere Rechnung des Monats hat sie berechnet).
|
||||
*/
|
||||
export function paidUnitsOf(doc) {
|
||||
const paidEnd = doc?.storagePaidPeriodEnd
|
||||
const periodEnd = doc?.currentPeriodEnd
|
||||
const samePeriod =
|
||||
Boolean(paidEnd && periodEnd) && (paidEnd === periodEnd || new Date(paidEnd).getTime() === new Date(periodEnd).getTime())
|
||||
return samePeriod ? Number(doc.storagePaidUnits) || 0 : unitsOf(doc)
|
||||
}
|
||||
|
||||
async function requireSubscription(customerId) {
|
||||
const doc = await getCurrentSubscription(customerId)
|
||||
if (!doc) throw httpError(404, 'Kein Abo vorhanden.')
|
||||
return doc
|
||||
}
|
||||
|
||||
// Position fuer subscriptions.update / invoices.createPreview: vorhandener Speicher-Posten per ID, sonst neu per Preis
|
||||
const storageItemParam = (storageItemId, priceId, quantity) => (storageItemId ? { id: storageItemId, quantity } : { price: priceId, quantity })
|
||||
|
||||
/**
|
||||
* Ein Minus wirkt zum Ende des Abrechnungsmonats; endet das Abo frueher (cancelAt), endet der Speicher
|
||||
* mit ihm (Spec 3.4 "Der Zusatzspeicher endet automatisch mit dem Abo").
|
||||
*/
|
||||
function reductionEffectiveAt(doc) {
|
||||
const periodEnd = doc.currentPeriodEnd || null
|
||||
const cancelAt = doc.cancelAt || null
|
||||
if (periodEnd && cancelAt && new Date(cancelAt) < new Date(periodEnd)) return cancelAt
|
||||
return periodEnd || cancelAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Anteiliger Betrag der Speicher-Zeilen einer Stripe-Vorschau (naechste Rechnung): Belastung fuer die neue
|
||||
* Menge abzueglich Gutschrift fuer die alte, jeweils ab prorationDate. Aeltere anteilige Zeilen (eine
|
||||
* fruehere Aenderung im selben Monat, schon als Rechnungsposition vorgemerkt) und die regulaeren Zeilen des
|
||||
* naechsten Monats zaehlen nicht.
|
||||
*/
|
||||
function storageProrationCents(preview, priceIds, prorationDate) {
|
||||
return (preview?.lines?.data || [])
|
||||
.filter((line) => {
|
||||
const parent = line.parent || {}
|
||||
const details = parent.subscription_item_details || parent.invoice_item_details || {}
|
||||
if (details.proration !== true) return false
|
||||
if (!priceIds.has(line.pricing?.price_details?.price)) return false
|
||||
// Toleranz fuer Sekunden zwischen Vorschau und Stripe-Rundung
|
||||
return !line.period?.start || line.period.start >= prorationDate - 300
|
||||
})
|
||||
.reduce((sum, line) => sum + (Number(line.amount) || 0), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorschau fuer newUnits Pakete: Richtung, Gesamtgroesse, Monatspreis des Speichers, Mehr-/Minderpreis je
|
||||
* Monat, anteiliger Betrag fuer den Rest des laufenden Monats (aus invoices.createPreview), naechste Abbuchung
|
||||
* und Wirksamkeit (Minus: Periodenende, sonst jetzt). Bis zur schon bezahlten Menge kostet ein Plus nichts.
|
||||
*/
|
||||
export async function previewStorageChange(customerId, newUnits) {
|
||||
const doc = await requireSubscription(customerId)
|
||||
const currentUnits = unitsOf(doc)
|
||||
const paidUnits = paidUnitsOf(doc)
|
||||
const plan = planStorageChange({ currentUnits, paidUnits, newUnits, maxUnits: config.mailStorage.maxUnits })
|
||||
const now = new Date()
|
||||
let prorationCents = 0
|
||||
if (plan.direction === 'plus' && newUnits > paidUnits) {
|
||||
const stripe = getStripe()
|
||||
const [priceId, stripeSub] = await Promise.all([ensurePrice('mailStorage'), stripe.subscriptions.retrieve(doc.$id)])
|
||||
const items = stripeSub.items?.data || []
|
||||
const { storageItemId } = describeItems(items)
|
||||
const prorationDate = Math.floor(now.getTime() / 1000)
|
||||
const preview = await stripe.invoices.createPreview({
|
||||
subscription: doc.$id,
|
||||
subscription_details: {
|
||||
items: [storageItemParam(storageItemId, priceId, newUnits)],
|
||||
proration_behavior: 'create_prorations',
|
||||
proration_date: prorationDate,
|
||||
},
|
||||
})
|
||||
// Gutschrift-Zeilen tragen den Preis des vorhandenen Postens (kann ein aelterer Speicher-Preis sein)
|
||||
const priceIds = new Set([priceId, items.find((item) => item.id === storageItemId)?.price?.id].filter(Boolean))
|
||||
const previewed = storageProrationCents(preview, priceIds, prorationDate)
|
||||
// F19: Stripe rechnet die Vorschau von der heutigen Menge (currentUnits) aus; berechnet werden nur die
|
||||
// Pakete ueber der schon bezahlten Menge, der erste Schritt bis dahin laeuft ohne Verrechnung
|
||||
prorationCents = Math.round((previewed * (newUnits - Math.max(currentUnits, paidUnits))) / (newUnits - currentUnits))
|
||||
}
|
||||
return {
|
||||
direction: plan.direction,
|
||||
units: newUnits,
|
||||
totalGb: totalGbOf(newUnits),
|
||||
monthlyCents: monthlyCentsOf(newUnits),
|
||||
deltaMonthlyCents: monthlyCentsOf(newUnits) - monthlyCentsOf(currentUnits),
|
||||
prorationCents,
|
||||
nextChargeAt: doc.currentPeriodEnd || null,
|
||||
effectiveAt: plan.direction === 'minus' ? reductionEffectiveAt(doc) : now.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf der Kunde die Menge aendern? Im Portal nur bei Status active, ohne Kuendigung und ohne Wechsel, der
|
||||
* auf die Zahlung wartet (wie canChangeStorage in GET /api/abo, F12). Ueber das Kuendigungsformular
|
||||
* (viaCancellation, Task 10; Bedingungen § 10 Abs. 5 "jederzeit") ist eine Verringerung in jedem laufenden
|
||||
* Status erlaubt, auch bei gekuendigtem Abo oder offener Zahlung - eine Erhoehung dort nie (F20).
|
||||
*/
|
||||
function assertChangeAllowed(doc, events, direction, viaCancellation) {
|
||||
if (viaCancellation) {
|
||||
if (direction !== 'minus') throw httpError(409, 'Über die Kündigung lässt sich der Zusatzspeicher nur verringern.')
|
||||
if (!ACTIVE_STATUSES.has(doc.status)) throw httpError(409, 'Das Abo läuft nicht mehr.')
|
||||
return
|
||||
}
|
||||
if (doc.status !== 'active') {
|
||||
throw httpError(
|
||||
409,
|
||||
'Der Zusatzspeicher lässt sich nur bei laufendem Abo ändern, nicht in der Wartezeit bis zum Leistungsbeginn und nicht bei einem Zahlungsproblem.'
|
||||
)
|
||||
}
|
||||
if (doc.cancelAtPeriodEnd || doc.cancelAt) throw httpError(409, 'Ihr Abo ist gekündigt. Der Zusatzspeicher endet mit dem Abo.')
|
||||
if (events.some((e) => e.type === 'wechsel' && e.status === 'offen')) {
|
||||
throw httpError(409, 'Ihr Wechsel wartet noch auf die Zahlung. Bitte versuchen Sie es danach erneut.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ereignis `speicher` (Nachweis der Bestellung bzw. Aufgabe fuers Team). Aeltere offene Aufgaben desselben
|
||||
* Abos sind damit ueberholt (F21: erledigt, doneBy 'ersetzt'). Ein Fehler beim Speichern bricht die schon
|
||||
* bei Stripe wirksame Aenderung nicht ab: er wird protokolliert, T13 traegt die Aufgabe trotzdem zum Team.
|
||||
*/
|
||||
async function recordStorageEvent({ doc, events, change, consent, dueAt, viaCancellation }) {
|
||||
try {
|
||||
const now = new Date().toISOString()
|
||||
for (const e of events.filter((e) => e.type === 'speicher' && e.status === 'offen')) {
|
||||
await updateContractEvent(e.$id, { status: 'erledigt', doneAt: now, doneBy: 'ersetzt' })
|
||||
}
|
||||
return await recordContractEvent({
|
||||
customerId: doc.customerId,
|
||||
subscriptionId: doc.$id,
|
||||
type: 'speicher',
|
||||
productKey: doc.productKey || 'wartung',
|
||||
details: {
|
||||
fromUnits: change.fromUnits,
|
||||
toUnits: change.toUnits,
|
||||
effectiveAt: change.effectiveAt,
|
||||
totalGb: change.totalGb,
|
||||
monthlyCents: change.monthlyCents,
|
||||
deltaMonthlyCents: change.deltaMonthlyCents,
|
||||
...(viaCancellation ? { viaCancellation: true } : {}),
|
||||
},
|
||||
// Nur eine Bestellung (Plus) traegt Zustimmungsdaten; ein Minus keine (Rechtsrunde 28.09.2026)
|
||||
consent:
|
||||
change.direction === 'plus'
|
||||
? {
|
||||
termsVersion: consent.termsVersion,
|
||||
consentAt: consent.consentAt,
|
||||
consentIp: consent.consentIp || null,
|
||||
consentUserAgent: consent.consentUserAgent || null,
|
||||
}
|
||||
: null,
|
||||
status: 'offen',
|
||||
dueAt,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`[speicher] Ereignis für Abo ${doc.$id} nicht gespeichert:`, err.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Vertragsunterlagen (PDF) in der zugestimmten Fassung als Mail-Anhang; ohne Datei geht die Mail ohne Anhang raus. */
|
||||
async function contractPdfAttachment(termsVersion) {
|
||||
const { filename, path } = contractAttachmentFor(termsVersion)
|
||||
const content = await readFile(path).catch((err) => {
|
||||
console.error(`[speicher] Vertragsunterlagen ${filename} nicht lesbar:`, err.message)
|
||||
return null
|
||||
})
|
||||
return content ? [{ filename, content, contentType: 'application/pdf' }] : []
|
||||
}
|
||||
|
||||
/**
|
||||
* K13 an den Kunden (Plus: mit PDF der zugestimmten Fassung, F22) und T13 ans Team. Ueber das
|
||||
* Kuendigungsformular bestaetigt K03, K13 entfaellt dort (F31). Ein Mailfehler bricht die Aenderung nie ab.
|
||||
*/
|
||||
async function sendStorageMails({ doc, change, dueAt, consent, viaCancellation, proofMissing }) {
|
||||
const plus = change.direction === 'plus'
|
||||
const customer = await getDocument(config.collections.customers, doc.customerId).catch(() => null)
|
||||
const email = customer?.email || doc.email || ''
|
||||
const wirksamAb = fmtDate(change.effectiveAt)
|
||||
if (!viaCancellation && email) {
|
||||
const attachments = plus ? await contractPdfAttachment(consent.termsVersion) : []
|
||||
await sendCustomerMail(email, {
|
||||
template: 'K13',
|
||||
values: {
|
||||
name: customer?.name || '',
|
||||
pakete: change.toUnits,
|
||||
gesamt_gb: change.totalGb,
|
||||
monatspreis_zusatz: fmtEuro(change.monthlyCents),
|
||||
wirksam_ab: wirksamAb,
|
||||
},
|
||||
blocks: { plus, minus: !plus },
|
||||
attachments,
|
||||
}).catch((e) => console.error(`[speicher] K13 an ${email} fehlgeschlagen:`, e.message))
|
||||
}
|
||||
await sendAdminNotice({
|
||||
template: 'T13',
|
||||
values: {
|
||||
aktion: plus ? 'einrichten' : `reduzieren zum ${wirksamAb}`,
|
||||
email,
|
||||
postfaecher_hinweis:
|
||||
(doc.productKey || 'wartung') === 'hosting' ? 'Hosting-Abo: 1 Postfach' : 'Wartungs-Abo: alle Postfächer des Kunden zusammen',
|
||||
neue_groesse_gb: change.totalGb,
|
||||
faellig: fmtDate(dueAt),
|
||||
},
|
||||
blocks: proofMissing ? { nachweis_fehlt: true } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Menge auf newUnits Pakete aendern: Stripe-Schritte nacheinander (die erste Position mit { price, quantity },
|
||||
* danach { id, quantity }; bei 0 bleibt die Position mit Menge 0 bestehen), Spiegel und bezahlte Menge
|
||||
* (Plus: max(paidUnits, newUnits) fuer den laufenden Monat), Ereignis `speicher` (offen; dueAt jetzt bzw.
|
||||
* Periodenende), K13 und T13. consent { termsVersion, consentAt, consentIp, consentUserAgent } ist beim Plus
|
||||
* Pflicht (400), beim Minus null. options.viaCancellation: Kuendigungsformular (nur Verringerung, F20).
|
||||
* Liefert { subscription, event, change }.
|
||||
*/
|
||||
export async function changeStorage(customerId, newUnits, consent, options = {}) {
|
||||
const viaCancellation = options.viaCancellation === true
|
||||
const doc = await requireSubscription(customerId)
|
||||
const currentUnits = unitsOf(doc)
|
||||
const paidUnits = paidUnitsOf(doc)
|
||||
const plan = planStorageChange({ currentUnits, paidUnits, newUnits, maxUnits: config.mailStorage.maxUnits })
|
||||
if (plan.direction === 'none') throw httpError(400, 'Die Anzahl der Pakete ist unverändert.')
|
||||
const plus = plan.direction === 'plus'
|
||||
if (plus && !(consent?.termsVersion && consent?.consentAt)) {
|
||||
throw httpError(400, 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.')
|
||||
}
|
||||
const events = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === doc.$id)
|
||||
assertChangeAllowed(doc, events, plan.direction, viaCancellation)
|
||||
|
||||
// Stripe: Speicher-Posten anlegen oder Menge aendern, Schritt fuer Schritt (planStorageChange)
|
||||
const stripe = getStripe()
|
||||
const [priceId, stripeSub] = await Promise.all([ensurePrice('mailStorage'), stripe.subscriptions.retrieve(doc.$id)])
|
||||
let { storageItemId } = describeItems(stripeSub.items?.data)
|
||||
const prorationDate = Math.floor(Date.now() / 1000)
|
||||
for (const step of plan.steps) {
|
||||
const updated = await stripe.subscriptions.update(doc.$id, {
|
||||
items: [storageItemParam(storageItemId, priceId, step.quantity)],
|
||||
proration_behavior: step.proration_behavior,
|
||||
...(step.proration_behavior === 'create_prorations' ? { proration_date: prorationDate } : {}),
|
||||
})
|
||||
storageItemId = describeItems(updated.items?.data).storageItemId || storageItemId
|
||||
}
|
||||
|
||||
// Spiegel (storageUnits aus den Positionen) und bezahlte Menge des laufenden Monats. Die Aenderung ist
|
||||
// bei Stripe schon wirksam: scheitert die Buchung der bezahlten Menge (z. B. Attribut fehlt, Setup-Skript
|
||||
// nicht gelaufen), laufen Ereignis und Mails trotzdem; ein Plus nach einem Minus im selben Monat wuerde
|
||||
// dann anteilig berechnet statt frei bleiben - Fehler im Log.
|
||||
let subscription = (await syncSubscription(doc.$id)) || doc
|
||||
if (plus) {
|
||||
const paid = { storagePaidUnits: Math.max(paidUnits, newUnits), storagePaidPeriodEnd: subscription.currentPeriodEnd || doc.currentPeriodEnd || null }
|
||||
subscription = await updateDocument(config.collections.subscriptions, doc.$id, paid).catch((err) => {
|
||||
console.error(`[speicher] Bezahlte Menge für Abo ${doc.$id} nicht gespeichert (${err.status || '?'}):`, err.message)
|
||||
return subscription
|
||||
})
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const effectiveAt = plus ? now : reductionEffectiveAt(subscription)
|
||||
const change = {
|
||||
direction: plan.direction,
|
||||
fromUnits: currentUnits,
|
||||
toUnits: newUnits,
|
||||
effectiveAt,
|
||||
totalGb: totalGbOf(newUnits),
|
||||
monthlyCents: monthlyCentsOf(newUnits),
|
||||
deltaMonthlyCents: monthlyCentsOf(newUnits) - monthlyCentsOf(currentUnits),
|
||||
}
|
||||
// Aufgabe fuers Team: Plus sofort einrichten, Minus zum Periodenende reduzieren
|
||||
const dueAt = plus ? now : effectiveAt
|
||||
const event = await recordStorageEvent({ doc, events, change, consent, dueAt, viaCancellation })
|
||||
await sendStorageMails({ doc: subscription, change, dueAt, consent, viaCancellation, proofMissing: !event })
|
||||
return { subscription, event, change }
|
||||
}
|
||||
|
||||
/**
|
||||
* Bezahlte Rechnung (Webhook invoice.paid, frisch geladen mit loadInvoice): hat sie eine regulaere
|
||||
* Zusatzspeicher-Zeile (nicht anteilig), ist deren Stueckzahl fuer den Zeitraum bis line.period.end bezahlt
|
||||
* (storagePaidUnits, storagePaidPeriodEnd). Idempotent; fehlt der Spiegel noch (Webhook-Reihenfolge),
|
||||
* wird er aus Stripe angelegt.
|
||||
*/
|
||||
export async function recordPaidStorage(invoice) {
|
||||
const lines = await invoiceLines(invoice)
|
||||
const loaded = { ...invoice, lines: { ...(invoice?.lines || {}), data: lines, has_more: false } }
|
||||
const line = regularStorageLine(loaded, await resolvePriceKeys(priceIdsOfInvoice(loaded)))
|
||||
if (!line) return { skipped: 'keine reguläre Speicher-Zeile' }
|
||||
const ref = line.parent?.subscription_item_details?.subscription || subscriptionIdOfInvoice(loaded)
|
||||
const subscriptionId = typeof ref === 'string' ? ref : ref?.id
|
||||
if (!subscriptionId) return { skipped: 'keine Abo-Rechnung' }
|
||||
const data = { storagePaidUnits: Number(line.quantity) || 0, storagePaidPeriodEnd: toIso(line.period?.end) }
|
||||
try {
|
||||
await updateDocument(config.collections.subscriptions, subscriptionId, data)
|
||||
} catch (err) {
|
||||
if (err.status !== 404) throw err
|
||||
if (!(await syncSubscription(subscriptionId))) return { skipped: 'Abo ohne Kundenzuordnung' }
|
||||
await updateDocument(config.collections.subscriptions, subscriptionId, data)
|
||||
}
|
||||
return { recorded: true, subscriptionId, units: data.storagePaidUnits, periodEnd: data.storagePaidPeriodEnd }
|
||||
}
|
||||
Reference in new issue
Block a user