Die Stelle in der Kette bestimmt die Buchung
Bisher blieb eine Buchung, wie sie beim Anlegen entstand - auch wenn der Eintrag danach an eine ganz andere Stelle wanderte. Damit stand ein CC, der in eine Kette geschoben wurde, weiter als gekauftes Paket im Konto, obwohl er inzwischen ein Redesign war. Jetzt folgt die Buchung der Position: Das unterste Glied ist der gekaufte CC und verbraucht ein CC-Paket, alles darueber sind Redesigns. Wandert ein Eintrag, wandert seine Bedeutung mit - in beide Richtungen. Wer mitten in eine Kette rutscht, wird zum Redesign und gibt sein CC-Guthaben zurueck; wer an den Anfang kommt oder herausgeloest wird, belegt einen eigenen Platz und kostet wieder ein Paket. Umgebucht wird ueber eine Gegenbuchung, nicht durch Ueberschreiben. Reicht das Guthaben nicht, geht das Konto ins Minus und zeigt es an - wie bei jedem Vorab-CC. Zwischen bezahltem Redesign und Kulanz wird nicht gewechselt: das ist eine Entscheidung, keine Folge der Reihenfolge. Eintraege ohne Buchung bleiben unangetastet. Dabei zaehlt die Buchung dessen, dem die Kette gehoert - der Spieler, der unten steht. Bei einem fremden Gruppen-CC in seiner Kette ist das seine Beitrittsbuchung; die des Erstellers bleibt, denn fuer den ist es sein eigener Platz in seiner eigenen Kette. Damit liess sich der Gruppen-CC ueberhaupt erst einreihen: Der Besitzer-Check fragte nach dem Ersteller, und der ist bei einem Gruppen-CC ein anderer als der, dessen Seite man gerade ordnet. Jetzt entscheidet die Beteiligung - wer an beidem beteiligt war, darf einreihen, ein Unbeteiligter nicht. Fehler beim Ziehen stehen ausserdem neben den Ketten statt am Seitenkopf: wer unten zieht, sieht dort oben nichts.
This commit is contained in:
@@ -209,6 +209,9 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
|
||||
z.object({
|
||||
lineageId: z.string().trim().min(1).max(64),
|
||||
afterId: z.number().int().positive().nullable().optional(),
|
||||
/// Wessen Seite gerade geordnet wird - bei einem Gruppen-CC ist das
|
||||
/// nicht zwingend sein Ersteller.
|
||||
forPlayerId: z.number().int().positive().optional(),
|
||||
}),
|
||||
request.body,
|
||||
);
|
||||
|
||||
@@ -832,18 +832,121 @@ export async function changeRedesignKind(
|
||||
* Umsortieren wird er deshalb aus den Positionen neu abgeleitet - dann können
|
||||
* beide nicht auseinanderlaufen.
|
||||
*/
|
||||
async function verketteNeu(tx: Prisma.TransactionClient, lineageId: string): Promise<void> {
|
||||
/**
|
||||
* Bucht ein Kettenglied auf die Art um, die seine Stelle in der Kette
|
||||
* verlangt.
|
||||
*
|
||||
* Die Regel steckt in der Kette selbst: **Das unterste Glied ist der CC, der
|
||||
* gekauft wurde - alles darüber sind seine Redesigns.** Wandert ein Eintrag,
|
||||
* wandert damit auch seine Bedeutung: Ein eigenständiger CC, der mitten in
|
||||
* eine fremde Kette geschoben wird, ist von da an ein Redesign; wer
|
||||
* umgekehrt an den Anfang rückt oder allein steht, belegt einen eigenen
|
||||
* Platz und kostet ein CC-Paket.
|
||||
*
|
||||
* Umgebucht wird wie jede Korrektur im Kontobuch: die alte Zeile wird
|
||||
* gegengebucht, die neue kommt dazu. Wo gar keine Buchung steht, bleibt es
|
||||
* dabei - wer ohne Buchung nachgetragen hat, wollte das so.
|
||||
*
|
||||
* Zwischen bezahltem Redesign und Kulanz wird nicht gewechselt: das ist eine
|
||||
* Entscheidung, keine Folge der Reihenfolge. Sie steht unter "Bezahlart".
|
||||
*/
|
||||
async function buchungFolgtPosition(
|
||||
tx: Prisma.TransactionClient,
|
||||
glied: { id: number; name: string },
|
||||
/**
|
||||
* Wessen Buchung angefasst wird: der Spieler, dem die Kette gehört - nicht
|
||||
* der Ersteller des einzelnen Gliedes. Bei einem fremden Gruppen-CC in
|
||||
* seiner Kette ist das seine Beitrittsbuchung, und nur die geht ihn etwas
|
||||
* an. Die des Erstellers bleibt unberührt; für den ist es sein eigener
|
||||
* Platz in seiner eigenen Kette.
|
||||
*/
|
||||
kettenBesitzer: number,
|
||||
sollCC: boolean,
|
||||
actorId: number | null,
|
||||
): Promise<boolean> {
|
||||
const offen = await tx.creditLedger.findFirst({
|
||||
where: {
|
||||
characterId: glied.id,
|
||||
playerId: kettenBesitzer,
|
||||
source: CreditSource.CONSUME,
|
||||
reversedBy: null,
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
if (!offen) return false;
|
||||
|
||||
const istCC = offen.kind === CreditKind.CC;
|
||||
if (istCC === sollCC) return false;
|
||||
|
||||
const neueArt = sollCC ? CreditKind.CC : CreditKind.REDESIGN_PAID;
|
||||
const grund = sollCC
|
||||
? `Steht jetzt am Anfang der Kette: aus einem CC-Paket bezahlt`
|
||||
: `Steht jetzt in einer Kette: als Redesign gebucht`;
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId: offen.playerId,
|
||||
kind: offen.kind,
|
||||
amount: -offen.amount,
|
||||
source: CreditSource.REVERSAL,
|
||||
groupId: offen.groupId,
|
||||
characterId: glied.id,
|
||||
reversalOfId: offen.id,
|
||||
note: `${grund} - Storno: ${glied.name}`,
|
||||
createdById: actorId,
|
||||
occurredAt: offen.occurredAt,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId: offen.playerId,
|
||||
kind: neueArt,
|
||||
amount: offen.amount,
|
||||
source: CreditSource.CONSUME,
|
||||
groupId: randomUUID(),
|
||||
characterId: glied.id,
|
||||
note: offen.note ?? glied.name,
|
||||
createdById: actorId,
|
||||
// Bezahlt wurde damals, nicht beim Umsortieren.
|
||||
occurredAt: offen.occurredAt,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function verketteNeu(
|
||||
tx: Prisma.TransactionClient,
|
||||
lineageId: string,
|
||||
actorId: number | null,
|
||||
): Promise<void> {
|
||||
const glieder = await tx.character.findMany({
|
||||
where: { lineageId },
|
||||
orderBy: [{ chainPosition: 'asc' }, { createdAt: 'asc' }, { id: 'asc' }],
|
||||
select: { id: true, predecessorId: true, status: true, inactiveReason: true },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
playerId: true,
|
||||
predecessorId: true,
|
||||
status: true,
|
||||
inactiveReason: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Die Kette gehört dem, der unten steht: bei ihm fing sie an.
|
||||
const kettenBesitzer = glieder[0]?.playerId ?? null;
|
||||
|
||||
let vorheriges: number | null = null;
|
||||
for (const [index, glied] of glieder.entries()) {
|
||||
const letztes = index === glieder.length - 1;
|
||||
const daten: Prisma.CharacterUpdateInput = {};
|
||||
|
||||
// Das unterste Glied ist der gekaufte CC, alles darüber sein Redesign.
|
||||
if (kettenBesitzer !== null) {
|
||||
await buchungFolgtPosition(tx, glied, kettenBesitzer, index === 0, actorId);
|
||||
}
|
||||
|
||||
if (glied.predecessorId !== vorheriges) {
|
||||
daten.predecessor = vorheriges === null ? { disconnect: true } : { connect: { id: vorheriges } };
|
||||
}
|
||||
@@ -883,20 +986,55 @@ async function verketteNeu(tx: Prisma.TransactionClient, lineageId: string): Pro
|
||||
*/
|
||||
export async function moveInChain(
|
||||
characterId: number,
|
||||
input: { lineageId: string; afterId?: number | null },
|
||||
input: {
|
||||
lineageId: string;
|
||||
afterId?: number | null;
|
||||
/// Wessen Geschichte hier geordnet wird. Bei einem Gruppen-CC ist das
|
||||
/// nicht zwingend der Ersteller.
|
||||
forPlayerId?: number | undefined;
|
||||
},
|
||||
ctx: ActionContext,
|
||||
): Promise<{ lineageId: string }> {
|
||||
const character = await loadCharacter(characterId);
|
||||
|
||||
// Die Ziel-Linie muss dem Spieler gehören, sonst wanderten CCs zwischen
|
||||
// Spielern - dafür gibt es hier keinen Weg und keinen Grund.
|
||||
const zielGlieder = await prisma.character.findMany({
|
||||
where: { lineageId: input.lineageId },
|
||||
orderBy: [{ chainPosition: 'asc' }, { id: 'asc' }],
|
||||
select: { id: true, playerId: true, chainPosition: true },
|
||||
});
|
||||
if (zielGlieder.length > 0 && zielGlieder.some((g) => g.playerId !== character.playerId)) {
|
||||
throw conflict('FOREIGN_CHAIN', 'Diese Kette gehört einem anderen Spieler');
|
||||
|
||||
/**
|
||||
* Wer hier ordnen darf.
|
||||
*
|
||||
* Nicht der Besitz entscheidet, sondern die Beteiligung: Ein Gruppen-CC
|
||||
* gehört dem, der ihn angelegt hat, aber seine Geschichte gehört allen,
|
||||
* die darin steckten. Wer seinen eigenen CC aufgegeben hat, um
|
||||
* dazuzukommen, ordnet damit seine eigene Kette - und muss den Gruppen-CC
|
||||
* dort einreihen können.
|
||||
*
|
||||
* Verlangt wird deshalb, dass derselbe Spieler an beidem beteiligt ist:
|
||||
* am verschobenen CC und an der Kette, in die er soll. Sonst würden fremde
|
||||
* Geschichten vermischt.
|
||||
*/
|
||||
const beteiligt = async (playerId: number, ids: number[]): Promise<boolean> => {
|
||||
if (ids.length === 0) return true;
|
||||
const treffer = await prisma.characterMember.findFirst({
|
||||
where: { playerId, characterId: { in: ids } },
|
||||
select: { id: true },
|
||||
});
|
||||
return treffer !== null;
|
||||
};
|
||||
|
||||
const kandidat = input.forPlayerId ?? character.playerId;
|
||||
const darfOrdnen =
|
||||
(await beteiligt(kandidat, [characterId])) &&
|
||||
(await beteiligt(kandidat, zielGlieder.map((g) => g.id)));
|
||||
|
||||
if (!darfOrdnen) {
|
||||
throw conflict(
|
||||
'FOREIGN_CHAIN',
|
||||
'Diese Kette gehört zu einer anderen Geschichte - verschieben kann nur, wer an beidem beteiligt war',
|
||||
);
|
||||
}
|
||||
|
||||
const vorgaenger =
|
||||
@@ -929,8 +1067,8 @@ export async function moveInChain(
|
||||
data: { lineageId: input.lineageId, chainPosition: ab },
|
||||
});
|
||||
|
||||
await verketteNeu(tx, input.lineageId);
|
||||
if (alteLinie !== input.lineageId) await verketteNeu(tx, alteLinie);
|
||||
await verketteNeu(tx, input.lineageId, ctx.actorId);
|
||||
if (alteLinie !== input.lineageId) await verketteNeu(tx, alteLinie, ctx.actorId);
|
||||
|
||||
// Das Neuordnen kann einen CC ans Ende rücken und damit wieder aktiv
|
||||
// machen. Ein Platz entsteht dadurch nicht: er muss gedeckt sein wie
|
||||
@@ -980,7 +1118,10 @@ export async function detachFromChain(
|
||||
},
|
||||
});
|
||||
|
||||
await verketteNeu(tx, character.lineageId);
|
||||
// Beide Seiten: die verlassene Kette rückt zusammen, und der
|
||||
// Herausgelöste steht allein - also am Anfang seiner eigenen Kette.
|
||||
await verketteNeu(tx, character.lineageId, ctx.actorId);
|
||||
await verketteNeu(tx, neueLinie, ctx.actorId);
|
||||
await assertSlotsReichen(tx, character.playerId, 'Dieses Herauslösen');
|
||||
|
||||
await recordAudit(tx, ctx, {
|
||||
|
||||
+235
-11
@@ -39,10 +39,13 @@ async function spielerMitPaketen(name: string, pakete = 1) {
|
||||
return player.id;
|
||||
}
|
||||
|
||||
/** Die Kette eines Spielers, wie sie angezeigt würde. */
|
||||
async function kette(playerId: number, lineageId: string) {
|
||||
/**
|
||||
* Eine Kette, wie sie angezeigt würde. Massgeblich ist die Linie, nicht der
|
||||
* Besitzer: ein Gruppen-CC gehört einem anderen und steht trotzdem darin.
|
||||
*/
|
||||
async function kette(_playerId: number, lineageId: string) {
|
||||
const glieder = await db.prisma.character.findMany({
|
||||
where: { playerId, lineageId },
|
||||
where: { lineageId },
|
||||
orderBy: [{ chainPosition: 'asc' }, { id: 'asc' }],
|
||||
select: { id: true, name: true, predecessorId: true, chainPosition: true },
|
||||
});
|
||||
@@ -189,9 +192,10 @@ describe('Lücken in einer Redesign-Kette schliessen', () => {
|
||||
assert.equal(allein.predecessorId, null, 'allein heisst ohne Vorgänger');
|
||||
});
|
||||
|
||||
it('lässt ein Redesign nicht ohne eigenen Platz allein stehen', async () => {
|
||||
// Ein Redesign hat keine eigene CC-Buchung - es tauscht einen Platz aus.
|
||||
// Allein gestellt braucht es einen, und der muss gedeckt sein.
|
||||
it('macht aus einem herausgelösten Redesign einen eigenen Platz - und zeigt das Minus', async () => {
|
||||
// Allein gestellt ist ein Redesign kein Redesign mehr, sondern ein
|
||||
// eigener CC. Also wird es auch als solcher gebucht. Reicht das Guthaben
|
||||
// nicht, geht das Konto ins Minus - sichtbar, wie bei jedem Vorab-CC.
|
||||
const spieler = await spielerMitPaketen('Knapp', 1);
|
||||
const eins = await characters.createCharacter(spieler, { name: 'Original' }, ctx);
|
||||
await credits.bookRedesignPackage(spieler, ctx);
|
||||
@@ -201,10 +205,20 @@ describe('Lücken in einer Redesign-Kette schliessen', () => {
|
||||
ctx,
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => characters.detachFromChain(zwei.character.id, ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'NO_FREE_SLOT',
|
||||
'sonst stünden zwei aktive CCs auf einem Paket',
|
||||
await characters.detachFromChain(zwei.character.id, ctx);
|
||||
|
||||
const verbrauch = await db.prisma.creditLedger.findFirst({
|
||||
where: { characterId: zwei.character.id, source: 'CONSUME', reversedBy: null },
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
assert.equal(verbrauch?.kind, 'CC', 'ein eigener Platz kostet ein CC-Paket');
|
||||
|
||||
const stand = await credits.getBalances(spieler);
|
||||
assert.equal(stand.CC, -1, 'dafür fehlt jetzt eines - und das steht sichtbar da');
|
||||
assert.equal(
|
||||
stand.REDESIGN_PAID,
|
||||
2,
|
||||
'das verbrauchte Redesign kommt zurück - eines aus dem CC-Paket, eines gekauft',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -237,7 +251,7 @@ describe('Lücken in einer Redesign-Kette schliessen', () => {
|
||||
assert.equal(a.inactiveReason, 'REWORKED', 'A hat jetzt einen Nachfolger');
|
||||
});
|
||||
|
||||
it('lässt keinen CC in die Kette eines anderen Spielers', async () => {
|
||||
it('lässt keinen CC in die Kette eines Unbeteiligten', async () => {
|
||||
const einer = await spielerMitPaketen('Einer', 1);
|
||||
const anderer = await spielerMitPaketen('Anderer', 1);
|
||||
const seiner = await characters.createCharacter(einer, { name: 'Seiner' }, ctx);
|
||||
@@ -254,6 +268,84 @@ describe('Lücken in einer Redesign-Kette schliessen', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('reiht einen Gruppen-CC in die Kette eines seiner Nutzer ein', async () => {
|
||||
// Ein Gruppen-CC gehört dem, der ihn angelegt hat. Seine Geschichte
|
||||
// gehört allen, die darin steckten: wer seinen eigenen CC aufgegeben
|
||||
// hat, um dazuzukommen, ordnet damit seine eigene Kette.
|
||||
const wirt = await spielerMitPaketen('Gruppen-Wirt', 2);
|
||||
const gast = await spielerMitPaketen('Gruppen-Gast', 2);
|
||||
|
||||
const gruppe = await characters.createCharacter(wirt, { name: 'Die Werkstatt' }, ctx);
|
||||
await characters.addMember(gruppe.character.id, gast, { useKind: 'CC' }, ctx);
|
||||
|
||||
const eigener = await characters.createCharacter(gast, { name: 'Sein Vorläufer' }, ctx);
|
||||
|
||||
await characters.moveInChain(
|
||||
gruppe.character.id,
|
||||
{
|
||||
lineageId: eigener.character.lineageId,
|
||||
afterId: eigener.character.id,
|
||||
forPlayerId: gast,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
const glieder = await kette(gast, eigener.character.lineageId);
|
||||
assert.deepEqual(
|
||||
glieder.map((g) => g.name),
|
||||
['Sein Vorläufer', 'Die Werkstatt'],
|
||||
'der Gruppen-CC steht jetzt hinter seinem Vorläufer',
|
||||
);
|
||||
verkettungStimmt(glieder);
|
||||
|
||||
// Und der Vorläufer gilt damit als ersetzt.
|
||||
const vorlaeufer = await db.prisma.character.findUniqueOrThrow({
|
||||
where: { id: eigener.character.id },
|
||||
});
|
||||
assert.equal(vorlaeufer.inactiveReason, 'REWORKED');
|
||||
});
|
||||
|
||||
it('lässt einen Unbeteiligten auch dann nicht heran', async () => {
|
||||
const wirt = await spielerMitPaketen('Wirt zwei', 1);
|
||||
const gast = await spielerMitPaketen('Gast zwei', 1);
|
||||
const fremder = await spielerMitPaketen('Ganz fremd', 1);
|
||||
|
||||
const gruppe = await characters.createCharacter(wirt, { name: 'Geschlossen' }, ctx);
|
||||
const fremdesCC = await characters.createCharacter(fremder, { name: 'Nicht dabei' }, ctx);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
characters.moveInChain(
|
||||
fremdesCC.character.id,
|
||||
{
|
||||
lineageId: gruppe.character.lineageId,
|
||||
afterId: gruppe.character.id,
|
||||
forPlayerId: fremder,
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
(error: Error & { code?: string }) => error.code === 'FOREIGN_CHAIN',
|
||||
'wer an der Gruppe nie beteiligt war, ordnet dort auch nichts',
|
||||
);
|
||||
|
||||
// Auch nicht, wenn er sich als jemand anderes ausgibt: der Gast war zwar
|
||||
// an der Gruppe beteiligt, nicht aber am fremden CC.
|
||||
await characters.addMember(gruppe.character.id, gast, { useKind: 'CC' }, ctx);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
characters.moveInChain(
|
||||
fremdesCC.character.id,
|
||||
{
|
||||
lineageId: gruppe.character.lineageId,
|
||||
afterId: gruppe.character.id,
|
||||
forPlayerId: gast,
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
(error: Error & { code?: string }) => error.code === 'FOREIGN_CHAIN',
|
||||
);
|
||||
});
|
||||
|
||||
it('lässt einen CC nicht hinter sich selbst stellen', async () => {
|
||||
const spieler = await spielerMitPaketen('Schlange', 1);
|
||||
const cc = await characters.createCharacter(spieler, { name: 'Beisst sich' }, ctx);
|
||||
@@ -494,3 +586,135 @@ describe('Angaben eines CCs nachtragen', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Die Stelle in der Kette bestimmt, was ein Eintrag kostet', () => {
|
||||
/** Offene Verbrauchsbuchung eines CCs. */
|
||||
async function verbrauch(characterId: number) {
|
||||
return db.prisma.creditLedger.findFirst({
|
||||
where: { characterId, source: 'CONSUME', reversedBy: null },
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
it('macht aus einem eigenständigen CC ein Redesign, sobald er eingereiht wird', async () => {
|
||||
const spieler = await spielerMitPaketen('Einreiher', 2);
|
||||
const erster = await characters.createCharacter(spieler, { name: 'Der Erste' }, ctx);
|
||||
const zweiter = await characters.createCharacter(spieler, { name: 'Der Zweite' }, ctx);
|
||||
|
||||
// Beide sind für sich gekauft.
|
||||
assert.equal((await verbrauch(erster.character.id))?.kind, 'CC');
|
||||
assert.equal((await verbrauch(zweiter.character.id))?.kind, 'CC');
|
||||
|
||||
await credits.bookRedesignPackage(spieler, ctx);
|
||||
const vorher = await credits.getBalances(spieler);
|
||||
|
||||
await characters.moveInChain(
|
||||
zweiter.character.id,
|
||||
{ lineageId: erster.character.lineageId, afterId: erster.character.id },
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
(await verbrauch(zweiter.character.id))?.kind,
|
||||
'REDESIGN_PAID',
|
||||
'über einem anderen Eintrag ist es ein Redesign',
|
||||
);
|
||||
|
||||
const danach = await credits.getBalances(spieler);
|
||||
assert.equal(danach.CC, vorher.CC + 1, 'das CC-Guthaben kommt zurück');
|
||||
assert.equal(danach.REDESIGN_PAID, vorher.REDESIGN_PAID - 1, 'dafür geht ein Redesign weg');
|
||||
});
|
||||
|
||||
it('und wieder zurück, sobald er allein steht', async () => {
|
||||
const spieler = await spielerMitPaketen('Herausloeser', 2);
|
||||
const erster = await characters.createCharacter(spieler, { name: 'Bleibt unten' }, ctx);
|
||||
const zweiter = await characters.createCharacter(spieler, { name: 'Wandert' }, ctx);
|
||||
|
||||
await characters.moveInChain(
|
||||
zweiter.character.id,
|
||||
{ lineageId: erster.character.lineageId, afterId: erster.character.id },
|
||||
ctx,
|
||||
);
|
||||
assert.equal((await verbrauch(zweiter.character.id))?.kind, 'REDESIGN_PAID');
|
||||
|
||||
await characters.detachFromChain(zweiter.character.id, ctx);
|
||||
|
||||
assert.equal(
|
||||
(await verbrauch(zweiter.character.id))?.kind,
|
||||
'CC',
|
||||
'allein heisst eigener Platz, und der kostet ein CC-Paket',
|
||||
);
|
||||
});
|
||||
|
||||
it('bucht das neue unterste Glied auf CC um', async () => {
|
||||
// Ein Redesign wird an den Anfang gezogen - es ist damit der gekaufte CC.
|
||||
const spieler = await spielerMitPaketen('Umkehrer', 2);
|
||||
const eins = await characters.createCharacter(spieler, { name: 'War unten' }, ctx);
|
||||
const zwei = await characters.redesignCharacter(
|
||||
eins.character.id,
|
||||
{ name: 'War oben', useKind: 'REDESIGN_PAID' },
|
||||
ctx,
|
||||
);
|
||||
assert.equal((await verbrauch(zwei.character.id))?.kind, 'REDESIGN_PAID');
|
||||
|
||||
await characters.moveInChain(
|
||||
zwei.character.id,
|
||||
{ lineageId: eins.character.lineageId, afterId: null },
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert.equal((await verbrauch(zwei.character.id))?.kind, 'CC', 'unten steht der gekaufte CC');
|
||||
assert.equal(
|
||||
(await verbrauch(eins.character.id))?.kind,
|
||||
'REDESIGN_PAID',
|
||||
'und der andere ist nach oben gerückt',
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
const zwei = await characters.redesignCharacter(
|
||||
eins.character.id,
|
||||
{ name: 'Aus Kulanz', useKind: 'REDESIGN_KULANZ' },
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Ein Verschieben innerhalb der Kette ändert an der Stelle nichts.
|
||||
const dritter = await characters.createCharacter(spieler, { name: 'Kommt dazu' }, ctx);
|
||||
await characters.moveInChain(
|
||||
dritter.character.id,
|
||||
{ lineageId: eins.character.lineageId, afterId: zwei.character.id },
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
(await verbrauch(zwei.character.id))?.kind,
|
||||
'REDESIGN_KULANZ',
|
||||
'bezahlt oder Kulanz ist eine Entscheidung, keine Folge der Reihenfolge',
|
||||
);
|
||||
});
|
||||
|
||||
it('lässt einen Eintrag ohne Buchung in Ruhe', async () => {
|
||||
// Nacherfassung ohne Guthaben-Angabe - da gibt es nichts umzustellen.
|
||||
const spieler = await spielerMitPaketen('Ohne Buchung', 1);
|
||||
const eins = await characters.createCharacter(spieler, { name: 'Mit Buchung' }, ctx);
|
||||
|
||||
const ohne = await db.prisma.character.create({
|
||||
data: { playerId: spieler, name: 'Ohne Buchung', status: 'ACTIVE' },
|
||||
});
|
||||
await db.prisma.characterMember.create({
|
||||
data: { characterId: ohne.id, playerId: spieler },
|
||||
});
|
||||
|
||||
const vorher = await credits.getBalances(spieler);
|
||||
await characters.moveInChain(
|
||||
ohne.id,
|
||||
{ lineageId: eins.character.lineageId, afterId: eins.character.id },
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert.deepEqual(await credits.getBalances(spieler), vorher, 'nichts gebucht, nichts geändert');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user