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:
@@ -421,6 +421,112 @@ export async function reactivateFree(
|
|||||||
return { character, balances, overdrawn: isOverdrawn(balances) };
|
return { character, balances, overdrawn: isOverdrawn(balances) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt einen CC zurück, indem er noch einmal erschaffen wird.
|
||||||
|
*
|
||||||
|
* Ein redesignter oder gestorbener CC existiert im Original nicht mehr.
|
||||||
|
* Seine Rückkehr ist deshalb ein weiteres Glied in der Liste: derselbe CC,
|
||||||
|
* noch einmal - und das kostet wie jedes andere Glied. Der alte Eintrag
|
||||||
|
* bleibt stehen, damit sichtbar bleibt, was war.
|
||||||
|
*
|
||||||
|
* Wer dafür ein CC-Guthaben einsetzt, bekommt keinen Nachfolger, sondern
|
||||||
|
* einen zweiten Platz: der neue Eintrag beginnt eine eigene Liste und kann
|
||||||
|
* damit neben seinem Nachfolger laufen.
|
||||||
|
*/
|
||||||
|
async function erschaffeNeu(
|
||||||
|
tx: Prisma.TransactionClient,
|
||||||
|
alt: Character,
|
||||||
|
useKind: ReviveKind,
|
||||||
|
ctx: ActionContext,
|
||||||
|
note: string,
|
||||||
|
): Promise<Character> {
|
||||||
|
const eigenerPlatz = useKind === CreditKind.CC;
|
||||||
|
|
||||||
|
const bisherige = await tx.characterMember.findMany({
|
||||||
|
where: { characterId: alt.id, leftAt: null },
|
||||||
|
select: {
|
||||||
|
playerId: true,
|
||||||
|
paidWith: true,
|
||||||
|
lineageId: true,
|
||||||
|
chainPosition: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eigeneListe = randomUUID();
|
||||||
|
const platzVon = (mitglied: { lineageId: string; chainPosition: number }) =>
|
||||||
|
eigenerPlatz
|
||||||
|
? { lineageId: randomUUID(), chainPosition: 0 }
|
||||||
|
: { lineageId: mitglied.lineageId, chainPosition: mitglied.chainPosition + 10 };
|
||||||
|
|
||||||
|
const besitzer = bisherige.find((m) => m.playerId === alt.playerId);
|
||||||
|
const platz = besitzer
|
||||||
|
? platzVon(besitzer)
|
||||||
|
: { lineageId: eigeneListe, chainPosition: 0 };
|
||||||
|
|
||||||
|
const neu = await tx.character.create({
|
||||||
|
data: {
|
||||||
|
playerId: alt.playerId,
|
||||||
|
name: alt.name,
|
||||||
|
loreText: alt.loreText,
|
||||||
|
status: CharacterStatus.ACTIVE,
|
||||||
|
lineageId: platz.lineageId,
|
||||||
|
chainPosition: platz.chainPosition,
|
||||||
|
...(eigenerPlatz ? {} : { predecessorId: alt.id }),
|
||||||
|
createdById: ctx.actorId,
|
||||||
|
members: {
|
||||||
|
create: bisherige.map((mitglied) => {
|
||||||
|
const eigen = platzVon(mitglied);
|
||||||
|
return {
|
||||||
|
playerId: mitglied.playerId,
|
||||||
|
paidWith: mitglied.paidWith,
|
||||||
|
lineageId:
|
||||||
|
mitglied.playerId === alt.playerId ? platz.lineageId : eigen.lineageId,
|
||||||
|
chainPosition:
|
||||||
|
mitglied.playerId === alt.playerId
|
||||||
|
? platz.chainPosition
|
||||||
|
: eigen.chainPosition,
|
||||||
|
...(eigenerPlatz ? {} : { predecessorId: alt.id }),
|
||||||
|
createdById: ctx.actorId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Der alte Eintrag ist damit ersetzt - so wie bei jedem Redesign.
|
||||||
|
await tx.character.update({
|
||||||
|
where: { id: alt.id },
|
||||||
|
data: {
|
||||||
|
status: CharacterStatus.INACTIVE,
|
||||||
|
inactiveReason: InactiveReason.REWORKED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await bucheVerbrauch(tx, {
|
||||||
|
playerId: alt.playerId,
|
||||||
|
characterId: neu.id,
|
||||||
|
kind: useKind,
|
||||||
|
redesign: !eigenerPlatz,
|
||||||
|
note,
|
||||||
|
actorId: ctx.actorId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bei einem Gruppen-CC zählt der Eintrag für jeden Nutzer einzeln.
|
||||||
|
for (const mitglied of bisherige) {
|
||||||
|
if (mitglied.playerId === alt.playerId) continue;
|
||||||
|
await bucheVerbrauch(tx, {
|
||||||
|
playerId: mitglied.playerId,
|
||||||
|
characterId: neu.id,
|
||||||
|
kind: eigenerPlatz ? CreditKind.CC : CreditKind.REDESIGN_PAID,
|
||||||
|
redesign: !eigenerPlatz,
|
||||||
|
note,
|
||||||
|
actorId: ctx.actorId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return neu;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bezahlte Reaktivierung, ausschließlich für redesignte CCs: deren Original
|
* Bezahlte Reaktivierung, ausschließlich für redesignte CCs: deren Original
|
||||||
* existiert nicht mehr, es muss also neu erschaffen werden. Kostet ein
|
* existiert nicht mehr, es muss also neu erschaffen werden. Kostet ein
|
||||||
@@ -461,39 +567,30 @@ export async function reactivatePaid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const character = await prisma.$transaction(async (tx) => {
|
const character = await prisma.$transaction(async (tx) => {
|
||||||
const updated = await tx.character.update({
|
const neu = await erschaffeNeu(
|
||||||
where: { id: characterId },
|
tx,
|
||||||
data: {
|
existing,
|
||||||
status: CharacterStatus.ACTIVE,
|
useKind,
|
||||||
inactiveReason: null,
|
ctx,
|
||||||
inactiveNote: null,
|
note ?? `Rückkehr: ${existing.name}`,
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await bucheVerbrauch(tx, {
|
await assertSlotsReichen(tx, neu.playerId, "Die Rückkehr dieses CCs");
|
||||||
playerId: updated.playerId,
|
|
||||||
characterId: updated.id,
|
|
||||||
kind: useKind,
|
|
||||||
redesign: null,
|
|
||||||
note: note ?? `Reaktivierung: ${updated.name}`,
|
|
||||||
actorId: ctx.actorId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await assertSlotsReichen(tx, updated.playerId, "Die Rückkehr dieses CCs");
|
|
||||||
|
|
||||||
await recordAudit(tx, ctx, {
|
await recordAudit(tx, ctx, {
|
||||||
action: "character.reactivate_paid",
|
action: "character.reactivate_paid",
|
||||||
targetType: "character",
|
targetType: "character",
|
||||||
targetId: characterId,
|
targetId: neu.id,
|
||||||
details: {
|
details: {
|
||||||
playerId: updated.playerId,
|
playerId: neu.playerId,
|
||||||
name: updated.name,
|
name: neu.name,
|
||||||
useKind,
|
useKind,
|
||||||
|
previousId: existing.id,
|
||||||
previousReason: existing.inactiveReason,
|
previousReason: existing.inactiveReason,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return updated;
|
return neu;
|
||||||
});
|
});
|
||||||
|
|
||||||
const balances = await getBalances(character.playerId);
|
const balances = await getBalances(character.playerId);
|
||||||
@@ -838,37 +935,39 @@ export async function reviveDeadCharacter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const character = await prisma.$transaction(async (tx) => {
|
const character = await prisma.$transaction(async (tx) => {
|
||||||
const updated = await tx.character.update({
|
// Kostet die Rückkehr etwas, ist sie ein weiteres Glied: derselbe CC,
|
||||||
|
// noch einmal. Gratis kommt er dagegen unverändert zurück - dann hat
|
||||||
|
// sich an der Liste nichts geändert.
|
||||||
|
const updated =
|
||||||
|
input.useKind === undefined
|
||||||
|
? await tx.character.update({
|
||||||
where: { id: characterId },
|
where: { id: characterId },
|
||||||
data: {
|
data: {
|
||||||
status: CharacterStatus.ACTIVE,
|
status: CharacterStatus.ACTIVE,
|
||||||
inactiveReason: null,
|
inactiveReason: null,
|
||||||
inactiveNote: null,
|
inactiveNote: null,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
: await erschaffeNeu(
|
||||||
if (input.useKind !== undefined) {
|
tx,
|
||||||
await bucheVerbrauch(tx, {
|
existing,
|
||||||
playerId: updated.playerId,
|
input.useKind,
|
||||||
characterId: updated.id,
|
ctx,
|
||||||
kind: input.useKind,
|
`Rückkehr nach Tod: ${input.reason}`,
|
||||||
redesign: null,
|
);
|
||||||
note: `Rückkehr nach Tod: ${input.reason}`,
|
|
||||||
actorId: ctx.actorId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await assertSlotsReichen(tx, updated.playerId, "Die Rückkehr dieses CCs");
|
await assertSlotsReichen(tx, updated.playerId, "Die Rückkehr dieses CCs");
|
||||||
|
|
||||||
await recordAudit(tx, ctx, {
|
await recordAudit(tx, ctx, {
|
||||||
action: "character.revive_dead",
|
action: "character.revive_dead",
|
||||||
targetType: "character",
|
targetType: "character",
|
||||||
targetId: characterId,
|
targetId: updated.id,
|
||||||
details: {
|
details: {
|
||||||
playerId: updated.playerId,
|
playerId: updated.playerId,
|
||||||
name: updated.name,
|
name: updated.name,
|
||||||
reason: input.reason,
|
reason: input.reason,
|
||||||
useKind: input.useKind ?? null,
|
useKind: input.useKind ?? null,
|
||||||
|
previousId: existing.id,
|
||||||
previousNote: existing.inactiveNote,
|
previousNote: existing.inactiveNote,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -906,7 +1005,7 @@ export async function changeFunding(
|
|||||||
|
|
||||||
// Die Buchung, mit der dieser Spieler seinen Platz bezahlt hat. Bei einem
|
// Die Buchung, mit der dieser Spieler seinen Platz bezahlt hat. Bei einem
|
||||||
// Gruppen-CC hat jeder Nutzer seine eigene; stornierte zählen nicht mehr.
|
// Gruppen-CC hat jeder Nutzer seine eigene; stornierte zählen nicht mehr.
|
||||||
const buchung = await prisma.creditLedger.findFirst({
|
const offene = await prisma.creditLedger.findMany({
|
||||||
where: {
|
where: {
|
||||||
characterId,
|
characterId,
|
||||||
playerId: spieler,
|
playerId: spieler,
|
||||||
@@ -915,6 +1014,7 @@ export async function changeFunding(
|
|||||||
},
|
},
|
||||||
orderBy: { id: "desc" },
|
orderBy: { id: "desc" },
|
||||||
});
|
});
|
||||||
|
const buchung = offene[0];
|
||||||
if (!buchung) {
|
if (!buchung) {
|
||||||
throw conflict(
|
throw conflict(
|
||||||
"NO_BOOKING",
|
"NO_BOOKING",
|
||||||
@@ -938,7 +1038,9 @@ export async function changeFunding(
|
|||||||
"Redesign um, dann steht es hier zur Verfügung.",
|
"Redesign um, dann steht es hier zur Verfügung.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (buchung.kind === useKind) {
|
// Bei mehr als einer Buchung ist das Umstellen zugleich das Aufräumen:
|
||||||
|
// ein Eintrag kostet genau einmal.
|
||||||
|
if (offene.length === 1 && buchung.kind === useKind) {
|
||||||
throw conflict("SAME_KIND", "Dieser CC ist bereits so gebucht");
|
throw conflict("SAME_KIND", "Dieser CC ist bereits so gebucht");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +1050,9 @@ export async function changeFunding(
|
|||||||
: "Umgestellt auf bezahltes Redesign";
|
: "Umgestellt auf bezahltes Redesign";
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await storniere(tx, buchung, `${grund}: ${character.name}`, ctx.actorId);
|
for (const eintrag of offene) {
|
||||||
|
await storniere(tx, eintrag, `${grund}: ${character.name}`, ctx.actorId);
|
||||||
|
}
|
||||||
|
|
||||||
await tx.creditLedger.create({
|
await tx.creditLedger.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -1041,6 +1145,21 @@ async function bucheVerbrauch(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const ausCcTopf = params.kind === CreditKind.CC;
|
const ausCcTopf = params.kind === CreditKind.CC;
|
||||||
|
|
||||||
|
// Ein Eintrag kostet genau einmal. Steht seine Buchung schon da - etwa bei
|
||||||
|
// der Rückkehr eines CCs, der einmal bezahlt wurde -, kommt keine zweite
|
||||||
|
// dazu. Ob der Platz für zwei gleichzeitig aktive CCs reicht, entscheidet
|
||||||
|
// die Slot-Prüfung.
|
||||||
|
const schonBezahlt = await tx.creditLedger.findFirst({
|
||||||
|
where: {
|
||||||
|
characterId: params.characterId,
|
||||||
|
playerId: params.playerId,
|
||||||
|
source: CreditSource.CONSUME,
|
||||||
|
reversedBy: null,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (schonBezahlt !== null) return;
|
||||||
|
|
||||||
if (params.redesign === true && ausCcTopf) {
|
if (params.redesign === true && ausCcTopf) {
|
||||||
throw conflict(
|
throw conflict(
|
||||||
"CONVERT_FIRST",
|
"CONVERT_FIRST",
|
||||||
@@ -1165,18 +1284,38 @@ async function buchungFolgtPosition(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steht schon etwas da, das zur Stelle passt, bleibt alles, wie es ist.
|
// Ein Glied kostet genau einmal. Steht mehr als eine Buchung daran, ist
|
||||||
// An einem Eintrag kann mehr als eine Buchung hängen - eine Rückkehr
|
// eine davon zu viel - die überzähligen werden zurückgenommen, sonst
|
||||||
// kostet zusätzlich, und das soll sie auch.
|
// zahlt jemand doppelt für denselben Eintrag.
|
||||||
if (offene.some((eintrag) => (eintrag.kind === CreditKind.CC) === sollCC))
|
// Es bleibt die älteste passende: sie gehört zu dem Vorgang, aus dem der
|
||||||
return false;
|
// Eintrag entstanden ist. Was später dazukam, ist die Dublette.
|
||||||
|
const passend = offene
|
||||||
|
.filter((eintrag) => (eintrag.kind === CreditKind.CC) === sollCC)
|
||||||
|
.sort((a, b) => a.id - b.id);
|
||||||
|
const behalten = passend[0] ?? null;
|
||||||
|
|
||||||
// Ein Eintrag, der in eine Kette rückt, braucht kein eigenes Paket mehr:
|
let geaendert = false;
|
||||||
// das Guthaben kommt zurück, dafür geht ein Redesign weg. Umgekehrt
|
for (const eintrag of offene) {
|
||||||
// genauso.
|
// Die eine, die bleibt, und - wenn keine passt - die jüngste, die
|
||||||
|
// gleich umgebucht wird.
|
||||||
|
if (eintrag.id === (behalten?.id ?? offen.id)) continue;
|
||||||
|
await storniere(
|
||||||
|
tx,
|
||||||
|
eintrag,
|
||||||
|
`Doppelt gebucht - Storno: ${glied.name}`,
|
||||||
|
actorId,
|
||||||
|
);
|
||||||
|
geaendert = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (behalten !== null) return geaendert;
|
||||||
|
|
||||||
|
// Keine passende dabei: Ein Eintrag, der in eine Liste rückt, braucht kein
|
||||||
|
// eigenes Paket mehr - das Guthaben kommt zurück, dafür geht ein Redesign
|
||||||
|
// weg. Umgekehrt genauso.
|
||||||
const grund = sollCC
|
const grund = sollCC
|
||||||
? "Steht jetzt am Anfang der Kette: aus einem CC-Paket bezahlt"
|
? "Steht jetzt am Anfang der Liste: aus einem CC-Paket bezahlt"
|
||||||
: "Steht jetzt in einer Kette: als Redesign gebucht";
|
: "Steht jetzt in einer Liste: als Redesign gebucht";
|
||||||
|
|
||||||
await storniere(tx, offen, `${grund} - Storno: ${glied.name}`, actorId);
|
await storniere(tx, offen, `${grund} - Storno: ${glied.name}`, actorId);
|
||||||
|
|
||||||
@@ -2348,6 +2487,30 @@ export async function deactivateMember(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ein entzogener Platz ist weg - seine Buchung auch. Nur so kostet die
|
||||||
|
// Rückkehr wieder etwas, ohne dass am selben Eintrag zwei Buchungen
|
||||||
|
// stehen. Wer geputzt wurde oder im Rollenspiel gestorben ist, behält
|
||||||
|
// seine Buchung: sein Platz ist bezahlt, die Rückkehr entscheidet ein
|
||||||
|
// Administrator.
|
||||||
|
if (reason === InactiveReason.NO_FREE_CLAIM) {
|
||||||
|
const offene = await tx.creditLedger.findMany({
|
||||||
|
where: {
|
||||||
|
characterId,
|
||||||
|
playerId,
|
||||||
|
source: CreditSource.CONSUME,
|
||||||
|
reversedBy: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const eintrag of offene) {
|
||||||
|
await storniere(
|
||||||
|
tx,
|
||||||
|
eintrag,
|
||||||
|
`Platz nicht mehr genutzt - Storno: ${mitglied.character.name}`,
|
||||||
|
ctx.actorId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await recordAudit(tx, ctx, {
|
await recordAudit(tx, ctx, {
|
||||||
action: "character.member_deactivated",
|
action: "character.member_deactivated",
|
||||||
targetType: "character",
|
targetType: "character",
|
||||||
@@ -2436,7 +2599,9 @@ export async function reactivateMember(
|
|||||||
playerId,
|
playerId,
|
||||||
characterId,
|
characterId,
|
||||||
kind: input.useKind,
|
kind: input.useKind,
|
||||||
redesign: null,
|
// Auch hier entscheidet die Stelle in seiner Liste: steht der CC
|
||||||
|
// dort unten, braucht die Rückkehr einen Platz.
|
||||||
|
redesign: mitglied.predecessorId !== null,
|
||||||
note: input.reason ?? `Rückkehr zu "${gruppe.name}"`,
|
note: input.reason ?? `Rückkehr zu "${gruppe.name}"`,
|
||||||
actorId: ctx.actorId,
|
actorId: ctx.actorId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -682,14 +682,12 @@ describe('Einzelne Mitglieder fallen aus', () => {
|
|||||||
(error: Error & { code?: string }) => error.code === 'NEED_KIND',
|
(error: Error & { code?: string }) => error.code === 'NEED_KIND',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sein Platz war entzogen, also kostet die Rückkehr wieder einen - in
|
||||||
|
// seiner Liste steht der CC ganz unten.
|
||||||
const vorher = await credits.getBalances(partner);
|
const vorher = await credits.getBalances(partner);
|
||||||
await characters.reactivateMember(character.id, partner, { useKind: 'REDESIGN_PAID' }, ctx);
|
await characters.reactivateMember(character.id, partner, { useKind: 'CC' }, ctx);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal((await credits.getBalances(partner)).CC, vorher.CC - 1, 'bezahlt');
|
||||||
(await credits.getBalances(partner)).REDESIGN_PAID,
|
|
||||||
vorher.REDESIGN_PAID - 1,
|
|
||||||
'bezahlt',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('verlangt für die Rückkehr eines Gestorbenen eine Begründung', async () => {
|
it('verlangt für die Rückkehr eines Gestorbenen eine Begründung', async () => {
|
||||||
|
|||||||
@@ -1383,3 +1383,86 @@ describe('Die Umstellung auf Listen je Nutzer', () => {
|
|||||||
assert.ok(drei.character.id > 0);
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user