Guthaben: "von wie vielen" anzeigen und Zugaben zuruecknehmen
In den Kaesten steht jetzt neben dem offenen Stand, wie viel jemals in den Topf kam: "0 von 3" liest sich anders als "0 von 0" - das eine heisst aufgebraucht, das andere nie etwas gehabt. Gezaehlt werden dabei nur echte Zugaenge: Die Ruecknahme eines Verbrauchs zaehlt dorthin, wo die stornierte Zeile zaehlte, sonst saehe ein zurueckgenommenes Redesign wie ein zusaetzliches Paket aus. Neben jedem Plus steht ein Minus, das die zuletzt von Hand eingetragene Zugabe dieser Art zurueckholt - fuer den vertippten oder doppelten Klick, ohne in der Historie nach der richtigen Zeile zu suchen. Zurueckgenommen wird ueber eine Gegenbuchung; bei einem CC-Paket gehen beide Zeilen mit. Aus einem Import gebuchte Pakete bleiben aussen vor: die gehoeren zu einer Bestellung und werden dort storniert, wo der Zusammenhang sichtbar ist. Die Rechte folgen dem Eintragen - Kulanz bleibt bei den Admins. Nebenbei zwei wacklige Tests festgezurrt: Die Testdatenbanken weichen auf den naechsten Port aus, wenn einer aus einem frueheren Lauf noch haengt, und der Sperr-Test des Abgleichs klopft nicht mehr bei GExtension an, sondern laeuft gegen eine tote Adresse.
This commit is contained in:
+25
-3
@@ -31,13 +31,35 @@ export async function startTestDatabase(port: number): Promise<TestDatabase> {
|
||||
await db.exec(readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
const server = new PGLiteSocketServer({ db, port, host: '127.0.0.1' });
|
||||
await server.start();
|
||||
// Die Testdateien laufen nebeneinander und bekommen feste Ports. Hängt aus
|
||||
// einem vorherigen Lauf noch einer fest, wäre der Start sonst ohne Grund
|
||||
// fehlgeschlagen - deshalb ein paar Versuche mit dem nächsten Port.
|
||||
let server: PGLiteSocketServer | null = null;
|
||||
let benutzt = port;
|
||||
|
||||
for (let versuch = 0; versuch < 20; versuch += 1) {
|
||||
const kandidat = new PGLiteSocketServer({ db, port: benutzt, host: '127.0.0.1' });
|
||||
try {
|
||||
await kandidat.start();
|
||||
server = kandidat;
|
||||
break;
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code !== 'EADDRINUSE') throw error;
|
||||
benutzt += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Kein freier Port ab ${port} für die Testdatenbank gefunden`);
|
||||
}
|
||||
|
||||
const belegterPort = benutzt;
|
||||
|
||||
return {
|
||||
// connection_limit=1, weil die Testinstanz immer nur eine Verbindung
|
||||
// gleichzeitig bedient. Echtes Postgres braucht die Einschränkung nicht.
|
||||
url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres?connection_limit=1`,
|
||||
url: `postgresql://postgres:postgres@127.0.0.1:${belegterPort}/postgres?connection_limit=1`,
|
||||
stop: async () => {
|
||||
await server.stop();
|
||||
await db.close();
|
||||
|
||||
@@ -385,6 +385,8 @@ describe('Guthaben über die API', () => {
|
||||
REDESIGN_PAID: 1,
|
||||
REDESIGN_KULANZ: 0,
|
||||
redesignTotal: 1,
|
||||
// Was offen ist, und wovon: "1 von 1".
|
||||
granted: { CC: 1, REDESIGN_PAID: 1, REDESIGN_KULANZ: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -314,3 +314,93 @@ describe('Korrekturen', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Von Hand eingetragene Guthaben zurücknehmen', () => {
|
||||
it('zählt, wie viel jemals in den Topf kam', async () => {
|
||||
const playerId = await freshPlayer('Zaehler');
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
await characters.createCharacter(playerId, { name: 'Verbraucht eines' }, ctx);
|
||||
|
||||
const stand = await credits.getBalances(playerId);
|
||||
assert.equal(stand.CC, 1, 'eines ist noch offen');
|
||||
assert.equal(stand.granted.CC, 2, 'zwei kamen jemals herein');
|
||||
assert.equal(stand.granted.REDESIGN_PAID, 2, 'jedes Paket bringt eines mit');
|
||||
});
|
||||
|
||||
it('zählt die Rücknahme eines Verbrauchs nicht als Zugang', async () => {
|
||||
// Sonst sähe ein storniertes Redesign wie ein zusätzliches Paket aus.
|
||||
const playerId = await freshPlayer('Stornierer');
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
const cc = await characters.createCharacter(playerId, { name: 'Wird zurückgenommen' }, ctx);
|
||||
await characters.revertCharacter(cc.character.id, 'Fehleintrag', ctx);
|
||||
|
||||
const stand = await credits.getBalances(playerId);
|
||||
assert.equal(stand.CC, 1, 'das Guthaben ist zurück');
|
||||
assert.equal(stand.granted.CC, 1, 'hereingekommen ist trotzdem nur eines');
|
||||
});
|
||||
|
||||
it('nimmt das zuletzt eingetragene CC-Paket zurück, beide Zeilen', async () => {
|
||||
const playerId = await freshPlayer('Vertipper');
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
await credits.bookCcPackage(playerId, ctx);
|
||||
|
||||
const { balances } = await credits.removeLastGrant(playerId, 'CC_PACKAGE', ctx);
|
||||
|
||||
assert.equal(balances.CC, 1);
|
||||
assert.equal(balances.REDESIGN_PAID, 1, 'das mitgelieferte Redesign geht mit zurück');
|
||||
assert.equal(balances.granted.CC, 1, 'und zählt auch nicht mehr als erhalten');
|
||||
});
|
||||
|
||||
it('nimmt eine Kulanz zurück', async () => {
|
||||
const playerId = await freshPlayer('Kulanzfall');
|
||||
await credits.bookKulanz(playerId, ctx, { note: 'War zu grosszügig' });
|
||||
|
||||
const { balances } = await credits.removeLastGrant(playerId, 'KULANZ', ctx);
|
||||
assert.equal(balances.REDESIGN_KULANZ, 0);
|
||||
assert.equal(balances.granted.REDESIGN_KULANZ, 0);
|
||||
});
|
||||
|
||||
it('meldet sich, wenn es nichts zurückzunehmen gibt', async () => {
|
||||
const playerId = await freshPlayer('Leerer Topf');
|
||||
|
||||
await assert.rejects(
|
||||
() => credits.removeLastGrant(playerId, 'CC_PACKAGE', ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_REMOVE',
|
||||
);
|
||||
});
|
||||
|
||||
it('lässt gebuchte Import-Pakete in Ruhe', async () => {
|
||||
// Ein Paket aus einer Bestellung gehört in die Historie, nicht unter
|
||||
// einen Knopf, der "das letzte weg" bedeutet.
|
||||
const playerId = await freshPlayer('Importiert');
|
||||
const importRecord = await db.prisma.import.create({
|
||||
data: { filename: 'shop.csv', status: 'COMMITTED', rowCount: 1 },
|
||||
});
|
||||
await db.prisma.creditLedger.create({
|
||||
data: {
|
||||
playerId,
|
||||
kind: 'CC',
|
||||
amount: 1,
|
||||
source: 'CC_PACKAGE',
|
||||
importId: importRecord.id,
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => credits.removeLastGrant(playerId, 'CC_PACKAGE', ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_REMOVE',
|
||||
);
|
||||
});
|
||||
|
||||
it('nimmt dieselbe Zugabe nicht zweimal zurück', async () => {
|
||||
const playerId = await freshPlayer('Doppelklicker');
|
||||
await credits.bookKulanz(playerId, ctx);
|
||||
|
||||
await credits.removeLastGrant(playerId, 'KULANZ', ctx);
|
||||
await assert.rejects(
|
||||
() => credits.removeLastGrant(playerId, 'KULANZ', ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'NOTHING_TO_REMOVE',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,10 @@ before(async () => {
|
||||
process.env.DATABASE_URL = database.url;
|
||||
process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000';
|
||||
process.env.NODE_ENV = 'test';
|
||||
// Der Sperr-Test stösst einen echten Abruf an. Er soll sofort scheitern
|
||||
// statt in ein Zeitlimit zu laufen - und vor allem nicht wirklich bei
|
||||
// GExtension anklopfen, nur weil hier Tests laufen.
|
||||
process.env.GEXTENSION_BASE_URL = 'http://127.0.0.1:1';
|
||||
|
||||
db = await import('../src/db.js');
|
||||
schedule = await import('../src/services/schedule.js');
|
||||
|
||||
Reference in New Issue
Block a user