Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send email on rsvp #42

Merged
merged 2 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ NOTION_TOKEN=
DATABASE_ID=
PORT=3000

ADMIN_EMAIL=

CALENDAR_URL=

GOOGLE_CLOUD_PROJECT=
Expand Down
28 changes: 27 additions & 1 deletion __tests__/controllers/rsvpController.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RsvpController from '../../src/controllers/rsvpController'
import { beforeEach, describe, expect, it } from '@jest/globals'
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
import { getMockReq, getMockRes } from '@jest-mock/express'
import { Timestamp, addDoc } from 'firebase/firestore'
import { FirebaseMock } from '../support/firebaseMock'
Expand All @@ -8,6 +8,8 @@ import FirestoreAdapter from '../../src/data/firestore/firestoreAdapter'
const { res, mockClear } = getMockRes()

beforeEach(() => {
process.env.ADMIN_EMAIL = 'admin@example.com'
jest.clearAllMocks()
mockClear()
})

Expand Down Expand Up @@ -55,6 +57,30 @@ describe('store', () => {
)
})

it('sends an email to admins', async () => {
const req = getMockReq({
params: { weekId: '2023-01-01' },
body: mockBody(),
})

await new RsvpController(firestoreAdapter).store(req, res)

expect(res.status).toHaveBeenCalledWith(201)
expect(addDoc).toHaveBeenCalledWith(
FirebaseMock.mockCollection('mail'),
{
to: 'admin@example.com',
message: {
subject: 'TNMC RSVP: test name',
// eslint-disable-next-line max-len
text: 'test name has RSVPed for 2023-01-01\n\nEmail: test@example.com\nPlus one: true',
// eslint-disable-next-line max-len
html: '<p>test name has RSVPed for 2023-01-01<p><ul><li>Email: test@example.com</li><li>Plus one: true</li></ul>',
},
}
)
})

describe('when email is missing', () => {
it('should return a 422', async () => {
const req = getMockReq({
Expand Down
22 changes: 22 additions & 0 deletions __tests__/data/firestore/firestoreAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,25 @@ describe('createRsvp', () => {
)
})
})

describe ('sendEmail', () => {
it('sends an email', async () => {
await firestore.sendEmail('jsmith@example.com', {
subject: 'test subject',
text: 'test text',
html: 'test <p>html</p>',
})

expect(addDoc).toHaveBeenCalledWith(
FirebaseMock.mockCollection('mail'),
{
to: 'jsmith@example.com',
message: {
subject: 'test subject',
text: 'test text',
html: 'test <p>html</p>',
},
}
)
})
})
9 changes: 9 additions & 0 deletions src/config/mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function adminEmail (): string {
const email = process.env.ADMIN_EMAIL

if (!email) {
throw new Error('ADMIN_EMAIL is not set')
}

return email
}
10 changes: 10 additions & 0 deletions src/controllers/rsvpController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type Request, type Response } from 'express'
import FirestoreAdapter from '../data/firestore/firestoreAdapter'
import { z } from 'zod'
import { adminEmail } from '../config/mail'

class RsvpController {
static PATHS = {
Expand Down Expand Up @@ -28,6 +29,15 @@ class RsvpController {
await this.firestore.createRsvp(weekId, name, email, plusOne)

res.status(201).json({ message: 'Successfully RSVP\'d' })

await this.firestore.sendEmail(adminEmail(), {
subject: `TNMC RSVP: ${name}`,
// eslint-disable-next-line max-len
text: `${name} has RSVPed for ${weekId}\n\nEmail: ${email}\nPlus one: ${plusOne}`,
// eslint-disable-next-line max-len
html: `<p>${name} has RSVPed for ${weekId}<p><ul><li>Email: ${email}</li><li>Plus one: ${plusOne}</li></ul>`,
})

}

private validate (req: Request, res: Response): boolean {
Expand Down
32 changes: 24 additions & 8 deletions src/data/firestore/firestoreAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import Week from '../../models/week.js'
import setupFirestore from '../../config/firestore.js'

export default class FirestoreAdapter {
static readonly WEEKS_COLLECTION_NAME = 'weeks'
static readonly MAIL_COLLECTION_NAME = 'mail'
static readonly RSVPS_COLLECTION_NAME = 'rsvps'
static readonly WEEKS_COLLECTION_NAME = 'weeks'

#firestore: FirestoreType

Expand Down Expand Up @@ -88,22 +89,37 @@ export default class FirestoreAdapter {
})
}

async sendEmail (to: string, message: EmailMessage): Promise<void> {
await addDoc(this.mailCollection, {
to,
message,
})
}

today (): Timestamp {
const today = new Date()
today.setHours(0, 0, 0, 0)

return Timestamp.fromDate(today)
}

private get weekCollection ():
CollectionReference<DocumentData,DocumentData>
{
return collection(this.#firestore, FirestoreAdapter.WEEKS_COLLECTION_NAME)
private get mailCollection (): Collection {
return collection(this.#firestore, FirestoreAdapter.MAIL_COLLECTION_NAME)
}

private get rsvpCollection ():
CollectionReference<DocumentData,DocumentData>
{
private get rsvpCollection (): Collection {
return collection(this.#firestore, FirestoreAdapter.RSVPS_COLLECTION_NAME)
}

private get weekCollection (): Collection {
return collection(this.#firestore, FirestoreAdapter.WEEKS_COLLECTION_NAME)
}
}

export type EmailMessage = {
subject: string,
text: string,
html: string,
}

type Collection = CollectionReference<DocumentData,DocumentData>
Loading