import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import type { FastifyInstance } from 'fastify'; import { buildApp } from '../src/server.js'; import { breakdown, normalizeDomain, parsePriceList, roundPrice, sellPrice, splitDomain } from '../src/modules/domains/logic.js'; import { call, code, login, makeUser } from './helpers.js'; const PASTE = `Domaintyp\tLaufzeit\t0-49\t50-199\t200-999\t1000+\tSetup com\t12\t12,50\t12,00\t11,00\t10,50\t com.au\t24\t69,00\t69,00\t68,00\t68,00\t cy\t12\t69,00\t69,00\t69,00\t69,00\t75,00 € voting\t12\t1.380,00\t1.360,00\t1.340,00\t1.320,00\t de\t12\t4,50\t4,00\t3,50\t3,00\t Laufzeit in Monaten, alle Preise zzgl. Ust.`; describe('Domain-Logik', () => { it('liest das eingefügte Preislistenformat (deutsche Zahlen, Setup, Kopf/Fuß)', () => { const { rows, skipped } = parsePriceList(PASTE); expect(rows.map((r) => r.tld)).toEqual(['com', 'com.au', 'cy', 'voting', 'de']); expect(rows[1]).toMatchObject({ termMonths: 24, costs: [6900, 6900, 6800, 6800] }); expect(rows[2]!.setupCents).toBe(7500); expect(rows[3]!.costs[0]).toBe(138000); expect(rows[0]!.setupCents).toBe(0); expect(skipped.length).toBe(1); }); it('Verkaufspreis = Einkauf + Aufschlag; ohne Aufschlag kein Preis', () => { const none = { type: null, value: null }; expect(sellPrice(1250, none, none)).toBeNull(); expect(sellPrice(1250, none, { type: 'percent', value: 2500 })).toBe(1563); expect(sellPrice(1250, none, { type: 'fixed', value: 300 })).toBe(1550); expect(sellPrice(1250, { type: 'fixed', value: 100 }, { type: 'percent', value: 2500 })).toBe(1350); // Endungs-Aufschlag hat Vorrang expect(sellPrice(400, { type: 'percent', value: 0 }, none)).toBe(400); }); it('Liste brutto: Netto wird herausgerechnet, keine zweite USt; Liste netto: USt wird aufgeschlagen', () => { const m = { type: 'percent' as const, value: 2000 }, none = { type: null, value: null }; expect(breakdown(1190, none, m, false, 'gross', 1900)).toEqual({ costGrossCents: 1190, costNetCents: 1000, priceGrossCents: 1428, priceNetCents: 1200, profitNetCents: 200 }); expect(breakdown(1000, none, m, false, 'net', 1900)).toEqual({ costGrossCents: 1190, costNetCents: 1000, priceGrossCents: 1428, priceNetCents: 1200, profitNetCents: 200 }); }); it('rundet auf ,50 bzw. ,99', () => { expect([1200, 1230, 1250, 1251, 1298, 1299, 450, 475, 1500].map(roundPrice)).toEqual([1250, 1250, 1250, 1299, 1299, 1299, 450, 499, 1550]); expect(sellPrice(1250, { type: null, value: null }, { type: 'percent', value: 2000 }, true)).toBe(1550); // 15,00 → 15,50 }); it('normalisiert und zerlegt Domainnamen', () => { expect(normalizeDomain(' https://www.Beispiel.DE/pfad?x=1 ')).toBe('beispiel.de'); expect(normalizeDomain('müller.de')).toBe('xn--mller-kva.de'); expect(normalizeDomain('a b.de')).toBeNull(); expect(normalizeDomain('-x.de')).toBeNull(); const known = new Set(['uk', 'co.uk', 'de']); expect(splitDomain('firma.co.uk', known)).toEqual({ label: 'firma', tld: 'co.uk' }); expect(splitDomain('firma.de', known)).toEqual({ label: 'firma', tld: 'de' }); }); }); let app: FastifyInstance; const realFetch = globalThis.fetch; beforeAll(async () => { vi.stubGlobal('fetch', (async (url: any, init: any) => { const u = String(url); if (u.includes('data.iana.org')) return new Response(JSON.stringify({ services: [[['de', 'com', 'net'], ['https://rdap.test/']]] }), { status: 200 }); if (u.startsWith('https://rdap.test/domain/')) return new Response('{}', { status: u.includes('vergeben') ? 200 : 404 }); return realFetch(url, init); }) as typeof fetch); app = await buildApp(); await app.ready(); }); afterAll(() => vi.unstubAllGlobals()); async function staff(email: string, role: string) { await makeUser({ email, kind: 'staff', staffRole: role }); const { client } = await login(app, email); const s = (await call(app, client, 'POST', '/auth/mfa/setup')).json(); await call(app, client, 'POST', '/auth/mfa/confirm', { code: code(s.secret) }); return client; } describe('Domains: Preisliste, Aufschlag, Prüfung', () => { it('Import, Aufschlag, Verkaufspreis, Verfügbarkeit; Kunden sehen keinen Einkauf', async () => { const admin = await staff('dom-admin@example.com', 'admin'); const sup = await staff('dom-support@example.com', 'support'); const imp = await call(app, admin, 'POST', '/admin/domain-tlds/import', { text: PASTE }); expect(imp.statusCode).toBe(200); expect(imp.json()).toMatchObject({ rows: 5, created: 5 }); expect((await call(app, sup, 'POST', '/admin/domain-tlds/import', { text: PASTE })).statusCode).toBe(403); expect((await call(app, sup, 'GET', '/admin/domain-tlds')).statusCode).toBe(200); const before = (await call(app, admin, 'GET', '/domains/check?name=frei.com')).json().results[0]; expect(before).toMatchObject({ domain: 'frei.com', status: 'available', method: 'rdap', offered: true, offer: null }); // noch kein Aufschlag → kein Preis expect((await call(app, admin, 'PUT', '/admin/domain-settings', { tier: 1, basis: 'gross', rounding: false, margin: { type: 'percent', value: 2000 } })).statusCode).toBe(200); expect((await call(app, admin, 'PATCH', '/admin/domain-tlds/de', { margin: { type: 'fixed', value: 500 } })).statusCode).toBe(200); const list = (await call(app, admin, 'GET', '/admin/domain-tlds')).json().tlds; const com = list.find((t: any) => t.tld === 'com'); const de = list.find((t: any) => t.tld === 'de'); expect(com).toMatchObject({ costGrossCents: 1250, costNetCents: 1050, priceGrossCents: 1500, priceNetCents: 1261, profitNetCents: 211 }); expect(de).toMatchObject({ costGrossCents: 450, priceGrossCents: 950, priceNetCents: 798 }); await call(app, admin, 'PUT', '/admin/domain-settings', { tier: 1, basis: 'gross', rounding: true, margin: { type: 'percent', value: 2000 } }); expect((await call(app, admin, 'GET', '/admin/domain-tlds')).json().tlds.find((t: any) => t.tld === 'com')).toMatchObject({ priceGrossCents: 1550, priceNetCents: 1303, profitNetCents: 253 }); await makeUser({ email: 'dom-kunde@example.com' }); const { client: cust } = await login(app, 'dom-kunde@example.com'); const r = (await call(app, cust, 'GET', '/domains/check?name=vergeben.com')).json().results[0]; expect(r).toMatchObject({ status: 'registered', offer: { priceGrossCents: 1550, priceNetCents: 1303, termMonths: 12 } }); expect(JSON.stringify(r)).not.toContain('cost'); const multi = (await call(app, cust, 'GET', '/domains/check?name=firma')).json().results; expect(multi.map((x: any) => x.domain).sort()).toEqual(['firma.com', 'firma.de']); expect((await call(app, cust, 'GET', '/domains/check?name=a%20b.de')).statusCode).toBe(400); expect((await call(app, cust, 'GET', '/admin/domain-tlds')).statusCode).toBe(403); // Aufstellung: Einkauf netto, Verkauf netto/brutto (19 %), Beschaffungsstatus const rec = await call(app, admin, 'POST', '/admin/domain-records', { domain: 'https://www.Kunde-Test.com/' }); expect(rec.statusCode).toBe(200); expect((await call(app, admin, 'POST', '/admin/domain-records', { domain: 'kunde-test.com' })).statusCode).toBe(400); const rlr = await call(app, admin, 'GET', '/admin/domain-records'); const rl = rlr.json(); expect(rl[0]).toMatchObject({ domain: 'kunde-test.com', costGrossCents: 1250, costNetCents: 1050, sellGrossCents: 1550, sellNetCents: 1303, profitNetCents: 253, procurement: 'open' }); expect((await call(app, admin, 'PATCH', `/admin/domain-records/${rec.json().id}`, { procurement: 'ordered', orderedRef: 'KCS-1' })).statusCode).toBe(200); expect((await call(app, admin, 'GET', '/admin/domain-records?status=ordered')).json()[0]).toMatchObject({ procurement: 'ordered', orderedRef: 'KCS-1' }); expect((await call(app, cust, 'GET', '/admin/domain-records')).statusCode).toBe(403); expect((await call(app, sup, 'POST', '/admin/domain-records', { domain: 'x.com' })).statusCode).toBe(403); expect((await call(app, admin, 'GET', '/admin/domain-records?orgId=00000000-0000-4000-8000-000000000000')).json()).toEqual([]); // Kundenfilter await call(app, admin, 'PATCH', '/admin/domain-tlds/com', { active: false }); expect((await call(app, cust, 'GET', '/domains/check?name=frei.com')).json().results[0]).toMatchObject({ offered: false, offer: null }); }); });