Öffnungszeiten: Fixrunde Server (Schema-Kopplung, K27, Wertersatz, Widerruf, Ticket nachlegen)
- activate() schreibt die Zeiten-Felder (aboEndeAt, behaltenSessionId) nur noch über extra
(nur Abo-Rückkehr und Team-Aktivierung der Zeiten); News und Terminplaner laufen auch vor zeiten-setup.mjs
- Abo-Abgleich: K27 gilt als offen, solange der Marker vor disabledAt liegt (kein Zurücksetzen nach disable() nötig);
K27 geht erst nach dem Ersatz-Ticket raus (scheitert es, wiederholt der nächste Lauf beides);
Löschung setzt auch activatedAt zurück
- Zustimmung "beginn" (bezahlt) mit Wertersatz-Satz (§ 357a Abs. 2 BGB) wie bei News und Terminplaner, Fassung 2026-10-05
- Widerruf: Bestellung "zeiten" in ORDERS, FEATURE_OF_ORDER, orderLabel und widerrufen.html
- Team "Ticket nachlegen" der Zeiten ohne Kauf geht über holeZeitenNach statt über den Zahlungsweg
- Einschalten-Route nimmt nur { zustimmung }, nicht die Altform { consent: true }
- Team-Aktivierung behält kauf/unlockedByPurchase bei einem bisher gekauften Dokument
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c10e50d0b3
commit
ecf67d6978
13 files changed
+337
-48
No files matched your search
@@ -462,11 +462,26 @@ export async function markCheckoutFailed(sessionId) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Oeffnungszeiten live (Spec 5): das erkannte Abo-Ende und eine offene Behalten-Zahlung gehoeren zum alten Lauf und enden mit der
|
||||
* Aktivierung. Als `extra` an activate() (nur der Zeiten-Pfad; die Felder legt scripts/zeiten-setup.mjs an, News und Terminplaner
|
||||
* teilen die Collection).
|
||||
*/
|
||||
export const ZEITEN_AKTIVIERUNG_FELDER = Object.freeze({ aboEndeAt: null, behaltenSessionId: '' })
|
||||
|
||||
function byNote(by, verb) {
|
||||
return by ? `${verb} von ${by}`.slice(0, 300) : undefined
|
||||
}
|
||||
|
||||
export async function activate(docId, { by, adminNote } = {}) {
|
||||
/**
|
||||
* Funktion aktivieren (jede Funktion gleich). `extra` sind zusaetzliche Felder, die der Aufrufer mitschreibt: Oeffnungszeiten live
|
||||
* leert damit sein erkanntes Abo-Ende und eine offene Behalten-Zahlung (ZEITEN_AKTIVIERUNG_FELDER). Diese Felder legt erst
|
||||
* scripts/zeiten-setup.mjs an; darum schreibt activate() sie nie von selbst, sonst scheiterte die Aktivierung von News und
|
||||
* Terminplaner mit "Unknown attribute", solange der Code vor dem Skript laeuft.
|
||||
* @param {string} docId
|
||||
* @param {{ by?: string, adminNote?: string, extra?: Record<string, unknown> }} [options]
|
||||
*/
|
||||
export async function activate(docId, { by, adminNote, extra } = {}) {
|
||||
const note = adminNote ?? byNote(by, 'Freigeschaltet')
|
||||
return updateDocument(config.collections.portalFeatures, docId, {
|
||||
...stateFields('active'),
|
||||
@@ -477,10 +492,7 @@ export async function activate(docId, { by, adminNote } = {}) {
|
||||
disabledAt: null,
|
||||
disabledReason: null,
|
||||
aboEndeMailAt: null,
|
||||
// Oeffnungszeiten live (Spec 5): das erkannte Abo-Ende und eine offene Behalten-Zahlung gehoeren zum alten Lauf und enden mit der
|
||||
// Aktivierung (die Felder legt scripts/zeiten-setup.mjs an; News und Terminplaner teilen die Collection)
|
||||
aboEndeAt: null,
|
||||
behaltenSessionId: '',
|
||||
...(extra ?? {}),
|
||||
...(note ? { adminNote: note } : {}),
|
||||
})
|
||||
}
|
||||
@@ -583,7 +595,7 @@ export async function adminActivate(customerId, projectId, key, { by, adminNote
|
||||
await assertOwnProject(customerId, projectId)
|
||||
const doc = await ensureFeatureDoc(customerId, projectId, key)
|
||||
if (key === 'termine' && doc.unlockedByPurchase === true) await assertAboLaeuftFuerAktivierung(customerId)
|
||||
return activate(doc.$id, { by, adminNote })
|
||||
return activate(doc.$id, { by, adminNote, ...(key === 'zeiten' ? { extra: ZEITEN_AKTIVIERUNG_FELDER } : {}) })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -814,6 +826,26 @@ async function ticketNachZahlung(doc, note) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket nachlegen (a) fuer Oeffnungszeiten live ohne Kauf (Abo, Team): es gibt keine Zahlung zu pruefen; das Einrichten holt
|
||||
* holeZeitenNach nach (Neubau, Einbau-Ticket, K24 nur bei Abo), derselbe Weg wie der 5-Minuten-Job und das Einschalten. Dienst
|
||||
* erst beim Aufruf laden (zeiten/* nur per await import()). Sperren: feature-ticket (hier gehalten) -> zeiten-nachholen ->
|
||||
* zeiten-neubau; nichts darin nimmt feature-admin. Wirft, wenn das Ticket nicht angelegt werden kann (der Aufrufer meldet es).
|
||||
*/
|
||||
async function ticketNachHolen(doc, note) {
|
||||
const { holeZeitenNach } = await import('./zeiten/einschalten.js')
|
||||
const nachher = await holeZeitenNach(doc)
|
||||
if (!nachher.ticketId) return { doc: nachher, created: false }
|
||||
if (!note) return { doc: nachher, created: true }
|
||||
try {
|
||||
return { doc: await updateDocument(config.collections.portalFeatures, doc.$id, { adminNote: note }), created: true }
|
||||
} catch (err) {
|
||||
// Ticket und Mail sind raus; die Notiz ist nur ein Vermerk
|
||||
console.error(`[funktionen] Notiz zum nachgelegten Ticket von ${doc.$id} nicht gespeichert:`, err.message)
|
||||
return { doc: nachher, created: true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket nachlegen (b): ticketId gesetzt, das Ticket aber geloescht. Ein neues Ticket ersetzt die ticketId; K15 und T16 gingen beim
|
||||
* ersten Mal raus, darum keine Mail. Laeuft unter der Sperre der Zahlung (feature-paid:<session>), damit kein Stripe-Retry daneben
|
||||
@@ -835,7 +867,8 @@ async function ersetzeGeloeschtesTicket(doc, note) {
|
||||
/**
|
||||
* "Ticket nachlegen": nur fuer eine Funktion in Einrichtung ('setup'; sonst 409, ohne Dokument 409).
|
||||
* (a) ticketId leer: Zahlung bei Stripe pruefen (nicht bezahlt 409, Stripe nicht erreichbar 503) und den Weg des Webhooks gehen,
|
||||
* Ticket + K15 + T16 genau einmal (ticketNachZahlung);
|
||||
* Ticket + K15 + T16 genau einmal (ticketNachZahlung); Oeffnungszeiten live ohne Kauf (Abo, Team) haben keine Zahlung: dort holt
|
||||
* holeZeitenNach Neubau, Ticket und K24 nach (ticketNachHolen);
|
||||
* (b) ticketId gesetzt, Ticket geloescht: neues Ticket, ticketId ersetzt, keine Mails (ersetzeGeloeschtesTicket);
|
||||
* (c) Ticket vorhanden: nichts, created false.
|
||||
* Eine Sperre feature-ticket:<id> je Dokument: gleichzeitige Aufrufe laufen nacheinander und sehen das Ergebnis des ersten
|
||||
@@ -849,7 +882,7 @@ export async function ticketNachlegen({ customerId, projectId, key, by, adminNot
|
||||
return withLocks([`feature-ticket:${vorab.$id}`], async () => {
|
||||
const doc = await frischesDokument(vorab, key)
|
||||
if (doc.state !== 'setup') throw httpError(409, TICKET_NUR_SETUP)
|
||||
if (!doc.ticketId) return ticketNachZahlung(doc, note)
|
||||
if (!doc.ticketId) return key === 'zeiten' && unlockedByVon(doc) !== 'kauf' ? ticketNachHolen(doc, note) : ticketNachZahlung(doc, note)
|
||||
if (await ticketBesteht(doc.ticketId)) return { doc, created: false }
|
||||
return ersetzeGeloeschtesTicket(doc, note)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { config } from '../../config.js'
|
||||
import { aboLage, aboMemo } from '../aboStatus.js'
|
||||
import { getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { activate, customerOf, disable, notizVorn, stateFields, unlockedByVon } from '../siteFeatures.js'
|
||||
import { activate, customerOf, disable, notizVorn, stateFields, unlockedByVon, ZEITEN_AKTIVIERUNG_FELDER } from '../siteFeatures.js'
|
||||
import { LOESCHFRIST_TAGE } from '../termine/frist.js'
|
||||
import { pruefeNow, zeitMs } from '../termine/util.js'
|
||||
import { heute, plusTage, zuUtc } from '../termine/zeit.js'
|
||||
@@ -22,8 +22,8 @@ import { ladeZeiten, loescheZeiten } from './speicher.js'
|
||||
*
|
||||
* setup/active, Abo läuft nicht, aboEndeAt leer Schritt 1: aboEndeAt = now, K26 (Variante nach art; Frist = Berliner Tag von
|
||||
* aboEndeAt + 30 Tage)
|
||||
* setup/active, Abo läuft nicht, Frist erreicht Schritt 2: disabled/'abo_ende', K27 (Löschdatum = Abschalten + 30 Tage), danach
|
||||
* das Ersatz-Ticket des Teams (nachAbschaltung)
|
||||
* setup/active, Abo läuft nicht, Frist erreicht Schritt 2: disabled/'abo_ende', dann das Ersatz-Ticket des Teams
|
||||
* (nachAbschaltung), dann K27 (Löschdatum = Abschalten + 30 Tage)
|
||||
* setup/active, Abo läuft, aboEndeAt gesetzt Schritt 3a: das Abo ist vor dem Abschalten zurück: aboEndeAt, Marker und offene
|
||||
* Behalten-Sitzung leeren, Zustand bleibt, KEIN Neubau (die Leisten waren nie weg),
|
||||
* K24 "wieder" ohne den Kasten "Wie geht es weiter?" (k24Mail einbau null, R17b)
|
||||
@@ -32,7 +32,7 @@ import { ladeZeiten, loescheZeiten } from './speicher.js'
|
||||
* mit dem echten einbau. Solange die Eingaben noch da sind: auch am Tag der Löschung
|
||||
* geht die Rückkehr der Löschung vor (die Rückkehr wird je Lauf zuerst entschieden).
|
||||
* disabled/'abo_ende', Abo läuft nicht, 30 Tage Schritt 4: loescheZeiten (daten, team, Merker) und state 'off'; ticketId, einbau,
|
||||
* nach disabledAt k24At, aboEndeAt und behaltenSessionId leer (ersatzHash lebt im gelöschten
|
||||
* nach disabledAt k24At, activatedAt, aboEndeAt und behaltenSessionId leer (ersatzHash lebt im gelöschten
|
||||
* Zeiten-Dokument). Grund 'team' (und jeder andere Grund) wird nie gelöscht.
|
||||
*
|
||||
* Tage zählen nach BERLINER KALENDERTAGEN (plusTage, nie Vielfache von 24 Stunden): "Tag 30" ist der 30. Berliner Tag nach dem Tag, an dem
|
||||
@@ -50,9 +50,11 @@ import { ladeZeiten, loescheZeiten } from './speicher.js'
|
||||
* eingeschaltet. Der Kunde wird übersprungen, `fehler` zählt, der nächste Lauf versucht es wieder. Ein Dokument mit unlesbarem Datum
|
||||
* wird übersprungen (`fehler`), die übrigen laufen weiter. Das Log nennt weder Namen noch Adressen.
|
||||
* - Mails: aboEndeMailAt ist der Marker "die Mail dieser Phase ging raus", gesetzt NACH dem Versand: in der Frist gilt er der K26, nach dem
|
||||
* Abschalten der K27 (er wird beim Abschalten zurückgesetzt). Scheitert der Versand, bleibt der Marker leer, der nächste Lauf sendet nach
|
||||
* (dieselben Daten; die Frist wächst nie, aboEndeAt und disabledAt ändert der Abgleich nicht mehr). Ein Kunde ohne Adresse bekommt den
|
||||
* Marker (es gibt nichts zu senden). K24 "wieder" geht nur an Kunden, die K26 oder K27 bekommen haben (Marker, vor dem Leeren gelesen)
|
||||
* Abschalten der K27 (nur ein Marker, der nicht vor disabledAt liegt: k27Gesendet; ein Zurücksetzen beim Abschalten gibt es nicht, es
|
||||
* könnte scheitern). Scheitert der Versand, bleibt der Marker leer, der nächste Lauf sendet nach (dieselben Daten; die Frist wächst
|
||||
* nie, aboEndeAt und disabledAt ändert der Abgleich nicht mehr). Die K27 geht erst nach dem Ersatz-Ticket raus: scheitert es, gibt es
|
||||
* weder K27 noch Marker, der nächste Lauf wiederholt beides (Ticket idempotent über den Hash, K27 so höchstens einmal). Ein Kunde ohne
|
||||
* Adresse bekommt den Marker (es gibt nichts zu senden). K24 "wieder" geht nur an Kunden, die K26 oder K27 bekommen haben (Marker, vor dem Leeren gelesen)
|
||||
* und hat keinen Marker: höchstens einmal je Rückkehr (scheitert der Versand, steht es im Log und in `fehler`).
|
||||
* - Ruling R25: die Rückkehr leert aboEndeAt und behaltenSessionId; eine noch offene Behalten-Zahlung bucht danach nicht mehr zum Kauf
|
||||
* (bucheBehalten: Sitzung passt nicht, T19), und starteBehalten lehnt bei laufendem Abo ab.
|
||||
@@ -102,7 +104,7 @@ function entscheide(doc, lage, now) {
|
||||
if (istAbgeschaltet(doc)) {
|
||||
if (lage.laeuft) return 'zurueck'
|
||||
if (tag >= loeschTag(doc)) return 'loeschen'
|
||||
return doc.aboEndeMailAt ? null : 'k27'
|
||||
return k27Gesendet(doc) ? null : 'k27'
|
||||
}
|
||||
if (lage.laeuft) return doc.aboEndeAt ? 'zurueck_ohne_neubau' : null
|
||||
if (!doc.aboEndeAt) return 'ende'
|
||||
@@ -110,6 +112,13 @@ function entscheide(doc, lage, now) {
|
||||
return doc.aboEndeMailAt ? null : 'k26'
|
||||
}
|
||||
|
||||
/**
|
||||
* Hat die K27 ihren Marker? aboEndeMailAt gilt der K27 nur, wenn es NACH dem Abschalten gesetzt wurde: der Marker der K26 (aus der Frist)
|
||||
* ist älter als disabledAt. So hängt die K27 nicht daran, dass der Abgleich nach disable() den Marker noch zurücksetzen konnte
|
||||
* (scheiterte dieses Schreiben, blieb der K26-Marker stehen und K27 samt Ersatz-Ticket liefen nie).
|
||||
*/
|
||||
const k27Gesendet = (doc) => Boolean(doc.aboEndeMailAt) && !(zeitMs(doc.aboEndeMailAt) < zeitMs(doc.disabledAt))
|
||||
|
||||
/** Sind Eingaben des Kunden gespeichert? Lesefehler werden weitergereicht (dann bleibt alles, wie es ist). */
|
||||
async function datenDa(featureId) {
|
||||
return (await ladeZeiten(featureId))?.daten != null
|
||||
@@ -123,6 +132,7 @@ async function loesche(frisch, ctx) {
|
||||
ticketId: '',
|
||||
einbau: '',
|
||||
k24At: null,
|
||||
activatedAt: null, // ein späteres Einschalten beginnt wieder in setup (sonst käme es durch das alte activatedAt zu früh auf active)
|
||||
aboEndeAt: null,
|
||||
behaltenSessionId: '',
|
||||
adminNote: notizVorn(frisch.adminNote, `${ctx.jetzt} ${NOTIZ_GELOESCHT}`),
|
||||
@@ -156,8 +166,8 @@ async function unterSperre(doc, ctx) {
|
||||
const aus = await disable(frisch.$id, { reason: GRUND, adminNote: notizVorn(frisch.adminNote, `${jetzt} ${NOTIZ_ABGESCHALTET}`) })
|
||||
if (aus.state !== 'disabled' || aus.disabledReason !== GRUND) return null // jemand war zwischen Lesen und Schreiben schneller
|
||||
erg.abgeschaltet += 1
|
||||
// ab jetzt gilt der Marker der K27, nicht mehr der der K26
|
||||
return { aktion: 'k27', doc: await updateDocument(portalFeatures(), frisch.$id, { aboEndeMailAt: null }), lage }
|
||||
// ab jetzt gilt der Marker der K27, nicht mehr der der K26 (k27Gesendet: der K26-Marker ist älter als disabledAt, kein Zurücksetzen nötig)
|
||||
return { aktion: 'k27', doc: aus, lage }
|
||||
}
|
||||
case 'loeschen':
|
||||
await loesche(frisch, ctx)
|
||||
@@ -170,7 +180,7 @@ async function unterSperre(doc, ctx) {
|
||||
}
|
||||
const note = notizVorn(frisch.adminNote, `${jetzt} ${NOTIZ_WIEDER}`)
|
||||
const zurueck = frisch.activatedAt
|
||||
? await activate(frisch.$id, { adminNote: note })
|
||||
? await activate(frisch.$id, { adminNote: note, extra: ZEITEN_AKTIVIERUNG_FELDER })
|
||||
: await updateDocument(portalFeatures(), frisch.$id, {
|
||||
...stateFields('setup'),
|
||||
disabledAt: null,
|
||||
@@ -210,8 +220,12 @@ async function ladeProjekt(projectId) {
|
||||
const projektName = (projekt) => projekt?.projectName || projekt?.subdomain || ''
|
||||
const preis = () => config.features.zeiten.priceLabel
|
||||
|
||||
/** K26 oder K27 an die E-Mail des Kundenkontos; danach der Marker. Wirft nie bei Versandfehlern (fehler zählt, der nächste Lauf wiederholt). */
|
||||
async function sendeMitMarker(doc, bauen, ctx) {
|
||||
/**
|
||||
* K26 oder K27 an die E-Mail des Kundenkontos; danach der Marker. Wirft nie bei Versandfehlern (fehler zählt, der nächste Lauf wiederholt).
|
||||
* `nichtVor`: der Marker liegt nie vor diesem Zeitpunkt (bei der K27: disabledAt, das die Wanduhr beim Abschalten ein paar Sekunden nach dem
|
||||
* `now` des Laufs schreibt; ein früherer Marker gälte k27Gesendet nicht und die K27 ginge jeden Tag erneut raus).
|
||||
*/
|
||||
async function sendeMitMarker(doc, bauen, ctx, nichtVor = null) {
|
||||
const customer = await customerOf(doc.customerId)
|
||||
const projekt = projektName(await ladeProjekt(doc.projectId))
|
||||
const { sent, reason } = await sendeZeitenMail(customer, bauen({ kunde: customer, projekt }))
|
||||
@@ -220,7 +234,8 @@ async function sendeMitMarker(doc, bauen, ctx) {
|
||||
ctx.erg.fehler += 1 // Marker bleibt leer: der nächste Lauf sendet nach
|
||||
return
|
||||
}
|
||||
await updateDocument(portalFeatures(), doc.$id, { aboEndeMailAt: ctx.jetzt })
|
||||
const marker = nichtVor && zeitMs(nichtVor) > zeitMs(ctx.jetzt) ? new Date(zeitMs(nichtVor)).toISOString() : ctx.jetzt
|
||||
await updateDocument(portalFeatures(), doc.$id, { aboEndeMailAt: marker })
|
||||
}
|
||||
|
||||
/** K24 "wieder" (ohne Marker, höchstens einmal je Rückkehr). einbau null = es gab keinen Neubau. */
|
||||
@@ -243,8 +258,13 @@ async function danach({ aktion, doc, lage, hatteMail }, ctx) {
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k26Mail({ kunde, projekt, art: lage.art, frist: mittags(fristTag(doc)), preis: preis() }), ctx)
|
||||
return
|
||||
case 'k27':
|
||||
if ((await nachAbschaltung(doc, { now: ctx.now })) === 'fehler') ctx.erg.fehler += 1
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k27Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis() }), ctx)
|
||||
// Erst das Ersatz-Ticket, dann K27 mit Marker. Scheitert das Ticket, gibt es weder K27 noch Marker: der nächste Lauf wiederholt beides
|
||||
// (das Ticket ist über den Hash idempotent, die K27 ging noch nicht raus und geht so höchstens einmal raus)
|
||||
if ((await nachAbschaltung(doc, { now: ctx.now })) === 'fehler') {
|
||||
ctx.erg.fehler += 1
|
||||
return
|
||||
}
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k27Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis() }), ctx, doc.disabledAt)
|
||||
return
|
||||
case 'zurueck': {
|
||||
const einbau = await stosseNeubauAn(await ladeProjekt(doc.projectId))
|
||||
|
||||
@@ -48,7 +48,7 @@ function notizDesTeams(staff, bisherige, now) {
|
||||
* @param {{ customerId: string, projectId: string, by?: 'kunde'|'team', staff?: { name?: string, adminNote?: string }|null,
|
||||
* nachweis?: (() => Promise<unknown>)|null, now?: Date }} p
|
||||
* by 'kunde': Abo frisch pruefen (kein_abo/gekuendigt/nicht_aktiv 409, Lesefehler 503), unlockedBy 'abo', K24 geht an den Kunden.
|
||||
* by 'team': keine Abo-Pruefung, unlockedBy 'team', keine K24; staff ergibt die Notiz am Dokument.
|
||||
* by 'team': keine Abo-Pruefung, unlockedBy 'team' (ein bisher gekauftes Dokument bleibt kauf), keine K24; staff ergibt die Notiz am Dokument.
|
||||
* nachweis: speichert den Zustimmungsnachweis (Schritt 3, nur wenn wirklich eingeschaltet wird); wirft er, bleibt der Zustand unveraendert.
|
||||
* @returns {Promise<{ doc: object, changed: boolean }>} doc: das Dokument nach dem Nachholen; changed: false bei idempotentem Aufruf
|
||||
* @throws {Error} 409 (Abo, anderer Zustand), 503 (Abo nicht lesbar; oder Zustand gespeichert, aber Neubau/Ticket nicht fertig: erneut aufrufen)
|
||||
@@ -77,12 +77,16 @@ export async function einschaltenZeiten({ customerId, projectId, by = 'kunde', s
|
||||
// einbau beginnt leer, der Neubau (holeZeitenNach) setzt es neu.
|
||||
const vorhanden = bisher ?? (await ensureFeatureDoc(customerId, projectId, KEY))
|
||||
const notiz = by === 'team' ? notizDesTeams(staff, vorhanden.adminNote, now) : null
|
||||
// Ein gekauftes Dokument (bisher kauf), das das Team wieder einschaltet, bleibt gekauft: unlockedBy/unlockedByPurchase ordnen den
|
||||
// Widerruf zu (paidFeatureOrders). Neubau und Ticket kennt dort markPaid, nicht das Nachholen (holeZeitenNach laesst kauf aus): einbau
|
||||
// bleibt darum stehen, statt auf leer gesetzt zu werden, ohne dass es je neu gefuellt wuerde.
|
||||
const gekauft = by === 'team' && unlockedByVon(bisher) === 'kauf'
|
||||
const gespeichert = await updateDocument(config.collections.portalFeatures, vorhanden.$id, {
|
||||
...stateFields('setup'),
|
||||
featureKey: KEY,
|
||||
unlockedBy: by === 'kunde' ? 'abo' : 'team',
|
||||
unlockedByPurchase: false,
|
||||
einbau: '',
|
||||
unlockedBy: gekauft ? 'kauf' : by === 'kunde' ? 'abo' : 'team',
|
||||
unlockedByPurchase: gekauft,
|
||||
...(gekauft ? {} : { einbau: '' }),
|
||||
aboEndeAt: null,
|
||||
behaltenSessionId: '',
|
||||
disabledAt: null,
|
||||
|
||||
Reference in new issue
Block a user