feat: add Cloud Functions scaffold with tested email builders

This commit is contained in:
belviskhoremk
2026-08-05 00:15:45 +00:00
parent ffd31ca1da
commit 9c47805f37
7 changed files with 4354 additions and 0 deletions

4
.gitignore vendored
View File

@@ -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

File diff suppressed because it is too large Load Diff

20
functions/package.json Normal file
View 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"
}
}

View 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
View 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
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"outDir": "lib",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
},
})