99 lines
8.2 KiB
TypeScript
99 lines
8.2 KiB
TypeScript
|
|
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 });
|
||
|
|
});
|
||
|
|
});
|