CC-Guthaben wird vorher umgewandelt, nicht am CC eingesetzt
Ein Redesign wird nie aus dem CC-Topf bezahlt. Wer sein Guthaben dafuer einsetzen will, tauscht es oben um und setzt es danach ein wie jedes andere Redesign. Damit faellt die Wahl CC-Guthaben an den CC-Aktionen weg; was ein Eintrag kostet, entscheidet seine Stelle in der Liste. Die Bezahlart gilt jetzt je Nutzer: bei einem Gruppen-CC hat jeder seine eigene Buchung, und umstellen liess sich bisher nur die des Erstellers. Die Guthaben-Historie laesst sich nach Topf filtern.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { prisma } from '../db.js';
|
||||
import { badRequest, conflict, notFound } from '../errors.js';
|
||||
import { getBalances, wandleCcInRedesign, type Balances } from './credits.js';
|
||||
import { getBalances, type Balances } from './credits.js';
|
||||
import { recordAudit, type ActionContext } from './audit.js';
|
||||
|
||||
export interface CharacterResult {
|
||||
@@ -397,7 +397,7 @@ export async function reactivatePaid(
|
||||
playerId: updated.playerId,
|
||||
characterId: updated.id,
|
||||
kind: useKind,
|
||||
redesign: updated.predecessorId !== null,
|
||||
redesign: null,
|
||||
note: note ?? `Reaktivierung: ${updated.name}`,
|
||||
actorId: ctx.actorId,
|
||||
});
|
||||
@@ -745,7 +745,7 @@ export async function reviveDeadCharacter(
|
||||
playerId: updated.playerId,
|
||||
characterId: updated.id,
|
||||
kind: input.useKind,
|
||||
redesign: updated.predecessorId !== null,
|
||||
redesign: null,
|
||||
note: `Rückkehr nach Tod: ${input.reason}`,
|
||||
actorId: ctx.actorId,
|
||||
});
|
||||
@@ -777,13 +777,9 @@ export async function reviveDeadCharacter(
|
||||
/**
|
||||
* Stellt um, woraus ein Eintrag bezahlt wurde.
|
||||
*
|
||||
* Zwei Dinge lassen sich hier entscheiden: ob ein Redesign aus dem bezahlten
|
||||
* Topf oder aus Kulanz kommt - und ob dafür ausnahmsweise ein CC-Guthaben
|
||||
* eingesetzt wird. Das Letzte ist keine eigene Bezahlart, sondern eine
|
||||
* Umwandlung: ein Platz weniger, ein Redesign mehr.
|
||||
*
|
||||
* Ob ein Eintrag überhaupt ein Redesign ist, steht nicht zur Wahl - das
|
||||
* entscheidet seine Stelle in der Kette.
|
||||
* Zu entscheiden bleibt eines: ob ein Redesign aus dem bezahlten Topf kommt
|
||||
* oder aus Kulanz. Ob ein Eintrag überhaupt ein Redesign ist, steht nicht
|
||||
* zur Wahl - das entscheidet seine Stelle in der Liste.
|
||||
*
|
||||
* Geändert wird wie jede Korrektur im Kontobuch: die alte Zeile wird
|
||||
* gegengebucht, die neue kommt dazu. So bleibt sichtbar, dass hier etwas
|
||||
@@ -793,19 +789,21 @@ export async function changeFunding(
|
||||
characterId: number,
|
||||
useKind: JoinKind,
|
||||
ctx: ActionContext,
|
||||
forPlayerId?: number,
|
||||
): Promise<CharacterResult> {
|
||||
const character = await loadCharacter(characterId);
|
||||
const spieler = forPlayerId ?? character.playerId;
|
||||
|
||||
if (!JOIN_KINDS.includes(useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art');
|
||||
}
|
||||
|
||||
// Die Buchung, mit der dieser CC bezahlt wurde. Stornierte zählen nicht
|
||||
// mehr; offen ist immer höchstens eine.
|
||||
// Die Buchung, mit der dieser Spieler seinen Platz bezahlt hat. Bei einem
|
||||
// Gruppen-CC hat jeder Nutzer seine eigene; stornierte zählen nicht mehr.
|
||||
const buchung = await prisma.creditLedger.findFirst({
|
||||
where: {
|
||||
characterId,
|
||||
playerId: character.playerId,
|
||||
playerId: spieler,
|
||||
source: CreditSource.CONSUME,
|
||||
reversedBy: null,
|
||||
},
|
||||
@@ -814,64 +812,42 @@ export async function changeFunding(
|
||||
if (!buchung) {
|
||||
throw conflict(
|
||||
'NO_BOOKING',
|
||||
'Für diesen CC ist nichts gebucht - hier gibt es nichts umzustellen',
|
||||
'Für diesen Spieler ist an diesem CC nichts gebucht - hier gibt es nichts umzustellen. ' +
|
||||
'Sein Platz hängt weiter unten in der Liste.',
|
||||
);
|
||||
}
|
||||
|
||||
const redesign = await istRedesignPosition(prisma, characterId, buchung.playerId);
|
||||
const redesign = await istRedesignPosition(prisma, characterId, spieler);
|
||||
if (!redesign && useKind !== CreditKind.CC) {
|
||||
throw conflict(
|
||||
'CHAIN_START',
|
||||
'Dieser Eintrag steht am Anfang einer Kette und bucht einen eigenen Platz. ' +
|
||||
'Dieser Eintrag steht am Anfang einer Liste und bucht einen eigenen Platz. ' +
|
||||
'Erst über einem anderen Eintrag wird daraus ein Redesign.',
|
||||
);
|
||||
}
|
||||
|
||||
// Ein CC-Guthaben für ein Redesign wird umgewandelt, nicht anders gebucht.
|
||||
const willUmwandeln = redesign && useKind === CreditKind.CC;
|
||||
const zielArt = willUmwandeln ? CreditKind.REDESIGN_PAID : useKind;
|
||||
const hatUmwandlung =
|
||||
(await prisma.creditLedger.count({
|
||||
where: {
|
||||
characterId,
|
||||
playerId: buchung.playerId,
|
||||
source: CreditSource.CONVERSION,
|
||||
reversedBy: null,
|
||||
},
|
||||
})) > 0;
|
||||
|
||||
if (buchung.kind === zielArt && willUmwandeln === hatUmwandlung) {
|
||||
if (redesign && useKind === CreditKind.CC) {
|
||||
throw conflict(
|
||||
'CONVERT_FIRST',
|
||||
'Ein Redesign wird nicht aus dem CC-Topf bezahlt. Wandle das CC-Guthaben oben in ein ' +
|
||||
'Redesign um, dann steht es hier zur Verfügung.',
|
||||
);
|
||||
}
|
||||
if (buchung.kind === useKind) {
|
||||
throw conflict('SAME_KIND', 'Dieser CC ist bereits so gebucht');
|
||||
}
|
||||
|
||||
const grund = willUmwandeln
|
||||
? 'CC-Guthaben umgewandelt'
|
||||
: useKind === CreditKind.CC
|
||||
? 'Umgestellt auf CC-Paket'
|
||||
: useKind === CreditKind.REDESIGN_KULANZ
|
||||
? 'Umgestellt auf Kulanz'
|
||||
: 'Umgestellt auf bezahltes Redesign';
|
||||
const grund =
|
||||
useKind === CreditKind.REDESIGN_KULANZ
|
||||
? 'Umgestellt auf Kulanz'
|
||||
: 'Umgestellt auf bezahltes Redesign';
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await storniere(tx, buchung, `${grund}: ${character.name}`, ctx.actorId);
|
||||
|
||||
if (willUmwandeln && !hatUmwandlung) {
|
||||
await wandleCcInRedesign(tx, {
|
||||
playerId: buchung.playerId,
|
||||
characterId,
|
||||
actorId: ctx.actorId,
|
||||
note: `CC-Guthaben als Redesign eingesetzt: ${character.name}`,
|
||||
occurredAt: buchung.occurredAt,
|
||||
});
|
||||
}
|
||||
if (!willUmwandeln && hatUmwandlung) {
|
||||
await nimmUmwandlungZurueck(tx, characterId, buchung.playerId, ctx.actorId);
|
||||
}
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId: buchung.playerId,
|
||||
kind: zielArt,
|
||||
playerId: spieler,
|
||||
kind: useKind,
|
||||
amount: buchung.amount,
|
||||
source: CreditSource.CONSUME,
|
||||
groupId: randomUUID(),
|
||||
@@ -887,11 +863,11 @@ export async function changeFunding(
|
||||
action: 'character.funding_changed',
|
||||
targetType: 'character',
|
||||
targetId: characterId,
|
||||
details: { name: character.name, from: buchung.kind, to: zielArt, umgewandelt: willUmwandeln },
|
||||
details: { name: character.name, playerId: spieler, from: buchung.kind, to: useKind },
|
||||
});
|
||||
});
|
||||
|
||||
const balances = await getBalances(character.playerId);
|
||||
const balances = await getBalances(spieler);
|
||||
return { character, balances, overdrawn: isOverdrawn(balances) };
|
||||
}
|
||||
|
||||
@@ -927,11 +903,11 @@ async function istRedesignPosition(
|
||||
/**
|
||||
* Bucht, was ein Eintrag kostet.
|
||||
*
|
||||
* Ein Platz am Anfang einer Kette kommt aus dem CC-Topf, alles darüber aus
|
||||
* einem Redesign-Topf. Wer sein CC-Guthaben für ein Redesign einsetzen will,
|
||||
* wandelt es hier um: ein Platz weniger, ein bezahltes Redesign mehr. Der
|
||||
* Eintrag selbst ist danach ein Redesign wie jedes andere - dadurch bleibt
|
||||
* die Rechnung überall dieselbe, ohne Sonderfall.
|
||||
* Am Anfang einer Liste steht ein gekaufter Platz, alles darüber ein
|
||||
* Redesign. Die Stelle entscheidet, nicht die Wahl - sonst ginge die
|
||||
* Rechnung "eine Liste, ein CC-Paket" nicht mehr auf. Wer sein CC-Guthaben
|
||||
* für ein Redesign einsetzen will, wandelt es vorher um; danach ist es ein
|
||||
* Redesign wie jedes andere.
|
||||
*/
|
||||
async function bucheVerbrauch(
|
||||
tx: Prisma.TransactionClient,
|
||||
@@ -939,30 +915,40 @@ async function bucheVerbrauch(
|
||||
playerId: number;
|
||||
characterId: number;
|
||||
kind: JoinKind;
|
||||
/// Steht der Eintrag über einem Vorgänger? Dann ist es ein Redesign.
|
||||
redesign: boolean;
|
||||
/**
|
||||
* Steht der Eintrag über einem Vorgänger? Dann ist es ein Redesign.
|
||||
*
|
||||
* `null` bei einer Buchung, die neben einer bestehenden steht - eine
|
||||
* Rückkehr etwa kostet zusätzlich, und was sie kostet, entscheidet der
|
||||
* Fall, nicht die Stelle in der Liste.
|
||||
*/
|
||||
redesign: boolean | null;
|
||||
note: string;
|
||||
actorId: number | null;
|
||||
occurredAt?: Date | undefined;
|
||||
},
|
||||
): Promise<void> {
|
||||
const ausCcTopf = params.kind === CreditKind.CC;
|
||||
const umwandeln = ausCcTopf && params.redesign;
|
||||
|
||||
if (umwandeln) {
|
||||
await wandleCcInRedesign(tx, {
|
||||
playerId: params.playerId,
|
||||
characterId: params.characterId,
|
||||
actorId: params.actorId,
|
||||
note: `CC-Guthaben als Redesign eingesetzt: ${params.note}`,
|
||||
occurredAt: params.occurredAt ?? null,
|
||||
});
|
||||
if (params.redesign === true && ausCcTopf) {
|
||||
throw conflict(
|
||||
'CONVERT_FIRST',
|
||||
'Ein Redesign wird nicht aus dem CC-Topf bezahlt. Wandle das CC-Guthaben oben in ein ' +
|
||||
'Redesign um, dann steht es hier zur Verfügung.',
|
||||
);
|
||||
}
|
||||
if (params.redesign === false && !ausCcTopf) {
|
||||
throw conflict(
|
||||
'CHAIN_START',
|
||||
'Dieser Eintrag steht am Anfang einer Liste und bucht einen eigenen Platz. Dafür wird ein ' +
|
||||
'CC-Guthaben gebraucht, kein Redesign.',
|
||||
);
|
||||
}
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId: params.playerId,
|
||||
kind: umwandeln ? CreditKind.REDESIGN_PAID : params.kind,
|
||||
kind: params.kind,
|
||||
amount: -1,
|
||||
source: CreditSource.CONSUME,
|
||||
groupId: randomUUID(),
|
||||
@@ -1005,27 +991,6 @@ async function storniere(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nimmt die Umwandlung zurück, aus der ein Eintrag bezahlt wurde.
|
||||
*
|
||||
* Wer ein CC-Guthaben in ein Redesign getauscht hat und den Eintrag später
|
||||
* an den Anfang einer Kette stellt, bekommt den Tausch zurück: der Eintrag
|
||||
* bucht dann wieder einen eigenen Platz.
|
||||
*/
|
||||
async function nimmUmwandlungZurueck(
|
||||
tx: Prisma.TransactionClient,
|
||||
characterId: number,
|
||||
playerId: number,
|
||||
actorId: number | null,
|
||||
): Promise<void> {
|
||||
const zeilen = await tx.creditLedger.findMany({
|
||||
where: { characterId, playerId, source: CreditSource.CONVERSION, reversedBy: null },
|
||||
});
|
||||
for (const zeile of zeilen) {
|
||||
await storniere(tx, zeile, 'Umwandlung zurückgenommen: bucht wieder einen Platz', actorId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucht ein Kettenglied auf die Art um, die seine Stelle in der Kette
|
||||
* verlangt.
|
||||
@@ -1084,12 +1049,6 @@ async function buchungFolgtPosition(
|
||||
|
||||
await storniere(tx, offen, `${grund} - Storno: ${glied.name}`, actorId);
|
||||
|
||||
if (sollCC) {
|
||||
// Wer sein CC-Guthaben einmal für dieses Redesign umgewandelt hat,
|
||||
// bekommt den Tausch zurück - hier bucht der Eintrag wieder einen Platz.
|
||||
await nimmUmwandlungZurueck(tx, glied.id, offen.playerId, actorId);
|
||||
}
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId: offen.playerId,
|
||||
@@ -2207,7 +2166,7 @@ export async function reactivateMember(
|
||||
playerId,
|
||||
characterId,
|
||||
kind: input.useKind,
|
||||
redesign: mitglied.predecessorId !== null,
|
||||
redesign: null,
|
||||
note: input.reason ?? `Rückkehr zu "${gruppe.name}"`,
|
||||
actorId: ctx.actorId,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user