Ketten mit Vorgeschichte: unterstes Glied ist nicht immer ein Kauf

Ein Spieler mit drei Paketen und drei Plaetzen stand bei "-1 von 3". Der
dritte Platz kam aus einem fremden Gruppen-CC: beigetreten mit einem
CC-Guthaben, spaeter per Redesign ausgestiegen und als eigener CC
weitergefuehrt. Damit beginnt zwar eine neue Kette, aber kein neuer Platz -
bezahlt ist er laengst, naemlich mit dem Beitritt.

Die Automatik sah nur "unterstes Glied" und buchte es auf CC-Paket um. Der
Platz stand damit zweimal im Konto, einmal fuer den Beitritt und einmal fuer
den eigenen CC. Jetzt zaehlt, ob der Kette etwas vorausging: Zeigt ihr erstes
Glied auf einen Vorgaenger ausserhalb, ist auch dieses Glied ein Redesign.

Dabei fiel ein zweiter Fehler auf: Das Neuverketten loeschte genau diesen
Verweis, weil es die Verkettung innerhalb der Linie normalisierte. Damit ging
die einzige Spur zur Herkunft verloren - und mit ihr der Eintrag "war dabei"
am Fuss der Kette. Ein Vorgaenger ausserhalb bleibt jetzt stehen.

Fuer die Korrektur bereits falsch gebuchter Eintraege kann "Bezahlart" jetzt
auch zwischen CC-Paket und Redesign wechseln, nicht nur zwischen bezahlt und
Kulanz. Der Knopf steht an jedem Eintrag, nicht nur an Redesigns.
This commit is contained in:
2026-09-21 19:07:29 +02:00
parent f7b48a9891
commit 1b643cb758
6 changed files with 180 additions and 56 deletions
+48 -23
View File
@@ -731,24 +731,26 @@ export async function reviveDeadCharacter(
/**
* Stellt nachträglich um, woraus ein Redesign bezahlt wurde.
* Stellt nachträglich um, woraus ein CC bezahlt wurde.
*
* Ob ein Redesign aus einem gekauften Paket oder aus Kulanz kam, stellt sich
* manchmal erst später heraus - etwa wenn eine Absprache auftaucht, von der
* beim Eintragen niemand wusste. Umgebucht wird nicht durch Überschreiben,
* sondern wie jede Korrektur im Kontobuch: die alte Zeile wird
* gegengebucht, die neue kommt dazu. So bleibt sichtbar, dass hier etwas
* geändert wurde, und wann.
* Ob etwas aus einem gekauften Paket, einem bezahlten Redesign oder aus
* Kulanz kam, stellt sich manchmal erst später heraus - wenn eine Absprache
* auftaucht, von der beim Eintragen niemand wusste, oder wenn sich zeigt,
* dass ein Eintrag gar nicht der Anfang einer Kette war.
*
* Umgebucht wird nicht durch Überschreiben, sondern wie jede Korrektur im
* Kontobuch: die alte Zeile wird gegengebucht, die neue kommt dazu. So
* bleibt sichtbar, dass hier etwas geändert wurde, und wann.
*/
export async function changeRedesignKind(
export async function changeFunding(
characterId: number,
useKind: RedesignKind,
useKind: JoinKind,
ctx: ActionContext,
): Promise<CharacterResult> {
const character = await loadCharacter(characterId);
if (!REDESIGN_KINDS.includes(useKind)) {
throw badRequest('INVALID_KIND', 'Ein Redesign kostet bezahltes oder Kulanz-Redesign-Guthaben');
if (!JOIN_KINDS.includes(useKind)) {
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art');
}
// Die Buchung, mit der dieser CC bezahlt wurde. Stornierte zählen nicht
@@ -756,26 +758,28 @@ export async function changeRedesignKind(
const buchung = await prisma.creditLedger.findFirst({
where: {
characterId,
playerId: character.playerId,
source: CreditSource.CONSUME,
kind: { in: [...REDESIGN_KINDS] },
reversedBy: null,
},
orderBy: { id: 'desc' },
});
if (!buchung) {
throw conflict(
'NO_REDESIGN_BOOKING',
'Für diesen CC ist kein offenes Redesign gebucht - hier gibt es nichts umzustellen',
'NO_BOOKING',
'Für diesen CC ist nichts gebucht - hier gibt es nichts umzustellen',
);
}
if (buchung.kind === useKind) {
throw conflict('SAME_KIND', 'Dieses Redesign ist bereits so gebucht');
throw conflict('SAME_KIND', 'Dieser CC ist bereits so gebucht');
}
const grund =
useKind === CreditKind.REDESIGN_KULANZ
? 'Umgestellt auf Kulanz'
: 'Umgestellt auf bezahltes Redesign';
useKind === CreditKind.CC
? 'Umgestellt auf CC-Paket'
: useKind === CreditKind.REDESIGN_KULANZ
? 'Umgestellt auf Kulanz'
: 'Umgestellt auf bezahltes Redesign';
await prisma.$transaction(async (tx) => {
await tx.creditLedger.create({
@@ -809,7 +813,7 @@ export async function changeRedesignKind(
});
await recordAudit(tx, ctx, {
action: 'character.redesign_kind_changed',
action: 'character.funding_changed',
targetType: 'character',
targetId: characterId,
details: { name: character.name, from: buchung.kind, to: useKind },
@@ -936,19 +940,40 @@ async function verketteNeu(
// Die Kette gehört dem, der unten steht: bei ihm fing sie an.
const kettenBesitzer = glieder[0]?.playerId ?? null;
const inDieserLinie = new Set(glieder.map((g) => g.id));
/**
* Eine Herkunft ausserhalb dieser Kette.
*
* Wer aus einem fremden Gruppen-CC herausredesignt, beginnt eine eigene
* Kette - bezahlt hat er sie trotzdem mit einem Redesign, nicht mit einem
* Paket. Ihr unterstes Glied ist deshalb kein Kauf, und der Verweis auf
* die Herkunft bleibt stehen: er ist das Einzige, was noch zeigt, woher
* die Kette kommt.
*/
const herkunft = glieder[0]?.predecessorId ?? null;
const hatHerkunft = herkunft !== null && !inDieserLinie.has(herkunft);
let vorheriges: number | null = null;
for (const [index, glied] of glieder.entries()) {
const letztes = index === glieder.length - 1;
const erstes = index === 0;
const daten: Prisma.CharacterUpdateInput = {};
// Das unterste Glied ist der gekaufte CC, alles darüber sein Redesign.
// Das unterste Glied ist der gekaufte CC - es sei denn, ihm ging etwas
// voraus. Alles darüber sind seine Redesigns.
if (kettenBesitzer !== null) {
await buchungFolgtPosition(tx, glied, kettenBesitzer, index === 0, actorId);
await buchungFolgtPosition(tx, glied, kettenBesitzer, erstes && !hatHerkunft, actorId);
}
if (glied.predecessorId !== vorheriges) {
daten.predecessor = vorheriges === null ? { disconnect: true } : { connect: { id: vorheriges } };
if (erstes) {
// Am Anfang wird nur eine Verkettung gelöst, die innerhalb dieser
// Linie ins Leere zeigt. Eine Herkunft von aussen bleibt.
if (glied.predecessorId !== null && inDieserLinie.has(glied.predecessorId)) {
daten.predecessor = { disconnect: true };
}
} else if (glied.predecessorId !== vorheriges) {
daten.predecessor = { connect: { id: vorheriges as number } };
}
// Was einen Nachfolger hat, wurde durch ihn ersetzt - anders ist eine