115 lines
4.6 KiB
JavaScript
115 lines
4.6 KiB
JavaScript
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
|
||
const warnings = []
|
||
|
||
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 {
|
||
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
|
||
}
|
||
}
|
||
|
||
await updateDocument(config.collections.websiteProjects, projectId, {
|
||
kind,
|
||
forCustomer: customer,
|
||
kindSetBy: staffName || '',
|
||
kindSetAt: now.toISOString(),
|
||
// Preview-Modus nur vorbelegen, wenn noch keiner gesetzt ist
|
||
...(project.previewMode ? {} : { previewMode: kind === 'website' || kind === 'webapp' ? 'auto' : 'off' }),
|
||
})
|
||
|
||
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.
|
||
warnings.push(`Die Preview konnte nicht eingeschaltet werden (${err.message}).`)
|
||
}
|
||
}
|
||
if (warnings.length) result.warnings = warnings
|
||
return result
|
||
})
|
||
}
|