Ein Eintrag kostet genau einmal, die Rueckkehr ist ein weiteres Glied

An einem Eintrag konnten zwei Buchungen haengen - etwa Kulanz und ein
gekauftes Redesign nebeneinander -, und aufloesen liess sich das
nirgends. Jetzt raeumt sowohl das Nachrechnen als auch das Umstellen der
Bezahlart auf: es bleibt die aelteste passende, der Rest wird storniert.

Die Rueckkehr eines redesignten oder gestorbenen CCs bucht deshalb nicht
mehr zusaetzlich, sondern erschafft ihn noch einmal: derselbe CC als
weiteres Glied ueber dem alten, und das kostet ein Redesign wie jedes
andere. Mit einem CC-Guthaben beginnt er stattdessen eine eigene Liste
und kann neben seinem Nachfolger laufen.

Ein entzogener Platz nimmt seine Buchung mit - sonst waere die Rueckkehr
umsonst oder stuende als zweite Buchung am selben Eintrag.
This commit is contained in:
2026-09-22 14:31:29 +02:00
parent 367f1aace4
commit b93ff2fb0e
3 changed files with 308 additions and 62 deletions
+83
View File
@@ -1383,3 +1383,86 @@ describe('Die Umstellung auf Listen je Nutzer', () => {
assert.ok(drei.character.id > 0);
});
});
describe('Zwei Buchungen an einem Eintrag', () => {
/// Die offenen Verbrauchsbuchungen eines Spielers an einem CC.
async function offene(characterId: number, playerId: number): Promise<string[]> {
const zeilen = await db.prisma.creditLedger.findMany({
where: { characterId, playerId, source: 'CONSUME', reversedBy: null },
orderBy: { id: 'asc' },
select: { kind: true },
});
return zeilen.map((z) => z.kind);
}
/** Der Fall aus dem Bestand: an einem Redesign hängen Kulanz und Kauf. */
async function doppeltGebucht(name: string) {
const spieler = await spielerMitPaketen(name, 1);
const eins = await characters.createCharacter(spieler, { name: `${name}-Anfang` }, ctx);
await credits.bookRedesignPackage(spieler, ctx);
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: `${name}-Zweites`, useKind: 'REDESIGN_PAID' },
ctx,
);
await credits.bookKulanz(spieler, ctx);
await db.prisma.creditLedger.create({
data: {
playerId: spieler,
kind: 'REDESIGN_KULANZ',
amount: -1,
source: 'CONSUME',
characterId: zwei.character.id,
},
});
assert.deepEqual(await offene(zwei.character.id, spieler), [
'REDESIGN_PAID',
'REDESIGN_KULANZ',
]);
return { spieler, zwei: zwei.character.id };
}
it('räumt das Nachrechnen wieder auf', async () => {
const { spieler, zwei } = await doppeltGebucht('Doppelt');
const vorher = await credits.getBalances(spieler);
const ergebnis = await characters.rebuildFunding(spieler, ctx);
assert.equal(ergebnis.umgebucht, 1, 'eine Buchung war zu viel');
assert.deepEqual(
await offene(zwei, spieler),
['REDESIGN_PAID'],
'ein Eintrag kostet genau einmal',
);
assert.equal(
ergebnis.balances.REDESIGN_KULANZ,
vorher.REDESIGN_KULANZ + 1,
'die Kulanz ist wieder frei',
);
});
it('lässt sich auch über die Bezahlart auflösen', async () => {
const { spieler, zwei } = await doppeltGebucht('Umstellen');
const vorher = await credits.getBalances(spieler);
const danach = await characters.changeFunding(zwei, 'REDESIGN_KULANZ', ctx);
assert.deepEqual(
await offene(zwei, spieler),
['REDESIGN_KULANZ'],
'umstellen ersetzt alles, was dort stand',
);
assert.equal(
danach.balances.REDESIGN_PAID,
vorher.REDESIGN_PAID + 1,
'das bezahlte Redesign ist wieder frei',
);
assert.equal(
danach.balances.REDESIGN_KULANZ,
vorher.REDESIGN_KULANZ,
'die Kulanz bleibt verbraucht - nur eben einmal',
);
});
});