feat: email receipts and repair estimates
All checks were successful
CI / ci (pull_request) Successful in 24s
CI / e2e (pull_request) Successful in 1m2s

Backend:
- Server-side HTML email templates (receipt + estimate) with inline CSS
- POST /v1/transactions/:id/email-receipt with per-transaction rate limiting
- POST /v1/repair-tickets/:id/email-estimate with per-ticket rate limiting
- customerEmail field added to receipt and ticket detail responses
- Test email provider for API tests (logs instead of sending)

Frontend:
- POS payment dialog Email button enabled with inline email input
- Pre-fills customer email from linked account
- Repair ticket detail page has Email Estimate button with dialog
- Pre-fills from account email

Tests:
- 12 unit tests for email template renderers
- 8 API tests for email receipt/estimate endpoints and validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-05 20:32:52 +00:00
parent 30233b430f
commit 45fd6d34eb
11 changed files with 783 additions and 10 deletions

View File

@@ -87,6 +87,21 @@ class SmtpProvider implements EmailProvider {
}
}
class TestProvider implements EmailProvider {
static lastEmail: SendOpts | null = null
static emails: SendOpts[] = []
async send(opts: SendOpts): Promise<void> {
TestProvider.lastEmail = opts
TestProvider.emails.push(opts)
}
static reset() {
TestProvider.lastEmail = null
TestProvider.emails = []
}
}
export const EmailService = {
async send(db: PostgresJsDatabase<any>, opts: SendOpts): Promise<void> {
const provider = await SettingsService.get(db, 'email.provider')
@@ -99,8 +114,14 @@ export const EmailService = {
return new SendGridProvider(db).send(opts)
case 'smtp':
return new SmtpProvider().send(opts)
case 'test':
return new TestProvider().send(opts)
default:
throw new Error('Email provider not configured. Set email.provider in app_settings.')
}
},
getTestEmails: () => TestProvider.emails,
getLastTestEmail: () => TestProvider.lastEmail,
resetTestEmails: () => TestProvider.reset(),
}