Beim Durcharbeiten von Spieler A stellt sich heraus, dass er schon vor drei
Redesigns bei einem Gruppen-CC von Spieler B dabei war. Das ging bisher nicht:
"Einem inaktiven CC kann niemand beitreten" - richtig fuer den laufenden
Betrieb, falsch fuers Nacherfassen. Dort geht es nicht um jetzt, sondern um
eine Geschichte, die laengst gelaufen ist.
An einem stillgelegten CC wird deshalb nachgetragen statt beigetreten: mit
Zeitraum, und die Guthaben-Angabe ist freigestellt, weil die Buchung im Konto
stehen kann. Wer weiter unten in derselben Kette schon steht, kommt auch an
einem laufenden CC ohne neue Buchung dazu - sein Platz wurde einmal bezahlt,
und ein Redesign tauscht ihn aus. Fuer alle anderen bleibt die Angabe Pflicht.
Damit vier Kettenglieder nicht vier Klicks bedeuten, traegt ein Aufruf einen
Spieler ab einem Glied in die ganze weitere Kette ein; gebucht wird einmal.
Ein Test spielt den ganzen Fall durch: B fertig, A nachgetragen, C steigt in
der Mitte ein und wieder aus, D kommt erst am Ende dazu - und am Schluss sieht
jeder seine eigene Geschichte.
Leere Import-Vorschauen blieben in der Liste stehen, sobald alle Zeilen beim
Spieler gebucht waren. Bestaetigen liess sich so eine Huelle nicht ("nichts zu
buchen"), sie war nur im Weg. Jetzt gilt sie als erledigt - sie ist es ja.
Auch der Bestand wird beim naechsten Aufruf der Liste abgeraeumt.
Und die Spielerliste laesst sich eingrenzen: nur mit aktivem CC, nur mit
ausschliesslich stillgelegten, nur Beteiligte eines Gruppen-CCs, nur ohne
jeden CC, nur Problemfaelle. Sortiert wird nach Namen, nach der Zahl der
CC-Eintraege oder nach Anlage.
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { after, before, describe, it } from 'node:test';
|
|
import { startTestDatabase, type TestDatabase } from './harness.js';
|
|
|
|
type Characters = typeof import('../src/services/characters.js');
|
|
type Credits = typeof import('../src/services/credits.js');
|
|
type Db = typeof import('../src/db.js');
|
|
|
|
let database: TestDatabase;
|
|
let characters: Characters;
|
|
let credits: Credits;
|
|
let db: Db;
|
|
let ctx: { actorId: number; actorName: string };
|
|
|
|
before(async () => {
|
|
database = await startTestDatabase(54341);
|
|
process.env.DATABASE_URL = database.url;
|
|
process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000';
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
db = await import('../src/db.js');
|
|
characters = await import('../src/services/characters.js');
|
|
credits = await import('../src/services/credits.js');
|
|
|
|
const user = await db.prisma.user.create({
|
|
data: { username: 'nacherfasser', passwordHash: 'x', role: 'ADMIN' },
|
|
});
|
|
ctx = { actorId: user.id, actorName: user.username };
|
|
});
|
|
|
|
after(async () => {
|
|
await db.prisma.$disconnect();
|
|
await database.stop();
|
|
});
|
|
|
|
async function spieler(name: string, pakete = 2) {
|
|
const p = await db.prisma.player.create({ data: { displayName: name } });
|
|
for (let i = 0; i < pakete; i += 1) await credits.bookCcPackage(p.id, ctx);
|
|
return p.id;
|
|
}
|
|
|
|
/** Die aktiven Mitglieder eines CCs, nach Namen. */
|
|
async function mitglieder(characterId: number): Promise<string[]> {
|
|
const liste = await db.prisma.characterMember.findMany({
|
|
where: { characterId, leftAt: null },
|
|
include: { player: { select: { displayName: true } } },
|
|
orderBy: { id: 'asc' },
|
|
});
|
|
return liste.map((m) => m.player.displayName).sort();
|
|
}
|
|
|
|
/**
|
|
* Der Fall aus dem Betrieb: Spieler B ist längst eingetragen, und erst beim
|
|
* Durcharbeiten von Spieler A stellt sich heraus, dass die beiden einen
|
|
* Gruppen-CC teilten - über mehrere Redesigns hinweg, mit wechselnder
|
|
* Besetzung.
|
|
*/
|
|
describe('Nachträglich aufräumen, was längst passiert ist', () => {
|
|
let b = 0;
|
|
let a = 0;
|
|
let c = 0;
|
|
let d = 0;
|
|
let alpha = 0;
|
|
let beta = 0;
|
|
let gamma = 0;
|
|
|
|
before(async () => {
|
|
b = await spieler('Spieler B');
|
|
a = await spieler('Spieler A');
|
|
c = await spieler('Spieler C');
|
|
d = await spieler('Spieler D');
|
|
|
|
// B wurde zuerst durchgearbeitet: ein CC, zweimal redesignt.
|
|
const ersterCC = await characters.createCharacter(b, { name: 'Alpha' }, ctx);
|
|
alpha = ersterCC.character.id;
|
|
|
|
await credits.bookRedesignPackage(b, ctx);
|
|
const zweiter = await characters.redesignCharacter(
|
|
alpha,
|
|
{ name: 'Beta', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
beta = zweiter.character.id;
|
|
|
|
await credits.bookRedesignPackage(b, ctx);
|
|
const dritter = await characters.redesignCharacter(
|
|
beta,
|
|
{ name: 'Gamma', useKind: 'REDESIGN_PAID' },
|
|
ctx,
|
|
);
|
|
gamma = dritter.character.id;
|
|
});
|
|
|
|
it('trägt A rückwirkend in ein längst redesigntes Glied ein', async () => {
|
|
// Alpha ist inaktiv - im laufenden Betrieb tritt dort niemand mehr bei.
|
|
// Beim Nachtragen geht es aber nicht um jetzt, sondern um damals.
|
|
const alt = await db.prisma.character.findUniqueOrThrow({ where: { id: alpha } });
|
|
assert.equal(alt.status, 'INACTIVE');
|
|
|
|
await characters.addMember(
|
|
alpha,
|
|
a,
|
|
{ useKind: 'CC', joinedAt: new Date('2024-03-01') },
|
|
ctx,
|
|
);
|
|
|
|
assert.deepEqual(await mitglieder(alpha), ['Spieler A', 'Spieler B']);
|
|
});
|
|
|
|
it('zieht ihn durch die Kette, ohne ein zweites Mal zu buchen', async () => {
|
|
const vorher = await credits.getBalances(a);
|
|
|
|
const ergebnis = await characters.addMemberToChain(beta, a, {}, ctx);
|
|
assert.equal(ergebnis.added, 2, 'Beta und Gamma kommen dazu');
|
|
|
|
assert.deepEqual(
|
|
await credits.getBalances(a),
|
|
vorher,
|
|
'sein Platz wurde einmal bezahlt und ist derselbe geblieben',
|
|
);
|
|
assert.deepEqual(await mitglieder(gamma), ['Spieler A', 'Spieler B']);
|
|
});
|
|
|
|
it('belegt dabei genau einen Platz', async () => {
|
|
const belegt = (await characters.listCharacters(a)).filter(
|
|
(cc) => cc.status === 'ACTIVE' && cc.members.some((m) => m.playerId === a && !m.leftAt),
|
|
);
|
|
assert.deepEqual(
|
|
belegt.map((cc) => cc.name),
|
|
['Gamma'],
|
|
'die früheren Glieder sind inaktiv und belegen nichts',
|
|
);
|
|
});
|
|
|
|
it('lässt C mittendrin ein- und wieder aussteigen', async () => {
|
|
// C war nur bei Beta dabei - davor und danach nicht.
|
|
await characters.addMember(beta, c, { useKind: 'CC', joinedAt: new Date('2024-06-01') }, ctx);
|
|
assert.deepEqual(await mitglieder(beta), ['Spieler A', 'Spieler B', 'Spieler C']);
|
|
|
|
await characters.removeMember(beta, c, 'Ausgestiegen vor dem nächsten Redesign', ctx);
|
|
assert.deepEqual(await mitglieder(beta), ['Spieler A', 'Spieler B']);
|
|
|
|
// Bei Gamma steht er gar nicht erst.
|
|
assert.equal((await mitglieder(gamma)).includes('Spieler C'), false);
|
|
});
|
|
|
|
it('nimmt D erst beim letzten Glied auf', async () => {
|
|
await characters.addMember(gamma, d, { useKind: 'CC' }, ctx);
|
|
|
|
assert.deepEqual(await mitglieder(gamma), ['Spieler A', 'Spieler B', 'Spieler D']);
|
|
assert.equal((await mitglieder(alpha)).includes('Spieler D'), false, 'damals war er nicht da');
|
|
});
|
|
|
|
it('zeigt jedem Beteiligten seine eigene Geschichte', async () => {
|
|
const seins = async (playerId: number) =>
|
|
(await characters.listCharacters(playerId)).map((cc) => cc.name).sort();
|
|
|
|
assert.deepEqual(await seins(a), ['Alpha', 'Beta', 'Gamma'], 'A war von Anfang an dabei');
|
|
assert.deepEqual(await seins(b), ['Alpha', 'Beta', 'Gamma']);
|
|
assert.deepEqual(await seins(c), ['Beta'], 'C nur in der Mitte');
|
|
assert.deepEqual(await seins(d), ['Gamma'], 'D nur am Ende');
|
|
});
|
|
|
|
it('verlangt an einem laufenden CC weiterhin eine Guthaben-Angabe', async () => {
|
|
const fremder = await spieler('Fremder', 1);
|
|
|
|
await assert.rejects(
|
|
() => characters.addMember(gamma, fremder, {}, ctx),
|
|
(error: Error & { code?: string }) => error.code === 'NEED_KIND',
|
|
'wer neu dazukommt, zahlt - die Ausnahme gilt nur der eigenen Kette',
|
|
);
|
|
});
|
|
|
|
it('weist einen verdrehten Zeitraum ab', async () => {
|
|
const spaeter = await spieler('Verdreht', 1);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
characters.addMember(
|
|
alpha,
|
|
spaeter,
|
|
{ joinedAt: new Date('2025-01-01'), leftAt: new Date('2024-01-01') },
|
|
ctx,
|
|
),
|
|
(error: Error & { code?: string }) => error.code === 'BAD_RANGE',
|
|
);
|
|
});
|
|
});
|