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:
belviskhoremk
2026-08-08 13:12:30 +00:00
parent aacb9dca12
commit 6e615113a7
5 changed files with 101 additions and 0 deletions

View 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')
})

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}`,
}
}

View File

@@ -0,0 +1,5 @@
{
"tasks": {
"test": "deno test --allow-none"
}
}

18
supabase/functions/deno.lock generated Normal file
View 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"
}
}
}

View File

@@ -14,6 +14,7 @@ export default defineConfig({
'**/cypress/**', '**/cypress/**',
'**/.{idea,git,cache,output,temp}/**', '**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
'supabase/functions/**',
], ],
}, },
}) })