From a1ae62bcc571f132eb3ee77b40b833bbe028277c Mon Sep 17 00:00:00 2001 From: Eric Chang <55172722+echang594@users.noreply.github.com> Date: Wed, 15 May 2024 14:49:05 -0700 Subject: [PATCH 1/3] Attach calendar info --- package.json | 1 + services/EmailService.ts | 38 +++++++++++++++++- services/MerchStoreService.ts | 17 +++++++-- tests/merchOrder.test.ts | 72 +++++++++++++++++------------------ tests/merchStore.test.ts | 2 +- yarn.lock | 49 ++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 79b11202..d251b20b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "ejs": "^3.1.3", "express": "^4.17.1", "faker": "^5.5.3", + "ics": "^3.7.2", "jsonwebtoken": "^8.5.1", "moment": "^2.27.0", "moment-timezone": "^0.5.34", diff --git a/services/EmailService.ts b/services/EmailService.ts index 70e1646a..3e744a45 100644 --- a/services/EmailService.ts +++ b/services/EmailService.ts @@ -1,4 +1,5 @@ import { MailService, MailDataRequired } from '@sendgrid/mail'; +import { createEvent } from 'ics'; import * as ejs from 'ejs'; import * as fs from 'fs'; import * as path from 'path'; @@ -75,7 +76,8 @@ export default class EmailService { } } - public async sendOrderConfirmation(email: string, firstName: string, order: OrderInfo): Promise { + public async sendOrderConfirmation(email: string, firstName: string, order: OrderInfo, + calendarInfo: OrderPickupEventCalendarInfo): Promise { try { const data = { to: email, @@ -88,6 +90,14 @@ export default class EmailService { pickupEvent: order.pickupEvent, link: `${Config.client}/store/orders`, }), + attachments: [ + { + content: EmailService.createCalendarFile(calendarInfo), + filename: 'invite.ics', + type: 'text/calendar', + disposition: 'attachment', + }, + ], }; await this.sendEmail(data); } catch (error) { @@ -169,7 +179,8 @@ export default class EmailService { } } - public async sendOrderPickupUpdated(email: string, firstName: string, order: OrderInfo) { + public async sendOrderPickupUpdated(email: string, firstName: string, order: OrderInfo, + calendarInfo: OrderPickupEventCalendarInfo) { try { const data = { to: email, @@ -181,6 +192,14 @@ export default class EmailService { orderItems: ejs.render(EmailService.itemDisplayTemplate, { items: order.items, totalCost: order.totalCost }), link: `${Config.client}/store/orders`, }), + attachments: [ + { + content: EmailService.createCalendarFile(calendarInfo), + filename: 'invite.ics', + type: 'text/calendar', + disposition: 'attachment', + }, + ], }; await this.sendEmail(data); } catch (error) { @@ -251,6 +270,14 @@ export default class EmailService { return fs.readFileSync(path.join(__dirname, `../templates/${filename}`), 'utf-8'); } + private static createCalendarFile(calendarInfo: OrderPickupEventCalendarInfo) { + const response = createEvent(calendarInfo); + if (response.error) { + throw response.error; + } + return Buffer.from(response.value).toString('base64'); + } + private sendEmail(data: EmailData) { return this.mailer.send(data); } @@ -278,3 +305,10 @@ export interface OrderInfo { totalCost: number; pickupEvent: OrderPickupEventInfo; } + +export interface OrderPickupEventCalendarInfo { + start: number; + end: number; + title: string; + description: string; +} diff --git a/services/MerchStoreService.ts b/services/MerchStoreService.ts index b32839c3..4559e4f5 100644 --- a/services/MerchStoreService.ts +++ b/services/MerchStoreService.ts @@ -35,7 +35,7 @@ import { EventModel } from '../models/EventModel'; import Repositories, { TransactionsManager } from '../repositories'; import { MerchandiseCollectionModel } from '../models/MerchandiseCollectionModel'; import { MerchCollectionPhotoModel } from '../models/MerchCollectionPhotoModel'; -import EmailService, { OrderInfo, OrderPickupEventInfo } from './EmailService'; +import EmailService, { OrderInfo, OrderPickupEventCalendarInfo, OrderPickupEventInfo } from './EmailService'; import { UserError } from '../utils/Errors'; import { OrderItemModel } from '../models/OrderItemModel'; import { OrderPickupEventModel } from '../models/OrderPickupEventModel'; @@ -593,7 +593,8 @@ export default class MerchStoreService { totalCost: order.totalCost, pickupEvent: MerchStoreService.toPickupEventUpdateInfo(order.pickupEvent), }; - this.emailService.sendOrderConfirmation(user.email, user.firstName, orderConfirmation); + const calendarInfo = MerchStoreService.toEventCalendarInfo(order.pickupEvent); + this.emailService.sendOrderConfirmation(user.email, user.firstName, orderConfirmation, calendarInfo); return order; } @@ -709,7 +710,8 @@ export default class MerchStoreService { throw new UserError('Cannot change order pickup to an event that starts in less than 2 days'); } const orderInfo = await MerchStoreService.buildOrderUpdateInfo(order, newPickupEventForOrder, txn); - await this.emailService.sendOrderPickupUpdated(user.email, user.firstName, orderInfo); + const calendarInfo = MerchStoreService.toEventCalendarInfo(newPickupEventForOrder); + await this.emailService.sendOrderPickupUpdated(user.email, user.firstName, orderInfo, calendarInfo); return orderRepository.upsertMerchOrder(order, { pickupEvent: newPickupEventForOrder, status: OrderStatus.PLACED, @@ -903,6 +905,15 @@ export default class MerchStoreService { }; } + private static toEventCalendarInfo(pickupEvent: OrderPickupEventModel): OrderPickupEventCalendarInfo { + return { + start: pickupEvent.start.getTime(), + end: pickupEvent.end.getTime(), + title: pickupEvent.title, + description: pickupEvent.description, + }; + } + /** * Process fulfillment updates for all order items of an order. * If all items get fulfilled after this update, then the order is considered fulfilled. diff --git a/tests/merchOrder.test.ts b/tests/merchOrder.test.ts index 4c7967fe..9d6ca8b7 100644 --- a/tests/merchOrder.test.ts +++ b/tests/merchOrder.test.ts @@ -48,7 +48,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); const order = [ { @@ -82,7 +82,7 @@ describe('merch orders', () => { expect(orderPlacedActivity.type).toStrictEqual(ActivityType.ORDER_PLACED); // check order confirmation email has been sent - verify(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + verify(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .called(); }); @@ -110,7 +110,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendOrderFulfillment(member.email, member.firstName, anything())) .thenResolve(); @@ -186,7 +186,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendOrderFulfillment(member.email, member.firstName, anything())) .thenResolve(); @@ -256,7 +256,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendPartialOrderFulfillment( member.email, member.firstName, anything(), anything(), anything(), anything(), @@ -338,7 +338,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendPartialOrderFulfillment( member.email, member.firstName, anything(), anything(), anything(), anything(), @@ -421,7 +421,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendOrderCancellation(member.email, member.firstName, anything())) .thenResolve(); @@ -485,7 +485,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendPartialOrderFulfillment( member.email, member.firstName, anything(), anything(), anything(), anything(), @@ -550,7 +550,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -591,7 +591,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -645,7 +645,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -695,7 +695,7 @@ describe('merch orders', () => { await member.reload(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); // Test Lifetime limit @@ -831,7 +831,7 @@ describe('merch orders', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchStoreController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -885,7 +885,7 @@ describe('merch orders', () => { const emailService = mock(EmailService); when(emailService.sendOrderCancellation(anyString(), anyString(), anything())) .thenResolve(); - when(emailService.sendOrderConfirmation(anyString(), anyString(), anything())) + when(emailService.sendOrderConfirmation(anyString(), anything(), anyString(), anything())) .thenResolve(); // cancel order @@ -927,7 +927,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -959,7 +959,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -985,7 +985,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const createPickupEventRequest = { pickupEvent: { ...pickupEvent, linkedEventUuid: linkedEvent.uuid } }; @@ -1080,7 +1080,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const params = { uuid: pickupEvent.uuid }; @@ -1106,7 +1106,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -1159,7 +1159,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anyString(), anyString(), anything())) + when(emailService.sendOrderConfirmation(anyString(), anything(), anyString(), anything())) .thenResolve(); when(emailService.sendOrderPickupCancelled(anyString(), anyString(), anything())) .thenResolve(); @@ -1224,7 +1224,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); when(emailService.sendOrderFulfillment(member1.email, member1.firstName, anything())) .thenResolve(); @@ -1442,9 +1442,9 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); - when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -1468,7 +1468,7 @@ describe('merch order pickup events', () => { const updatedOrderResponse = await merchController.getOneMerchOrder(orderUuid, member); expect(updatedOrderResponse.order.pickupEvent).toStrictEqual(anotherPickupEvent.getPublicOrderPickupEvent()); - verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .called(); }); @@ -1490,9 +1490,9 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); - when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -1527,7 +1527,7 @@ describe('merch order pickup events', () => { const updatedOrderResponse = await merchController.getOneMerchOrder(orderUuid, member); expect(updatedOrderResponse.order.pickupEvent).toStrictEqual(anotherPickupEvent.getPublicOrderPickupEvent()); - verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .called(); }); @@ -1549,9 +1549,9 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); - when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -1586,7 +1586,7 @@ describe('merch order pickup events', () => { const updatedOrderResponse = await merchController.getOneMerchOrder(orderUuid, member); expect(updatedOrderResponse.order.pickupEvent).toStrictEqual(anotherPickupEvent.getPublicOrderPickupEvent()); - verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + verify(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .called(); }); @@ -1611,9 +1611,9 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); - when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything())) + when(emailService.sendOrderPickupUpdated(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order @@ -1663,7 +1663,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -1702,7 +1702,7 @@ describe('merch order pickup events', () => { }; const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -1740,7 +1740,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(anything(), anything(), anything())) + when(emailService.sendOrderConfirmation(anything(), anything(), anything(), anything())) .thenResolve(); const merchController = ControllerFactory.merchStore(conn, instance(emailService)); @@ -1796,7 +1796,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); when(emailService.sendOrderCancellation(member.email, member.firstName, anything())) .thenResolve(); diff --git a/tests/merchStore.test.ts b/tests/merchStore.test.ts index cbaa4bce..23a0e2f2 100644 --- a/tests/merchStore.test.ts +++ b/tests/merchStore.test.ts @@ -497,7 +497,7 @@ describe('merch items with options', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); // placing order fails diff --git a/yarn.lock b/yarn.lock index c8425572..73f965c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2902,6 +2902,15 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ics@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/ics/-/ics-3.7.2.tgz#c4ebdd49327c65edff4d98174f111934cfe0c024" + integrity sha512-UC5bBJYKyzkYZv4/AoIV9dsZw+rwFkUOtHHhnxmb0HTUEpfDmfl5sB9DlePKrTxx6SGMXiIQbiElf66viSTB0A== + dependencies: + nanoid "^3.1.23" + runes2 "^1.1.2" + yup "^1.2.0" + ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -4214,6 +4223,11 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nanoid@^3.1.23: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -4716,6 +4730,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +property-expr@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" + integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -4985,6 +5004,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +runes2@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/runes2/-/runes2-1.1.4.tgz#aa38d3d7946e147ac4718ed0fb19b22340ae5c66" + integrity sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g== + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -5414,6 +5438,11 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -5441,6 +5470,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -5568,6 +5602,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -6020,6 +6059,16 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yup@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.4.0.tgz#898dcd660f9fb97c41f181839d3d65c3ee15a43e" + integrity sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0" + zen-observable-ts@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83" From 1984dbb7c17541d3a6ec1a6d9f07191b86de1b5b Mon Sep 17 00:00:00 2001 From: Eric Chang <55172722+echang594@users.noreply.github.com> Date: Sat, 18 May 2024 22:15:36 -0700 Subject: [PATCH 2/3] Added location to OrderPickupEvent --- api/validators/MerchStoreRequests.ts | 3 +++ ...6-add-location-column-to-orderPickupEvent.ts | 17 +++++++++++++++++ models/OrderPickupEventModel.ts | 4 ++++ services/EmailService.ts | 1 + services/MerchStoreService.ts | 1 + tests/Seeds.ts | 3 +++ tests/data/MerchFactory.ts | 1 + types/ApiRequests.ts | 1 + types/ApiResponses.ts | 1 + 9 files changed, 32 insertions(+) create mode 100644 migrations/0046-add-location-column-to-orderPickupEvent.ts diff --git a/api/validators/MerchStoreRequests.ts b/api/validators/MerchStoreRequests.ts index 5159025e..97c457da 100644 --- a/api/validators/MerchStoreRequests.ts +++ b/api/validators/MerchStoreRequests.ts @@ -283,6 +283,9 @@ export class OrderPickupEvent implements IOrderPickupEvent { @IsNotEmpty() description: string; + @IsNotEmpty() + location: string; + @IsDefined() @Min(1) orderLimit: number; diff --git a/migrations/0046-add-location-column-to-orderPickupEvent.ts b/migrations/0046-add-location-column-to-orderPickupEvent.ts new file mode 100644 index 00000000..3488aac3 --- /dev/null +++ b/migrations/0046-add-location-column-to-orderPickupEvent.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +const TABLE_NAME = 'OrderPickupEvents'; +const COLUMN_NAME = 'location'; + +export class AddLocationColumnToOrderPickupEvent1716061560746 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn(TABLE_NAME, new TableColumn({ + name: COLUMN_NAME, + type: 'varchar(255)', + })); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn(TABLE_NAME, COLUMN_NAME); + } +} diff --git a/models/OrderPickupEventModel.ts b/models/OrderPickupEventModel.ts index 928fe1da..1f90e3ce 100644 --- a/models/OrderPickupEventModel.ts +++ b/models/OrderPickupEventModel.ts @@ -20,6 +20,9 @@ export class OrderPickupEventModel extends BaseEntity { @Column('text') description: string; + @Column('varchar', { length: 255 }) + location: string; + @Column('integer') orderLimit: number; @@ -41,6 +44,7 @@ export class OrderPickupEventModel extends BaseEntity { start: this.start, end: this.end, description: this.description, + location: this.location, orderLimit: this.orderLimit, status: this.status, linkedEvent: this.linkedEvent ? this.linkedEvent.getPublicEvent() : null, diff --git a/services/EmailService.ts b/services/EmailService.ts index 3e744a45..79d06913 100644 --- a/services/EmailService.ts +++ b/services/EmailService.ts @@ -311,4 +311,5 @@ export interface OrderPickupEventCalendarInfo { end: number; title: string; description: string; + location: string; } diff --git a/services/MerchStoreService.ts b/services/MerchStoreService.ts index 4559e4f5..a6546c56 100644 --- a/services/MerchStoreService.ts +++ b/services/MerchStoreService.ts @@ -911,6 +911,7 @@ export default class MerchStoreService { end: pickupEvent.end.getTime(), title: pickupEvent.title, description: pickupEvent.description, + location: pickupEvent.location, }; } diff --git a/tests/Seeds.ts b/tests/Seeds.ts index 979db0fc..963f1ccc 100644 --- a/tests/Seeds.ts +++ b/tests/Seeds.ts @@ -683,6 +683,7 @@ async function seed(): Promise { const PAST_ORDER_PICKUP_EVENT = MerchFactory.fakeOrderPickupEvent({ title: 'ACM Merch Distribution Event 1', description: 'This is a test event to pickup orders from. It has already passed :(', + location: 'Qualcomm Room', orderLimit: 10, start: moment().subtract(3, 'days').subtract(2, 'hours').toDate(), end: moment().subtract(3, 'days').toDate(), @@ -690,6 +691,7 @@ async function seed(): Promise { const ONGOING_ORDER_PICKUP_EVENT = MerchFactory.fakeOrderPickupEvent({ title: 'ACM Merch Distribution Event 2', description: 'Another test event', + location: 'Henry Booker Room', orderLimit: 10, start: moment().subtract(30, 'minutes').toDate(), end: moment().add(90, 'minutes').toDate(), @@ -697,6 +699,7 @@ async function seed(): Promise { const FUTURE_ORDER_PICKUP_EVENT = MerchFactory.fakeFutureOrderPickupEvent({ title: 'Example Other Order Pickup Event', description: 'This is another test event to pickup orders from.', + location: 'ASML Room', orderLimit: 10, }); diff --git a/tests/data/MerchFactory.ts b/tests/data/MerchFactory.ts index 5885fde0..036e71e5 100644 --- a/tests/data/MerchFactory.ts +++ b/tests/data/MerchFactory.ts @@ -130,6 +130,7 @@ export class MerchFactory { uuid: uuid(), title: faker.datatype.hexaDecimal(10), description: faker.lorem.sentences(2), + location: faker.datatype.hexaDecimal(10), start, end, orderLimit: FactoryUtils.getRandomNumber(1, 5), diff --git a/types/ApiRequests.ts b/types/ApiRequests.ts index 816b59f7..b7984fc5 100644 --- a/types/ApiRequests.ts +++ b/types/ApiRequests.ts @@ -342,6 +342,7 @@ export interface OrderPickupEvent { start: Date; end: Date; description: string; + location: string; orderLimit: number; linkedEventUuid?: Uuid; } diff --git a/types/ApiResponses.ts b/types/ApiResponses.ts index d3dfa353..ea692f20 100644 --- a/types/ApiResponses.ts +++ b/types/ApiResponses.ts @@ -438,6 +438,7 @@ export interface PublicOrderPickupEvent { start: Date; end: Date; description: string; + location: string; orders?: PublicOrderWithItems[]; orderLimit?: number; status: OrderPickupEventStatus; From ae4deeb7d86373def3cb53a8a98ce0678845d93e Mon Sep 17 00:00:00 2001 From: Eric Chang <55172722+echang594@users.noreply.github.com> Date: Wed, 22 May 2024 16:24:28 -0700 Subject: [PATCH 3/3] Fix tests after merge --- tests/merchOrder.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/merchOrder.test.ts b/tests/merchOrder.test.ts index cf19eb8c..c1996399 100644 --- a/tests/merchOrder.test.ts +++ b/tests/merchOrder.test.ts @@ -1743,7 +1743,7 @@ describe('merch order pickup events', () => { .write(); const emailService = mock(EmailService); - when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything(), anything())) .thenResolve(); // place order to secondPickupEvent