Bisher zahlte nur, wer neu einstieg: wer in einer Kette mitgezogen wurde oder einen Gruppen-CC mitbenutzte, stand darin ohne eigene Buchung. Damit ging die Rechnung nicht auf - verbrauchte Redesigns sind alle Glieder aller Listen minus die Anzahl der Listen, und das gilt fuer jeden Nutzer einzeln. Jetzt bucht jedes Glied: das unterste einen Platz, jedes weitere ein Redesign. Wo eine Buchung fehlt, traegt der Abgleich sie nach, und das Redesign eines Gruppen-CCs kostet jeden Nutzer sein eigenes. Am Pfeil stehen alle offenen Buchungen eines Eintrags, nicht nur die juengste - sonst bleibt eine zweite unsichtbar.
193 lines
6.7 KiB
TypeScript
193 lines
6.7 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 und bucht jedes Glied einzeln', async () => {
|
|
// Für ihn ist jedes Glied ein eigener Eintrag in seiner Liste: der
|
|
// unterste kostet den Platz, jedes weitere ein Redesign.
|
|
const vorher = await credits.getBalances(a);
|
|
|
|
const ergebnis = await characters.addMemberToChain(beta, a, {}, ctx);
|
|
assert.equal(ergebnis.added, 2, 'Beta und Gamma kommen dazu');
|
|
|
|
const danach = await credits.getBalances(a);
|
|
assert.equal(danach.CC, vorher.CC, 'sein Platz wurde einmal bezahlt');
|
|
assert.equal(
|
|
danach.REDESIGN_PAID,
|
|
vorher.REDESIGN_PAID - 2,
|
|
'für Beta und Gamma je ein Redesign',
|
|
);
|
|
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',
|
|
);
|
|
});
|
|
});
|