Webshop B2: Team-Entscheidung über die Anfrage, Übersicht /api/team/shop und Kundenmail K24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
608ff61d3d
commit
0fb84021f8
6 files changed
+512
-4
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { config } from '../../config.js'
|
||||
import { createDocument, getDocument, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { sendAdminNotice } from '../mailer.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
|
||||
import { assertOwnProject, customerOf, ensureFeatureDoc, getFeatureDoc } from '../siteFeatures.js'
|
||||
import { shopFuerKunde } from './schalter.js'
|
||||
|
||||
@@ -28,6 +28,9 @@ const MELDUNG = {
|
||||
vorhanden: 'Für dieses Projekt gibt es schon einen Webshop.',
|
||||
laeuft: 'Für dieses Projekt gibt es schon eine Webshop-Anfrage.',
|
||||
nichtOffen: 'Die Anfrage kann nur zurückgezogen werden, solange sie offen ist.',
|
||||
nichtEntscheidbar: 'Diese Anfrage ist nicht (mehr) offen und kann nicht entschieden werden.',
|
||||
entscheidung: 'Bitte wähle annehmen oder ablehnen.',
|
||||
begruendung: 'Bitte nenne eine Begründung für die Ablehnung (höchstens 1000 Zeichen).',
|
||||
ungueltig: 'Ungültige Eingabe.',
|
||||
}
|
||||
|
||||
@@ -47,7 +50,7 @@ export async function anfrageOffenePunkte({ customerId, projectId }) {
|
||||
}
|
||||
|
||||
/** shopConfig-Dokument zur ID oder null, wenn es (noch) keines gibt. Fehlt die Collection selbst, wird der Fehler weitergereicht. */
|
||||
async function leseShopConfig(id) {
|
||||
export async function leseShopConfig(id) {
|
||||
try {
|
||||
return await getDocument(config.collections.shopConfig, id)
|
||||
} catch (err) {
|
||||
@@ -230,3 +233,75 @@ export async function zieheZurueck({ customerId, projectId, person = {} }) {
|
||||
})
|
||||
return anfrageAnsicht(gespeichert)
|
||||
}
|
||||
|
||||
const BEGRUENDUNG_MAX = 1000
|
||||
|
||||
/** 'Name <email>' (wie createdBy der Angebote); ohne Name die Adresse, ohne beides 'Admin'. */
|
||||
const personLabel = (person) => (person?.name ? `${person.name}${person.email ? ` <${person.email}>` : ''}` : person?.email || 'Admin')
|
||||
|
||||
/**
|
||||
* Das Team entscheidet eine offene Anfrage (B2): annehmen oder ablehnen (Begruendung Pflicht, 1-1000 Zeichen). Unter der Sperre je
|
||||
* Projekt; das Projekt muss dem Kunden gehoeren (403), der Status muss `offen` sein (409). Speichert Status, Zeit und Person
|
||||
* (anfrageEntschiedenVon), bei annehmen eine leere Begruendung. Danach geht K24 an den Kunden (BCC Team); scheitert die Mail, bleibt
|
||||
* die Entscheidung bestehen, wird geloggt und die Antwort traegt `mailFehler: true`.
|
||||
* @returns {Promise<{ anfrage: object, mailFehler: boolean }>} anfrageAnsicht des neuen Standes
|
||||
*/
|
||||
export async function entscheideAnfrage({ customerId, projectId, entscheidung, begruendung, person = {}, now = new Date() }) {
|
||||
if (entscheidung !== 'annehmen' && entscheidung !== 'ablehnen') throw stop(400, MELDUNG.entscheidung)
|
||||
let grund = ''
|
||||
if (entscheidung === 'ablehnen') {
|
||||
grund = typeof begruendung === 'string' ? begruendung.toWellFormed().trim() : ''
|
||||
if (!grund || grund.length > BEGRUENDUNG_MAX) throw stop(400, MELDUNG.begruendung)
|
||||
}
|
||||
if (typeof projectId !== 'string' || !projectId) throw stop(400, MELDUNG.projekt)
|
||||
|
||||
const { gespeichert, project } = await withLocks([`shop-anfrage:${projectId}`], async () => {
|
||||
let besitz
|
||||
try {
|
||||
besitz = await assertOwnProject(customerId, projectId)
|
||||
} catch (err) {
|
||||
throw err?.status === 400 && err.type ? stop(403, 'Kein Zugriff auf dieses Projekt') : err
|
||||
}
|
||||
const feature = await getFeatureDoc(customerId, projectId, 'shop')
|
||||
const shop = feature ? await leseShopConfig(feature.$id) : null
|
||||
if (!shop || shop.anfrageStatus !== 'offen') throw stop(409, MELDUNG.nichtEntscheidbar, 'nicht_offen')
|
||||
const neu = await updateDocument(config.collections.shopConfig, shop.$id, {
|
||||
anfrageStatus: entscheidung === 'annehmen' ? 'angenommen' : 'abgelehnt',
|
||||
anfrageEntschiedenAt: now.toISOString(),
|
||||
anfrageEntschiedenVon: personLabel(person).slice(0, 255),
|
||||
anfrageBegruendung: grund,
|
||||
version: (Number.isInteger(shop.version) ? shop.version : 0) + 1,
|
||||
})
|
||||
return { gespeichert: neu, project: besitz }
|
||||
})
|
||||
|
||||
const mailFehler = !(await sendeEntscheidung({ customerId, projectId, project, angenommen: entscheidung === 'annehmen', begruendung: grund }))
|
||||
return { anfrage: anfrageAnsicht(gespeichert), mailFehler }
|
||||
}
|
||||
|
||||
/** K24 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). */
|
||||
async function sendeEntscheidung({ customerId, projectId, project, angenommen, begruendung }) {
|
||||
try {
|
||||
const kunde = await customerOf(customerId)
|
||||
if (!kunde?.email) {
|
||||
console.warn(`[shop] K24 nicht gesendet: Kunde ${customerId} ohne E-Mail-Adresse`)
|
||||
return false
|
||||
}
|
||||
await sendCustomerMail(kunde.email, {
|
||||
template: 'K24',
|
||||
values: {
|
||||
kunde_name: String(kunde.name || '').trim() || 'Kunde',
|
||||
projekt: project?.projectName || projectId,
|
||||
preis: config.features.shop.priceLabel,
|
||||
ust_hinweis: config.ustHinweis,
|
||||
begruendung,
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#webshop?projekt=${encodeURIComponent(projectId)}`,
|
||||
},
|
||||
blocks: { angenommen, abgelehnt: !angenommen },
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn(`[shop] K24 nicht gesendet: ${err?.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user