Reaktivierung haengt nur noch am Redesign, nicht am Grund
Ein CC, der nicht redesignt wurde, existiert unveraendert weiter und kommt
deshalb immer gratis zurueck - egal ob der Spieler inaktiv war oder die
Einheit voruebergehend aus dem Spiel genommen wurde. Bisher kostete der Fall
OTHER ein Guthaben, was die Regel unnoetig verkompliziert hat.
Damit der zweite Fall brauchbar wird, haelt inactiveNote den Grund im Klartext
fest ("Einheit bis Maerz entfernt"); die Notiz steht an der CC-Karte und wird
beim Reaktivieren wieder geleert.
This commit is contained in:
@@ -92,6 +92,7 @@ CREATE TABLE "Character" (
|
||||
"loreText" TEXT,
|
||||
"status" "CharacterStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"inactiveReason" "InactiveReason",
|
||||
"inactiveNote" TEXT,
|
||||
"lineageId" TEXT NOT NULL,
|
||||
"predecessorId" INTEGER,
|
||||
"createdById" INTEGER,
|
||||
|
||||
@@ -150,6 +150,8 @@ model Character {
|
||||
loreText String?
|
||||
status CharacterStatus @default(ACTIVE)
|
||||
inactiveReason InactiveReason?
|
||||
/// Klartext zum Grund, etwa "Einheit bis Maerz entfernt". Nur zur Anzeige.
|
||||
inactiveNote String?
|
||||
|
||||
/// Alle CCs einer Redesign-Kette teilen sich die lineageId.
|
||||
lineageId String @default(uuid())
|
||||
|
||||
@@ -186,7 +186,11 @@ export async function deactivateCharacter(
|
||||
const character = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.character.update({
|
||||
where: { id: characterId },
|
||||
data: { status: CharacterStatus.INACTIVE, inactiveReason: reason },
|
||||
data: {
|
||||
status: CharacterStatus.INACTIVE,
|
||||
inactiveReason: reason,
|
||||
inactiveNote: note?.trim() || null,
|
||||
},
|
||||
});
|
||||
|
||||
await recordAudit(tx, ctx, {
|
||||
@@ -204,9 +208,10 @@ export async function deactivateCharacter(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gratis-Reaktivierung. Greift nur, wenn der CC ausschliesslich wegen
|
||||
* Spieler-Inaktivitaet vom Server geputzt wurde: er existiert unveraendert
|
||||
* weiter und kostet deshalb kein Guthaben.
|
||||
* Gratis-Reaktivierung. Moeglich, solange der CC nicht redesignt wurde. Der
|
||||
* Grund des Verschwindens spielt dabei keine Rolle - ob Spieler-Inaktivitaet
|
||||
* oder eine zeitweise entfernte Einheit: der CC existiert unveraendert weiter
|
||||
* und kostet deshalb kein Guthaben.
|
||||
*/
|
||||
export async function reactivateFree(
|
||||
characterId: number,
|
||||
@@ -217,17 +222,17 @@ export async function reactivateFree(
|
||||
if (existing.status === CharacterStatus.ACTIVE) {
|
||||
throw conflict('ALREADY_ACTIVE', 'Dieser CC ist bereits aktiv');
|
||||
}
|
||||
if (existing.inactiveReason !== InactiveReason.PLAYER_INACTIVITY) {
|
||||
if (existing.inactiveReason === InactiveReason.REWORKED) {
|
||||
throw conflict(
|
||||
'NOT_FREE',
|
||||
'Nur ein wegen Spieler-Inaktivitaet entfernter CC kommt gratis zurueck. Dieser CC braucht eine bezahlte Reaktivierung.',
|
||||
'Dieser CC wurde redesignt und existiert im Original nicht mehr. Seine Reaktivierung kostet ein Guthaben.',
|
||||
);
|
||||
}
|
||||
|
||||
const character = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.character.update({
|
||||
where: { id: characterId },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null, inactiveNote: null },
|
||||
});
|
||||
|
||||
await recordAudit(tx, ctx, {
|
||||
@@ -245,9 +250,9 @@ export async function reactivateFree(
|
||||
}
|
||||
|
||||
/**
|
||||
* Bezahlte Reaktivierung fuer CCs, die im Original nicht mehr existieren
|
||||
* (reworkt oder aus sonstigem Grund inaktiv). Kostet ein Redesign- oder
|
||||
* ein CC-Guthaben, der Nutzer entscheidet welches.
|
||||
* Bezahlte Reaktivierung, ausschliesslich fuer redesignte CCs: deren Original
|
||||
* existiert nicht mehr, es muss also neu erschaffen werden. Kostet ein
|
||||
* Redesign- oder ein CC-Guthaben, der Nutzer entscheidet welches.
|
||||
*/
|
||||
export async function reactivatePaid(
|
||||
characterId: number,
|
||||
@@ -260,10 +265,10 @@ export async function reactivatePaid(
|
||||
if (existing.status === CharacterStatus.ACTIVE) {
|
||||
throw conflict('ALREADY_ACTIVE', 'Dieser CC ist bereits aktiv');
|
||||
}
|
||||
if (existing.inactiveReason === InactiveReason.PLAYER_INACTIVITY) {
|
||||
if (existing.inactiveReason !== InactiveReason.REWORKED) {
|
||||
throw conflict(
|
||||
'IS_FREE',
|
||||
'Dieser CC kommt gratis zurueck, eine Guthaben-Buchung waere falsch',
|
||||
'Dieser CC existiert unveraendert weiter und kommt gratis zurueck. Eine Guthaben-Buchung waere hier falsch.',
|
||||
);
|
||||
}
|
||||
if (!REVIVE_KINDS.includes(useKind)) {
|
||||
@@ -273,7 +278,7 @@ export async function reactivatePaid(
|
||||
const character = await prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.character.update({
|
||||
where: { id: characterId },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null, inactiveNote: null },
|
||||
});
|
||||
|
||||
await tx.creditLedger.create({
|
||||
@@ -389,7 +394,7 @@ export async function revertCharacter(
|
||||
if (restorePredecessor && predecessor) {
|
||||
await tx.character.update({
|
||||
where: { id: predecessor.id },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null },
|
||||
data: { status: CharacterStatus.ACTIVE, inactiveReason: null, inactiveNote: null },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -178,16 +178,46 @@ describe('Reaktivierung', () => {
|
||||
assert.equal(result.balances.CC, before.CC - 1);
|
||||
});
|
||||
|
||||
it('eine bezahlte Reaktivierung wird bei Gratis-Faellen verweigert', async () => {
|
||||
it('auch ein aus anderem Grund entfernter CC kommt gratis zurueck', async () => {
|
||||
const playerId = await freshPlayer('Einheit-weg');
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
const created = await characters.createCharacter(playerId, { name: 'MTF-Operativer' }, ctx);
|
||||
|
||||
await characters.deactivateCharacter(
|
||||
created.character.id,
|
||||
'OTHER',
|
||||
ctx,
|
||||
'Einheit bis Maerz entfernt',
|
||||
);
|
||||
|
||||
const deactivated = await db.prisma.character.findUniqueOrThrow({
|
||||
where: { id: created.character.id },
|
||||
});
|
||||
assert.equal(deactivated.inactiveNote, 'Einheit bis Maerz entfernt', 'der Grund bleibt lesbar');
|
||||
|
||||
const before = await credits.getBalances(playerId);
|
||||
const result = await characters.reactivateFree(created.character.id, ctx);
|
||||
|
||||
assert.equal(result.character.status, 'ACTIVE');
|
||||
assert.equal(result.character.inactiveNote, null, 'die Notiz wird beim Zurueckholen geloescht');
|
||||
assert.deepEqual(result.balances, before, 'kein Guthaben wurde bewegt');
|
||||
});
|
||||
|
||||
it('eine bezahlte Reaktivierung wird bei jedem Gratis-Fall verweigert', async () => {
|
||||
const playerId = await freshPlayer('Gratis-Fall');
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
const created = await characters.createCharacter(playerId, { name: 'Geputzt' }, ctx);
|
||||
await characters.deactivateCharacter(created.character.id, 'PLAYER_INACTIVITY', ctx);
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
|
||||
await assert.rejects(
|
||||
() => characters.reactivatePaid(created.character.id, 'CC', ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'IS_FREE',
|
||||
);
|
||||
for (const reason of ['PLAYER_INACTIVITY', 'OTHER'] as const) {
|
||||
const created = await characters.createCharacter(playerId, { name: `CC-${reason}` }, ctx);
|
||||
await characters.deactivateCharacter(created.character.id, reason, ctx);
|
||||
|
||||
await assert.rejects(
|
||||
() => characters.reactivatePaid(created.character.id, 'CC', ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'IS_FREE',
|
||||
`Grund ${reason} darf nichts kosten`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user