269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
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 <andrej@webklar.com>')
|
|
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)
|
|
})
|
|
|
|
const ansichtAlt = { ...config.kundenansichtNeu }; after(() => Object.assign(config.kundenansichtNeu, ansichtAlt))
|
|
const neueAnsicht = (an) => { config.kundenansichtNeu.mode = an ? 'pilot' : 'off'; config.kundenansichtNeu.pilotEmails = [customers.c1.email] }
|
|
test('Abo-Modell: Zusage als K42 ohne Preis und ohne „bestellen“, Absage mit Begründung; alte Ansicht bekommt weiter K24 mit 299 €', async () => {
|
|
for (const an of [true, false]) {
|
|
neueAnsicht(an); await leereAlles(); await stelle(); mails.sent.length = 0
|
|
assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
|
|
const m = k24()[0]; assert.equal(m.subject, 'Deine Webshop-Anfrage für Töpferei'); assert.match(m.html, /dashboard\.html#webshop\?projekt=pt1"/)
|
|
if (an) { assert.doesNotMatch(m.text + m.html, /€|UStG|bestell/i); assert.match(m.text, /jetzt im Kundenportal einschalten/) } else { assert.match(m.text, /299 €/); assert.match(m.text, /bestellen/) }
|
|
}
|
|
neueAnsicht(true); await leereAlles(); await stelle(); mails.sent.length = 0
|
|
assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200)
|
|
assert.match(k24()[0].text, /leider[\s\S]*Passt nicht\./); assert.doesNotMatch(k24()[0].text, /299 €/)
|
|
})
|