Files
lunarfront-app/packages/backend/__tests__/utils/email-templates.test.ts
ryan 45fd6d34eb
All checks were successful
CI / ci (pull_request) Successful in 24s
CI / e2e (pull_request) Successful in 1m2s
feat: email receipts and repair estimates
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>
2026-04-05 20:32:52 +00:00

145 lines
5.1 KiB
TypeScript

import { describe, it, expect } from 'bun:test'
import {
renderReceiptEmailHtml,
renderReceiptEmailText,
renderEstimateEmailHtml,
renderEstimateEmailText,
} from '../../src/utils/email-templates.js'
const mockReceipt = {
transaction: {
transactionNumber: 'TXN-20260405-001',
subtotal: '100.00',
discountTotal: '10.00',
taxTotal: '7.43',
total: '97.43',
paymentMethod: 'cash',
amountTendered: '100.00',
changeGiven: '2.57',
roundingAdjustment: null,
completedAt: '2026-04-05T12:00:00Z',
lineItems: [
{ description: 'Guitar Strings', qty: 2, unitPrice: '12.99', taxAmount: '2.14', lineTotal: '25.98', discountAmount: null },
{ description: 'Tuner', qty: 1, unitPrice: '74.02', taxAmount: '5.29', lineTotal: '74.02', discountAmount: null },
],
},
company: { name: 'Test Store', phone: '555-1234', email: 'store@test.com', address: { street: '123 Main St', city: 'Austin', state: 'TX', zip: '78701' } },
location: null,
}
const mockTicket = {
ticketNumber: 'RPR-001',
customerName: 'Jane Doe',
customerPhone: '555-5678',
itemDescription: 'Acoustic Guitar',
serialNumber: 'AG-12345',
problemDescription: 'Cracked neck joint',
estimatedCost: '250.00',
promisedDate: '2026-04-12',
status: 'pending_approval',
}
const mockLineItems = [
{ itemType: 'labor', description: 'Neck repair', qty: 1, unitPrice: '150.00', totalPrice: '150.00' },
{ itemType: 'part', description: 'Wood glue & clamps', qty: 1, unitPrice: '25.00', totalPrice: '25.00' },
{ itemType: 'flat_rate', description: 'Setup & restring', qty: 1, unitPrice: '75.00', totalPrice: '75.00' },
]
const mockCompany = { name: 'Test Store', phone: '555-1234', email: 'store@test.com', address: null }
describe('renderReceiptEmailHtml', () => {
it('renders HTML with transaction details', () => {
const html = renderReceiptEmailHtml(mockReceipt as any)
expect(html).toContain('TXN-20260405-001')
expect(html).toContain('Test Store')
expect(html).toContain('Guitar Strings')
expect(html).toContain('Tuner')
expect(html).toContain('$97.43')
expect(html).toContain('$100.00')
expect(html).toContain('Powered by LunarFront')
})
it('includes discount when present', () => {
const html = renderReceiptEmailHtml(mockReceipt as any)
expect(html).toContain('Discount')
expect(html).toContain('$10.00')
})
it('includes payment details for cash', () => {
const html = renderReceiptEmailHtml(mockReceipt as any)
expect(html).toContain('Cash')
expect(html).toContain('$2.57')
})
it('includes receipt config when provided', () => {
const config = { receipt_footer: 'Thank you!', receipt_return_policy: '30-day returns' }
const html = renderReceiptEmailHtml(mockReceipt as any, config)
expect(html).toContain('Thank you!')
expect(html).toContain('30-day returns')
})
it('includes company address', () => {
const html = renderReceiptEmailHtml(mockReceipt as any)
expect(html).toContain('123 Main St')
expect(html).toContain('Austin')
})
})
describe('renderReceiptEmailText', () => {
it('renders plain text with transaction details', () => {
const text = renderReceiptEmailText(mockReceipt as any)
expect(text).toContain('TXN-20260405-001')
expect(text).toContain('Guitar Strings')
expect(text).toContain('Total: $97.43')
expect(text).toContain('Cash')
})
})
describe('renderEstimateEmailHtml', () => {
it('renders HTML with ticket details', () => {
const html = renderEstimateEmailHtml(mockTicket as any, mockLineItems as any, mockCompany as any)
expect(html).toContain('RPR-001')
expect(html).toContain('Jane Doe')
expect(html).toContain('Acoustic Guitar')
expect(html).toContain('AG-12345')
expect(html).toContain('Cracked neck joint')
expect(html).toContain('$250.00')
expect(html).toContain('Powered by LunarFront')
})
it('renders line items with types', () => {
const html = renderEstimateEmailHtml(mockTicket as any, mockLineItems as any, mockCompany as any)
expect(html).toContain('Labor')
expect(html).toContain('Neck repair')
expect(html).toContain('Part')
expect(html).toContain('Flat Rate')
expect(html).toContain('Setup &amp; restring')
})
it('includes promised date', () => {
const html = renderEstimateEmailHtml(mockTicket as any, mockLineItems as any, mockCompany as any)
expect(html).toContain('Estimated completion')
expect(html).toContain('Apr')
})
it('renders without line items using estimatedCost', () => {
const html = renderEstimateEmailHtml(mockTicket as any, [], mockCompany as any)
expect(html).toContain('$250.00')
})
it('includes company phone', () => {
const html = renderEstimateEmailHtml(mockTicket as any, mockLineItems as any, mockCompany as any)
expect(html).toContain('555-1234')
})
})
describe('renderEstimateEmailText', () => {
it('renders plain text with ticket details', () => {
const text = renderEstimateEmailText(mockTicket as any, mockLineItems as any, mockCompany as any)
expect(text).toContain('RPR-001')
expect(text).toContain('Jane Doe')
expect(text).toContain('Neck repair')
expect(text).toContain('$250.00')
})
})