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

Email ics file for pickup events #443

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions api/validators/MerchStoreRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ export class OrderPickupEvent implements IOrderPickupEvent {
@IsNotEmpty()
description: string;

@IsNotEmpty()
location: string;

@IsDefined()
@Min(1)
orderLimit: number;
Expand Down
17 changes: 17 additions & 0 deletions migrations/0046-add-location-column-to-orderPickupEvent.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await queryRunner.addColumn(TABLE_NAME, new TableColumn({
name: COLUMN_NAME,
type: 'varchar(255)',
}));
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn(TABLE_NAME, COLUMN_NAME);
}
}
4 changes: 4 additions & 0 deletions models/OrderPickupEventModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class OrderPickupEventModel extends BaseEntity {
@Column('text')
description: string;

@Column('varchar', { length: 255 })
location: string;

@Column('integer')
orderLimit: number;

Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 37 additions & 2 deletions services/EmailService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -75,7 +76,8 @@ export default class EmailService {
}
}

public async sendOrderConfirmation(email: string, firstName: string, order: OrderInfo): Promise<void> {
public async sendOrderConfirmation(email: string, firstName: string, order: OrderInfo,
calendarInfo: OrderPickupEventCalendarInfo): Promise<void> {
try {
const data = {
to: email,
Expand All @@ -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',
nik-dange marked this conversation as resolved.
Show resolved Hide resolved
disposition: 'attachment',
},
],
};
await this.sendEmail(data);
} catch (error) {
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -278,3 +305,11 @@ export interface OrderInfo {
totalCost: number;
pickupEvent: OrderPickupEventInfo;
}

export interface OrderPickupEventCalendarInfo {
start: number;
end: number;
title: string;
description: string;
location: string;
}
18 changes: 15 additions & 3 deletions services/MerchStoreService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -717,7 +718,8 @@ export default class MerchStoreService {
throw new UserError('This merch pickup event is full! Please choose a different pickup event');
}
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,
Expand Down Expand Up @@ -911,6 +913,16 @@ export default class MerchStoreService {
};
}

private static toEventCalendarInfo(pickupEvent: OrderPickupEventModel): OrderPickupEventCalendarInfo {
return {
start: pickupEvent.start.getTime(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on adding location as a field to OrderPickupEvents?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing, we've decided to add it and copy over the location from the linkedEvent if none is provided. One potential issue is that if the linkedEvent location updates, this field will not, however, we do not think this will be a big problem as the user will be notified through discord.

end: pickupEvent.end.getTime(),
title: pickupEvent.title,
description: pickupEvent.description,
location: pickupEvent.location,
};
}

/**
* Process fulfillment updates for all order items of an order.
* If all items get fulfilled after this update, then the order is considered fulfilled.
Expand Down
3 changes: 3 additions & 0 deletions tests/Seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,20 +683,23 @@ async function seed(): Promise<void> {
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(),
});
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(),
});
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,
});

Expand Down
1 change: 1 addition & 0 deletions tests/data/MerchFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading
Loading