import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { startTestDatabase, type TestDatabase } from './harness.js'; type Imports = typeof import('../src/services/imports.js'); type Credits = typeof import('../src/services/credits.js'); type Db = typeof import('../src/db.js'); let database: TestDatabase; let imports: Imports; let credits: Credits; let db: Db; let ctx: { actorId: number; actorName: string }; const CSV = [ 'Bestellnummer;Steam-ID;Spielername;Produkt;Kaufdatum', 'A-1001;76561198000000001;Rabe;CC-Paket Gross;2025-01-15', 'A-1002;76561198000000002;Fuchs;Redesign Gutschein;2025-01-16', 'A-1003;76561198000000001;Rabe;Spielerfarbe Blau;2025-01-17', 'A-1004;76561198000000003;Dachs;CC-Paket Klein;2025-01-18', ].join('\n'); const MAPPING = { playerKey: 'Steam-ID', playerName: 'Spielername', product: 'Produkt', orderId: 'Bestellnummer', purchasedAt: 'Kaufdatum', createMissingPlayers: true, }; before(async () => { database = await startTestDatabase(54332); process.env.DATABASE_URL = database.url; process.env.SESSION_SECRET = 'test-secret-that-is-long-enough-000000'; process.env.NODE_ENV = 'test'; db = await import('../src/db.js'); imports = await import('../src/services/imports.js'); credits = await import('../src/services/credits.js'); const user = await db.prisma.user.create({ data: { username: 'importeur', passwordHash: 'x', role: 'ADMIN' }, }); ctx = { actorId: user.id, actorName: user.username }; await db.prisma.productMapping.createMany({ data: [ { matchPattern: 'cc-paket', target: 'CC_PACKAGE', priority: 10 }, { matchPattern: 'redesign', target: 'REDESIGN_PACKAGE', priority: 20 }, { matchPattern: 'spielerfarbe', target: 'IGNORE', priority: 30 }, ], }); }); after(async () => { await db.prisma.$disconnect(); await database.stop(); }); describe('Datei einlesen', () => { it('erkennt Semikolon als Trennzeichen', () => { const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8')); assert.equal(parsed.rows.length, 4); assert.deepEqual(parsed.columns, [ 'Bestellnummer', 'Steam-ID', 'Spielername', 'Produkt', 'Kaufdatum', ]); }); it('liest auch JSON-Listen', () => { const json = JSON.stringify([{ order: 'B-1', product: 'CC-Paket', user: '765' }]); const parsed = imports.parseUpload('export.json', Buffer.from(json, 'utf8')); assert.equal(parsed.rows.length, 1); assert.equal(parsed.rows[0]?.product, 'CC-Paket'); }); it('weist eine leere Datei ab', () => { assert.throws(() => imports.parseUpload('leer.csv', Buffer.from('', 'utf8'))); }); }); describe('Vorschau und Buchung', () => { it('ordnet jede Zeile ihrem Plan zu', async () => { const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8')); const importId = await imports.createImport('export.csv', parsed, ctx); const summary = await imports.applyMapping(importId, MAPPING, ctx); assert.equal(summary.CREATE_PLAYER_AND_BOOK, 3, 'drei neue Spieler mit gueltigem Produkt'); assert.equal(summary.SKIP_IGNORED, 1, 'die Spielerfarbe wird ignoriert'); assert.equal(summary.SKIP_DUPLICATE, 0); }); it('bucht CC-Pakete mit beiden Guthaben und legt Spieler an', async () => { const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8')); const importId = await imports.createImport('lauf1.csv', parsed, ctx); await imports.applyMapping(importId, MAPPING, ctx); const result = await imports.commitImport(importId, ctx); assert.equal(result.booked, 3); assert.equal(result.createdPlayers, 3); const rabe = await db.prisma.player.findUniqueOrThrow({ where: { steamId: '76561198000000001' }, }); const balances = await credits.getBalances(rabe.id); assert.equal(balances.CC, 1, 'CC-Paket gibt einen CC'); assert.equal(balances.REDESIGN_PAID, 1, 'und das inklusive Redesign'); const fuchs = await db.prisma.player.findUniqueOrThrow({ where: { steamId: '76561198000000002' }, }); const fuchsBalances = await credits.getBalances(fuchs.id); assert.equal(fuchsBalances.CC, 0, 'ein Redesign-Paket gibt keinen CC'); assert.equal(fuchsBalances.REDESIGN_PAID, 1); }); it('bucht dieselbe Bestell-ID kein zweites Mal', async () => { const parsed = imports.parseUpload('export.csv', Buffer.from(CSV, 'utf8')); const importId = await imports.createImport('lauf2.csv', parsed, ctx); const summary = await imports.applyMapping(importId, MAPPING, ctx); assert.equal(summary.SKIP_DUPLICATE, 3, 'alle bereits gebuchten Bestellungen'); assert.equal(summary.BOOK, 0); assert.equal(summary.CREATE_PLAYER_AND_BOOK, 0); await assert.rejects( () => imports.commitImport(importId, ctx), (error: Error & { code?: string }) => error.code === 'NOTHING_TO_BOOK', ); }); it('erkennt Dubletten auch innerhalb einer Datei', async () => { const doubled = [ 'Bestellnummer;Steam-ID;Produkt', 'C-1;76561198000000009;CC-Paket', 'C-1;76561198000000009;CC-Paket', ].join('\n'); const parsed = imports.parseUpload('doppelt.csv', Buffer.from(doubled, 'utf8')); const importId = await imports.createImport('doppelt.csv', parsed, ctx); const summary = await imports.applyMapping( importId, { playerKey: 'Steam-ID', product: 'Produkt', orderId: 'Bestellnummer', createMissingPlayers: true, }, ctx, ); assert.equal(summary.CREATE_PLAYER_AND_BOOK, 1); assert.equal(summary.SKIP_DUPLICATE, 1); }); it('ueberspringt unbekannte Spieler, wenn sie nicht angelegt werden sollen', async () => { const csv = ['Bestellnummer;Steam-ID;Produkt', 'D-1;76561198000009999;CC-Paket'].join('\n'); const parsed = imports.parseUpload('fremd.csv', Buffer.from(csv, 'utf8')); const importId = await imports.createImport('fremd.csv', parsed, ctx); const summary = await imports.applyMapping( importId, { playerKey: 'Steam-ID', product: 'Produkt', orderId: 'Bestellnummer', createMissingPlayers: false, }, ctx, ); assert.equal(summary.SKIP_NO_PLAYER, 1); }); it('ein abgeschlossener Import laesst sich nicht erneut buchen', async () => { const csv = ['Bestellnummer;Steam-ID;Produkt', 'E-1;76561198000000001;CC-Paket'].join('\n'); const parsed = imports.parseUpload('nochmal.csv', Buffer.from(csv, 'utf8')); const importId = await imports.createImport('nochmal.csv', parsed, ctx); await imports.applyMapping( importId, { playerKey: 'Steam-ID', product: 'Produkt', orderId: 'Bestellnummer', createMissingPlayers: false }, ctx, ); await imports.commitImport(importId, ctx); await assert.rejects( () => imports.commitImport(importId, ctx), (error: Error & { code?: string }) => error.code === 'NOT_PENDING', ); }); });