Files
Webklar-Kundenbereich/server/services/projectKindAssign.js
T
2026-09-30 18:30:53 +00:00

109 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config } from '../config.js'
import { getDocument, updateDocument } from './appwriteAdmin.js'
import { isKind, kindLabel, ticketTypeFor } from './projectKinds.js'
import { createTicketForProject, deleteTicket } from './ticketAdmin.js'
import { assignProject } from './projectAssignment.js'
import { updateWebsiteProject } from './giteaAdmin.js'
import { withLocks } from './keyedLock.js'
function fail(status, message, extra = {}) {
const err = new Error(message)
err.status = status
Object.assign(err, extra)
return err
}
async function loadOrNull(collection, id) {
if (!id) return null
return getDocument(collection, id).catch(() => null)
}
/**
* Schliesst das Zuordnen eines Projekts ab (WOMS-Flow): speichert die Projektart, legt bei
* Bedarf erst JETZT ein Ticket an, weist Kunde/Ticket zu und schaltet optional die Preview ein.
* Intern (`forCustomer: false`): nur Art speichern, `ticketId: null`.
*
* Serialisiert ueber den EIGENEN Schluessel 'zuordnen:<id>' (R6): `withLocks` ist nicht
* reentrant, und `assignProject` nimmt 'project:<id>' bzw. die Repo-Sperren selbst.
*
* `deps` ist nur fuer Tests gedacht (Gitea-Aufrufe ersetzen).
*/
export async function completeAssignment(
projectId,
{ kind, forCustomer, customerId = '', ticketId = '', newTicket = null, preview = null } = {},
{ staffName = '', now = new Date(), deps = {} } = {}
) {
const assign = deps.assignProject || assignProject
const updateProject = deps.updateWebsiteProject || updateWebsiteProject
return withLocks(['zuordnen:' + projectId], async () => {
const project = await getDocument(config.collections.websiteProjects, projectId).catch((err) => {
if (err.status === 404) throw fail(404, 'Projekt nicht gefunden')
throw err
})
if (project.kind) {
throw fail(409, 'bereits zugeordnet', { kindSetBy: project.kindSetBy || '', kindSetAt: project.kindSetAt || '' })
}
if (!isKind(kind)) throw fail(400, 'Unbekannte Projektart.')
const customer = !!forCustomer
let createdTicketId = null
let finalTicketId = null
if (customer) {
if (!(await loadOrNull(config.collections.customers, customerId))) throw fail(400, 'Kunde nicht gefunden.')
if (!!ticketId === !!newTicket) throw fail(400, 'Bitte genau eins angeben: vorhandenes Ticket oder neues Ticket.')
if (ticketId) {
const ticket = await loadOrNull(config.collections.workorders, ticketId)
if (!ticket) throw fail(400, 'Ticket nicht gefunden.')
if (ticket.customerId !== customerId) throw fail(400, 'Das Ticket gehört einem anderen Kunden.')
if (ticket.status === 'Closed' || ticket.status === 'Cancelled') throw fail(400, 'Das Ticket ist bereits abgeschlossen.')
finalTicketId = ticketId
} else {
const repoName = String(project.repoFullName || '').split('/').pop()
const topic = String(newTicket.topic || '').trim() || `${kindLabel(kind)} – ${repoName}`
const created = await createTicketForProject(
{
customerId,
topic,
type: ticketTypeFor(kind),
requestedBy: newTicket.requestedBy,
startDate: newTicket.startDate,
details: newTicket.details,
},
{ now }
)
createdTicketId = created.$id
finalTicketId = created.$id
}
try {
await assign(projectId, { customerId, ticketId: finalTicketId }, { now })
} catch (err) {
if (createdTicketId) await deleteTicket(createdTicketId).catch(() => {})
throw err
}
}
await updateDocument(config.collections.websiteProjects, projectId, {
kind,
forCustomer: customer,
kindSetBy: staffName || '',
kindSetAt: now.toISOString(),
})
const result = { ticketId: finalTicketId }
if (kind === 'website' && customer && preview?.subdomain) {
try {
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}).`]
}
}
return result
})
}