feat: add Cloud Functions scaffold with tested email builders
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,6 +12,10 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Cloud Functions
|
||||
functions/lib
|
||||
functions/node_modules
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
4228
functions/package-lock.json
generated
Normal file
4228
functions/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
functions/package.json
Normal file
20
functions/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "functions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": { "node": "20" },
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase-admin": "^12.0.0",
|
||||
"firebase-functions": "^5.0.0",
|
||||
"resend": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
53
functions/src/email.test.ts
Normal file
53
functions/src/email.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { buildBookingNotificationEmail, buildInquiryNotificationEmail } from './email.js'
|
||||
|
||||
describe('buildBookingNotificationEmail', () => {
|
||||
it('includes the service, date, time, and contact details', () => {
|
||||
const { subject, html } = buildBookingNotificationEmail({
|
||||
serviceName: 'Knotless Braids',
|
||||
date: '2026-09-01',
|
||||
time: '10:00 AM',
|
||||
contact: { name: 'Jane Doe', phone: '555-0100', email: 'jane@example.com', notes: 'First time client' },
|
||||
})
|
||||
expect(subject).toBe('New booking: Knotless Braids on 2026-09-01')
|
||||
expect(html).toContain('Jane Doe')
|
||||
expect(html).toContain('Knotless Braids')
|
||||
expect(html).toContain('2026-09-01')
|
||||
expect(html).toContain('10:00 AM')
|
||||
expect(html).toContain('555-0100')
|
||||
expect(html).toContain('jane@example.com')
|
||||
expect(html).toContain('First time client')
|
||||
})
|
||||
|
||||
it('omits the notes paragraph when notes are empty', () => {
|
||||
const { html } = buildBookingNotificationEmail({
|
||||
serviceName: 'Cornrows',
|
||||
date: '2026-09-02',
|
||||
time: '1:00 PM',
|
||||
contact: { name: 'Amy', phone: '555-0101', email: 'amy@example.com', notes: '' },
|
||||
})
|
||||
expect(html).not.toContain('Notes:')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildInquiryNotificationEmail', () => {
|
||||
it('includes the product and contact details', () => {
|
||||
const { subject, html } = buildInquiryNotificationEmail({
|
||||
productName: 'Silk Edge Control',
|
||||
contact: { name: 'Nadia', email: 'nadia@example.com', notes: 'Is this in stock?' },
|
||||
})
|
||||
expect(subject).toBe('New inquiry: Silk Edge Control')
|
||||
expect(html).toContain('Nadia')
|
||||
expect(html).toContain('nadia@example.com')
|
||||
expect(html).toContain('Silk Edge Control')
|
||||
expect(html).toContain('Is this in stock?')
|
||||
})
|
||||
|
||||
it('omits the notes paragraph when notes are empty', () => {
|
||||
const { html } = buildInquiryNotificationEmail({
|
||||
productName: 'Braid Oil',
|
||||
contact: { name: 'Kim', email: 'kim@example.com', notes: '' },
|
||||
})
|
||||
expect(html).not.toContain('Notes:')
|
||||
})
|
||||
})
|
||||
28
functions/src/email.ts
Normal file
28
functions/src/email.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface BookingEmailInput {
|
||||
serviceName: string
|
||||
date: string
|
||||
time: string
|
||||
contact: { name: string; phone: string; email: string; notes: string }
|
||||
}
|
||||
|
||||
export function buildBookingNotificationEmail(input: BookingEmailInput): { subject: string; html: string } {
|
||||
const notesLine = input.contact.notes ? `<p>Notes: ${input.contact.notes}</p>` : ''
|
||||
return {
|
||||
subject: `New booking: ${input.serviceName} on ${input.date}`,
|
||||
html: `<p><strong>${input.contact.name}</strong> booked <strong>${input.serviceName}</strong> on ${input.date} at ${input.time}.</p>` +
|
||||
`<p>Phone: ${input.contact.phone}<br/>Email: ${input.contact.email}</p>${notesLine}`,
|
||||
}
|
||||
}
|
||||
|
||||
export interface InquiryEmailInput {
|
||||
productName: string
|
||||
contact: { name: string; email: string; notes: string }
|
||||
}
|
||||
|
||||
export function buildInquiryNotificationEmail(input: InquiryEmailInput): { subject: string; html: string } {
|
||||
const notesLine = input.contact.notes ? `<p>Notes: ${input.contact.notes}</p>` : ''
|
||||
return {
|
||||
subject: `New inquiry: ${input.productName}`,
|
||||
html: `<p><strong>${input.contact.name}</strong> (${input.contact.email}) asked about <strong>${input.productName}</strong>.</p>${notesLine}`,
|
||||
}
|
||||
}
|
||||
13
functions/tsconfig.json
Normal file
13
functions/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "es2022",
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
8
functions/vitest.config.ts
Normal file
8
functions/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user