import { test, after, beforeEach } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' // Webshop B2: Team-Uebersicht und Entscheidung ueber die Anfrage (routes/team/shop.js, services/shop/anfrage.js entscheideAnfrage), K24 const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), shopConfig: schemaKeys('shopConfig'), shopProducts: schemaKeys('shopProducts'), shopOrders: schemaKeys('shopOrders'), featureConsents: schemaKeys('featureConsents'), }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { startTestApp, rawGet, post } = await import('./support/testApp.js') const { seedFixtures, customers } = await import('./support/fixtures.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') const anfrage = await import('../server/services/shop/anfrage.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { mailSink } = await import('./support/mailSink.js') const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js') const { featureHaken } = await import('../server/services/featureRegistry.js') const { ensureFeatureDoc } = await import('../server/services/siteFeatures.js') const { resetShopRoutesForTests } = await import('../server/routes/shop.js') const app = await startTestApp({ '/api/team': teamRoutes }) const mails = mailSink() mails.versandFehler = false const sinkMail = mails.sendMail.bind(mails) mails.sendMail = async (mail) => { if (mails.versandFehler && /Webshop-Anfrage/.test(mail.subject)) throw new Error('SMTP nicht erreichbar') return sinkMail(mail) } setMailTransportForTests(mails) after(() => { setMailTransportForTests(null) return Promise.all([app.close(), aw.close(), st.close()]) }) seedFixtures(aw) aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' }) aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Töpferei' }) aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Zweiter Shop' }) aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremdes Projekt' }) aw.seed('websiteProjects', { $id: 'pt4', customerId: 'c1', projectName: 'Altprojekt', archived: true }) aw.seed('websiteProjects', { $id: 'pt5', customerId: 'c1', projectName: 'Altshop', archived: true }) const staff = { headers: { authorization: 'Bearer jwt-staff' } } const GUELTIG = { warenart: 'kunst', beschreibung: 'Handgemachte Keramik und Tassen aus eigener Werkstatt.', artikelzahl: 'bis20', versand: true, abholung: false, unternehmer: true, keineAusgeschlossenen: true, nachricht: '', } const person = { name: 'Kunde Eins', email: customers.c1.email } const stelle = (projectId = 'pt1') => anfrage.stelleAnfrage({ customerId: 'c1', projectId, eingabe: GUELTIG, person }) const entscheide = (body, opts = staff, customerId = 'c1') => post(`/api/team/shop/${customerId}/anfrage`, { projectId: 'pt1', ...body }, opts) const k24 = () => mails.sent.filter((m) => /Webshop-Anfrage/.test(m.subject)) const shopDoc = (projectId = 'pt1') => [...aw.docs('shopConfig').values()].find((d) => d.projectId === projectId) ?? null const uebersicht = async (customerId = 'c1') => (await rawGet(`/api/team/shop/${customerId}`, staff)).body.projects let origin beforeEach(() => { origin ??= { mode: config.shop.mode, pilotEmails: config.shop.pilotEmails } config.shop.mode = 'pilot' config.shop.pilotEmails = [customers.c1.email] mails.versandFehler = false resetShopRoutesForTests() }) async function leereAlles() { for (const col of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders']) { for (const id of aw.docs(col).keys()) await deleteDocument(config.collections[col], id) } mails.sent.length = 0 } test('ohne Anmeldung 401, mit Mitarbeiter-JWT erreichbar', async () => { assert.equal((await rawGet('/api/team/shop/c1')).status, 401) assert.equal((await post('/api/team/shop/c1/anfrage', { projectId: 'pt1', entscheidung: 'annehmen' })).status, 401) assert.equal((await rawGet('/api/team/shop/c1', staff)).status, 200) }) test('GET: Projekte des Kunden ohne Shop, archivierte nur mit Shop-Dokument', async () => { await leereAlles() const ohne = await uebersicht() assert.deepEqual(ohne.map((p) => p.projectId).sort(), ['pt1', 'pt2']) assert.deepEqual(ohne[0], { projectId: ohne[0].projectId, projectName: ohne[0].projectName, state: 'off', anfrage: null, stripe: { accountId: false, status: null }, geoeffnet: false, zahlen: { artikel: 0, offeneBestellungen: 0 }, }) assert.deepEqual(await uebersicht('c3'), []) await ensureFeatureDoc('c1', 'pt5', 'shop') assert.deepEqual((await uebersicht()).map((p) => p.projectId).sort(), ['pt1', 'pt2', 'pt5']) }) test('GET: Anfrage, Stripe-Stand, geoeffnet und Zahlen je Projekt', async () => { await leereAlles() await stelle('pt1') const doc = shopDoc('pt1') await updateDocument(config.collections.shopConfig, doc.$id, { stripeAccountId: 'acct_1', stripeStatus: JSON.stringify({ chargesEnabled: true, detailsSubmitted: true }), geoeffnet: true, }) for (const [id, shopId, status] of [['o1', doc.$id, 'bezahlt'], ['o2', doc.$id, 'bezahlt'], ['o3', doc.$id, 'offen'], ['o4', doc.$id, 'versendet'], ['o5', 'anderer', 'bezahlt']]) { aw.seed('shopOrders', { $id: id, shopId, customerId: 'c1', status }) } aw.seed('shopProducts', { $id: 'a1', shopId: doc.$id, customerId: 'c1', name: 'Tasse' }) aw.seed('shopProducts', { $id: 'a2', shopId: doc.$id, customerId: 'c1', name: 'Teller' }) const p1 = (await uebersicht()).find((p) => p.projectId === 'pt1') assert.equal(p1.anfrage.status, 'offen') assert.equal(p1.anfrage.anfrage.warenart, 'kunst') assert.deepEqual(p1.stripe, { accountId: true, status: { chargesEnabled: true, detailsSubmitted: true } }) assert.equal(p1.geoeffnet, true) assert.deepEqual(p1.zahlen, { artikel: 2, offeneBestellungen: 2 }) const p2 = (await uebersicht()).find((p) => p.projectId === 'pt2') assert.equal(p2.anfrage, null) await updateDocument(config.collections.shopConfig, doc.$id, { stripeStatus: '{kaputt' }) assert.equal((await uebersicht()).find((p) => p.projectId === 'pt1').stripe.status, null, 'unlesbares JSON => null') }) test('annehmen: speichert Entscheidung und Person, K24 an den Kunden, Bestell-Haken liefert []', async () => { await leereAlles() await stelle() assert.deepEqual(await featureHaken('shop').bestellpruefung({ customerId: 'c1', projectId: 'pt1' }), [anfrage.ANFRAGE_OFFEN_TEXT]) mails.sent.length = 0 const res = await entscheide({ entscheidung: 'annehmen', begruendung: 'wird ignoriert' }) assert.equal(res.status, 200, JSON.stringify(res.body)) assert.equal(res.body.mailFehler, undefined) assert.equal(res.body.anfrage.status, 'angenommen') assert.ok(res.body.anfrage.entschiedenAt) assert.equal(res.body.anfrage.begruendung, '') const doc = shopDoc() assert.equal(doc.anfrageStatus, 'angenommen') assert.equal(doc.anfrageEntschiedenVon, 'Andrej ') assert.equal(doc.anfrageBegruendung, '') assert.equal(doc.version, 2) assert.deepEqual(await featureHaken('shop').bestellpruefung({ customerId: 'c1', projectId: 'pt1' }), []) const mail = k24() assert.equal(mail.length, 1) assert.equal(mail[0].to[0].address, customers.c1.email) assert.equal(mail[0].subject, 'Deine Webshop-Anfrage für Töpferei') assert.match(mail[0].text, /Hallo Kunde Eins/) assert.match(mail[0].text, /angenommen/) assert.match(mail[0].text, /299 €/) assert.match(mail[0].text, /§ 19 UStG/) assert.doesNotMatch(mail[0].text, /leider/) assert.match(mail[0].html, /dashboard\.html#webshop\?projekt=pt1"/) assert.ok(mail[0].html.includes(config.publicBaseUrl)) }) test('ablehnen: Begründung Pflicht (1-1000), gespeichert und in K24', async () => { await leereAlles() await stelle() mails.sent.length = 0 for (const begruendung of [undefined, '', ' ', 5, 'x'.repeat(1001)]) { const res = await entscheide({ entscheidung: 'ablehnen', begruendung }) assert.equal(res.status, 400, String(begruendung).slice(0, 10)) } assert.equal(shopDoc().anfrageStatus, 'offen') assert.equal(k24().length, 0) const res = await entscheide({ entscheidung: 'ablehnen', begruendung: ` ${'y'.repeat(1000)} ` }) assert.equal(res.status, 200, JSON.stringify(res.body)) assert.equal(res.body.anfrage.status, 'abgelehnt') assert.equal(res.body.anfrage.begruendung, 'y'.repeat(1000)) assert.equal(shopDoc().anfrageStatus, 'abgelehnt') assert.deepEqual(await featureHaken('shop').bestellpruefung({ customerId: 'c1', projectId: 'pt1' }), [anfrage.ANFRAGE_OFFEN_TEXT]) assert.match(k24()[0].text, /leider/) assert.match(k24()[0].text, /yyyy/) assert.doesNotMatch(k24()[0].text, /299 €/) }) test('falscher Zustand 409, fremdes Projekt 403, ungültige Eingabe 400', async () => { await leereAlles() // kein Dokument let res = await entscheide({ entscheidung: 'annehmen' }) assert.equal(res.status, 409) assert.equal(res.body.code, 'nicht_offen') await stelle() assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200) mails.sent.length = 0 // schon entschieden: weder annehmen noch ablehnen assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 409) assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Grund' })).status, 409) assert.equal(shopDoc().anfrageStatus, 'angenommen') // zurückgezogen await stelle('pt2') await anfrage.zieheZurueck({ customerId: 'c1', projectId: 'pt2', person }) assert.equal((await entscheide({ projectId: 'pt2', entscheidung: 'annehmen' })).status, 409) assert.equal(k24().length, 0, 'bei 409 keine Mail') assert.equal((await entscheide({ projectId: 'pt3', entscheidung: 'annehmen' })).status, 403, 'Projekt eines anderen Kunden') assert.equal((await entscheide({ projectId: 'gibtsnicht', entscheidung: 'annehmen' })).status, 403) assert.equal((await entscheide({ projectId: undefined, entscheidung: 'annehmen' })).status, 400) assert.equal((await entscheide({ entscheidung: 'vielleicht' })).status, 400) assert.equal((await entscheide({ entscheidung: undefined })).status, 400) assert.equal((await entscheide({ entscheidung: 'annehmen' }, staff, 'c2')).status, 403, 'Projekt gehört nicht zum Kunden der URL') }) test('Mailfehler: Entscheidung bleibt, Antwort trägt mailFehler', async () => { await leereAlles() await stelle() mails.versandFehler = true const res = await entscheide({ entscheidung: 'annehmen' }) assert.equal(res.status, 200) assert.equal(res.body.mailFehler, true) assert.equal(res.body.anfrage.status, 'angenommen') assert.equal(shopDoc().anfrageStatus, 'angenommen') assert.equal(k24().length, 0) }) test('zwei gleichzeitige Entscheidungen: genau eine gewinnt, genau eine Mail', async () => { await leereAlles() await stelle() mails.sent.length = 0 const [a, b] = await Promise.all([entscheide({ entscheidung: 'annehmen' }), entscheide({ entscheidung: 'ablehnen', begruendung: 'Nein' })]) assert.deepEqual([a.status, b.status].sort(), [200, 409]) assert.equal(k24().length, 1) assert.equal(shopDoc().anfrageStatus, a.status === 200 ? 'angenommen' : 'abgelehnt') }) test('nach Ablehnung kann der Kunde neu anfragen, die alte Entscheidung ist gelöscht', async () => { await leereAlles() await stelle() await entscheide({ entscheidung: 'ablehnen', begruendung: 'Nein' }) await stelle() const doc = shopDoc() assert.equal(doc.anfrageStatus, 'offen') assert.equal(doc.anfrageBegruendung, '') assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200) })