diff --git a/server/services/projectKindAssign.js b/server/services/projectKindAssign.js index fe85f32..ec3fc25 100644 --- a/server/services/projectKindAssign.js +++ b/server/services/projectKindAssign.js @@ -50,6 +50,7 @@ export async function completeAssignment( const customer = !!forCustomer let createdTicketId = null let finalTicketId = null + const warnings = [] if (customer) { if (!(await loadOrNull(config.collections.customers, customerId))) throw fail(400, 'Kunde nicht gefunden.') @@ -80,7 +81,9 @@ export async function completeAssignment( } try { - await assign(projectId, { customerId, ticketId: finalTicketId }, { now }) + const assigned = await assign(projectId, { customerId, ticketId: finalTicketId }, { now }) + // assignProject schluckt Umzug-/Nachtrage-Fehler und meldet sie als Warnungen. + if (assigned?.warnings?.length) warnings.push(...assigned.warnings) } catch (err) { if (createdTicketId) await deleteTicket(createdTicketId).catch(() => {}) throw err @@ -100,9 +103,10 @@ export async function completeAssignment( await updateProject(projectId, { subdomain: preview.subdomain, projectType: 'preview' }) } catch (err) { // Art und Zuordnung sind gespeichert - die Preview laesst sich nachtraeglich einschalten. - result.warnings = [`Die Preview konnte nicht eingeschaltet werden (${err.message}).`] + warnings.push(`Die Preview konnte nicht eingeschaltet werden (${err.message}).`) } } + if (warnings.length) result.warnings = warnings return result }) } diff --git a/test/projectKindAssign.test.js b/test/projectKindAssign.test.js index 92db8d4..bcecf93 100644 --- a/test/projectKindAssign.test.js +++ b/test/projectKindAssign.test.js @@ -44,6 +44,10 @@ before(async () => { newProj('p9', 'doppelt'), newProj('p10', 'prev'), newProj('p11', 'route'), + newProj('p13', 'warn'), + newProj('p14', 'race'), + newProj('p15', 'kunde'), + newProj('p16', 'eins'), newProj('p12', 'schon', { kind: 'app', kindSetBy: 'Max', kindSetAt: '2026-09-29T08:00:00.000Z' }), ], workorders: [ @@ -176,6 +180,47 @@ test('Website mit Subdomain ⇒ updateWebsiteProject(preview)', async () => { assert.deepEqual(s.calls.update, [['p10', { subdomain: 'prev', projectType: 'preview' }]]) }) +test('Warnungen von assignProject und Preview-Fehler werden zusammengefuehrt', async () => { + const s = stubs() + s.deps.assignProject = async () => ({ ok: true, warnings: ['Umzug fehlgeschlagen'] }) + s.deps.updateWebsiteProject = async () => { throw new Error('Gitea aus') } + const r = await completeAssignment('p13', { kind: 'website', forCustomer: true, customerId: 'c1', ticketId: 't1', preview: { subdomain: 'w' } }, opts(s.deps)) + assert.equal(r.ticketId, 't1') + assert.equal(r.warnings.length, 2) + assert.equal(r.warnings[0], 'Umzug fehlgeschlagen') + assert.match(r.warnings[1], /Preview/) + assert.equal(aw.get('websiteProjects', 'p13').kind, 'website') +}) + +test('gleichzeitiger Doppelaufruf ⇒ genau ein Ticket und ein 409', async () => { + const s = stubs() + const before = aw.all('workorders').length + const body = { kind: 'app', forCustomer: true, customerId: 'c1', newTicket: { topic: 'race' } } + const res = await Promise.allSettled([completeAssignment('p14', body, opts(s.deps)), completeAssignment('p14', body, opts(s.deps))]) + assert.equal(res.filter((x) => x.status === 'fulfilled').length, 1) + const rej = res.find((x) => x.status === 'rejected') + assert.equal(rej.reason.status, 409) + assert.equal(aw.all('workorders').length, before + 1) +}) + +test('forCustomer mit unbekanntem Kunden ⇒ 400', async () => { + const s = stubs() + await assert.rejects( + () => completeAssignment('p15', { kind: 'website', forCustomer: true, customerId: 'nix', newTicket: { topic: 'x' } }, opts(s.deps)), + (e) => e.status === 400 + ) + assert.equal(aw.get('websiteProjects', 'p15').kind, undefined) +}) + +test('ticketId und newTicket beide oder keins ⇒ 400', async () => { + const s = stubs() + const base = { kind: 'website', forCustomer: true, customerId: 'c1' } + for (const extra of [{ ticketId: 't1', newTicket: { topic: 'x' } }, {}]) { + await assert.rejects(() => completeAssignment('p16', { ...base, ...extra }, opts(s.deps)), (e) => e.status === 400) + } + assert.equal(aw.get('websiteProjects', 'p16').kind, undefined) +}) + test('Route: ohne Staff-JWT ⇒ 401, mit JWT intern 200, bereits zugeordnet 409, unbekannt 404', async () => { const post = (id, body, jwt) => fetch(`${baseUrl}/api/admin/website-projects/${id}/zuordnen`, {