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:
@@ -68,6 +68,11 @@ du mitten in eine Kette schiebst, ist von da an ein Redesign, und das CC-Guthabe
|
|||||||
Ziehst du einen Eintrag an den Anfang oder löst ihn heraus, belegt er einen eigenen Platz und
|
Ziehst du einen Eintrag an den Anfang oder löst ihn heraus, belegt er einen eigenen Platz und
|
||||||
kostet wieder ein CC-Paket.
|
kostet wieder ein CC-Paket.
|
||||||
|
|
||||||
|
Eine Ausnahme gibt es: **Hat die Kette eine Herkunft außerhalb**, ist auch ihr unterstes Glied
|
||||||
|
kein Kauf. Wer aus einem fremden Gruppen-CC herausredesignt, beginnt zwar eine eigene Kette -
|
||||||
|
bezahlt hat er sie trotzdem mit einem Redesign. Sonst stünde derselbe Platz zweimal im Konto:
|
||||||
|
einmal für den Beitritt, einmal für den eigenen CC.
|
||||||
|
|
||||||
Umgebucht wird über eine Gegenbuchung, nicht durch Überschreiben - in der Historie bleibt
|
Umgebucht wird über eine Gegenbuchung, nicht durch Überschreiben - in der Historie bleibt
|
||||||
sichtbar, was passiert ist. Reicht das Guthaben nicht, geht das Konto ins Minus und zeigt das an.
|
sichtbar, was passiert ist. Reicht das Guthaben nicht, geht das Konto ins Minus und zeigt das an.
|
||||||
Zwischen bezahltem Redesign und Kulanz wird dabei **nicht** gewechselt: Das ist eine
|
Zwischen bezahltem Redesign und Kulanz wird dabei **nicht** gewechselt: Das ist eine
|
||||||
@@ -81,9 +86,9 @@ woanders steht. Ein Platz entsteht dabei nicht aus dem Nichts: Wird ein CC dadur
|
|||||||
muss er gedeckt sein wie jeder andere.
|
muss er gedeckt sein wie jeder andere.
|
||||||
|
|
||||||
Über *Bearbeiten* lassen sich Name, Beschreibung und der verknüpfte Forums-Antrag nachtragen -
|
Über *Bearbeiten* lassen sich Name, Beschreibung und der verknüpfte Forums-Antrag nachtragen -
|
||||||
die Thread-Nummer genügt, die volle Adresse geht auch. Über *Bezahlart* wird umgestellt, ob ein
|
die Thread-Nummer genügt, die volle Adresse geht auch. Über *Bezahlart* wird umgestellt, woraus ein Eintrag
|
||||||
Redesign aus einem gekauften Paket oder aus Kulanz kam; umgebucht wird über eine Gegenbuchung,
|
bezahlt wurde - aus einem CC-Paket, einem bezahlten Redesign oder aus Kulanz; umgebucht wird über
|
||||||
in der Historie bleibt beides sichtbar.
|
eine Gegenbuchung, in der Historie bleibt beides sichtbar.
|
||||||
|
|
||||||
Bei einem **Gruppen-CC** gilt das für die eigene Buchung: Wer ihn in seine Kette einreiht, stellt
|
Bei einem **Gruppen-CC** gilt das für die eigene Buchung: Wer ihn in seine Kette einreiht, stellt
|
||||||
damit seinen eigenen Beitritt um - die Buchung des Erstellers bleibt unberührt, für den ist es
|
damit seinen eigenen Beitritt um - die Buchung des Erstellers bleibt unberührt, für den ist es
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
removeLink,
|
removeLink,
|
||||||
moveInChain,
|
moveInChain,
|
||||||
detachFromChain,
|
detachFromChain,
|
||||||
changeRedesignKind,
|
changeFunding,
|
||||||
addMemberToChain,
|
addMemberToChain,
|
||||||
} from '../services/characters.js';
|
} from '../services/characters.js';
|
||||||
import { parseThreadId } from '../lib/forum.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
|
* Nachträglich umstellen, woraus ein CC bezahlt wurde - aus einem Paket,
|
||||||
* wird im Kontobuch, nicht überschrieben.
|
* 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 { id } = parseWith(idParam, request.params);
|
||||||
const body = parseWith(
|
const body = parseWith(
|
||||||
z.object({
|
z.object({
|
||||||
useKind: z.enum([CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
|
useKind: z.enum([CreditKind.CC, CreditKind.REDESIGN_PAID, CreditKind.REDESIGN_KULANZ]),
|
||||||
}),
|
}),
|
||||||
request.body,
|
request.body,
|
||||||
);
|
);
|
||||||
return changeRedesignKind(id, body.useKind, contextOf(request));
|
return changeFunding(id, body.useKind, contextOf(request));
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Ketten ---
|
// --- 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
|
* Ob etwas aus einem gekauften Paket, einem bezahlten Redesign oder aus
|
||||||
* manchmal erst später heraus - etwa wenn eine Absprache auftaucht, von der
|
* Kulanz kam, stellt sich manchmal erst später heraus - wenn eine Absprache
|
||||||
* beim Eintragen niemand wusste. Umgebucht wird nicht durch Überschreiben,
|
* auftaucht, von der beim Eintragen niemand wusste, oder wenn sich zeigt,
|
||||||
* sondern wie jede Korrektur im Kontobuch: die alte Zeile wird
|
* dass ein Eintrag gar nicht der Anfang einer Kette war.
|
||||||
* gegengebucht, die neue kommt dazu. So bleibt sichtbar, dass hier etwas
|
*
|
||||||
* geändert wurde, und wann.
|
* 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,
|
characterId: number,
|
||||||
useKind: RedesignKind,
|
useKind: JoinKind,
|
||||||
ctx: ActionContext,
|
ctx: ActionContext,
|
||||||
): Promise<CharacterResult> {
|
): Promise<CharacterResult> {
|
||||||
const character = await loadCharacter(characterId);
|
const character = await loadCharacter(characterId);
|
||||||
|
|
||||||
if (!REDESIGN_KINDS.includes(useKind)) {
|
if (!JOIN_KINDS.includes(useKind)) {
|
||||||
throw badRequest('INVALID_KIND', 'Ein Redesign kostet bezahltes oder Kulanz-Redesign-Guthaben');
|
throw badRequest('INVALID_KIND', 'Ungültige Guthaben-Art');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Die Buchung, mit der dieser CC bezahlt wurde. Stornierte zählen nicht
|
// 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({
|
const buchung = await prisma.creditLedger.findFirst({
|
||||||
where: {
|
where: {
|
||||||
characterId,
|
characterId,
|
||||||
|
playerId: character.playerId,
|
||||||
source: CreditSource.CONSUME,
|
source: CreditSource.CONSUME,
|
||||||
kind: { in: [...REDESIGN_KINDS] },
|
|
||||||
reversedBy: null,
|
reversedBy: null,
|
||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
});
|
});
|
||||||
if (!buchung) {
|
if (!buchung) {
|
||||||
throw conflict(
|
throw conflict(
|
||||||
'NO_REDESIGN_BOOKING',
|
'NO_BOOKING',
|
||||||
'Für diesen CC ist kein offenes Redesign gebucht - hier gibt es nichts umzustellen',
|
'Für diesen CC ist nichts gebucht - hier gibt es nichts umzustellen',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (buchung.kind === useKind) {
|
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 =
|
const grund =
|
||||||
useKind === CreditKind.REDESIGN_KULANZ
|
useKind === CreditKind.CC
|
||||||
? 'Umgestellt auf Kulanz'
|
? 'Umgestellt auf CC-Paket'
|
||||||
: 'Umgestellt auf bezahltes Redesign';
|
: useKind === CreditKind.REDESIGN_KULANZ
|
||||||
|
? 'Umgestellt auf Kulanz'
|
||||||
|
: 'Umgestellt auf bezahltes Redesign';
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.creditLedger.create({
|
await tx.creditLedger.create({
|
||||||
@@ -809,7 +813,7 @@ export async function changeRedesignKind(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await recordAudit(tx, ctx, {
|
await recordAudit(tx, ctx, {
|
||||||
action: 'character.redesign_kind_changed',
|
action: 'character.funding_changed',
|
||||||
targetType: 'character',
|
targetType: 'character',
|
||||||
targetId: characterId,
|
targetId: characterId,
|
||||||
details: { name: character.name, from: buchung.kind, to: useKind },
|
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.
|
// Die Kette gehört dem, der unten steht: bei ihm fing sie an.
|
||||||
const kettenBesitzer = glieder[0]?.playerId ?? null;
|
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;
|
let vorheriges: number | null = null;
|
||||||
for (const [index, glied] of glieder.entries()) {
|
for (const [index, glied] of glieder.entries()) {
|
||||||
const letztes = index === glieder.length - 1;
|
const letztes = index === glieder.length - 1;
|
||||||
|
const erstes = index === 0;
|
||||||
const daten: Prisma.CharacterUpdateInput = {};
|
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) {
|
if (kettenBesitzer !== null) {
|
||||||
await buchungFolgtPosition(tx, glied, kettenBesitzer, index === 0, actorId);
|
await buchungFolgtPosition(tx, glied, kettenBesitzer, erstes && !hatHerkunft, actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (glied.predecessorId !== vorheriges) {
|
if (erstes) {
|
||||||
daten.predecessor = vorheriges === null ? { disconnect: true } : { connect: { id: vorheriges } };
|
// 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
|
// Was einen Nachfolger hat, wurde durch ihn ersetzt - anders ist eine
|
||||||
|
|||||||
+102
-8
@@ -448,7 +448,7 @@ describe('Redesign nachträglich umbuchen', () => {
|
|||||||
assert.equal(zwei.balances.REDESIGN_PAID, 0, 'das bezahlte ist verbraucht');
|
assert.equal(zwei.balances.REDESIGN_PAID, 0, 'das bezahlte ist verbraucht');
|
||||||
|
|
||||||
await credits.bookKulanz(spieler, 1, 'Absprache aufgetaucht', ctx);
|
await credits.bookKulanz(spieler, 1, 'Absprache aufgetaucht', ctx);
|
||||||
const danach = await characters.changeRedesignKind(zwei.character.id, 'REDESIGN_KULANZ', ctx);
|
const danach = await characters.changeFunding(zwei.character.id, 'REDESIGN_KULANZ', ctx);
|
||||||
|
|
||||||
assert.equal(danach.balances.REDESIGN_PAID, 1, 'das bezahlte ist wieder frei');
|
assert.equal(danach.balances.REDESIGN_PAID, 1, 'das bezahlte ist wieder frei');
|
||||||
assert.equal(danach.balances.REDESIGN_KULANZ, 0, 'dafür ist die Kulanz verbraucht');
|
assert.equal(danach.balances.REDESIGN_KULANZ, 0, 'dafür ist die Kulanz verbraucht');
|
||||||
@@ -473,7 +473,7 @@ describe('Redesign nachträglich umbuchen', () => {
|
|||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
const danach = await characters.changeRedesignKind(zwei.character.id, 'REDESIGN_PAID', ctx);
|
const danach = await characters.changeFunding(zwei.character.id, 'REDESIGN_PAID', ctx);
|
||||||
assert.equal(danach.balances.REDESIGN_KULANZ, 1);
|
assert.equal(danach.balances.REDESIGN_KULANZ, 1);
|
||||||
assert.equal(danach.balances.REDESIGN_PAID, 0, 'das aus dem CC-Paket ist jetzt dran');
|
assert.equal(danach.balances.REDESIGN_PAID, 0, 'das aus dem CC-Paket ist jetzt dran');
|
||||||
});
|
});
|
||||||
@@ -488,19 +488,26 @@ describe('Redesign nachträglich umbuchen', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => characters.changeRedesignKind(zwei.character.id, 'REDESIGN_PAID', ctx),
|
() => characters.changeFunding(zwei.character.id, 'REDESIGN_PAID', ctx),
|
||||||
(error: Error & { code?: string }) => error.code === 'SAME_KIND',
|
(error: Error & { code?: string }) => error.code === 'SAME_KIND',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('weist einen CC ohne Redesign-Buchung ab', async () => {
|
it('stellt einen gekauften CC auf Redesign um', async () => {
|
||||||
const spieler = await spielerMitPaketen('Ohne Redesign', 1);
|
// Genau der Korrekturfall: ein Eintrag war gar nicht der Anfang einer
|
||||||
|
// Kette, sondern ging aus einem anderen hervor.
|
||||||
|
const spieler = await spielerMitPaketen('Umsteller', 1);
|
||||||
const eins = await characters.createCharacter(spieler, { name: 'Direkt aus dem Paket' }, ctx);
|
const eins = await characters.createCharacter(spieler, { name: 'Direkt aus dem Paket' }, ctx);
|
||||||
|
await credits.bookRedesignPackage(spieler, ctx);
|
||||||
|
|
||||||
|
const danach = await characters.changeFunding(eins.character.id, 'REDESIGN_PAID', ctx);
|
||||||
|
assert.equal(danach.balances.CC, 1, 'das CC-Guthaben kommt zurück');
|
||||||
|
assert.equal(danach.balances.REDESIGN_PAID, 1, 'dafür geht ein Redesign weg');
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => characters.changeRedesignKind(eins.character.id, 'REDESIGN_KULANZ', ctx),
|
() => characters.changeFunding(eins.character.id, 'REDESIGN_PAID', ctx),
|
||||||
(error: Error & { code?: string }) => error.code === 'NO_REDESIGN_BOOKING',
|
(error: Error & { code?: string }) => error.code === 'SAME_KIND',
|
||||||
'ein CC aus dem Paket ist kein Redesign',
|
'ein CC aus dem Paket ist schon als CC gebucht',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -718,3 +725,90 @@ describe('Die Stelle in der Kette bestimmt, was ein Eintrag kostet', () => {
|
|||||||
assert.deepEqual(await credits.getBalances(spieler), vorher, 'nichts gebucht, nichts geändert');
|
assert.deepEqual(await credits.getBalances(spieler), vorher, 'nichts gebucht, nichts geändert');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Eine Kette mit Vorgeschichte ausserhalb', () => {
|
||||||
|
/**
|
||||||
|
* Der Fall aus dem Betrieb: Ein Spieler tritt einem fremden Gruppen-CC
|
||||||
|
* bei, steigt später per Redesign aus und führt seinen eigenen CC weiter.
|
||||||
|
* Dessen Kette beginnt nicht bei null - sie hat eine Herkunft, die
|
||||||
|
* woanders liegt.
|
||||||
|
*/
|
||||||
|
async function ausgestiegen() {
|
||||||
|
const dargon = await spielerMitPaketen('Aussteiger', 3);
|
||||||
|
const wirt = await spielerMitPaketen('Gruppen-Wirt', 1);
|
||||||
|
|
||||||
|
await characters.createCharacter(dargon, { name: 'Platz eins' }, ctx);
|
||||||
|
await characters.createCharacter(dargon, { name: 'Platz zwei' }, ctx);
|
||||||
|
|
||||||
|
const fremd = await characters.createCharacter(wirt, { name: 'Fremde Gruppe' }, ctx);
|
||||||
|
await characters.addMember(fremd.character.id, dargon, { useKind: 'CC' }, ctx);
|
||||||
|
|
||||||
|
await credits.bookRedesignPackage(dargon, ctx);
|
||||||
|
const eigenes = await characters.redesignMembership(
|
||||||
|
fremd.character.id,
|
||||||
|
dargon,
|
||||||
|
{ name: 'Eigener Strang', useKind: 'REDESIGN_PAID' },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
await credits.bookRedesignPackage(dargon, ctx);
|
||||||
|
const weiter = await characters.redesignCharacter(
|
||||||
|
eigenes.character.id,
|
||||||
|
{ name: 'Und weiter', useKind: 'REDESIGN_PAID' },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { dargon, eigenes: eigenes.character, weiter: weiter.character };
|
||||||
|
}
|
||||||
|
|
||||||
|
it('zählt drei Plätze und drei verbrauchte Pakete', async () => {
|
||||||
|
const { dargon } = await ausgestiegen();
|
||||||
|
|
||||||
|
const stand = await credits.getBalances(dargon);
|
||||||
|
assert.equal(stand.granted.CC, 3, 'drei Pakete gekauft');
|
||||||
|
assert.equal(stand.CC, 0, 'drei Plätze belegt, keiner zu viel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bucht das erste Glied einer hergeleiteten Kette nicht auf CC um', async () => {
|
||||||
|
// Es ist zwar das unterste seiner Kette, aber kein Kauf: es ging aus dem
|
||||||
|
// Gruppen-CC hervor. Wer das übersieht, bucht einen Platz doppelt.
|
||||||
|
const { dargon, eigenes, weiter } = await ausgestiegen();
|
||||||
|
const vorher = await credits.getBalances(dargon);
|
||||||
|
|
||||||
|
// Ein Verschieben innerhalb der Kette ordnet neu - und darf dabei nichts
|
||||||
|
// erfinden.
|
||||||
|
await characters.moveInChain(
|
||||||
|
weiter.id,
|
||||||
|
{ lineageId: eigenes.lineageId, afterId: eigenes.id, forPlayerId: dargon },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await credits.getBalances(dargon),
|
||||||
|
vorher,
|
||||||
|
'am Konto ändert sich dabei nichts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const buchung = await db.prisma.creditLedger.findFirst({
|
||||||
|
where: { characterId: eigenes.id, source: 'CONSUME', reversedBy: null },
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
});
|
||||||
|
assert.equal(buchung?.kind, 'REDESIGN_PAID', 'es bleibt ein Redesign');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('behält den Verweis auf die Herkunft', async () => {
|
||||||
|
const { dargon, eigenes, weiter } = await ausgestiegen();
|
||||||
|
|
||||||
|
await characters.moveInChain(
|
||||||
|
weiter.id,
|
||||||
|
{ lineageId: eigenes.lineageId, afterId: eigenes.id, forPlayerId: dargon },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
const danach = await db.prisma.character.findUniqueOrThrow({ where: { id: eigenes.id } });
|
||||||
|
assert.ok(
|
||||||
|
danach.predecessorId,
|
||||||
|
'sonst wäre nicht mehr zu sehen, woher die Kette kommt',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -527,16 +527,14 @@ function ChainEntry({
|
|||||||
<button type="button" className="btn btn-tiny btn-ghost" onClick={() => onAction('edit', character)}>
|
<button type="button" className="btn btn-tiny btn-ghost" onClick={() => onAction('edit', character)}>
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</button>
|
</button>
|
||||||
{character.predecessorId !== null && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="btn btn-tiny btn-ghost"
|
||||||
className="btn btn-tiny btn-ghost"
|
title="Aus einem CC-Paket, einem bezahlten Redesign oder aus Kulanz? Lässt sich nachträglich umbuchen."
|
||||||
title="Bezahltes Redesign oder Kulanz? Lässt sich nachträglich umbuchen."
|
onClick={() => onAction('redesignKind', character)}
|
||||||
onClick={() => onAction('redesignKind', character)}
|
>
|
||||||
>
|
Bezahlart
|
||||||
Bezahlart
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button type="button" className="btn btn-tiny btn-ghost" onClick={() => onAction('links', character)}>
|
<button type="button" className="btn btn-tiny btn-ghost" onClick={() => onAction('links', character)}>
|
||||||
Verweise
|
Verweise
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ export default function PlayerDetail() {
|
|||||||
onSubmit={(useKind) =>
|
onSubmit={(useKind) =>
|
||||||
void run(
|
void run(
|
||||||
() =>
|
() =>
|
||||||
api(`/api/characters/${dialog.character.id}/redesign-kind`, {
|
api(`/api/characters/${dialog.character.id}/funding`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { useKind },
|
body: { useKind },
|
||||||
}),
|
}),
|
||||||
@@ -1749,7 +1749,7 @@ function ProblemModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stellt um, woraus ein Redesign bezahlt wurde.
|
* Stellt um, woraus ein CC bezahlt wurde.
|
||||||
*
|
*
|
||||||
* Umgebucht wird im Kontobuch: die alte Zeile wird gegengebucht, die neue
|
* Umgebucht wird im Kontobuch: die alte Zeile wird gegengebucht, die neue
|
||||||
* kommt dazu. Überschrieben wird nichts - in der Historie bleibt sichtbar,
|
* kommt dazu. Überschrieben wird nichts - in der Historie bleibt sichtbar,
|
||||||
@@ -1763,7 +1763,7 @@ function RedesignKindModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
character: Character;
|
character: Character;
|
||||||
balances: { REDESIGN_PAID: number; REDESIGN_KULANZ: number };
|
balances: { CC: number; REDESIGN_PAID: number; REDESIGN_KULANZ: number };
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (useKind: CreditKind) => void;
|
onSubmit: (useKind: CreditKind) => void;
|
||||||
@@ -1773,14 +1773,15 @@ function RedesignKindModal({
|
|||||||
return (
|
return (
|
||||||
<Modal title={`Bezahlart von "${character.name}"`} onClose={onClose}>
|
<Modal title={`Bezahlart von "${character.name}"`} onClose={onClose}>
|
||||||
<Notice kind="info">
|
<Notice kind="info">
|
||||||
Ob ein Redesign aus einem gekauften Paket oder aus Kulanz kam, stellt sich manchmal erst
|
Woraus ein CC bezahlt wurde, stellt sich manchmal erst später heraus - etwa wenn eine
|
||||||
später heraus. Umgebucht wird über eine Gegenbuchung - in der Historie bleibt beides
|
Absprache auftaucht oder sich zeigt, dass ein Eintrag gar nicht der Anfang einer Kette
|
||||||
sichtbar.
|
war. Umgebucht wird über eine Gegenbuchung; in der Historie bleibt beides sichtbar.
|
||||||
</Notice>
|
</Notice>
|
||||||
|
|
||||||
<div className="radio-group">
|
<div className="radio-group">
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
|
['CC', 'CC-Paket (eigener Platz)', balances.CC],
|
||||||
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
['REDESIGN_KULANZ', 'Kulanz-Redesign', balances.REDESIGN_KULANZ],
|
||||||
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
['REDESIGN_PAID', 'Bezahltes Redesign', balances.REDESIGN_PAID],
|
||||||
] as const
|
] as const
|
||||||
|
|||||||
Reference in New Issue
Block a user