197 lines
11 KiB
JavaScript
197 lines
11 KiB
JavaScript
// test/auftraegeArbeitsblaetter.test.js
|
|
import { test, after, beforeEach } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { schemaKeys } from './support/schema.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js'
|
|
|
|
// worksheets gehört dem Ticketsystem und hat im Nachbau kein Schema (jedes Feld ist erlaubt).
|
|
const aw = await startAppwriteMock({ schema: { projektRechnungen: schemaKeys('projektRechnungen') }, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25 })
|
|
after(() => aw.close())
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
|
|
|
const { config } = await import('../server/config.js')
|
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const { createProjektRechnung, updateProjektRechnung } = await import('../server/services/auftraege/projektRechnungenStore.js')
|
|
const A = await import('../server/services/auftraege/arbeitsblaetter.js')
|
|
|
|
const WS = config.collections.worksheets
|
|
const PR = config.collections.projektRechnungen
|
|
beforeEach(async () => {
|
|
for (const col of [WS, PR]) for (const id of [...aw.docs(col).keys()]) await deleteDocument(col, id)
|
|
aw.resetListCalls()
|
|
})
|
|
|
|
const UNTERSTUETZUNG = JSON.stringify([{ employeeId: 'e2', name: 'Justin Klein', task: 'Review' }])
|
|
const ZWEI_UNTERSTUETZER = JSON.stringify([
|
|
{ employeeId: 'e2', name: 'Justin Klein', task: 'Review' },
|
|
{ employeeId: 'e3', name: 'Nico', task: 'Test' },
|
|
])
|
|
const blatt = (id, felder = {}) =>
|
|
aw.seed(WS, {
|
|
$id: id, workorderId: 't1', woid: '100101', wsid: id, startDate: '06.10.2026', endDate: '06.10.2026', totalTime: 60,
|
|
details: 'Startseite überarbeitet', serviceType: 'REMOTE', employeeName: 'Kenso Grimm', countsToPlan: false, isComment: false, supporters: '', ...felder,
|
|
})
|
|
const nachId = async (ticketIds = ['t1']) => Object.fromEntries((await A.ladeArbeitsblaetter(ticketIds)).map((b) => [b.id, b]))
|
|
|
|
test('ladeArbeitsblaetter liefert die normierte Form je Blatt', async () => {
|
|
blatt('w1')
|
|
assert.deepEqual(await A.ladeArbeitsblaetter(['t1']), [
|
|
{ id: 'w1', ticketId: 't1', woid: '100101', datum: '2026-10-06', minuten: 60, text: 'Startseite überarbeitet', aboZeit: false, kommentar: false },
|
|
])
|
|
})
|
|
|
|
test('Datum eines Arbeitsblatts: TT.MM.JJJJ, ISO als Berliner Tag, leer fällt auf $createdAt, ein unmögliches Datum auch', async () => {
|
|
const faelle = {
|
|
d1: [{ startDate: '06.10.2026' }, '2026-10-06'],
|
|
d2: [{ startDate: '', endDate: '07.10.2026' }, '2026-10-07'],
|
|
d3: [{ startDate: '2026-10-06T21:59:59.000Z' }, '2026-10-06'], // 23:59:59 in Berlin
|
|
d4: [{ startDate: '2026-10-06T22:30:00.000Z' }, '2026-10-07'], // 00:30 in Berlin am nächsten Tag
|
|
d5: [{ startDate: '2026-10-06' }, '2026-10-06'],
|
|
d6: [{ startDate: '', $createdAt: '2026-10-08T21:30:00.000+00:00' }, '2026-10-08'],
|
|
d7: [{ startDate: '', $createdAt: '2026-10-08T22:00:00.000+00:00' }, '2026-10-09'],
|
|
d8: [{ startDate: '31.02.2026', $createdAt: '2026-10-01T10:00:00.000+00:00' }, '2026-10-01'],
|
|
d9: [{ startDate: 'gestern', $createdAt: '2026-10-02T10:00:00.000+00:00' }, '2026-10-02'],
|
|
d10: [{ startDate: '12.13.2026', $createdAt: '2026-10-03T10:00:00.000+00:00' }, '2026-10-03'], // nie als Monatsangabe der US-Schreibweise lesen
|
|
}
|
|
for (const [id, [felder]] of Object.entries(faelle)) blatt(id, { endDate: '', ...felder })
|
|
const datum = Object.fromEntries(Object.values(await nachId()).map((b) => [b.id, b.datum]))
|
|
assert.deepEqual(datum, Object.fromEntries(Object.entries(faelle).map(([id, [, tag]]) => [id, tag])))
|
|
})
|
|
|
|
test('Minuten: je Blatt inklusive Unterstützung auf 15 Minuten aufgerundet', async () => {
|
|
const faelle = [
|
|
['m1', { totalTime: 10 }, 15],
|
|
['m2', { totalTime: 15 }, 15],
|
|
['m3', { totalTime: 16 }, 30],
|
|
['m4', { totalTime: 0 }, 0],
|
|
['m5', { totalTime: 'abc' }, 0],
|
|
['m6', { totalTime: 80, supporters: UNTERSTUETZUNG }, 135], // 80 + 45 Unterstützung = 125
|
|
['m7', { totalTime: 20, supporters: UNTERSTUETZUNG }, 45], // 20 + 15 = 35
|
|
['m8', { totalTime: 60, supporters: ZWEI_UNTERSTUETZER }, 120], // 60 + 30 + 30
|
|
]
|
|
for (const [id, felder] of faelle) blatt(id, felder)
|
|
const minuten = Object.fromEntries(Object.values(await nachId()).map((b) => [b.id, b.minuten]))
|
|
assert.deepEqual(minuten, Object.fromEntries(faelle.map(([id, , erwartet]) => [id, erwartet])))
|
|
})
|
|
|
|
test('Abo-Zeit und Kommentar werden erkannt und sind nie abrechenbar, ebenso 0 Minuten', async () => {
|
|
blatt('f1')
|
|
blatt('f2', { countsToPlan: true })
|
|
blatt('f3', { isComment: true, totalTime: 0 })
|
|
blatt('f4', { totalTime: 0 })
|
|
const b = await nachId()
|
|
assert.deepEqual([b.f1.aboZeit, b.f1.kommentar, A.abrechenbar(b.f1)], [false, false, true])
|
|
assert.deepEqual([b.f2.aboZeit, A.abrechenbar(b.f2)], [true, false])
|
|
assert.deepEqual([b.f3.kommentar, A.abrechenbar(b.f3)], [true, false])
|
|
assert.equal(A.abrechenbar(b.f4), false)
|
|
const basis = { aboZeit: false, kommentar: false, minuten: 15 }
|
|
assert.equal(A.abrechenbar(basis), true)
|
|
assert.equal(A.abrechenbar({ ...basis, aboZeit: true }), false)
|
|
assert.equal(A.abrechenbar({ ...basis, kommentar: true }), false)
|
|
assert.equal(A.abrechenbar({ ...basis, minuten: 0 }), false)
|
|
})
|
|
|
|
test('Text: wie in der Kundenansicht, eine Zeile, höchstens 200 Zeichen, Git ohne Kürzel, Link und Namen', async () => {
|
|
blatt('x1', { details: ' Startseite\n\nKontaktformular überarbeitet ' })
|
|
blatt('x2', { details: 'ä'.repeat(300) })
|
|
blatt('x3', {
|
|
serviceType: 'GIT',
|
|
totalTime: 45,
|
|
details: [
|
|
'Git Push auf `knso/x` (Branch: main)',
|
|
'Commit: 88d3c37',
|
|
'',
|
|
'- [88d3c37](https://git.webklar.com/knso/x/commit/88d3c37ab7b15337772cf59a5a9294ec0cc8cca4): feat: Kontaktformular repariert (Kenso Grimm)',
|
|
'- [1a2b3c4](https://git.webklar.com/knso/x/commit/1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b): fix: Footer',
|
|
'',
|
|
].join('\n'),
|
|
})
|
|
blatt('x4', { isComment: true, details: 'nur intern' })
|
|
blatt('x5', { details: '' })
|
|
const b = await nachId()
|
|
assert.equal(b.x1.text, 'Startseite Kontaktformular überarbeitet')
|
|
assert.equal(b.x2.text.length, 200)
|
|
assert.ok(b.x2.text.endsWith('…'))
|
|
assert.equal(b.x3.text, 'Kontaktformular repariert; Footer')
|
|
assert.doesNotMatch(b.x3.text, /88d3c37|git\.webklar|Kenso/)
|
|
assert.equal(b.x4.text, '')
|
|
assert.equal(b.x5.text, '')
|
|
})
|
|
|
|
test('ladeArbeitsblaetter: nur die genannten Tickets, nach Datum und ID sortiert, in Blöcken zu 100 Tickets', async () => {
|
|
blatt('w3', { startDate: '07.10.2026' })
|
|
blatt('w2', { startDate: '06.10.2026' })
|
|
blatt('w1', { startDate: '06.10.2026' })
|
|
blatt('w9', { workorderId: 't2', startDate: '05.10.2026' })
|
|
blatt('w8', { workorderId: 't3' })
|
|
assert.deepEqual((await A.ladeArbeitsblaetter(['t1', 't2'])).map((b) => b.id), ['w9', 'w1', 'w2', 'w3'])
|
|
assert.deepEqual((await A.ladeArbeitsblaetter(['t1', 't1', '', null, 5])).map((b) => b.id), ['w1', 'w2', 'w3'])
|
|
aw.resetListCalls()
|
|
for (const leer of [[], undefined, null, [''], 'kein-array']) assert.deepEqual(await A.ladeArbeitsblaetter(leer), [], String(leer))
|
|
assert.equal(aw.listCalls(WS).length, 0, 'ohne Ticket keine Abfrage')
|
|
const viele = Array.from({ length: 250 }, (_, i) => `t_${i}`)
|
|
viele[7] = 't1'
|
|
assert.deepEqual((await A.ladeArbeitsblaetter(viele)).map((b) => b.id), ['w1', 'w2', 'w3'])
|
|
const aufrufe = aw.listCalls(WS)
|
|
assert.equal(aufrufe.length, 3)
|
|
for (const aufruf of aufrufe) {
|
|
assert.ok(aufruf.queries.find((q) => q.method === 'equal' && q.attribute === 'workorderId').values.length <= 100)
|
|
}
|
|
})
|
|
|
|
test('ladeArbeitsblaetterNachIds liefert nur vorhandene Blätter, sortiert wie ladeArbeitsblaetter', async () => {
|
|
blatt('w1', { startDate: '07.10.2026' })
|
|
blatt('w2', { workorderId: 't2', startDate: '06.10.2026' })
|
|
assert.deepEqual((await A.ladeArbeitsblaetterNachIds(['w1', 'w2', 'gibtsnicht', 'w1'])).map((b) => [b.id, b.ticketId]), [['w2', 't2'], ['w1', 't1']])
|
|
assert.deepEqual(await A.ladeArbeitsblaetterNachIds([]), [])
|
|
})
|
|
|
|
test('abgerechneteArbeitsblaetter: nur aktive Projektrechnungen dieses Kunden; ein Entwurf hat die Nummer leer', async () => {
|
|
const daten = (extra) => ({ kundenId: 'c1', projektId: 'p1', art: 'aufwand', betragCents: 1000, ...extra })
|
|
await createProjektRechnung('in_gesendet', daten({ arbeitsblattIds: ['w1', 'w2'] }))
|
|
await updateProjektRechnung('in_gesendet', { rechnungsnummer: 'WEBKLAR-0009' })
|
|
await createProjektRechnung('in_entwurf', daten({ arbeitsblattIds: ['w5'] }))
|
|
await createProjektRechnung('in_storniert', daten({ arbeitsblattIds: ['w3'] }))
|
|
await updateProjektRechnung('in_storniert', { aktiv: false })
|
|
await createProjektRechnung('in_fremd', daten({ kundenId: 'c2', arbeitsblattIds: ['w4'] }))
|
|
const karte = await A.abgerechneteArbeitsblaetter('c1')
|
|
assert.ok(karte instanceof Map)
|
|
assert.deepEqual([...karte.entries()].sort(), [
|
|
['w1', { invoiceId: 'in_gesendet', rechnungsnummer: 'WEBKLAR-0009' }],
|
|
['w2', { invoiceId: 'in_gesendet', rechnungsnummer: 'WEBKLAR-0009' }],
|
|
['w5', { invoiceId: 'in_entwurf', rechnungsnummer: '' }],
|
|
])
|
|
await assert.rejects(() => A.abgerechneteArbeitsblaetter(''), TypeError)
|
|
})
|
|
|
|
test('offeneArbeitsblaetter: abrechenbar und noch nicht abgerechnet; ein stornierter Eintrag gibt das Blatt frei', async () => {
|
|
blatt('w1') // abgerechnet (siehe unten)
|
|
blatt('w6', { totalTime: 30 }) // offen
|
|
blatt('w7', { countsToPlan: true }) // Abo-Zeit
|
|
blatt('w8', { isComment: true, totalTime: 0 }) // Kommentar
|
|
blatt('w9', { totalTime: 0 }) // 0 Minuten
|
|
blatt('w10', { workorderId: 't2' }) // anderes Ticket
|
|
await createProjektRechnung('in_a', { kundenId: 'c1', projektId: 'p1', art: 'aufwand', betragCents: 1000, arbeitsblattIds: ['w1'] })
|
|
assert.deepEqual((await A.offeneArbeitsblaetter(['t1'], 'c1')).map((b) => b.id), ['w6'])
|
|
await updateProjektRechnung('in_a', { aktiv: false })
|
|
assert.deepEqual((await A.offeneArbeitsblaetter(['t1'], 'c1')).map((b) => b.id), ['w1', 'w6'])
|
|
})
|
|
|
|
test('zeitquelleFuerProjekt: Vereinigung der vereinbarten ABs, sonst das Ticket des Projekts', () => {
|
|
const ab = (status, zeitquelle) => ({ status, zeitquelle })
|
|
const projekt = { $id: 'p1', ticketId: 't0' }
|
|
const alle = [
|
|
ab('versendet', ['t1', 't2']), ab('bestaetigt', ['t2', 't3']), ab('abgerechnet', ['t4']),
|
|
ab('entwurf', ['t9']), ab('storniert', ['t8']), ab('ersetzt', ['t7']), ab('widerrufen', ['t6']),
|
|
]
|
|
assert.deepEqual(A.zeitquelleFuerProjekt(projekt, alle), ['t1', 't2', 't3', 't4'])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt(projekt, []), ['t0'])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt(projekt, [ab('entwurf', ['t9'])]), ['t0'])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt(projekt, [ab('versendet', [])]), ['t0'])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt({ $id: 'p1' }, []), [])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt({ ticketId: '' }, undefined), [])
|
|
assert.deepEqual(A.zeitquelleFuerProjekt(null, []), [])
|
|
})
|