Rueckfragen im Stil der Anwendung, Kulanz-Ausgleich, Redesign aus dem CC-Topf
window.confirm und window.prompt sind raus - alle vierzehn Stellen. Sie sahen nicht nur fremd aus: Wer sie im Browser abschaltet, bekommt stillschweigend "nein" zurueck, und eine Aktion passiert einfach nicht, ohne dass jemand erfaehrt warum. Stattdessen gibt es einen Dialog-Dienst mit drei Formen - bestaetigen, abfragen, waehlen -, der sich wie der Rest der Anwendung anfuehlt. Die Wahl kam dazu, weil eine Stelle ein Ja/Nein fuer eine Auswahl missbrauchte: "OK = Kulanz, Abbrechen = bezahltes Redesign". Das fuehrt jeden in die Irre, der es zum ersten Mal sieht. Neu: Steht der bezahlte Redesign-Topf im Minus, waehrend Kulanz uebrig ist, erscheint "Minus mit Kulanz ausgleichen". Es bucht so viele Uebergaenge um, wie beide Seiten hergeben - die aeltesten zuerst, damit das Ergebnis wiederholbar ist. Unterm Strich aendert sich nichts, nur die Verteilung. Ebenfalls neu: Ein Redesign laesst sich ausnahmsweise aus dem CC-Topf bezahlen. Damit das Ausrichten an den Ketten so eine Entscheidung nicht beim naechsten Umsortieren wieder einebnet, tragen von Hand gewaehlte Buchungen jetzt einen Merker. Dabei aufgefallen: Die Tests wurden gar nicht typgeprueft - tsconfig schloss nur src ein. Sieben Aufrufe von bookKulanz hatten vertauschte Argumente und liefen trotzdem durch, weil JavaScript das hinnimmt; sie pruefte damit etwas anderes als gedacht. Tests haengen jetzt in der Typpruefung.
This commit is contained in:
@@ -162,7 +162,7 @@ export async function redesignCharacter(
|
||||
input: {
|
||||
name: string;
|
||||
loreText?: string | undefined;
|
||||
useKind: RedesignKind;
|
||||
useKind: JoinKind;
|
||||
note?: string | undefined;
|
||||
},
|
||||
ctx: ActionContext,
|
||||
@@ -172,9 +172,13 @@ export async function redesignCharacter(
|
||||
if (old.status !== CharacterStatus.ACTIVE) {
|
||||
throw conflict('NOT_ACTIVE', 'Nur ein aktiver CC kann redesignt werden');
|
||||
}
|
||||
if (!REDESIGN_KINDS.includes(input.useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ein Redesign kostet bezahltes oder Kulanz-Redesign-Guthaben');
|
||||
// Der Regelfall ist ein Redesign-Guthaben. Ausnahmsweise lässt sich auch
|
||||
// ein CC-Paket dafür einsetzen - dann gilt die Entscheidung als von Hand
|
||||
// getroffen und bleibt beim Umsortieren stehen.
|
||||
if (!JOIN_KINDS.includes(input.useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art für ein Redesign');
|
||||
}
|
||||
const ausnahme = input.useKind === CreditKind.CC;
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const predecessor = await tx.character.update({
|
||||
@@ -221,6 +225,7 @@ export async function redesignCharacter(
|
||||
characterId: created.id,
|
||||
note: input.note ?? `Redesign: ${old.name} -> ${created.name}`,
|
||||
createdById: ctx.actorId,
|
||||
...(ausnahme ? { fundingPinned: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -809,6 +814,8 @@ export async function changeFunding(
|
||||
createdById: ctx.actorId,
|
||||
// Bezahlt wurde damals, nicht heute.
|
||||
occurredAt: buchung.occurredAt,
|
||||
// Von Hand gewählt - das Ausrichten an den Ketten lässt es stehen.
|
||||
fundingPinned: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -879,6 +886,10 @@ async function buchungFolgtPosition(
|
||||
});
|
||||
if (!offen) return false;
|
||||
|
||||
// Wer die Bezahlart von Hand gewählt hat, meinte es so - etwa wenn ein
|
||||
// Redesign ausnahmsweise aus einem CC-Paket bezahlt wurde.
|
||||
if (offen.fundingPinned) return false;
|
||||
|
||||
const istCC = offen.kind === CreditKind.CC;
|
||||
if (istCC === sollCC) return false;
|
||||
|
||||
@@ -1053,6 +1064,102 @@ export async function rebuildFunding(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Begleicht ein Minus im Redesign-Topf mit vorhandener Kulanz.
|
||||
*
|
||||
* Kommt vor, wenn nachträglich klar wird, dass ein Teil der Redesigns
|
||||
* Kulanz war: Dann stehen dort Guthaben herum, während der bezahlte Topf im
|
||||
* Minus hängt. Statt einzeln Übergang für Übergang umzustellen, wird hier so
|
||||
* viel umgebucht, wie beide Seiten hergeben.
|
||||
*
|
||||
* Welche Übergänge es trifft, spielt inhaltlich keine Rolle - am Ende zählt,
|
||||
* wie viel woraus bezahlt wurde. Genommen werden die ältesten zuerst, damit
|
||||
* das Ergebnis wiederholbar ist und nicht vom Zufall abhängt.
|
||||
*/
|
||||
export async function settleWithKulanz(
|
||||
playerId: number,
|
||||
ctx: ActionContext,
|
||||
): Promise<{ umgebucht: number; balances: Balances }> {
|
||||
const stand = await getBalances(playerId);
|
||||
|
||||
const schulden = Math.max(0, -stand.REDESIGN_PAID);
|
||||
const kulanz = Math.max(0, stand.REDESIGN_KULANZ);
|
||||
const moeglich = Math.min(schulden, kulanz);
|
||||
|
||||
if (moeglich === 0) {
|
||||
throw conflict(
|
||||
'NOTHING_TO_SETTLE',
|
||||
schulden === 0
|
||||
? 'Im bezahlten Redesign-Topf steht kein Minus - hier gibt es nichts auszugleichen'
|
||||
: 'Es ist keine Kulanz übrig, mit der sich das Minus ausgleichen liesse',
|
||||
);
|
||||
}
|
||||
|
||||
// Die ältesten bezahlten Redesigns zuerst: wer nachträglich Kulanz
|
||||
// zugestanden bekommt, bekommt sie für das, was am längsten zurückliegt.
|
||||
const kandidaten = await prisma.creditLedger.findMany({
|
||||
where: {
|
||||
playerId,
|
||||
kind: CreditKind.REDESIGN_PAID,
|
||||
amount: { lt: 0 },
|
||||
source: CreditSource.CONSUME,
|
||||
reversedBy: null,
|
||||
},
|
||||
orderBy: [{ occurredAt: 'asc' }, { id: 'asc' }],
|
||||
take: moeglich,
|
||||
include: { character: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
if (kandidaten.length === 0) {
|
||||
throw conflict(
|
||||
'NOTHING_TO_SETTLE',
|
||||
'Es gibt keine bezahlten Redesigns, die sich auf Kulanz umstellen liessen',
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const buchung of kandidaten) {
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId,
|
||||
kind: buchung.kind,
|
||||
amount: -buchung.amount,
|
||||
source: CreditSource.REVERSAL,
|
||||
groupId: buchung.groupId,
|
||||
characterId: buchung.characterId,
|
||||
reversalOfId: buchung.id,
|
||||
note: `Auf Kulanz umgestellt - Storno: ${buchung.character?.name ?? ''}`.trim(),
|
||||
createdById: ctx.actorId,
|
||||
occurredAt: buchung.occurredAt,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.creditLedger.create({
|
||||
data: {
|
||||
playerId,
|
||||
kind: CreditKind.REDESIGN_KULANZ,
|
||||
amount: buchung.amount,
|
||||
source: CreditSource.CONSUME,
|
||||
groupId: randomUUID(),
|
||||
characterId: buchung.characterId,
|
||||
note: buchung.note ?? buchung.character?.name ?? null,
|
||||
createdById: ctx.actorId,
|
||||
occurredAt: buchung.occurredAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await recordAudit(tx, ctx, {
|
||||
action: 'credit.settled_with_kulanz',
|
||||
targetType: 'player',
|
||||
targetId: playerId,
|
||||
details: { umgebucht: kandidaten.length },
|
||||
});
|
||||
});
|
||||
|
||||
return { umgebucht: kandidaten.length, balances: await getBalances(playerId) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt einen CC innerhalb seiner Kette oder in eine andere.
|
||||
*
|
||||
@@ -1560,7 +1667,7 @@ export async function redesignMembership(
|
||||
input: {
|
||||
name: string;
|
||||
loreText?: string | undefined;
|
||||
useKind: RedesignKind;
|
||||
useKind: JoinKind;
|
||||
note?: string | undefined;
|
||||
},
|
||||
ctx: ActionContext,
|
||||
@@ -1570,8 +1677,8 @@ export async function redesignMembership(
|
||||
if (gruppe.status !== CharacterStatus.ACTIVE) {
|
||||
throw conflict('NOT_ACTIVE', 'Nur ein aktiver CC kann redesignt werden');
|
||||
}
|
||||
if (!REDESIGN_KINDS.includes(input.useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ein Redesign kostet bezahltes oder Kulanz-Redesign-Guthaben');
|
||||
if (!JOIN_KINDS.includes(input.useKind)) {
|
||||
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art für ein Redesign');
|
||||
}
|
||||
const name = input.name.trim();
|
||||
if (!name) throw badRequest('NO_NAME', 'Für den neuen CC wird ein Name gebraucht');
|
||||
@@ -1630,6 +1737,7 @@ export async function redesignMembership(
|
||||
characterId: erstellt.id,
|
||||
note: input.note ?? `Redesign aus "${gruppe.name}" heraus: ${erstellt.name}`,
|
||||
createdById: ctx.actorId,
|
||||
...(input.useKind === CreditKind.CC ? { fundingPinned: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user