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>
145 lines
5.1 KiB
TypeScript
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 & 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')
|
|
})
|
|
})
|