From 6e615113a7ed3d889c97440a0a977df20a23cd7b Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Sat, 8 Aug 2026 13:12:30 +0000 Subject: [PATCH] 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. --- supabase/functions/_shared/email.test.ts | 49 ++++++++++++++++++++++++ supabase/functions/_shared/email.ts | 28 ++++++++++++++ supabase/functions/deno.json | 5 +++ supabase/functions/deno.lock | 18 +++++++++ vite.config.ts | 1 + 5 files changed, 101 insertions(+) create mode 100644 supabase/functions/_shared/email.test.ts create mode 100644 supabase/functions/_shared/email.ts create mode 100644 supabase/functions/deno.json create mode 100644 supabase/functions/deno.lock diff --git a/supabase/functions/_shared/email.test.ts b/supabase/functions/_shared/email.test.ts new file mode 100644 index 0000000..9961f06 --- /dev/null +++ b/supabase/functions/_shared/email.test.ts @@ -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') +}) diff --git a/supabase/functions/_shared/email.ts b/supabase/functions/_shared/email.ts new file mode 100644 index 0000000..cf8eb89 --- /dev/null +++ b/supabase/functions/_shared/email.ts @@ -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 ? `

Notes: ${input.contact.notes}

` : '' + return { + subject: `New booking: ${input.serviceName} on ${input.date}`, + html: `

${input.contact.name} booked ${input.serviceName} on ${input.date} at ${input.time}.

` + + `

Phone: ${input.contact.phone}
Email: ${input.contact.email}

${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 ? `

Notes: ${input.contact.notes}

` : '' + return { + subject: `New inquiry: ${input.productName}`, + html: `

${input.contact.name} (${input.contact.email}) asked about ${input.productName}.

${notesLine}`, + } +} diff --git a/supabase/functions/deno.json b/supabase/functions/deno.json new file mode 100644 index 0000000..8ab0875 --- /dev/null +++ b/supabase/functions/deno.json @@ -0,0 +1,5 @@ +{ + "tasks": { + "test": "deno test --allow-none" + } +} diff --git a/supabase/functions/deno.lock b/supabase/functions/deno.lock new file mode 100644 index 0000000..a4437a0 --- /dev/null +++ b/supabase/functions/deno.lock @@ -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" + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 32ba018..5bc4af7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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/**', ], }, })