Zusatzspeicher: Plan nach Stripe-Abgleich erneut prüfen, Aufgabenliste unter Sperre, K13 ohne Kosten
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
662f8d8144
commit
f66a713531
4 files changed
+153
-25
No files matched your search
@@ -8,8 +8,8 @@
|
|||||||
Absender: „WEBklar“ <Portal-Postfach>
|
Absender: „WEBklar“ <Portal-Postfach>
|
||||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern · Anhang bei einer Bestellung (Plus): Vertragsunterlagen der zugestimmten Fassung (PDF)
|
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern · Anhang bei einer Bestellung (Plus): Vertragsunterlagen der zugestimmten Fassung (PDF)
|
||||||
Platzhalter: {{name}}, {{pakete}}, {{gesamt_gb}}, {{monatspreis_zusatz}}, {{wirksam_ab}}
|
Platzhalter: {{name}}, {{pakete}}, {{gesamt_gb}}, {{monatspreis_zusatz}}, {{wirksam_ab}}
|
||||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): plus, minus
|
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): plus, plus_bezahlt, bestellung, minus
|
||||||
Stand Aufgabe 8 (Fix 1): plus = Bestellung (sofort, anteilig, mit Widerrufshinweis und PDF), minus = Verringerung zum Ende des Abrechnungsmonats (Spec 3.4); Endfassung der Texte in Aufgabe 12.
|
Stand Aufgabe 8 (Fix 2): plus = Bestellung über die schon bezahlte Menge (Einrichtung, anteilige Berechnung), plus_bezahlt = Bestellung bis zur schon bezahlten Menge (keine Kosten, nichts einzurichten), bestellung = Unterlagen und Widerrufshinweis (bei beiden Bestellungen), minus = Verringerung zum Ende des Abrechnungsmonats (Spec 3.4); Endfassung der Texte in Aufgabe 12.
|
||||||
-->
|
-->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
@@ -35,9 +35,9 @@ a{color:#111111;}
|
|||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">✓ E-Mail-Zusatzspeicher</td></tr></table></td></tr>
|
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">✓ E-Mail-Zusatzspeicher</td></tr></table></td></tr>
|
||||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihr E-Mail-Zusatzspeicher</td></tr>
|
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Ihr E-Mail-Zusatzspeicher</td></tr>
|
||||||
<!-- WENN plus -->
|
<!-- WENN bestellung -->
|
||||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag {{name}},<br>vielen Dank für Ihre Bestellung. Hiermit bestätigen wir Ihren E-Mail-Zusatzspeicher.</td></tr>
|
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag {{name}},<br>vielen Dank für Ihre Bestellung. Hiermit bestätigen wir Ihren E-Mail-Zusatzspeicher.</td></tr>
|
||||||
<!-- ENDE plus -->
|
<!-- ENDE bestellung -->
|
||||||
<!-- WENN minus -->
|
<!-- WENN minus -->
|
||||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag {{name}},<br>hiermit bestätigen wir die Änderung Ihres E-Mail-Zusatzspeichers.</td></tr>
|
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Guten Tag {{name}},<br>hiermit bestätigen wir die Änderung Ihres E-Mail-Zusatzspeichers.</td></tr>
|
||||||
<!-- ENDE minus -->
|
<!-- ENDE minus -->
|
||||||
@@ -46,14 +46,17 @@ a{color:#111111;}
|
|||||||
<!-- WENN plus -->
|
<!-- WENN plus -->
|
||||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Bereitstellung und Abrechnung</div><div class="mut" style="color:#4a4843;">Wir stellen die Grenzen Ihrer Postfächer in der Regel innerhalb eines Werktags um; bis dahin zeigt das Kundenportal „wird eingerichtet“. Den Rest des laufenden Abrechnungsmonats berechnen wir anteilig mit der nächsten Abbuchung, danach {{monatspreis_zusatz}} im Monat. Pakete, die für den laufenden Monat schon bezahlt waren, berechnen wir nicht noch einmal.</div></td></tr></table></td></tr>
|
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Bereitstellung und Abrechnung</div><div class="mut" style="color:#4a4843;">Wir stellen die Grenzen Ihrer Postfächer in der Regel innerhalb eines Werktags um; bis dahin zeigt das Kundenportal „wird eingerichtet“. Den Rest des laufenden Abrechnungsmonats berechnen wir anteilig mit der nächsten Abbuchung, danach {{monatspreis_zusatz}} im Monat. Pakete, die für den laufenden Monat schon bezahlt waren, berechnen wir nicht noch einmal.</div></td></tr></table></td></tr>
|
||||||
<!-- ENDE plus -->
|
<!-- ENDE plus -->
|
||||||
|
<!-- WENN plus_bezahlt -->
|
||||||
|
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Keine zusätzlichen Kosten in diesem Monat</div><div class="mut" style="color:#4a4843;">Die Pakete sind für den laufenden Monat schon bezahlt; es entstehen keine zusätzlichen Kosten. Ab dem nächsten Abrechnungsmonat gelten {{pakete}} Pakete.</div></td></tr></table></td></tr>
|
||||||
|
<!-- ENDE plus_bezahlt -->
|
||||||
<!-- WENN minus -->
|
<!-- WENN minus -->
|
||||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wirksam zum Ende des Abrechnungsmonats</div><div class="mut" style="color:#4a4843;">Ihre Änderung wirkt zum {{wirksam_ab}}, ohne anteilige Erstattung; bis dahin bleibt der bisherige Speicher bestehen. Passen die gespeicherten E-Mails dann nicht mehr hinein, nimmt das betroffene Postfach keine neuen E-Mails an, bis wieder Platz ist.</div></td></tr></table></td></tr>
|
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Wirksam zum Ende des Abrechnungsmonats</div><div class="mut" style="color:#4a4843;">Ihre Änderung wirkt zum {{wirksam_ab}}, ohne anteilige Erstattung; bis dahin bleibt der bisherige Speicher bestehen. Passen die gespeicherten E-Mails dann nicht mehr hinein, nimmt das betroffene Postfach keine neuen E-Mails an, bis wieder Platz ist.</div></td></tr></table></td></tr>
|
||||||
<!-- ENDE minus -->
|
<!-- ENDE minus -->
|
||||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="https://project.webklar.com/dashboard.html" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal →</a></td></tr></table></td></tr>
|
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="https://project.webklar.com/dashboard.html" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal →</a></td></tr></table></td></tr>
|
||||||
<!-- WENN plus -->
|
<!-- WENN bestellung -->
|
||||||
<tr><td class="px" style="padding:32px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen im Anhang (PDF)</div><div class="mut" style="color:#4a4843;">📎 Vertragsbedingungen mit den Regeln zum E-Mail-Zusatzspeicher<br>📎 Widerrufsbelehrung mit Muster-Widerrufsformular</div></td></tr></table></td></tr>
|
<tr><td class="px" style="padding:32px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Ihre Vertragsunterlagen im Anhang (PDF)</div><div class="mut" style="color:#4a4843;">📎 Vertragsbedingungen mit den Regeln zum E-Mail-Zusatzspeicher<br>📎 Widerrufsbelehrung mit Muster-Widerrufsformular</div></td></tr></table></td></tr>
|
||||||
<tr><td class="px mut" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Sie können diese Bestellung innerhalb von 14 Tagen widerrufen; die Widerrufsbelehrung finden Sie in den beigefügten Vertragsunterlagen. Widerrufen Sie, nehmen wir die Pakete zurück und erstatten alles, was Sie dafür gezahlt haben.</td></tr>
|
<tr><td class="px mut" style="padding:28px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Sie können diese Bestellung innerhalb von 14 Tagen widerrufen; die Widerrufsbelehrung finden Sie in den beigefügten Vertragsunterlagen. Widerrufen Sie, nehmen wir die Pakete zurück und erstatten alles, was Sie dafür gezahlt haben.</td></tr>
|
||||||
<!-- ENDE plus -->
|
<!-- ENDE bestellung -->
|
||||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Ihr WEBklar-Team</strong></td></tr>
|
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Ihr WEBklar-Team</strong></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
|||||||
@@ -207,10 +207,23 @@ export async function previewStorageChange(customerId, newUnits) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertPlanAllowed(plan, consent) {
|
/**
|
||||||
|
* Richtung und Zustimmung pruefen: unveraendert 400, Plus ohne Zustimmung 400. reconciled: der Plan wurde nach
|
||||||
|
* dem Abgleich mit Stripe neu berechnet (Spiegel und Stripe wichen ab) - dann sagt die Meldung, dass die
|
||||||
|
* Seite neu geladen werden muss; ueber das Kuendigungsformular ist dann nur ein Minus erlaubt (409).
|
||||||
|
*/
|
||||||
|
function assertPlanAllowed(plan, consent, { reconciled = false, viaCancellation = false } = {}) {
|
||||||
if (plan.direction === 'none') throw httpError(400, 'Die Anzahl der Pakete ist unverändert.')
|
if (plan.direction === 'none') throw httpError(400, 'Die Anzahl der Pakete ist unverändert.')
|
||||||
|
if (reconciled && viaCancellation && plan.direction !== 'minus') {
|
||||||
|
throw httpError(409, 'Über die Kündigung lässt sich der Zusatzspeicher nur verringern.')
|
||||||
|
}
|
||||||
if (plan.direction === 'plus' && !(consent?.termsVersion && consent?.consentAt)) {
|
if (plan.direction === 'plus' && !(consent?.termsVersion && consent?.consentAt)) {
|
||||||
throw httpError(400, 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.')
|
throw httpError(
|
||||||
|
400,
|
||||||
|
reconciled
|
||||||
|
? 'Ihr Speicherstand hat sich geändert. Bitte Seite neu laden und der Bestellung zustimmen.'
|
||||||
|
: 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +258,7 @@ function assertChangeAllowed(doc, events, direction, viaCancellation) {
|
|||||||
*/
|
*/
|
||||||
async function recordStorageEvent({ doc, change, consent, viaCancellation }) {
|
async function recordStorageEvent({ doc, change, consent, viaCancellation }) {
|
||||||
try {
|
try {
|
||||||
return await recordContractEvent({
|
const event = await recordContractEvent({
|
||||||
customerId: doc.customerId,
|
customerId: doc.customerId,
|
||||||
subscriptionId: doc.$id,
|
subscriptionId: doc.$id,
|
||||||
type: 'speicher',
|
type: 'speicher',
|
||||||
@@ -270,9 +283,15 @@ async function recordStorageEvent({ doc, change, consent, viaCancellation }) {
|
|||||||
consentUserAgent: consent.consentUserAgent || null,
|
consentUserAgent: consent.consentUserAgent || null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
status: 'offen',
|
status: change.task ? 'offen' : 'erledigt',
|
||||||
dueAt: change.dueAt,
|
dueAt: change.dueAt,
|
||||||
})
|
})
|
||||||
|
if (change.task) return event
|
||||||
|
// Kein Auftrag fuers Team: als erledigt markiert, damit die Aufgabenliste (Task 13) es nicht zeigt
|
||||||
|
return await updateContractEvent(event.$id, { doneAt: new Date().toISOString(), doneBy: 'keine Aufgabe' }).catch((e) => {
|
||||||
|
console.error(`[speicher] Ereignis ${event.$id} nicht als erledigt markiert:`, e.message)
|
||||||
|
return event
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[speicher] Ereignis für Abo ${doc.$id} nicht gespeichert:`, err.message)
|
console.error(`[speicher] Ereignis für Abo ${doc.$id} nicht gespeichert:`, err.message)
|
||||||
return null
|
return null
|
||||||
@@ -327,10 +346,14 @@ async function sendStorageMails({ doc, change, consent, viaCancellation, proofMi
|
|||||||
monatspreis_zusatz: fmtEuro(change.monthlyCents),
|
monatspreis_zusatz: fmtEuro(change.monthlyCents),
|
||||||
wirksam_ab: fmtDate(change.effectiveAt),
|
wirksam_ab: fmtDate(change.effectiveAt),
|
||||||
},
|
},
|
||||||
blocks: { plus, minus: !plus },
|
// plus: Einrichtung mit anteiliger Berechnung; plus_bezahlt: bis zur schon bezahlten Menge (keine Kosten);
|
||||||
|
// bestellung: Unterlagen und Widerruf bei beiden; minus: Verringerung zum Periodenende
|
||||||
|
blocks: { plus: plus && change.kind === 'einrichten', plus_bezahlt: plus && change.kind !== 'einrichten', bestellung: plus, minus: !plus },
|
||||||
attachments,
|
attachments,
|
||||||
}).catch((e) => console.error(`[speicher] K13 an ${email} fehlgeschlagen:`, e.message))
|
}).catch((e) => console.error(`[speicher] K13 an ${email} fehlgeschlagen:`, e.message))
|
||||||
}
|
}
|
||||||
|
// Ohne Auftrag fuers Team (Plus genau zurueck auf die bezahlte Menge) kein T13 - es gibt nichts zu tun
|
||||||
|
if (!change.task && !proofMissing) return
|
||||||
await sendAdminNotice({
|
await sendAdminNotice({
|
||||||
template: 'T13',
|
template: 'T13',
|
||||||
values: {
|
values: {
|
||||||
@@ -371,12 +394,13 @@ export async function changeStorage(customerId, newUnits, consent, options = {})
|
|||||||
// Vorpruefung mit dem Spiegel: Grenzen, Richtung, Zustimmung, Status - ohne Stripe-Aufruf
|
// Vorpruefung mit dem Spiegel: Grenzen, Richtung, Zustimmung, Status - ohne Stripe-Aufruf
|
||||||
const plan = planStorageChange({ currentUnits: unitsOf(doc), paidUnits: paidUnitsOf(doc), newUnits, maxUnits: config.mailStorage.maxUnits })
|
const plan = planStorageChange({ currentUnits: unitsOf(doc), paidUnits: paidUnitsOf(doc), newUnits, maxUnits: config.mailStorage.maxUnits })
|
||||||
assertPlanAllowed(plan, consent)
|
assertPlanAllowed(plan, consent)
|
||||||
const events = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === doc.$id)
|
// Sperre pruefen und setzen ohne await dazwischen; die offenen Ereignisse erst unter der Sperre lesen, damit
|
||||||
assertChangeAllowed(doc, events, plan.direction, viaCancellation)
|
// das Ersetzen aelterer Aufgaben nie auf einer veralteten Liste arbeitet
|
||||||
// Sperre pruefen und setzen ohne await dazwischen
|
|
||||||
if (activeChanges.has(doc.$id)) throw httpError(409, 'Eine Änderung läuft bereits. Bitte warten Sie einen Moment und laden Sie die Seite neu.')
|
if (activeChanges.has(doc.$id)) throw httpError(409, 'Eine Änderung läuft bereits. Bitte warten Sie einen Moment und laden Sie die Seite neu.')
|
||||||
activeChanges.add(doc.$id)
|
activeChanges.add(doc.$id)
|
||||||
try {
|
try {
|
||||||
|
const events = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === doc.$id)
|
||||||
|
assertChangeAllowed(doc, events, plan.direction, viaCancellation)
|
||||||
return await performChange({ doc, events, newUnits, consent, viaCancellation })
|
return await performChange({ doc, events, newUnits, consent, viaCancellation })
|
||||||
} finally {
|
} finally {
|
||||||
activeChanges.delete(doc.$id)
|
activeChanges.delete(doc.$id)
|
||||||
@@ -393,14 +417,20 @@ async function performChange({ doc, events, newUnits, consent, viaCancellation }
|
|||||||
const currentUnits = unitsOf(fresh)
|
const currentUnits = unitsOf(fresh)
|
||||||
const paidUnits = paidUnitsOf(fresh)
|
const paidUnits = paidUnitsOf(fresh)
|
||||||
const plan = planStorageChange({ currentUnits, paidUnits, newUnits, maxUnits: config.mailStorage.maxUnits })
|
const plan = planStorageChange({ currentUnits, paidUnits, newUnits, maxUnits: config.mailStorage.maxUnits })
|
||||||
if (plan.direction === 'none') throw httpError(400, 'Die Anzahl der Pakete ist unverändert.')
|
// Weicht der frische Stand vom Spiegel ab, kann aus einem Minus ein Plus werden: Zustimmung und
|
||||||
|
// Kuendigungsweg erneut pruefen, bevor irgendetwas geschrieben wird (Fix 2)
|
||||||
|
assertPlanAllowed(plan, consent, { reconciled: true, viaCancellation })
|
||||||
const plus = plan.direction === 'plus'
|
const plus = plan.direction === 'plus'
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const kind = plus && newUnits > paidUnits ? 'einrichten' : 'periodenende'
|
const kind = plus && newUnits > paidUnits ? 'einrichten' : 'periodenende'
|
||||||
|
// Ein Plus genau zurueck auf die bezahlte Menge: konsoleH steht schon darauf, zum Periodenende ist nichts zu
|
||||||
|
// tun - kein offener Auftrag fuers Team, das Ereignis bleibt als Nachweis (erledigt)
|
||||||
|
const task = !(plus && newUnits === paidUnits)
|
||||||
const reductionAt = reductionEffectiveAt(fresh)
|
const reductionAt = reductionEffectiveAt(fresh)
|
||||||
const change = {
|
const change = {
|
||||||
direction: plan.direction,
|
direction: plan.direction,
|
||||||
kind,
|
kind,
|
||||||
|
task,
|
||||||
fromUnits: currentUnits,
|
fromUnits: currentUnits,
|
||||||
toUnits: newUnits,
|
toUnits: newUnits,
|
||||||
// Fuer den Kunden: ein Plus gilt sofort, ein Minus zum Periodenende; fuers Team: die Einrichtung ist
|
// Fuer den Kunden: ein Plus gilt sofort, ein Minus zum Periodenende; fuers Team: die Einrichtung ist
|
||||||
@@ -413,7 +443,7 @@ async function performChange({ doc, events, newUnits, consent, viaCancellation }
|
|||||||
partial: false,
|
partial: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Ereignis zuerst: Nachweis und Aufgabe, Status offen
|
// 1. Ereignis zuerst: Nachweis und Aufgabe, Status offen (ohne Aufgabe: erledigt)
|
||||||
const event = await recordStorageEvent({ doc: fresh, change, consent, viaCancellation })
|
const event = await recordStorageEvent({ doc: fresh, change, consent, viaCancellation })
|
||||||
|
|
||||||
// 2. Stripe, Schritt fuer Schritt (planStorageChange)
|
// 2. Stripe, Schritt fuer Schritt (planStorageChange)
|
||||||
|
|||||||
@@ -413,22 +413,81 @@ test('Plus in zwei Schritten: erst die bezahlte Menge ohne Verrechnung, dann ant
|
|||||||
test('Plus bis zur bezahlten Menge: ein Schritt ohne Verrechnung, Aufgabe zum Periodenende, kein Einrichten', async () => {
|
test('Plus bis zur bezahlten Menge: ein Schritt ohne Verrechnung, Aufgabe zum Periodenende, kein Einrichten', async () => {
|
||||||
await resetSubH({ units: 1, paidUnits: 3 })
|
await resetSubH({ units: 1, paidUnits: 3 })
|
||||||
const before = st.calls.length
|
const before = st.calls.length
|
||||||
const result = await changeStorage('c1', 3, consent)
|
const result = await changeStorage('c1', 2, consent)
|
||||||
const upd = subscriptionUpdates(before)
|
const upd = subscriptionUpdates(before)
|
||||||
assert.equal(upd.length, 1)
|
assert.equal(upd.length, 1)
|
||||||
assert.equal(upd[0].body['items[0][quantity]'], '3')
|
assert.equal(upd[0].body['items[0][quantity]'], '2')
|
||||||
assert.equal(upd[0].body['proration_behavior'], 'none')
|
assert.equal(upd[0].body['proration_behavior'], 'none')
|
||||||
assert.equal(doc().storageUnits, 3)
|
assert.equal(doc().storageUnits, 2)
|
||||||
assert.equal(doc().storagePaidUnits, 3)
|
assert.equal(doc().storagePaidUnits, 3)
|
||||||
const [ev] = await listContractEvents('c1')
|
const [ev] = await listContractEvents('c1')
|
||||||
|
assert.equal(ev.status, 'offen')
|
||||||
assert.equal(ev.details.kind, 'periodenende')
|
assert.equal(ev.details.kind, 'periodenende')
|
||||||
assert.equal(ev.details.toUnits, 3)
|
assert.equal(ev.details.toUnits, 2)
|
||||||
assert.equal(ev.dueAt, hostingDoc.currentPeriodEnd)
|
assert.equal(ev.dueAt, hostingDoc.currentPeriodEnd)
|
||||||
assert.equal(ev.termsVersion, consent.termsVersion, 'Bestellung mit Zustimmung')
|
assert.equal(ev.termsVersion, consent.termsVersion, 'Bestellung mit Zustimmung')
|
||||||
assert.equal(result.change.kind, 'periodenende')
|
assert.equal(result.change.kind, 'periodenende')
|
||||||
assert.ok(Math.abs(new Date(result.change.effectiveAt).getTime() - Date.now()) < 5000, 'fuer den Kunden sofort wirksam')
|
assert.ok(Math.abs(new Date(result.change.effectiveAt).getTime() - Date.now()) < 5000, 'fuer den Kunden sofort wirksam')
|
||||||
assert.equal(t13().subject, `[Speicher] reduzieren zum ${PERIOD_END_LABEL}: k@example.invalid`)
|
assert.equal(t13().subject, `[Speicher] reduzieren zum ${PERIOD_END_LABEL}: k@example.invalid`)
|
||||||
assert.ok(k13().attachments.length === 1, 'K13 mit PDF (Bestellung)')
|
// Fix 2 (3): K13 als Bestellung (PDF, Widerruf), aber ohne Einrichtung/anteilige Berechnung - schon bezahlt
|
||||||
|
const customerMail = k13()
|
||||||
|
assert.equal(customerMail.attachments.length, 1, 'K13 mit PDF (Bestellung)')
|
||||||
|
for (const text of ['schon bezahlt', 'keine zusätzlichen Kosten', '2 Pakete', '14 Tagen widerrufen', 'Vertragsunterlagen im Anhang']) {
|
||||||
|
assert.ok(customerMail.text.includes(text), `K13 ohne "${text}":\n${customerMail.text}`)
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(customerMail.text, /wird eingerichtet|anteilig|ohne anteilige Erstattung/)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix 2 (5): ein Plus genau zurueck auf die bezahlte Menge - konsoleH steht schon darauf, zum Periodenende ist
|
||||||
|
// nichts zu tun: die offene Verringerung wird geschlossen, keine neue Aufgabe, das Ereignis bleibt als Nachweis
|
||||||
|
test('Plus genau zurück auf die bezahlte Menge: Verringerung aufgehoben, keine neue Aufgabe, Nachweis bleibt', async () => {
|
||||||
|
await resetSubH({ units: 3, paidUnits: 3 })
|
||||||
|
await changeStorage('c1', 1, null)
|
||||||
|
const [minusEvent] = await listContractEvents('c1')
|
||||||
|
mails.sent.length = 0
|
||||||
|
const before = st.calls.length
|
||||||
|
const result = await changeStorage('c1', 3, consent)
|
||||||
|
assert.equal(subscriptionUpdates(before)[0].body['proration_behavior'], 'none')
|
||||||
|
assert.equal(doc().storageUnits, 3)
|
||||||
|
assert.deepEqual(await openTasks(), [])
|
||||||
|
const events = await listContractEvents('c1')
|
||||||
|
const superseded = events.find((e) => e.$id === minusEvent.$id)
|
||||||
|
assert.equal(superseded.status, 'erledigt')
|
||||||
|
assert.equal(superseded.doneBy, 'ersetzt')
|
||||||
|
const plusEvent = events.find((e) => e.$id === result.event.$id)
|
||||||
|
assert.equal(plusEvent.status, 'erledigt')
|
||||||
|
assert.equal(plusEvent.doneBy, 'keine Aufgabe')
|
||||||
|
assert.ok(plusEvent.doneAt)
|
||||||
|
assert.equal(plusEvent.details.kind, 'periodenende')
|
||||||
|
assert.equal(plusEvent.details.toUnits, 3)
|
||||||
|
assert.equal(plusEvent.termsVersion, consent.termsVersion, 'Nachweis der Bestellung')
|
||||||
|
assert.equal(result.change.task, false)
|
||||||
|
// K13 bestaetigt die Bestellung (nichts berechnet), T13 entfaellt: nichts zu tun
|
||||||
|
assert.equal(mails.sent.length, 1)
|
||||||
|
assert.ok(k13().text.includes('keine zusätzlichen Kosten'))
|
||||||
|
assert.equal(t13(), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fix 2 (1): weichen Spiegel und Stripe ab, wird der Plan nach dem Abgleich erneut geprueft - ein Plus ohne
|
||||||
|
// Zustimmung oder ueber das Kuendigungsformular darf nicht durchrutschen
|
||||||
|
test('Spiegel und Stripe weichen ab: Plan nach dem Abgleich erneut geprüft (400/409), nichts angelegt', async () => {
|
||||||
|
await resetSubH({ units: 3 })
|
||||||
|
// Spiegel sagt 3 Pakete, Stripe nur 1: aus dem vermeintlichen Minus auf 2 wird ein Plus
|
||||||
|
st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting', storageUnits: 1 }))
|
||||||
|
const before = st.calls.length
|
||||||
|
await assert.rejects(changeStorage('c1', 2, null), (e) => e.status === 400 && /zustimmen/.test(e.message))
|
||||||
|
assert.deepEqual(await listContractEvents('c1'), [], 'kein Ereignis')
|
||||||
|
assert.deepEqual(subscriptionUpdates(before), [], 'keine Stripe-Aenderung')
|
||||||
|
assert.deepEqual(mails.sent, [])
|
||||||
|
assert.equal(storageItem().quantity, 1)
|
||||||
|
await assert.rejects(changeStorage('c1', 2, null, { viaCancellation: true }), (e) => e.status === 409)
|
||||||
|
assert.deepEqual(await listContractEvents('c1'), [])
|
||||||
|
assert.deepEqual(subscriptionUpdates(before), [])
|
||||||
|
// Mit Zustimmung geht das Plus, mit dem frischen Stand aus Stripe
|
||||||
|
const result = await changeStorage('c1', 2, consent)
|
||||||
|
assert.equal(result.change.fromUnits, 1)
|
||||||
|
assert.equal(result.change.kind, 'einrichten')
|
||||||
|
assert.equal(doc().storageUnits, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Plus ohne Zustimmung: 400; gekündigt: 409', async () => {
|
test('Plus ohne Zustimmung: 400; gekündigt: 409', async () => {
|
||||||
@@ -704,6 +763,35 @@ test('Spiegel nicht aktualisierbar nach der Stripe-Änderung: Aufruf gelingt, Er
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fix 2 (4): aeltere offene Aufgaben werden nur ersetzt, wenn die neue Aufgabe existiert und Stripe die Aenderung
|
||||||
|
// uebernommen hat - sonst bliebe das Team ohne Aufgabe
|
||||||
|
test('Ältere offene Aufgabe bleibt, wenn das Ereignis nicht speicherbar ist oder Stripe Schritt 1 ablehnt', async () => {
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
await changeStorage('c1', 2, consent)
|
||||||
|
const [first] = await listContractEvents('c1')
|
||||||
|
const original = console.error
|
||||||
|
console.error = () => {}
|
||||||
|
try {
|
||||||
|
aw.failNext({ collection: config.collections.contractEvents, method: 'POST', status: 500 })
|
||||||
|
await changeStorage('c1', 3, consent)
|
||||||
|
let events = await listContractEvents('c1')
|
||||||
|
assert.equal(events.length, 1)
|
||||||
|
assert.equal(events[0].$id, first.$id)
|
||||||
|
assert.equal(events[0].status, 'offen', 'aeltere Aufgabe bleibt offen')
|
||||||
|
assert.equal(storageItem().quantity, 3)
|
||||||
|
|
||||||
|
st.failNext({ method: 'POST', path: '/v1/subscriptions/sub_h', status: 400, message: 'Nachbau: abgelehnt' })
|
||||||
|
await assert.rejects(changeStorage('c1', 4, consent), (e) => e.status === 502)
|
||||||
|
events = await listContractEvents('c1')
|
||||||
|
assert.equal(events.length, 2)
|
||||||
|
assert.equal(events.find((e) => e.$id === first.$id).status, 'offen', 'aeltere Aufgabe bleibt offen')
|
||||||
|
assert.equal(events.find((e) => e.$id !== first.$id).status, 'gescheitert')
|
||||||
|
assert.equal((await openTasks()).length, 1)
|
||||||
|
} finally {
|
||||||
|
console.error = original
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('Ereignis nicht speicherbar: Stripe-Änderung läuft, T13 mit Block nachweis_fehlt', async () => {
|
test('Ereignis nicht speicherbar: Stripe-Änderung läuft, T13 mit Block nachweis_fehlt', async () => {
|
||||||
await resetSubH({ units: 0 })
|
await resetSubH({ units: 0 })
|
||||||
const errors = []
|
const errors = []
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ test('Wechsel-Mails nennen keine feste Frist von 23 Stunden', () => {
|
|||||||
test('K13, T13: Betreff, Platzhalter und Blöcke des Zusatzspeichers', () => {
|
test('K13, T13: Betreff, Platzhalter und Blöcke des Zusatzspeichers', () => {
|
||||||
assert.equal(loadTemplate('K13').subject, 'Ihr E-Mail-Zusatzspeicher – Bestätigung')
|
assert.equal(loadTemplate('K13').subject, 'Ihr E-Mail-Zusatzspeicher – Bestätigung')
|
||||||
assert.deepEqual(loadTemplate('K13').placeholders, ['name', 'pakete', 'gesamt_gb', 'monatspreis_zusatz', 'wirksam_ab'])
|
assert.deepEqual(loadTemplate('K13').placeholders, ['name', 'pakete', 'gesamt_gb', 'monatspreis_zusatz', 'wirksam_ab'])
|
||||||
assert.deepEqual(loadTemplate('K13').blocks, ['plus', 'minus'])
|
// Fix 2 (3): plus = Einrichtung mit anteiliger Berechnung, plus_bezahlt = Plus bis zur schon bezahlten Menge,
|
||||||
|
// bestellung = Unterlagen und Widerruf (bei beiden), minus = Verringerung zum Periodenende
|
||||||
|
assert.deepEqual(loadTemplate('K13').blocks, ['plus', 'plus_bezahlt', 'bestellung', 'minus'])
|
||||||
assert.equal(loadTemplate('T13').subject, '[Speicher] {{aktion}}: {{email}}')
|
assert.equal(loadTemplate('T13').subject, '[Speicher] {{aktion}}: {{email}}')
|
||||||
for (const p of ['aktion', 'email', 'name', 'stripe_abo_id', 'postfaecher_hinweis', 'neue_groesse_gb', 'pakete', 'faellig']) {
|
for (const p of ['aktion', 'email', 'name', 'stripe_abo_id', 'postfaecher_hinweis', 'neue_groesse_gb', 'pakete', 'faellig']) {
|
||||||
assert.ok(loadTemplate('T13').placeholders.includes(p), p)
|
assert.ok(loadTemplate('T13').placeholders.includes(p), p)
|
||||||
@@ -217,17 +219,22 @@ test('K13, T13: Betreff, Platzhalter und Blöcke des Zusatzspeichers', () => {
|
|||||||
assert.deepEqual([...loadTemplate('T13').blocks].sort(), ['einrichten', 'nachweis_fehlt', 'periodenende', 'teilweise'])
|
assert.deepEqual([...loadTemplate('T13').blocks].sort(), ['einrichten', 'nachweis_fehlt', 'periodenende', 'teilweise'])
|
||||||
|
|
||||||
const values = sampleValues(loadTemplate('K13'))
|
const values = sampleValues(loadTemplate('K13'))
|
||||||
const plus = renderMail('K13', values, { plus: true })
|
const plus = renderMail('K13', values, { plus: true, bestellung: true })
|
||||||
assert.deepEqual(plus.problems, [])
|
assert.deepEqual(plus.problems, [])
|
||||||
assert.match(plus.text, /gemäß § 19 UStG ohne Umsatzsteuer/)
|
assert.match(plus.text, /gemäß § 19 UStG ohne Umsatzsteuer/)
|
||||||
assert.match(plus.text, /wird eingerichtet/)
|
assert.match(plus.text, /wird eingerichtet/)
|
||||||
assert.match(plus.text, /14 Tagen widerrufen/)
|
assert.match(plus.text, /14 Tagen widerrufen/)
|
||||||
assert.match(plus.text, /Vertragsunterlagen im Anhang/)
|
assert.match(plus.text, /Vertragsunterlagen im Anhang/)
|
||||||
assert.doesNotMatch(plus.text, /ohne anteilige Erstattung/)
|
assert.doesNotMatch(plus.text, /ohne anteilige Erstattung|keine zusätzlichen Kosten/)
|
||||||
|
const paid = renderMail('K13', values, { plus_bezahlt: true, bestellung: true })
|
||||||
|
assert.ok(paid.text.includes('Die Pakete sind für den laufenden Monat schon bezahlt; es entstehen keine zusätzlichen Kosten. Ab dem nächsten Abrechnungsmonat gelten Wert-pakete Pakete.'))
|
||||||
|
assert.match(paid.text, /14 Tagen widerrufen/)
|
||||||
|
assert.match(paid.text, /Vertragsunterlagen im Anhang/)
|
||||||
|
assert.doesNotMatch(paid.text, /wird eingerichtet|anteilig/)
|
||||||
const minus = renderMail('K13', values, { minus: true })
|
const minus = renderMail('K13', values, { minus: true })
|
||||||
assert.match(minus.text, /ohne anteilige Erstattung/)
|
assert.match(minus.text, /ohne anteilige Erstattung/)
|
||||||
assert.match(minus.text, /keine neuen E-Mails/)
|
assert.match(minus.text, /keine neuen E-Mails/)
|
||||||
assert.doesNotMatch(minus.text, /wird eingerichtet|14 Tagen widerrufen|im Anhang/)
|
assert.doesNotMatch(minus.text, /wird eingerichtet|14 Tagen widerrufen|im Anhang|schon bezahlt/)
|
||||||
|
|
||||||
const t = loadTemplate('T13')
|
const t = loadTemplate('T13')
|
||||||
const teilweise = renderMail('T13', { ...sampleValues(t), pakete: 5 }, { einrichten: true, teilweise: true })
|
const teilweise = renderMail('T13', { ...sampleValues(t), pakete: 5 }, { einrichten: true, teilweise: true })
|
||||||
|
|||||||
Reference in new issue
Block a user