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
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Cloud Functions
|
||||||
|
functions/lib
|
||||||
|
functions/node_modules
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env.local
|
.env.local
|
||||||
.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