Ketten mit Vorgeschichte: unterstes Glied ist nicht immer ein Kauf
Ein Spieler mit drei Paketen und drei Plaetzen stand bei "-1 von 3". Der dritte Platz kam aus einem fremden Gruppen-CC: beigetreten mit einem CC-Guthaben, spaeter per Redesign ausgestiegen und als eigener CC weitergefuehrt. Damit beginnt zwar eine neue Kette, aber kein neuer Platz - bezahlt ist er laengst, naemlich mit dem Beitritt. Die Automatik sah nur "unterstes Glied" und buchte es auf CC-Paket um. Der Platz stand damit zweimal im Konto, einmal fuer den Beitritt und einmal fuer den eigenen CC. Jetzt zaehlt, ob der Kette etwas vorausging: Zeigt ihr erstes Glied auf einen Vorgaenger ausserhalb, ist auch dieses Glied ein Redesign. Dabei fiel ein zweiter Fehler auf: Das Neuverketten loeschte genau diesen Verweis, weil es die Verkettung innerhalb der Linie normalisierte. Damit ging die einzige Spur zur Herkunft verloren - und mit ihr der Eintrag "war dabei" am Fuss der Kette. Ein Vorgaenger ausserhalb bleibt jetzt stehen. Fuer die Korrektur bereits falsch gebuchter Eintraege kann "Bezahlart" jetzt auch zwischen CC-Paket und Redesign wechseln, nicht nur zwischen bezahlt und Kulanz. Der Knopf steht an jedem Eintrag, nicht nur an Redesigns.
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
removeLink,
|
||||
moveInChain,
|
||||
detachFromChain,
|
||||
changeRedesignKind,
|
||||
changeFunding,
|
||||
addMemberToChain,
|
||||
} from '../services/characters.js';
|
||||
import { parseThreadId } from '../lib/forum.js';
|
||||
@@ -183,18 +183,19 @@ export async function characterRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
|
||||
/**
|
||||
* Nachträglich umstellen, woraus ein Redesign bezahlt wurde. Umgebucht
|
||||
* wird im Kontobuch, nicht überschrieben.
|
||||
* Nachträglich umstellen, woraus ein CC bezahlt wurde - aus einem Paket,
|
||||
* einem bezahlten Redesign oder aus Kulanz. Umgebucht wird im Kontobuch,
|
||||
* nicht überschrieben.
|
||||
*/
|
||||
app.post('/api/characters/:id/redesign-kind', { preHandler: requireEditor }, async (request) => {
|
||||
app.post('/api/characters/:id/funding', { preHandler: requireEditor }, async (request) => {
|
||||
const { id } = parseWith(idParam, request.params);
|
||||
const body = parseWith(
|
||||
z.object({
|
||||
useKind: z.enum([CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
|
||||
useKind: z.enum([CreditKind.CC, CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
|
||||
}),
|
||||
request.body,
|
||||
);
|
||||
return changeRedesignKind(id, body.useKind, contextOf(request));
|
||||
return changeFunding(id, body.useKind, contextOf(request));
|
||||
});
|
||||
|
||||
// --- Ketten ---
|
||||
|
||||
@@ -731,24 +731,26 @@ export async function reviveDeadCharacter(
|
||||
|
||||
|
||||
/**
|
||||
* Stellt nachträglich um, woraus ein Redesign bezahlt wurde.
|
||||
* Stellt nachträglich um, woraus ein CC bezahlt wurde.
|
||||
*
|
||||
* Ob ein Redesign aus einem gekauften Paket oder aus Kulanz kam, stellt sich
|
||||
* manchmal erst später heraus - etwa wenn eine Absprache auftaucht, von der
|
||||
* beim Eintragen niemand wusste. Umgebucht wird nicht durch Überschreiben,
|
||||
* sondern wie jede Korrektur im Kontobuch: die alte Zeile wird
|
||||
* gegengebucht, die neue kommt dazu. So bleibt sichtbar, dass hier etwas
|
||||
* geändert wurde, und wann.
|
||||
* Ob etwas aus einem gekauften Paket, einem bezahlten Redesign oder aus
|
||||
* Kulanz kam, stellt sich manchmal erst später heraus - wenn eine Absprache
|
||||
* auftaucht, von der beim Eintragen niemand wusste, oder wenn sich zeigt,
|
||||
* dass ein Eintrag gar nicht der Anfang einer Kette war.
|
||||
*
|
||||
* Umgebucht wird nicht durch Überschreiben, sondern wie jede Korrektur im
|
||||
* Kontobuch: die alte Zeile wird gegengebucht, die neue kommt dazu. So
|
||||
* bleibt sichtbar, dass hier etwas geändert wurde, und wann.
|
||||
*/
|
||||
export async function changeRedesignKind(
|
||||
export async function changeFunding(
|
||||
characterId: number,
|
||||
useKind: RedesignKind,
|
||||
useKind: JoinKind,
|
||||
ctx: ActionContext,
|
||||
): Promise<CharacterResult> {
|
||||
const character = await loadCharacter(characterId);
|
||||
|
||||
if (!REDESIGN_KINDS.includes(useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ein Redesign kostet bezahltes oder Kulanz-Redesign-Guthaben');
|
||||
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
|
||||
@@ -756,26 +758,28 @@ export async function changeRedesignKind(
|
||||
const buchung = await prisma.creditLedger.findFirst({
|
||||
where: {
|
||||
characterId,
|
||||
playerId: character.playerId,
|
||||
source: CreditSource.CONSUME,
|
||||
kind: { in: [...REDESIGN_KINDS] },
|
||||
reversedBy: null,
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
if (!buchung) {
|
||||
throw conflict(
|
||||
'NO_REDESIGN_BOOKING',
|
||||
'Für diesen CC ist kein offenes Redesign gebucht - hier gibt es nichts umzustellen',
|
||||
'NO_BOOKING',
|
||||
'Für diesen CC ist nichts gebucht - hier gibt es nichts umzustellen',
|
||||
);
|
||||
}
|
||||
if (buchung.kind === useKind) {
|
||||
throw conflict('SAME_KIND', 'Dieses Redesign ist bereits so gebucht');
|
||||
throw conflict('SAME_KIND', 'Dieser CC ist bereits so gebucht');
|
||||
}
|
||||
|
||||
const grund =
|
||||
useKind === CreditKind.REDESIGN_KULANZ
|
||||
? 'Umgestellt auf Kulanz'
|
||||
: 'Umgestellt auf bezahltes Redesign';
|
||||
useKind === CreditKind.CC
|
||||
? 'Umgestellt auf CC-Paket'
|
||||
: useKind === CreditKind.REDESIGN_KULANZ
|
||||
? 'Umgestellt auf Kulanz'
|
||||
: 'Umgestellt auf bezahltes Redesign';
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.creditLedger.create({
|
||||
@@ -809,7 +813,7 @@ export async function changeRedesignKind(
|
||||
});
|
||||
|
||||
await recordAudit(tx, ctx, {
|
||||
action: 'character.redesign_kind_changed',
|
||||
action: 'character.funding_changed',
|
||||
targetType: 'character',
|
||||
targetId: characterId,
|
||||
details: { name: character.name, from: buchung.kind, to: useKind },
|
||||
@@ -936,19 +940,40 @@ async function verketteNeu(
|
||||
|
||||
// Die Kette gehört dem, der unten steht: bei ihm fing sie an.
|
||||
const kettenBesitzer = glieder[0]?.playerId ?? null;
|
||||
const inDieserLinie = new Set(glieder.map((g) => g.id));
|
||||
|
||||
/**
|
||||
* Eine Herkunft ausserhalb dieser Kette.
|
||||
*
|
||||
* Wer aus einem fremden Gruppen-CC herausredesignt, beginnt eine eigene
|
||||
* Kette - bezahlt hat er sie trotzdem mit einem Redesign, nicht mit einem
|
||||
* Paket. Ihr unterstes Glied ist deshalb kein Kauf, und der Verweis auf
|
||||
* die Herkunft bleibt stehen: er ist das Einzige, was noch zeigt, woher
|
||||
* die Kette kommt.
|
||||
*/
|
||||
const herkunft = glieder[0]?.predecessorId ?? null;
|
||||
const hatHerkunft = herkunft !== null && !inDieserLinie.has(herkunft);
|
||||
|
||||
let vorheriges: number | null = null;
|
||||
for (const [index, glied] of glieder.entries()) {
|
||||
const letztes = index === glieder.length - 1;
|
||||
const erstes = index === 0;
|
||||
const daten: Prisma.CharacterUpdateInput = {};
|
||||
|
||||
// Das unterste Glied ist der gekaufte CC, alles darüber sein Redesign.
|
||||
// Das unterste Glied ist der gekaufte CC - es sei denn, ihm ging etwas
|
||||
// voraus. Alles darüber sind seine Redesigns.
|
||||
if (kettenBesitzer !== null) {
|
||||
await buchungFolgtPosition(tx, glied, kettenBesitzer, index === 0, actorId);
|
||||
await buchungFolgtPosition(tx, glied, kettenBesitzer, erstes && !hatHerkunft, actorId);
|
||||
}
|
||||
|
||||
if (glied.predecessorId !== vorheriges) {
|
||||
daten.predecessor = vorheriges === null ? { disconnect: true } : { connect: { id: vorheriges } };
|
||||
if (erstes) {
|
||||
// Am Anfang wird nur eine Verkettung gelöst, die innerhalb dieser
|
||||
// Linie ins Leere zeigt. Eine Herkunft von aussen bleibt.
|
||||
if (glied.predecessorId !== null && inDieserLinie.has(glied.predecessorId)) {
|
||||
daten.predecessor = { disconnect: true };
|
||||
}
|
||||
} else if (glied.predecessorId !== vorheriges) {
|
||||
daten.predecessor = { connect: { id: vorheriges as number } };
|
||||
}
|
||||
|
||||
// Was einen Nachfolger hat, wurde durch ihn ersetzt - anders ist eine
|
||||
|
||||
Reference in New Issue
Block a user