Slots durchsetzen, Abgleiche nach Zeitplan, Kaufdatum im Kontobuch

Ein CC-Paket ist ein Platz fuer einen gleichzeitig aktiven CC - ein Redesign
tauscht einen CC aus, statt einen Platz hinzuzufuegen. Beim Gruppen-Beitritt
galt das seit dem letzten Stand, an anderer Stelle aber noch nicht: ein
redesignter CC liess sich mit einem Redesign-Guthaben zurueckholen, waehrend
sein Nachfolger aktiv blieb. Zwei aktive CCs auf ein einziges Paket.

Geprueft wird das jetzt an einer Stelle statt an jedem Weg einzeln, fuer
Erschaffen, Redesign, Reaktivierung, Rueckkehr und Beitritt. Der Massstab ist
die Deckung, nicht der Kontostand: jeder aktive CC braucht eine eigene Buchung
im CC-Topf. Ein bewusst ueberzogenes Konto bleibt damit moeglich - die Buchung
ist da, nur das Guthaben fehlt, und das Minus steht sichtbar auf der
Spielerseite.

Beide Abgleiche laufen jetzt von allein: die Kaeufe alle zwei Stunden, bei
Spielern ohne Kauf in den letzten drei Jahren alle sechs, das Forum einmal
nachts. Das spart den Grossteil der Anfragen, denn ruhende Konten sind mit der
Zeit die Mehrheit. Gebucht wird dabei nichts - neue Pakete landen in der
Vorschau, neue Antraege in der Liste. Der Stand steht in der Datenbank, damit
ein Neustart nicht alles erneut anstoesst. Unter Verwaltung steht, wann
zuletzt gelaufen wurde; auf der Uebersicht, wie viele Pakete auf eine Buchung
warten und bei wem.

Ausserdem:

- Die Guthaben-Historie zeigt bei Importen den Kauf im Shop, nicht den
  Zeitpunkt der Eingabe hier. Bereits gebuchte Importe werden nachgetragen.
- Forums-Antraege tragen ihr Datum, im Forum ausgelesen statt geraten.
- Die Hinweisbox ueber den Antraegen beim Spieler faellt weg.
- Die Aktionsspalte der Nutzerverwaltung war per display:flex aus dem
  Tabellenlayout gefallen, daher die versetzte Trennlinie.
- Die Zeitzone steht jetzt im Compose-Stack, sonst laege "nachts um vier"
  nach UTC und im Sommer zwei Stunden daneben.
This commit is contained in:
2026-09-21 12:35:36 +02:00
parent 293d882cb4
commit dbde5b7d11
26 changed files with 974 additions and 22 deletions
@@ -0,0 +1,38 @@
-- Wann die Bewegung tatsaechlich stattfand. Beim Import ist das der Kauf im
-- Shop, nicht der Zeitpunkt, an dem jemand hier auf "Buchen" geklickt hat.
ALTER TABLE "CreditLedger" ADD COLUMN IF NOT EXISTS "occurredAt" TIMESTAMP(3);
-- Bestand: erst einmal der Eintragungszeitpunkt, ...
UPDATE "CreditLedger" SET "occurredAt" = "createdAt" WHERE "occurredAt" IS NULL;
-- ... und fuer bereits gebuchte Importe das echte Kaufdatum nachtragen. Der
-- Weg fuehrt ueber die Bestellung: sie verbindet die Buchung mit der Zeile,
-- in der das Kaufdatum steht.
UPDATE "CreditLedger" cl
SET "occurredAt" = r."purchasedAt"
FROM "ImportedOrder" o
JOIN "ImportRow" r
ON r."externalId" = o."externalId"
AND r."importId" = o."importId"
WHERE cl."groupId" = o."groupId"
AND r."purchasedAt" IS NOT NULL;
ALTER TABLE "CreditLedger" ALTER COLUMN "occurredAt" SET NOT NULL;
ALTER TABLE "CreditLedger" ALTER COLUMN "occurredAt" SET DEFAULT CURRENT_TIMESTAMP;
CREATE INDEX IF NOT EXISTS "CreditLedger_occurredAt_idx" ON "CreditLedger"("occurredAt");
-- Wann ein Antrag gestellt wurde, steht in der Boarduebersicht. Bestehende
-- Threads bekommen den Wert beim naechsten Abgleich nachgetragen.
ALTER TABLE "ForumThread" ADD COLUMN IF NOT EXISTS "postedAt" TIMESTAMP(3);
-- Stand der wiederkehrenden Laeufe. Im Code steht der Zeitplan, hier steht,
-- wann zuletzt gelaufen wurde - sonst stiesse jeder Neustart alles neu an.
CREATE TABLE IF NOT EXISTS "ScheduledRun" (
"name" TEXT NOT NULL,
"lastRunAt" TIMESTAMP(3),
"lastStatus" TEXT,
"lastNote" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ScheduledRun_pkey" PRIMARY KEY ("name")
);
+23
View File
@@ -206,10 +206,17 @@ model CreditLedger {
createdBy User? @relation("LedgerCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
/// Wann die Bewegung tatsächlich stattfand - beim Import das Kaufdatum aus
/// dem Shop, sonst der Zeitpunkt der Eingabe. Danach richtet sich die
/// Anzeige: wann jemand gekauft hat, ist die Frage, nicht wann es jemand
/// hier eingetippt hat.
occurredAt DateTime @default(now())
@@index([playerId])
@@index([playerId, kind])
@@index([groupId])
@@index([createdAt])
@@index([occurredAt])
}
model Character {
@@ -352,6 +359,9 @@ model ForumThread {
player Player? @relation(fields: [playerId], references: [id], onDelete: SetNull)
currentBoard ForumBoard
/// Wann der Antrag im Forum gestellt wurde. Anders als firstSeenAt sagt das
/// etwas über den Antrag aus und nicht darüber, wann wir ihn bemerkt haben.
postedAt DateTime?
firstSeenAt DateTime @default(now())
lastSeenAt DateTime @default(now())
lastBoardChangeAt DateTime?
@@ -438,3 +448,16 @@ model CharacterMember {
@@index([playerId])
@@index([characterId])
}
/// Wann ein wiederkehrender Lauf zuletzt lief. Der Zeitplan steht im Code,
/// der Stand in der Datenbank - sonst würde jeder Neustart des Dienstes alles
/// erneut anstoßen, und bei mehreren Neustarts am Tag liefe der Abgleich
/// häufiger als gewollt.
model ScheduledRun {
name String @id
lastRunAt DateTime?
/// OK oder FAILED, mit einer kurzen Notiz für die Fehlersuche.
lastStatus String?
lastNote String?
updatedAt DateTime @updatedAt
}