feat(admin): eigene Vorlage fuer die Willkommensmail mit Platzhaltern, Vorschau und Testmail
Administrator -> Willkommensmail: Betreff, Ueberschrift, Einleitung, Abschluss je
Mandant (Tabelle WelcomeMailTemplate, RLS je Mandant, Migration 20260930150000);
Platzhalter {{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}},
unbekannte -> 400 bzw. Hinweis beim Tippen; Werte escaped, Vorlage reiner Text.
Live-Vorschau per API gerendert, Testmail an die eigene Adresse ohne Token,
Zuruecksetzen auf Standard. Feste Bausteine (Kopf, Zugangsdaten, Anmeldehinweis,
Knoepfe, Fusszeile) bleiben immer drin.
Kopf: Wellenzelle dunkel statt weiss, Streifen 600x40, Inhalt 24 px naeher –
keine weisse Luecke, wenn OWA das CID-Bild nicht zeigt.
Lokal nachgewiesen: Hinweis/Sperre bei {{xyz}}, Speichern, Testmail (Link nur
/login), echte Mail mit eigener Vorlage und 7-Tage-Link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { DEFAULT_WELCOME_MAIL_TEXTS } from '@tessera/shared';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { WelcomeMailTemplateService } from './welcome-mail-template.service';
|
||||
|
||||
/**
|
||||
* WelcomeMailTemplateService — Speicherung der eigenen Vorlage je Mandant.
|
||||
*
|
||||
* Festgenagelt: jeder Zugriff gebunden an den uebergebenen Mandanten
|
||||
* (`forTenant`), Standardtexte ohne Vorlage, Speichern als upsert auf
|
||||
* `tenantId`, Zuruecksetzen loescht die Vorlage.
|
||||
*/
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__bound(tenantId)),
|
||||
}));
|
||||
|
||||
type Row = {
|
||||
tenantId: string;
|
||||
subject: string;
|
||||
heading: string;
|
||||
intro: string;
|
||||
closing: string;
|
||||
updatedBy: string | null;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
function makePrisma(rows: Row[] = []) {
|
||||
const log: { tenantId: string; model: string; method: string; args: any }[] = [];
|
||||
return {
|
||||
__log: log,
|
||||
__rows: rows,
|
||||
__bound(tenantId: string) {
|
||||
return {
|
||||
welcomeMailTemplate: {
|
||||
findUnique: vi.fn(async (args: any) => {
|
||||
log.push({ tenantId, model: 'welcomeMailTemplate', method: 'findUnique', args });
|
||||
// RLS-Nachbildung: nur Zeilen des gebundenen Mandanten sichtbar
|
||||
return (
|
||||
rows.find((r) => r.tenantId === args.where.tenantId && r.tenantId === tenantId) ??
|
||||
null
|
||||
);
|
||||
}),
|
||||
upsert: vi.fn(async (args: any) => {
|
||||
log.push({ tenantId, model: 'welcomeMailTemplate', method: 'upsert', args });
|
||||
const existing = rows.find((r) => r.tenantId === args.where.tenantId);
|
||||
if (existing) Object.assign(existing, args.update, { updatedAt: new Date() });
|
||||
else rows.push({ ...args.create, updatedAt: new Date() });
|
||||
return {};
|
||||
}),
|
||||
deleteMany: vi.fn(async (args: any) => {
|
||||
log.push({ tenantId, model: 'welcomeMailTemplate', method: 'deleteMany', args });
|
||||
const before = rows.length;
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
if (rows[i].tenantId === args.where.tenantId) rows.splice(i, 1);
|
||||
}
|
||||
return { count: before - rows.length };
|
||||
}),
|
||||
},
|
||||
tenant: {
|
||||
findUnique: vi.fn(async (args: any) => {
|
||||
log.push({ tenantId, model: 'tenant', method: 'findUnique', args });
|
||||
return args.where.id === 't1' ? { name: 'Beispiel GmbH' } : null;
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const texts = { subject: 'S {{name}}', heading: 'H', intro: 'I', closing: 'C' };
|
||||
|
||||
let prisma: ReturnType<typeof makePrisma>;
|
||||
let service: WelcomeMailTemplateService;
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = makePrisma();
|
||||
service = new WelcomeMailTemplateService(prisma as any);
|
||||
});
|
||||
|
||||
describe('WelcomeMailTemplateService', () => {
|
||||
it('ohne Vorlage: Standardtexte, custom=false, getCustomTexts=null', async () => {
|
||||
const state = await service.getState('t1');
|
||||
expect(state.custom).toBe(false);
|
||||
expect(state.texts).toEqual(DEFAULT_WELCOME_MAIL_TEXTS);
|
||||
expect(state.defaults).toEqual(DEFAULT_WELCOME_MAIL_TEXTS);
|
||||
expect(await service.getCustomTexts('t1')).toBeNull();
|
||||
expect(prisma.__log.every((c) => c.tenantId === 't1')).toBe(true);
|
||||
});
|
||||
|
||||
it('speichern: upsert auf tenantId, gebunden, mit Bearbeiter; danach custom=true', async () => {
|
||||
const state = await service.save('t1', texts, 'ada');
|
||||
const upsert = prisma.__log.find((c) => c.method === 'upsert');
|
||||
expect(upsert?.tenantId).toBe('t1');
|
||||
expect(upsert?.args.where).toEqual({ tenantId: 't1' });
|
||||
expect(upsert?.args.create).toEqual({ tenantId: 't1', ...texts, updatedBy: 'ada' });
|
||||
expect(state.custom).toBe(true);
|
||||
expect(state.texts).toEqual(texts);
|
||||
expect(state.updatedBy).toBe('ada');
|
||||
expect(await service.getCustomTexts('t1')).toEqual(texts);
|
||||
});
|
||||
|
||||
it('Mandantenbindung: Vorlage von t1 ist fuer t2 nicht sichtbar', async () => {
|
||||
await service.save('t1', texts, 'ada');
|
||||
expect(await service.getCustomTexts('t2')).toBeNull();
|
||||
expect(prisma.__log.filter((c) => c.method === 'findUnique').at(-1)?.tenantId).toBe('t2');
|
||||
});
|
||||
|
||||
it('zuruecksetzen: loescht gebunden, danach wieder Standard', async () => {
|
||||
await service.save('t1', texts, 'ada');
|
||||
const state = await service.reset('t1');
|
||||
const del = prisma.__log.find((c) => c.method === 'deleteMany');
|
||||
expect(del?.tenantId).toBe('t1');
|
||||
expect(del?.args.where).toEqual({ tenantId: 't1' });
|
||||
expect(state.custom).toBe(false);
|
||||
expect(state.texts).toEqual(DEFAULT_WELCOME_MAIL_TEXTS);
|
||||
});
|
||||
|
||||
it('Firmenname gebunden gelesen; unbekannter Mandant → leer', async () => {
|
||||
expect(await service.getTenantName('t1')).toBe('Beispiel GmbH');
|
||||
expect(await service.getTenantName('tx')).toBe('');
|
||||
expect(prisma.__log.filter((c) => c.model === 'tenant').map((c) => c.tenantId)).toEqual([
|
||||
't1',
|
||||
'tx',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user