feat: add Edge Functions workspace with tested email builders
Adds supabase/functions/ (deno.json, _shared/email.ts) with pure buildBookingNotificationEmail/buildInquiryNotificationEmail functions for Task 6's Edge Functions, tested via Deno's test runner. Excludes supabase/functions/** from the root Vitest config, which otherwise tried to resolve the jsr: import and failed.
This commit is contained in:
49
supabase/functions/_shared/email.test.ts
Normal file
49
supabase/functions/_shared/email.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { assertEquals, assertStringIncludes } from 'jsr:@std/assert'
|
||||
import { buildBookingNotificationEmail, buildInquiryNotificationEmail } from './email.ts'
|
||||
|
||||
Deno.test('buildBookingNotificationEmail 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' },
|
||||
})
|
||||
assertEquals(subject, 'New booking: Knotless Braids on 2026-09-01')
|
||||
assertStringIncludes(html, 'Jane Doe')
|
||||
assertStringIncludes(html, 'Knotless Braids')
|
||||
assertStringIncludes(html, '2026-09-01')
|
||||
assertStringIncludes(html, '10:00 AM')
|
||||
assertStringIncludes(html, '555-0100')
|
||||
assertStringIncludes(html, 'jane@example.com')
|
||||
assertStringIncludes(html, 'First time client')
|
||||
})
|
||||
|
||||
Deno.test('buildBookingNotificationEmail 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: '' },
|
||||
})
|
||||
if (html.includes('Notes:')) throw new Error('expected no Notes: paragraph')
|
||||
})
|
||||
|
||||
Deno.test('buildInquiryNotificationEmail 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?' },
|
||||
})
|
||||
assertEquals(subject, 'New inquiry: Silk Edge Control')
|
||||
assertStringIncludes(html, 'Nadia')
|
||||
assertStringIncludes(html, 'nadia@example.com')
|
||||
assertStringIncludes(html, 'Silk Edge Control')
|
||||
assertStringIncludes(html, 'Is this in stock?')
|
||||
})
|
||||
|
||||
Deno.test('buildInquiryNotificationEmail omits the notes paragraph when notes are empty', () => {
|
||||
const { html } = buildInquiryNotificationEmail({
|
||||
productName: 'Braid Oil',
|
||||
contact: { name: 'Kim', email: 'kim@example.com', notes: '' },
|
||||
})
|
||||
if (html.includes('Notes:')) throw new Error('expected no Notes: paragraph')
|
||||
})
|
||||
28
supabase/functions/_shared/email.ts
Normal file
28
supabase/functions/_shared/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}`,
|
||||
}
|
||||
}
|
||||
5
supabase/functions/deno.json
Normal file
5
supabase/functions/deno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tasks": {
|
||||
"test": "deno test --allow-none"
|
||||
}
|
||||
}
|
||||
18
supabase/functions/deno.lock
generated
Normal file
18
supabase/functions/deno.lock
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@*": "1.0.19",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.14"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.19": {
|
||||
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.14": {
|
||||
"integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
||||
'**/cypress/**',
|
||||
'**/.{idea,git,cache,output,temp}/**',
|
||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
||||
'supabase/functions/**',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user