Rueckfragen im Stil der Anwendung, Kulanz-Ausgleich, Redesign aus dem CC-Topf

window.confirm und window.prompt sind raus - alle vierzehn Stellen. Sie sahen
nicht nur fremd aus: Wer sie im Browser abschaltet, bekommt stillschweigend
"nein" zurueck, und eine Aktion passiert einfach nicht, ohne dass jemand
erfaehrt warum. Stattdessen gibt es einen Dialog-Dienst mit drei Formen -
bestaetigen, abfragen, waehlen -, der sich wie der Rest der Anwendung
anfuehlt.

Die Wahl kam dazu, weil eine Stelle ein Ja/Nein fuer eine Auswahl
missbrauchte: "OK = Kulanz, Abbrechen = bezahltes Redesign". Das fuehrt jeden
in die Irre, der es zum ersten Mal sieht.

Neu: Steht der bezahlte Redesign-Topf im Minus, waehrend Kulanz uebrig ist,
erscheint "Minus mit Kulanz ausgleichen". Es bucht so viele Uebergaenge um,
wie beide Seiten hergeben - die aeltesten zuerst, damit das Ergebnis
wiederholbar ist. Unterm Strich aendert sich nichts, nur die Verteilung.

Ebenfalls neu: Ein Redesign laesst sich ausnahmsweise aus dem CC-Topf
bezahlen. Damit das Ausrichten an den Ketten so eine Entscheidung nicht beim
naechsten Umsortieren wieder einebnet, tragen von Hand gewaehlte Buchungen
jetzt einen Merker.

Dabei aufgefallen: Die Tests wurden gar nicht typgeprueft - tsconfig schloss
nur src ein. Sieben Aufrufe von bookKulanz hatten vertauschte Argumente und
liefen trotzdem durch, weil JavaScript das hinnimmt; sie pruefte damit etwas
anderes als gedacht. Tests haengen jetzt in der Typpruefung.
This commit is contained in:
2026-09-22 09:39:57 +02:00
parent 9b93844be2
commit 34819d44cc
19 changed files with 709 additions and 61 deletions
+137 -4
View File
@@ -447,7 +447,7 @@ describe('Redesign nachträglich umbuchen', () => {
);
assert.equal(zwei.balances.REDESIGN_PAID, 0, 'das bezahlte ist verbraucht');
await credits.bookKulanz(spieler, 1, 'Absprache aufgetaucht', ctx);
await credits.bookKulanz(spieler, ctx, { note: 'Absprache aufgetaucht' });
const danach = await characters.changeFunding(zwei.character.id, 'REDESIGN_KULANZ', ctx);
assert.equal(danach.balances.REDESIGN_PAID, 1, 'das bezahlte ist wieder frei');
@@ -466,7 +466,7 @@ describe('Redesign nachträglich umbuchen', () => {
it('und wieder zurück', async () => {
const spieler = await spielerMitPaketen('Hin und her', 1);
const eins = await characters.createCharacter(spieler, { name: 'A' }, ctx);
await credits.bookKulanz(spieler, 1, 'Erstmal Kulanz', ctx);
await credits.bookKulanz(spieler, ctx, { note: 'Erstmal Kulanz' });
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'B', useKind: 'REDESIGN_KULANZ' },
@@ -681,7 +681,7 @@ describe('Die Stelle in der Kette bestimmt, was ein Eintrag kostet', () => {
it('lässt eine Kulanz-Entscheidung unangetastet', async () => {
const spieler = await spielerMitPaketen('Kulanzfall', 2);
const eins = await characters.createCharacter(spieler, { name: 'Basis' }, ctx);
await credits.bookKulanz(spieler, 1, 'Absprache', ctx);
await credits.bookKulanz(spieler, ctx, { note: 'Absprache' });
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'Aus Kulanz', useKind: 'REDESIGN_KULANZ' },
@@ -951,7 +951,7 @@ describe('Die Rechnung geht auf', () => {
it('lässt eine Kulanz-Entscheidung auch beim Nachrechnen stehen', async () => {
const spieler = await spielerMitPaketen('Kulanz bleibt', 1);
const eins = await characters.createCharacter(spieler, { name: 'Start' }, ctx);
await credits.bookKulanz(spieler, 1, 'Absprache', ctx);
await credits.bookKulanz(spieler, ctx, { note: 'Absprache' });
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'Aus Kulanz', useKind: 'REDESIGN_KULANZ' },
@@ -967,3 +967,136 @@ describe('Die Rechnung geht auf', () => {
assert.equal(buchung?.kind, 'REDESIGN_KULANZ');
});
});
describe('Kulanz gegen ein Minus verrechnen', () => {
/** Ein Spieler mit Redesigns im Minus und Kulanz auf der Seite. */
async function schieflage() {
const spieler = await spielerMitPaketen('Schieflage', 1);
const erster = await characters.createCharacter(spieler, { name: 'Anfang' }, ctx);
// Vier Redesigns, aber nur eines bezahlt - der Topf geht ins Minus.
let letzter = erster.character.id;
for (let i = 2; i <= 5; i += 1) {
const naechster = await characters.redesignCharacter(
letzter,
{ name: `Stufe ${i}`, useKind: 'REDESIGN_PAID' },
ctx,
);
letzter = naechster.character.id;
}
const stand = await credits.getBalances(spieler);
assert.equal(stand.REDESIGN_PAID, -3, 'vier verbraucht, eines aus dem Paket');
return spieler;
}
it('gleicht so viel aus, wie beide Seiten hergeben', async () => {
const spieler = await schieflage();
for (let i = 0; i < 2; i += 1) {
await credits.bookKulanz(spieler, ctx, { note: 'Nachträglich zugestanden' });
}
const ergebnis = await characters.settleWithKulanz(spieler, ctx);
assert.equal(ergebnis.umgebucht, 2, 'zwei Kulanzen, zwei Übergänge');
assert.equal(ergebnis.balances.REDESIGN_PAID, -1, 'das Minus schrumpft um zwei');
assert.equal(ergebnis.balances.REDESIGN_KULANZ, 0, 'die Kulanz ist aufgebraucht');
assert.equal(
ergebnis.balances.redesignTotal,
-1,
'unterm Strich bleibt dasselbe - nur anders verteilt',
);
});
it('nimmt die ältesten Übergänge zuerst', async () => {
const spieler = await schieflage();
await credits.bookKulanz(spieler, ctx, { note: 'Eine Geste' });
await characters.settleWithKulanz(spieler, ctx);
const kulanzZeile = await db.prisma.creditLedger.findFirstOrThrow({
where: { playerId: spieler, kind: 'REDESIGN_KULANZ', amount: { lt: 0 } },
include: { character: { select: { name: true } } },
});
assert.equal(
kulanzZeile.character?.name,
'Stufe 2',
'wer nachträglich Kulanz bekommt, bekommt sie für das Älteste',
);
});
it('meldet sich, wenn es nichts auszugleichen gibt', async () => {
const spieler = await spielerMitPaketen('Im Plus', 1);
await credits.bookKulanz(spieler, ctx, { note: 'Übrig' });
await assert.rejects(
() => characters.settleWithKulanz(spieler, ctx),
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_SETTLE',
);
});
it('meldet sich, wenn keine Kulanz da ist', async () => {
const spieler = await schieflage();
await assert.rejects(
() => characters.settleWithKulanz(spieler, ctx),
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_SETTLE',
);
});
});
describe('Ein Redesign ausnahmsweise aus dem CC-Topf', () => {
it('nimmt ein CC-Guthaben an', async () => {
const spieler = await spielerMitPaketen('Ausnahmefall', 2);
const eins = await characters.createCharacter(spieler, { name: 'Basis' }, ctx);
const vorher = await credits.getBalances(spieler);
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'Aus dem CC-Topf', useKind: 'CC' },
ctx,
);
assert.equal(zwei.balances.CC, vorher.CC - 1, 'bezahlt aus dem CC-Topf');
assert.equal(zwei.balances.REDESIGN_PAID, vorher.REDESIGN_PAID, 'die Redesigns bleiben');
});
it('lässt sich vom Ausrichten an den Ketten nicht überschreiben', async () => {
// Ohne Merker würde die Automatik beim nächsten Umsortieren daraus
// wieder ein gewöhnliches Redesign machen.
const spieler = await spielerMitPaketen('Bleibt so', 2);
const eins = await characters.createCharacter(spieler, { name: 'Erstes' }, ctx);
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'Zweites', useKind: 'CC' },
ctx,
);
await characters.rebuildFunding(spieler, ctx);
const buchung = await db.prisma.creditLedger.findFirst({
where: { characterId: zwei.character.id, source: 'CONSUME', reversedBy: null },
orderBy: { id: 'desc' },
});
assert.equal(buchung?.kind, 'CC', 'die bewusste Entscheidung bleibt stehen');
});
it('merkt sich auch eine von Hand geänderte Bezahlart', async () => {
const spieler = await spielerMitPaketen('Von Hand', 2);
const eins = await characters.createCharacter(spieler, { name: 'A' }, ctx);
const zwei = await characters.redesignCharacter(
eins.character.id,
{ name: 'B', useKind: 'REDESIGN_PAID' },
ctx,
);
await characters.changeFunding(zwei.character.id, 'CC', ctx);
await characters.rebuildFunding(spieler, ctx);
const buchung = await db.prisma.creditLedger.findFirst({
where: { characterId: zwei.character.id, source: 'CONSUME', reversedBy: null },
orderBy: { id: 'desc' },
});
assert.equal(buchung?.kind, 'CC');
});
});