Antraege durchsuchbar, Fortschritt sichtbar, Spieler an einem Stueck
Der Abgleich zeigt jetzt, was er tut. Gelesen wird zuerst von jedem Board die erste Seite - danach steht der Umfang fest, und der Balken zeigt einen ehrlichen Anteil statt einer Schaetzung. Ein Protokoll schreibt Seite fuer Seite mit. Beide Antragslisten lassen sich durchsuchen und wahlweise mit dem neuesten oder dem aeltesten Antrag zuerst anzeigen. Die Liste ohne zugeordneten Spieler blaettert seitenweise, statt bei fuenfhundert Eintraegen die ersten fuenfzig zu zeigen und den Rest zu verschweigen. Neu darueber: "Ersteller ohne Spieler" - dieselben Antraege, nach Ersteller zusammengefasst. Wer dreiundzwanzig Antraege gestellt hat, faellt dort auf, waehrend er zwischen fuenfhundert Einzelantraegen untergeht. Ein Klick legt den Spieler mit bereits eingetragenem Forum-Namen an. Ein Spieler laesst sich jetzt an einem Stueck abarbeiten, statt zwischen drei Seiten zu wechseln: - Seine Forums-Antraege stehen auf der Spielerseite, aeltester zuerst, jeder als Link ins Forum. Offene sind dort direkt entscheidbar, ueber denselben Weg wie auf der Antraege-Seite - danach stehen sie dort nicht mehr offen. - Pakete, die noch in einer Import-Vorschau liegen, erscheinen unter "Offene Pakete aus dem Abgleich" und lassen sich einzeln oder am Stueck buchen. Gebuchtes wird ueberall abgehakt, wo dieselbe Bestellung noch offen liegt, auch in einem zweiten Import - doppelt buchen ist ausgeschlossen. Wer ein Paket gar nicht buchen will, legt es weg, ohne das Konto zu beruehren. Die beiden Antrags-Dialoge liegen dafuer jetzt unter components/, weil beide Seiten sie brauchen.
This commit is contained in:
@@ -241,3 +241,145 @@ describe('Kollisionen beim Buchen', () => {
|
||||
assert.equal(balances.CC, 1, 'genau einmal gebucht, nicht doppelt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Einzelne Zeilen beim Spieler buchen', () => {
|
||||
// Jeder Fall bekommt eigene Bestellnummern: ein gebuchter Dedup-Anker gilt
|
||||
// dauerhaft und wuerde sonst den naechsten Fall stillschweigend leerlaufen
|
||||
// lassen. Die Spieler stehen fest - beim Weg ueber die Spielerseite geht es
|
||||
// genau darum, einen Bekannten abzuarbeiten.
|
||||
let eule = 0;
|
||||
let specht = 0;
|
||||
|
||||
function datei(praefix: string): Buffer {
|
||||
return Buffer.from(
|
||||
[
|
||||
'Bestellnummer;Steam-ID;Spielername;Produkt;Kaufdatum',
|
||||
`${praefix}-1;76561198000000011;Eule;CC-Paket Groß;2025-02-01`,
|
||||
`${praefix}-2;76561198000000011;Eule;Redesign Gutschein;2025-02-02`,
|
||||
`${praefix}-3;76561198000000012;Specht;CC-Paket Klein;2025-02-03`,
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
async function vorschau(praefix: string, name = `${praefix}.csv`) {
|
||||
const parsed = imports.parseUpload(name, datei(praefix));
|
||||
const id = await imports.createImport(name, parsed, ctx);
|
||||
await imports.applyMapping(id, MAPPING, ctx);
|
||||
return id;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
eule = (
|
||||
await db.prisma.player.create({
|
||||
data: { displayName: 'Eule', steamId: '76561198000000011' },
|
||||
})
|
||||
).id;
|
||||
specht = (
|
||||
await db.prisma.player.create({
|
||||
data: { displayName: 'Specht', steamId: '76561198000000012' },
|
||||
})
|
||||
).id;
|
||||
});
|
||||
|
||||
it('bucht nur die gewählte Zeile und lässt den Rest der Vorschau offen', async () => {
|
||||
const importId = await vorschau('EINZ');
|
||||
|
||||
const offen = await imports.listPendingRowsFor(eule);
|
||||
assert.equal(offen.length, 2, 'ein CC-Paket und ein Redesign-Gutschein');
|
||||
assert.equal(offen[0]?.productName, 'CC-Paket Groß', 'ältester Kauf zuerst');
|
||||
|
||||
const ergebnis = await imports.bookRows([offen[0]!.id], ctx);
|
||||
assert.equal(ergebnis.booked, 1);
|
||||
|
||||
// Ein CC-Paket schreibt zwei Zeilen - dieselbe Regel wie beim Import.
|
||||
const stand = await credits.getBalances(eule);
|
||||
assert.equal(stand.CC, 1);
|
||||
assert.equal(stand.REDESIGN_PAID, 1);
|
||||
|
||||
assert.equal(
|
||||
(await imports.listPendingRowsFor(eule)).length,
|
||||
1,
|
||||
'die gebuchte Zeile ist aus der Liste, die andere wartet weiter',
|
||||
);
|
||||
|
||||
const nachher = await db.prisma.import.findUniqueOrThrow({ where: { id: importId } });
|
||||
assert.equal(nachher.status, 'PENDING', 'der Import als Ganzes bleibt offen');
|
||||
|
||||
await imports.discardImport(importId, ctx);
|
||||
});
|
||||
|
||||
it('bucht dieselbe Bestellung nicht ein zweites Mal über den Import', async () => {
|
||||
const importId = await vorschau('ZWEI');
|
||||
|
||||
const offen = await imports.listPendingRowsFor(specht);
|
||||
assert.equal(offen.length, 1);
|
||||
await imports.bookRows([offen[0]!.id], ctx);
|
||||
|
||||
const vorher = await credits.getBalances(specht);
|
||||
|
||||
// Der Import wird danach ganz normal bestätigt - die eben gebuchte Zeile
|
||||
// steht dabei gar nicht mehr zur Buchung an, sie ist ja abgehakt.
|
||||
const ergebnis = await imports.commitImport(importId, ctx);
|
||||
assert.equal(ergebnis.booked, 2, 'nur die beiden Zeilen der Eule bleiben übrig');
|
||||
|
||||
assert.deepEqual(
|
||||
await credits.getBalances(specht),
|
||||
vorher,
|
||||
'sonst stünde das Paket doppelt im Konto',
|
||||
);
|
||||
});
|
||||
|
||||
it('hakt dieselbe Bestellung auch in einem zweiten offenen Import ab', async () => {
|
||||
// Zweimal dieselbe Datei eingelesen, etwa weil der Abgleich doppelt lief.
|
||||
const ersterImport = await vorschau('DREI', 'lauf-a.csv');
|
||||
const zweiterImport = await vorschau('DREI', 'lauf-b.csv');
|
||||
|
||||
const offen = await imports.listPendingRowsFor(eule);
|
||||
const redesigns = offen.filter((row) => row.externalId === 'DREI-2');
|
||||
assert.equal(redesigns.length, 2, 'dieselbe Bestellung liegt in beiden Vorschauen');
|
||||
|
||||
const vorher = await credits.getBalances(eule);
|
||||
await imports.bookRows([redesigns[0]!.id], ctx);
|
||||
|
||||
assert.equal(
|
||||
(await imports.listPendingRowsFor(eule)).filter((row) => row.externalId === 'DREI-2').length,
|
||||
0,
|
||||
'die Zwillingszeile im anderen Import ist mit abgehakt',
|
||||
);
|
||||
assert.equal(
|
||||
(await credits.getBalances(eule)).REDESIGN_PAID,
|
||||
vorher.REDESIGN_PAID + 1,
|
||||
'einmal gekauft, einmal gebucht',
|
||||
);
|
||||
|
||||
await imports.discardImport(ersterImport, ctx);
|
||||
await imports.discardImport(zweiterImport, ctx);
|
||||
});
|
||||
|
||||
it('weist eine leere Auswahl ab', async () => {
|
||||
await assert.rejects(
|
||||
() => imports.bookRows([], ctx),
|
||||
(error: Error & { code?: string }) => error.code === 'NOTHING_SELECTED',
|
||||
);
|
||||
});
|
||||
|
||||
it('nimmt Zeilen auch ohne Buchung aus der Vorschau', async () => {
|
||||
const importId = await vorschau('VIER');
|
||||
|
||||
const offen = await imports.listPendingRowsFor(specht);
|
||||
assert.equal(offen.length, 1);
|
||||
const vorher = await credits.getBalances(specht);
|
||||
|
||||
const ergebnis = await imports.dismissRows(
|
||||
offen.map((row) => row.id),
|
||||
ctx,
|
||||
);
|
||||
assert.equal(ergebnis.dismissed, 1);
|
||||
|
||||
assert.deepEqual(await imports.listPendingRowsFor(specht), []);
|
||||
assert.deepEqual(await credits.getBalances(specht), vorher, 'weggelegt ist nicht gebucht');
|
||||
|
||||
await imports.discardImport(importId, ctx);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user