Files
teamwallet/docs/superpowers/plans/2026-07-31-mail-versand.md
2026-07-31 22:22:27 +02:00

24 KiB

Mailversand reparieren + Templates neu gestalten — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Mailversand für Registrierung und Passwort-vergessen im NestJS-Backend (myteamwallet_backend) wieder funktionsfähig machen, das mail-only nestjs-i18n Setup entfernen (Texte direkt auf Deutsch), und die zwei Handlebars-Templates in einem zu "TeamWallet" (Grün #2e7d32) passenden, hübschen Design neu bauen.

Architecture: MailService (@nestjs-modules/mailer + Nodemailer + Handlebars) bleibt die zentrale Versandstelle. Der Bug ist ein totes return; vor sendMail(...) in beiden Methoden — Fix ist ein reiner Code-Fix, keine Config-Änderung nötig (.env ist bereits lokal korrekt befüllt). nestjs-i18n wird komplett entfernt, deutsche Texte wandern direkt in die .hbs-Templates bzw. als String-Literale in mail.service.ts. Die zwei Templates teilen sich ein gemeinsames Handlebars-Block-Partial (partials/layout.hbs) für Header/Footer, um Duplikation zu vermeiden.

Tech Stack: NestJS 9, @nestjs-modules/mailer 1.8.1 (Nodemailer 6.8.0), Handlebars 4.7.7, Jest 29 (Unit-Tests), TypeScript 4.8.

Global Constraints

  • Nur Deutsch — keine mehrsprachige i18n-Infrastruktur für Mails, keine Sprachdateien.
  • nestjs-i18n wird vollständig aus dem Backend entfernt (Modul, Dependency, src/i18n/).
  • Branding: Akzentfarbe #2e7d32 (Grün), Textwordmark „TeamWallet" (kein Bild-Logo), Roboto mit Fallback-Stack Roboto, Helvetica, Arial, sans-serif, abgerundete Card-Optik (~12px Radius), max-width: 600px, Inline-CSS-safe (die HandlebarsAdapter inlined <style>-CSS automatisch via inline-css, inlineCssEnabled ist standardmäßig true).
  • Kein MJML, kein Build-Pipeline-Zusatz für Templates.
  • .env (in myteamwallet_backend/.env) wird nicht verändert — existiert bereits mit gültigen Werten.
  • Kein neuer e2e-/MailDev-Aufbau — Verifikation über Unit-Tests plus einen manuellen echten Testversand (siehe Task 4).

Task 1: Bugfix MailService + Vornamen-Personalisierung

Files:

  • Modify: myteamwallet_backend/src/mail/mail.service.ts
  • Modify: myteamwallet_backend/src/auth/auth.service.ts:191-196 (register) und :246-251 (forgotPassword)
  • Test: myteamwallet_backend/src/mail/mail.service.spec.ts (neu)

Interfaces:

  • Produces: MailService.userSignUp(mailData: MailData<{ hash: string; firstName?: string | null }>): Promise<void>

  • Produces: MailService.forgotPassword(mailData: MailData<{ hash: string; firstName?: string | null }>): Promise<void>

  • Beide senden über this.mailerService.sendMail({ to, subject, text, template, context }) mit context immer inkl. der Keys title, year, firstName, url, actionTitle (Task 3 Templates lesen genau diese Keys).

  • Step 1: Failing Test schreiben

Datei myteamwallet_backend/src/mail/mail.service.spec.ts:

import { ConfigService } from '@nestjs/config';
import { MailerService } from '@nestjs-modules/mailer';
import { MailService } from './mail.service';

describe('MailService', () => {
  let service: MailService;
  let sendMail: jest.Mock;
  let configGet: jest.Mock;

  beforeEach(() => {
    sendMail = jest.fn().mockResolvedValue(undefined);
    configGet = jest.fn().mockReturnValue('https://app.example.com');

    service = new MailService(
      { sendMail } as unknown as MailerService,
      { get: configGet } as unknown as ConfigService,
    );
  });

  it('sends the activation mail with the confirm-email link', async () => {
    await service.userSignUp({
      to: 'user@example.com',
      data: { hash: 'abc123', firstName: 'Max' },
    });

    expect(sendMail).toHaveBeenCalledTimes(1);
    const call = sendMail.mock.calls[0][0];
    expect(call.to).toBe('user@example.com');
    expect(call.template).toBe('activation');
    expect(call.context.url).toBe(
      'https://app.example.com/confirm-email/abc123',
    );
    expect(call.context.firstName).toBe('Max');
  });

  it('sends the reset-password mail with the password-change link', async () => {
    await service.forgotPassword({
      to: 'user@example.com',
      data: { hash: 'xyz789', firstName: 'Erika' },
    });

    expect(sendMail).toHaveBeenCalledTimes(1);
    const call = sendMail.mock.calls[0][0];
    expect(call.to).toBe('user@example.com');
    expect(call.template).toBe('reset-password');
    expect(call.context.url).toBe(
      'https://app.example.com/password-change/xyz789',
    );
    expect(call.context.firstName).toBe('Erika');
  });

  it('works without a firstName (optional personalization)', async () => {
    await service.userSignUp({
      to: 'user@example.com',
      data: { hash: 'abc123' },
    });

    const call = sendMail.mock.calls[0][0];
    expect(call.context.firstName).toBeUndefined();
  });
});
  • Step 2: Test laufen lassen, erwartetes Scheitern bestätigen

Run: cd myteamwallet_backend && npx jest mail.service.spec.ts Expected: FAIL — MailService erwartet aktuell drei Constructor-Parameter (I18n, MailerService, ConfigService), der Test übergibt nur zwei, und selbst bei passendem Aufruf würde sendMail wegen des toten return; nie aufgerufen. Der Test schlägt fehl (z. B. expect(sendMail).toHaveBeenCalledTimes(1) erhält 0, oder ein TypeError beim Zugriff auf this.i18n.t).

  • Step 3: mail.service.ts neu implementieren

Datei myteamwallet_backend/src/mail/mail.service.ts komplett ersetzen:

import { MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MailData } from './interfaces/mail-data.interface';

@Injectable()
export class MailService {
  constructor(
    private mailerService: MailerService,
    private configService: ConfigService,
  ) {}

  async userSignUp(
    mailData: MailData<{ hash: string; firstName?: string | null }>,
  ) {
    const actionTitle = 'E-Mail bestätigen';
    const url = `${this.configService.get('app.frontendDomain')}/confirm-email/${
      mailData.data.hash
    }`;

    await this.mailerService.sendMail({
      to: mailData.to,
      subject: 'Bestätige deine E-Mail-Adresse',
      text: `${url} ${actionTitle}`,
      template: 'activation',
      context: {
        title: 'Bestätige deine E-Mail-Adresse',
        year: new Date().getFullYear(),
        firstName: mailData.data.firstName,
        url,
        actionTitle,
      },
    });
  }

  async forgotPassword(
    mailData: MailData<{ hash: string; firstName?: string | null }>,
  ) {
    const actionTitle = 'Passwort zurücksetzen';
    const url = `${this.configService.get('app.frontendDomain')}/password-change/${
      mailData.data.hash
    }`;

    await this.mailerService.sendMail({
      to: mailData.to,
      subject: actionTitle,
      text: `${url} ${actionTitle}`,
      template: 'reset-password',
      context: {
        title: actionTitle,
        year: new Date().getFullYear(),
        firstName: mailData.data.firstName,
        url,
        actionTitle,
      },
    });
  }
}
  • Step 4: Test laufen lassen, Erfolg bestätigen

Run: cd myteamwallet_backend && npx jest mail.service.spec.ts Expected: PASS — alle 3 Tests grün.

  • Step 5: auth.service.ts — Vornamen mitgeben

In myteamwallet_backend/src/auth/auth.service.ts, die zwei bestehenden Aufrufstellen anpassen (nur die data-Objekte erweitern, sonst nichts ändern):

Zeilen 191-196 (in register(), user ist zu diesem Zeitpunkt bereits erstellt):

    await this.mailService.userSignUp({
      to: user.email,
      data: {
        hash,
        firstName: user.firstName,
      },
    });

Zeilen 246-251 (in forgotPassword(), im else-Zweig nach user lookup):

      await this.mailService.forgotPassword({
        to: email,
        data: {
          hash,
          firstName: user.firstName,
        },
      });
  • Step 6: TypeScript-Build prüfen

Run: cd myteamwallet_backend && npm run build Expected: Build erfolgreich, keine Type-Fehler (insbesondere keine Fehler zu firstName an den beiden Aufrufstellen).

  • Step 7: Commit
git add myteamwallet_backend/src/mail/mail.service.ts myteamwallet_backend/src/mail/mail.service.spec.ts myteamwallet_backend/src/auth/auth.service.ts
git commit -m "fix(mail): remove dead return before sendMail, add firstName personalization"

Task 2: nestjs-i18n vollständig entfernen

Files:

  • Modify: myteamwallet_backend/src/app.module.ts
  • Modify: myteamwallet_backend/src/config/app.config.ts
  • Delete: myteamwallet_backend/src/i18n/ (kompletter Ordner: en/common.json, en/confirm-email.json, en/reset-password.json)
  • Modify: myteamwallet_backend/package.json, myteamwallet_backend/package-lock.json

Interfaces:

  • Consumes: nichts aus Task 1.

  • Produces: keine neuen Symbole — reine Entfernung. Spätere Tasks verlassen sich nicht auf nestjs-i18n.

  • Step 1: src/i18n/ Ordner löschen

Run: cd myteamwallet_backend && rm -rf src/i18n

  • Step 2: app.module.ts bereinigen

In myteamwallet_backend/src/app.module.ts:

Import-Block (Zeilen 1-30) — import * as path from 'path'; (Zeile 9), import { I18nModule } from 'nestjs-i18n/dist/i18n.module'; (Zeile 13) und import { HeaderResolver } from 'nestjs-i18n'; (Zeile 14) entfernen; import { ConfigModule, ConfigService } from '@nestjs/config'; (Zeile 11) zu import { ConfigModule } from '@nestjs/config'; ändern (kein anderer Verbraucher von ConfigService mehr in dieser Datei). Ergebnis:

import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import databaseConfig from './config/database.config';
import authConfig from './config/auth.config';
import appConfig from './config/app.config';
import mailConfig from './config/mail.config';
import fileConfig from './config/file.config';
import { MailerModule } from '@nestjs-modules/mailer';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './database/typeorm-config.service';
import { MailConfigService } from './mail/mail-config.service';
import { ForgotModule } from './forgot/forgot.module';
import { MailModule } from './mail/mail.module';
import { DataSource } from 'typeorm';
import { PlayersModule } from './players/players.module';
import { TeamsModule } from './teams/teams.module';
import { TransactionsModule } from './transactions/transactions.module';
import { TeamSettingsModule } from './team-settings/team-settings.module';
import { TeamWalletTransactionsModule } from './team-wallet-transactions/team-wallet-transactions.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { LoggingModule } from './database/logging/logging.module';
import { TranslateModule } from './translate/translate.module';
import { PenaltyModule } from './penalty/penalty.module';

Im @Module({ imports: [...] }) Array den kompletten I18nModule.forRootAsync({...}) Block (bisher direkt nach MailerModule.forRootAsync({...})) entfernen, sodass MailerModule direkt von ServeStaticModule gefolgt wird:

    MailerModule.forRootAsync({
      useClass: MailConfigService,
    }),
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '../client'),
      exclude: ['*/api*'],
    }),
  • Step 3: app.config.ts bereinigen

myteamwallet_backend/src/config/app.config.ts — Zeilen fallbackLanguage und headerLanguage entfernen (waren ausschließlich für I18nModule gedacht, kein anderer Konsument im Code):

import { registerAs } from '@nestjs/config';

export default registerAs('app', () => ({
  nodeEnv: process.env.NODE_ENV,
  name: process.env.APP_NAME,
  workingDirectory: process.env.PWD || process.cwd(),
  frontendDomain: process.env.FRONTEND_DOMAIN,
  backendDomain: process.env.BACKEND_DOMAIN,
  port: parseInt(process.env.APP_PORT || process.env.PORT, 10) || 3000,
  apiPrefix: process.env.API_PREFIX || 'api',
}));
  • Step 4: Dependency entfernen

Run: cd myteamwallet_backend && npm uninstall nestjs-i18n Expected: package.json und package-lock.json werden automatisch aktualisiert (Eintrag "nestjs-i18n": "9.2.2" verschwindet aus dependencies).

  • Step 5: Keine verbleibenden Referenzen prüfen

Run: cd myteamwallet_backend && grep -rn "nestjs-i18n\|I18nModule\|HeaderResolver\|I18nRequestScopeService" src/ Expected: keine Treffer (leere Ausgabe).

  • Step 6: Build prüfen

Run: cd myteamwallet_backend && npm run build Expected: Build erfolgreich, keine Fehler zu fehlenden nestjs-i18n-Imports oder unbenutzten Imports.

  • Step 7: Bestehende Unit-Tests laufen lassen

Run: cd myteamwallet_backend && npm test Expected: alle Tests grün, inkl. der in Task 1 hinzugefügten mail.service.spec.ts.

  • Step 8: Commit
git add myteamwallet_backend/src/app.module.ts myteamwallet_backend/src/config/app.config.ts myteamwallet_backend/package.json myteamwallet_backend/package-lock.json
git status
git add myteamwallet_backend/src/i18n
git commit -m "chore(mail): remove nestjs-i18n, only ever used for the two mail templates"

(Der zweite git add erfasst die Löschung von src/i18n/* — je nach Git-Version reicht auch ein einzelnes git add -A myteamwallet_backend/src/i18n myteamwallet_backend/src/app.module.ts myteamwallet_backend/src/config/app.config.ts myteamwallet_backend/package.json myteamwallet_backend/package-lock.json.)


Task 3: E-Mail-Templates neu gestalten

Files:

  • Modify: myteamwallet_backend/src/mail/mail-config.service.ts
  • Create: myteamwallet_backend/src/mail/mail-templates/partials/layout.hbs
  • Modify: myteamwallet_backend/src/mail/mail-templates/activation.hbs
  • Modify: myteamwallet_backend/src/mail/mail-templates/reset-password.hbs
  • Test: myteamwallet_backend/src/mail/mail-templates/mail-templates.spec.ts (neu)

Interfaces:

  • Consumes: Context-Keys aus Task 1 — title, year, firstName, url, actionTitle.

  • Produces: registriertes Handlebars-Partial layout (Name = Dateiname ohne Endung, da es direkt in partials/ liegt), eingebunden via {{#> layout}} ... {{/layout}} in beiden Content-Templates.

  • Step 1: Failing Test schreiben

Datei myteamwallet_backend/src/mail/mail-templates/mail-templates.spec.ts:

import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';

describe('mail templates rendering', () => {
  const templatesDir = __dirname;

  beforeAll(() => {
    const layoutSource = fs.readFileSync(
      path.join(templatesDir, 'partials', 'layout.hbs'),
      'utf-8',
    );
    Handlebars.registerPartial('layout', layoutSource);
  });

  const baseContext = {
    title: 'Test-Betreff',
    year: 2026,
    firstName: 'Max',
    url: 'https://app.example.com/confirm-email/abc123',
    actionTitle: 'Jetzt bestätigen',
  };

  it('renders activation.hbs with greeting, link and button text', () => {
    const source = fs.readFileSync(
      path.join(templatesDir, 'activation.hbs'),
      'utf-8',
    );
    const html = Handlebars.compile(source, { strict: true })(baseContext);

    expect(html).toContain('TeamWallet');
    expect(html).toContain('Hallo Max,');
    expect(html).toContain(baseContext.url);
    expect(html).toContain(baseContext.actionTitle);
  });

  it('renders reset-password.hbs with greeting, link and button text', () => {
    const source = fs.readFileSync(
      path.join(templatesDir, 'reset-password.hbs'),
      'utf-8',
    );
    const html = Handlebars.compile(source, { strict: true })(baseContext);

    expect(html).toContain('TeamWallet');
    expect(html).toContain('Hallo Max,');
    expect(html).toContain(baseContext.url);
    expect(html).toContain(baseContext.actionTitle);
  });

  it('falls back to a generic greeting when firstName is missing', () => {
    const source = fs.readFileSync(
      path.join(templatesDir, 'activation.hbs'),
      'utf-8',
    );
    const html = Handlebars.compile(source, { strict: true })({
      ...baseContext,
      firstName: undefined,
    });

    expect(html).toContain('Hallo,');
    expect(html).not.toContain('Hallo Max,');
  });
});
  • Step 2: Test laufen lassen, erwartetes Scheitern bestätigen

Run: cd myteamwallet_backend && npx jest mail-templates.spec.ts Expected: FAIL — partials/layout.hbs existiert noch nicht (ENOENT), bzw. die bestehenden activation.hbs/reset-password.hbs enthalten weder „TeamWallet" noch „Hallo Max,".

  • Step 3: Layout-Partial erstellen

Datei myteamwallet_backend/src/mail/mail-templates/partials/layout.hbs:

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<style>
  body { margin: 0; padding: 0; background: #f4f6f4; font-family: Roboto, Helvetica, Arial, sans-serif; }
  .tw-container { max-width: 600px; margin: 0 auto; padding: 32px 16px; }
  .tw-card { background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); }
  .tw-header { background: #2e7d32; padding: 28px 32px; text-align: center; }
  .tw-wordmark { color: #ffffff; font-size: 22px; font-weight: 700; letter-spacing: 0.5px; }
  .tw-body { padding: 32px; color: #20251f; font-size: 15px; line-height: 1.6; }
  .tw-body p { margin: 0 0 16px; }
  .tw-button-row { text-align: center; padding: 8px 0 24px; }
  .tw-button { display: inline-block; background: #2e7d32; color: #ffffff !important; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-weight: 600; font-size: 15px; }
  .tw-footer { text-align: center; padding: 20px 16px 0; color: #8a938a; font-size: 12px; line-height: 1.6; }
  .tw-footer a { color: #8a938a; }
</style>
</head>
<body>
  <div class="tw-container">
    <div class="tw-card">
      <div class="tw-header">
        <span class="tw-wordmark">TeamWallet</span>
      </div>
      <div class="tw-body">
        {{> @partial-block }}
      </div>
    </div>
    <div class="tw-footer">
      <p>Diese E-Mail wurde automatisch von TeamWallet verschickt.<br>&copy; {{year}} TeamWallet</p>
    </div>
  </div>
</body>
</html>
  • Step 4: activation.hbs neu gestalten

Datei myteamwallet_backend/src/mail/mail-templates/activation.hbs komplett ersetzen:

{{#> layout}}
<p>Hallo{{#if firstName}} {{firstName}}{{/if}},</p>
<p>schön, dass du bei TeamWallet dabei bist! Bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren.</p>
<div class="tw-button-row">
  <a class="tw-button" href="{{url}}">{{actionTitle}}</a>
</div>
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
{{/layout}}
  • Step 5: reset-password.hbs neu gestalten

Datei myteamwallet_backend/src/mail/mail-templates/reset-password.hbs komplett ersetzen:

{{#> layout}}
<p>Hallo{{#if firstName}} {{firstName}}{{/if}},</p>
<p>du hast angefragt, dein TeamWallet-Passwort zurückzusetzen. Klicke auf den Button, um ein neues Passwort zu vergeben.</p>
<div class="tw-button-row">
  <a class="tw-button" href="{{url}}">{{actionTitle}}</a>
</div>
<p>Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren — es wird nichts verändert.</p>
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
{{/layout}}
  • Step 6: Partials-Verzeichnis in mail-config.service.ts registrieren

In myteamwallet_backend/src/mail/mail-config.service.ts den template.options Block erweitern (partials.dir zeigt auf den neuen Ordner, damit HandlebarsAdapter die .hbs Dateien darin beim Versand automatisch als Partials lädt):

  createMailerOptions(): MailerOptions {
    return {
      transport: {
        host: this.configService.get('mail.host'),
        port: this.configService.get('mail.port'),
        ignoreTLS: this.configService.get('mail.ignoreTLS'),
        secure: this.configService.get('mail.secure'),
        requireTLS: this.configService.get('mail.requireTLS'),
        auth: {
          user: this.configService.get('mail.user'),
          pass: this.configService.get('mail.password'),
        },
      },
      defaults: {
        from: `"${this.configService.get(
          'mail.defaultName',
        )}" <${this.configService.get('mail.defaultEmail')}>`,
      },
      template: {
        dir: path.join(
          this.configService.get('app.workingDirectory'),
          'src',
          'mail',
          'mail-templates',
        ),
        adapter: new HandlebarsAdapter(),
        options: {
          strict: true,
          partials: {
            dir: path.join(
              this.configService.get('app.workingDirectory'),
              'src',
              'mail',
              'mail-templates',
              'partials',
            ),
          },
        },
      },
    } as MailerOptions;
  }
  • Step 7: Test laufen lassen, Erfolg bestätigen

Run: cd myteamwallet_backend && npx jest mail-templates.spec.ts Expected: PASS — alle 3 Tests grün.

  • Step 8: Alle Unit-Tests + Build

Run: cd myteamwallet_backend && npm test && npm run build Expected: alle Tests grün, Build erfolgreich.

  • Step 9: Commit
git add myteamwallet_backend/src/mail/mail-config.service.ts myteamwallet_backend/src/mail/mail-templates
git commit -m "feat(mail): redesign email templates with TeamWallet branding and shared layout partial"

Task 4: Manuelle Verifikation mit echtem Versand

Kein Code-Task — Nachweis, dass Registrierung und Passwort-vergessen tatsächlich E-Mails verschicken (Strato-SMTP, kein lokaler MailDev vorhanden, siehe Spec).

Files: keine.

  • Step 1: Backend lokal starten

Voraussetzung: lokale MySQL-Instanz läuft (Container brave_einstein, Port 3306, bereits aktiv laut docker ps), myteamwallet_backend/.env unverändert vorhanden.

Run: cd myteamwallet_backend && npm run start:dev Expected: Server startet ohne Fehler auf Port 3999 (kein Absturz durch die entfernten nestjs-i18n-Imports, keine MailerModule-Config-Fehler).

  • Step 2: Registrierung auslösen (Aktivierungsmail)

In einem zweiten Terminal, mit einer echten, von dir kontrollierten Test-Adresse:

curl -X POST http://localhost:3999/api/v1/auth/email/register \
  -H "Content-Type: application/json" \
  -d '{"email":"DEINE-TEST-ADRESSE@example.com","password":"Test1234!","firstName":"Max","lastName":"Mustermann"}'

Expected: HTTP 201, und innerhalb kurzer Zeit trifft eine E-Mail „Bestätige deine E-Mail-Adresse" mit grünem TeamWallet-Header, Begrüßung „Hallo Max," und funktionierendem Bestätigungs-Button in der Test-Mailbox ein.

  • Step 3: Passwort-vergessen auslösen
curl -X POST http://localhost:3999/api/v1/auth/forgot/password \
  -H "Content-Type: application/json" \
  -d '{"email":"DEINE-TEST-ADRESSE@example.com"}'

Expected: HTTP 204/200 (je nach Response des Endpoints), und eine E-Mail „Passwort zurücksetzen" mit gleichem Layout trifft ein.

  • Step 4: Server stoppen

npm run start:dev Prozess beenden (Ctrl+C).

  • Step 5: Ergebnis festhalten

Kein Commit nötig — dies ist ein manueller Verifikationsschritt. Falls eine der beiden Mails nicht ankommt, zurück zu systematic-debugging (SMTP-Verbindung, Firewall, Spam-Ordner prüfen) bevor der Task als abgeschlossen gilt.


Self-Review

  • Spec coverage: Bugfix (Task 1), i18n-Entfernung (Task 2), Template-Redesign inkl. Branding/Partial (Task 3), Personalisierung (Task 1), Testing-Strategie laut korrigiertem Spec — Unit-Tests statt e2e/MailDev (Task 1 + 3), manueller Realversand (Task 4) — alles abgedeckt. .env explizit als "keine Aktion" markiert (Global Constraints), passend zum korrigierten Spec.
  • Placeholder-Scan: keine TBD/TODO, jeder Step enthält vollständigen Code.
  • Typ-Konsistenz: MailData<{ hash: string; firstName?: string | null }> konsistent in mail.service.ts (Task 1) und den Aufrufstellen in auth.service.ts (Task 1) verwendet; Context-Keys title/year/firstName/url/actionTitle konsistent zwischen mail.service.ts (Task 1) und den Templates (Task 3).