diff --git a/api/controllers/MerchStoreController.ts b/api/controllers/MerchStoreController.ts index 97b6963e..9735045f 100644 --- a/api/controllers/MerchStoreController.ts +++ b/api/controllers/MerchStoreController.ts @@ -84,7 +84,8 @@ export class MerchStoreController { private storageService: StorageService; - constructor(merchStoreService: MerchStoreService, merchOrderService: MerchOrderService, storageService: StorageService) { + constructor(merchStoreService: MerchStoreService, merchOrderService: MerchOrderService, + storageService: StorageService) { this.merchStoreService = merchStoreService; this.merchOrderService = merchOrderService; this.storageService = storageService; diff --git a/services/MerchOrderService.ts b/services/MerchOrderService.ts index 1ffb1edc..6122c461 100644 --- a/services/MerchOrderService.ts +++ b/services/MerchOrderService.ts @@ -31,7 +31,6 @@ import Repositories, { TransactionsManager } from '../repositories'; @Service() export default class MerchOrderService { - private emailService: EmailService; private transactions: TransactionsManager; @@ -61,7 +60,7 @@ export default class MerchOrderService { .getAllOrdersForAllUsers()); } - /** + /** * Places an order with the list of options and their quantities for the given user. * * The order is placed if the following conditions are met: @@ -78,105 +77,105 @@ export default class MerchOrderService { * @param user user placing the order * @returns the finalized order, including sale price, discount, and fulfillment details */ - public async placeOrder(originalOrder: MerchItemOptionAndQuantity[], - user: UserModel, - pickupEventUuid: Uuid): Promise { - const [order, merchItemOptions] = await this.transactions.readWrite(async (txn) => { - const merchItemOptionRepository = Repositories.merchStoreItemOption(txn); - const itemOptions = await merchItemOptionRepository.batchFindByUuid(originalOrder.map((oi) => oi.option)); - await this.validateOrderInTransaction(originalOrder, user, txn); - - // Verify the requested pickup event exists, - // and that the order is placed at least 2 days before the pickup event starts - const pickupEvent = await Repositories.merchOrderPickupEvent(txn).findByUuid(pickupEventUuid); - if (!pickupEvent) { - throw new NotFoundError('Pickup event requested is not found'); - } - if (MerchOrderService.isLessThanTwoDaysBeforePickupEvent(pickupEvent)) { - throw new NotFoundError('Cannot pickup order at an event that starts in less than 2 days'); - } + public async placeOrder(originalOrder: MerchItemOptionAndQuantity[], + user: UserModel, + pickupEventUuid: Uuid): Promise { + const [order, merchItemOptions] = await this.transactions.readWrite(async (txn) => { + const merchItemOptionRepository = Repositories.merchStoreItemOption(txn); + const itemOptions = await merchItemOptionRepository.batchFindByUuid(originalOrder.map((oi) => oi.option)); + await this.validateOrderInTransaction(originalOrder, user, txn); + + // Verify the requested pickup event exists, + // and that the order is placed at least 2 days before the pickup event starts + const pickupEvent = await Repositories.merchOrderPickupEvent(txn).findByUuid(pickupEventUuid); + if (!pickupEvent) { + throw new NotFoundError('Pickup event requested is not found'); + } + if (MerchOrderService.isLessThanTwoDaysBeforePickupEvent(pickupEvent)) { + throw new NotFoundError('Cannot pickup order at an event that starts in less than 2 days'); + } - // Verify that this order would not set the pickup event's order count - // over the order limit - const currentOrderCount = pickupEvent.orders.filter((o) => o.status !== OrderStatus.CANCELLED).length; - if (currentOrderCount >= pickupEvent.orderLimit) { - throw new UserError('This merch pickup event is full! Please choose a different pickup event'); - } - const totalCost = MerchOrderService.totalCost(originalOrder, itemOptions); - const merchOrderRepository = Repositories.merchOrder(txn); - - // if all checks pass, the order is placed - const createdOrder = await merchOrderRepository.upsertMerchOrder(OrderModel.create({ - user, - totalCost, - items: flatten(originalOrder.map((optionAndQuantity) => { - const option = itemOptions.get(optionAndQuantity.option); - const quantityRequested = optionAndQuantity.quantity; - return Array(quantityRequested).fill(OrderItemModel.create({ - option, - salePriceAtPurchase: option.getPrice(), - discountPercentageAtPurchase: option.discountPercentage, - })); - })), - pickupEvent, - })); - - const activityRepository = Repositories.activity(txn); - await activityRepository.logActivity({ - user, - type: ActivityType.ORDER_PLACED, - description: `Order ${createdOrder.uuid}`, - }); + // Verify that this order would not set the pickup event's order count + // over the order limit + if (MerchOrderService.isLessThanTwoDaysBeforePickupEvent(pickupEvent)) { + throw new UserError('Cannot change order pickup to an event that starts in less than 2 days'); + } - await Promise.all(originalOrder.map(async (optionAndQuantity) => { + const totalCost = MerchOrderService.totalCost(originalOrder, itemOptions); + const merchOrderRepository = Repositories.merchOrder(txn); + + // if all checks pass, the order is placed + const createdOrder = await merchOrderRepository.upsertMerchOrder(OrderModel.create({ + user, + totalCost, + items: flatten(originalOrder.map((optionAndQuantity) => { const option = itemOptions.get(optionAndQuantity.option); - const updatedQuantity = option.quantity - optionAndQuantity.quantity; - return merchItemOptionRepository.upsertMerchItemOption(option, { quantity: updatedQuantity }); - })); + const quantityRequested = optionAndQuantity.quantity; + return Array(quantityRequested).fill(OrderItemModel.create({ + option, + salePriceAtPurchase: option.getPrice(), + discountPercentageAtPurchase: option.discountPercentage, + })); + })), + pickupEvent, + })); - const userRepository = Repositories.user(txn); - await userRepository.upsertUser(user, { credits: user.credits - totalCost }); - return [createdOrder, itemOptions]; + const activityRepository = Repositories.activity(txn); + await activityRepository.logActivity({ + user, + type: ActivityType.ORDER_PLACED, + description: `Order ${createdOrder.uuid}`, }); - const orderConfirmation = { - uuid: order.uuid, - items: originalOrder.map((oi) => { - const option = merchItemOptions.get(oi.option); - const { item } = option; - return { - ...item, - picture: item.getDefaultPhotoUrl(), - quantityRequested: oi.quantity, - salePrice: option.getPrice(), - total: oi.quantity * option.getPrice(), - }; - }), - totalCost: order.totalCost, - pickupEvent: MerchOrderService.toPickupEventUpdateInfo(order.pickupEvent), - }; - this.emailService.sendOrderConfirmation(user.email, user.firstName, orderConfirmation); + await Promise.all(originalOrder.map(async (optionAndQuantity) => { + const option = itemOptions.get(optionAndQuantity.option); + const updatedQuantity = option.quantity - optionAndQuantity.quantity; + return merchItemOptionRepository.upsertMerchItemOption(option, { quantity: updatedQuantity }); + })); - return order; - } + const userRepository = Repositories.user(txn); + await userRepository.upsertUser(user, { credits: user.credits - totalCost }); + return [createdOrder, itemOptions]; + }); - private static toPickupEventUpdateInfo(pickupEvent: OrderPickupEventModel): OrderPickupEventInfo { - return { - ...pickupEvent, - start: MerchOrderService.humanReadableDateString(pickupEvent.start), - end: MerchOrderService.humanReadableDateString(pickupEvent.end), - }; - } + const orderConfirmation = { + uuid: order.uuid, + items: originalOrder.map((oi) => { + const option = merchItemOptions.get(oi.option); + const { item } = option; + return { + ...item, + picture: item.getDefaultPhotoUrl(), + quantityRequested: oi.quantity, + salePrice: option.getPrice(), + total: oi.quantity * option.getPrice(), + }; + }), + totalCost: order.totalCost, + pickupEvent: MerchOrderService.toPickupEventUpdateInfo(order.pickupEvent), + }; + this.emailService.sendOrderConfirmation(user.email, user.firstName, orderConfirmation); - private static humanReadableDateString(date: Date): string { - return moment(date).tz('America/Los_Angeles').format('MMMM D, h:mm A'); - } + return order; + } - public async validateOrder(originalOrder: MerchItemOptionAndQuantity[], user: UserModel): Promise { - return this.transactions.readWrite(async (txn) => this.validateOrderInTransaction(originalOrder, user, txn)); - } + public async validateOrder(originalOrder: MerchItemOptionAndQuantity[], user: UserModel): Promise { + return this.transactions.readWrite(async (txn) => this.validateOrderInTransaction(originalOrder, user, txn)); + } - /** + private static toPickupEventUpdateInfo(pickupEvent: OrderPickupEventModel): OrderPickupEventInfo { + return { + ...pickupEvent, + start: MerchOrderService.humanReadableDateString(pickupEvent.start), + end: MerchOrderService.humanReadableDateString(pickupEvent.end), + }; + } + + private static humanReadableDateString(date: Date): string { + return moment(date).tz('America/Los_Angeles').format('MMMM D, h:mm A'); + } + + /** * Validates a merch order. An order is considered valid if all the below are true: * - all the ordered item options exist within the database * - the ordered item options were placed for non-hidden items @@ -248,10 +247,6 @@ export default class MerchOrderService { if (user.credits < totalCost) throw new UserError('You don\'t have enough credits for this order'); } - private static isLessThanTwoDaysBeforePickupEvent(pickupEvent: OrderPickupEventModel): boolean { - return new Date() > moment(pickupEvent.start).subtract(2, 'days').toDate(); - } - private static isPickupEventOrderLimitFull(pickupEvent: OrderPickupEventModel): boolean { const currentOrderCount = pickupEvent.orders.filter((o) => o.status !== OrderStatus.CANCELLED).length; return currentOrderCount >= pickupEvent.orderLimit; @@ -295,6 +290,10 @@ export default class MerchOrderService { }); } + private static isLessThanTwoDaysBeforePickupEvent(pickupEvent: OrderPickupEventModel): boolean { + return new Date() > moment(pickupEvent.start).subtract(2, 'days').toDate(); + } + private static isInactiveOrder(order: OrderModel): boolean { return order.status === OrderStatus.FULFILLED || order.status === OrderStatus.CANCELLED; } @@ -622,7 +621,6 @@ export default class MerchOrderService { return moment().isBefore(moment(pickupEvent.start)); } - /** * Completes an order pickup event, marking any orders that haven't been fulfilled * or partially fulfilled as missed. @@ -661,41 +659,40 @@ export default class MerchOrderService { && order.status !== OrderStatus.CANCELLED; } - - /** + /** * Builds an order update info object to be sent in emails, based on the order. * @param order order * @param txn transaction * @returns order update info for email */ - private static async buildOrderUpdateInfo(order: OrderModel, pickupEvent: OrderPickupEventModel, - txn: EntityManager): Promise { - // maps an item option to its price at purchase and quantity ordered by the user - const optionPricesAndQuantities = MerchOrderService.getPriceAndQuantityByOption(order); - const itemOptionsOrdered = Array.from(optionPricesAndQuantities.keys()); - const itemOptionByUuid = await Repositories - .merchStoreItemOption(txn) - .batchFindByUuid(itemOptionsOrdered); - - return { - uuid: order.uuid, - items: itemOptionsOrdered.map((option) => { - const { item } = itemOptionByUuid.get(option); - const { quantity, price } = optionPricesAndQuantities.get(option); - return { - ...item, - picture: item.getDefaultPhotoUrl(), - quantityRequested: quantity, - salePrice: price, - total: quantity * price, - }; - }), - totalCost: order.totalCost, - pickupEvent: MerchOrderService.toPickupEventUpdateInfo(pickupEvent), - }; - } + private static async buildOrderUpdateInfo(order: OrderModel, pickupEvent: OrderPickupEventModel, + txn: EntityManager): Promise { + // maps an item option to its price at purchase and quantity ordered by the user + const optionPricesAndQuantities = MerchOrderService.getPriceAndQuantityByOption(order); + const itemOptionsOrdered = Array.from(optionPricesAndQuantities.keys()); + const itemOptionByUuid = await Repositories + .merchStoreItemOption(txn) + .batchFindByUuid(itemOptionsOrdered); + + return { + uuid: order.uuid, + items: itemOptionsOrdered.map((option) => { + const { item } = itemOptionByUuid.get(option); + const { quantity, price } = optionPricesAndQuantities.get(option); + return { + ...item, + picture: item.getDefaultPhotoUrl(), + quantityRequested: quantity, + salePrice: price, + total: quantity * price, + }; + }), + totalCost: order.totalCost, + pickupEvent: MerchOrderService.toPickupEventUpdateInfo(pickupEvent), + }; + } - /** + /** * Maps an item's option to its price at purchase and quantity ordered by the user * @param order order * @returns map of item option to its price at purchase and quantity ordered by the user @@ -832,7 +829,6 @@ export default class MerchOrderService { }); } - private static isActivePickupEvent(pickupEvent: OrderPickupEventModel) { return pickupEvent.status === OrderPickupEventStatus.ACTIVE; } @@ -844,4 +840,4 @@ export default class MerchOrderService { return linkedEvent; }); } -} \ No newline at end of file +} diff --git a/services/MerchStoreService.ts b/services/MerchStoreService.ts index ca541594..28c6f4b5 100644 --- a/services/MerchStoreService.ts +++ b/services/MerchStoreService.ts @@ -2,40 +2,31 @@ import { Service } from 'typedi'; import { InjectManager } from 'typeorm-typedi-extensions'; import { NotFoundError, ForbiddenError } from 'routing-controllers'; import { EntityManager } from 'typeorm'; -import { difference, flatten, intersection } from 'underscore'; +import { difference } from 'underscore'; import * as moment from 'moment-timezone'; -import { MerchItemWithQuantity, OrderItemPriceAndQuantity } from 'types/internal'; import { MerchandiseItemOptionModel } from '../models/MerchandiseItemOptionModel'; import { Uuid, PublicMerchCollection, - ActivityType, - OrderItemFulfillmentUpdate, MerchCollection, MerchCollectionEdit, MerchItem, MerchItemOption, - MerchItemOptionAndQuantity, MerchItemEdit, PublicMerchItemOption, OrderStatus, PublicMerchItemWithPurchaseLimits, - OrderPickupEventStatus, PublicMerchItemPhoto, MerchItemPhoto, PublicMerchCollectionPhoto, MerchCollectionPhoto, } from '../types'; import { MerchandiseItemModel } from '../models/MerchandiseItemModel'; -import { OrderModel } from '../models/OrderModel'; import { UserModel } from '../models/UserModel'; import Repositories, { TransactionsManager } from '../repositories'; import { MerchandiseCollectionModel } from '../models/MerchandiseCollectionModel'; import { MerchCollectionPhotoModel } from '../models/MerchCollectionPhotoModel'; -import EmailService from './EmailService'; import { UserError } from '../utils/Errors'; -import { OrderItemModel } from '../models/OrderItemModel'; -import { OrderPickupEventModel } from '../models/OrderPickupEventModel'; import { MerchandiseItemPhotoModel } from '../models/MerchandiseItemPhotoModel'; @Service() @@ -44,13 +35,10 @@ export default class MerchStoreService { private static readonly MAX_COLLECTION_PHOTO_COUNT = 5; - private emailService: EmailService; - private transactions: TransactionsManager; - constructor(@InjectManager() entityManager: EntityManager, emailService: EmailService) { + constructor(@InjectManager() entityManager: EntityManager) { this.transactions = new TransactionsManager(entityManager); - this.emailService = emailService; } public async findItemByUuid(uuid: Uuid, user: UserModel): Promise { @@ -343,7 +331,10 @@ export default class MerchStoreService { .merchStoreCollection(txn) .findByUuid(updatedCollection); if (!collection) throw new NotFoundError('Merch collection not found'); + + updatedItem.collection = collection; } + return merchItemRepository.upsertMerchItem(updatedItem); }); } @@ -476,166 +467,6 @@ export default class MerchStoreService { }); } - private static humanReadableDateString(date: Date): string { - return moment(date).tz('America/Los_Angeles').format('MMMM D, h:mm A'); - } - - public async validateOrder(originalOrder: MerchItemOptionAndQuantity[], user: UserModel): Promise { - return this.transactions.readWrite(async (txn) => this.validateOrderInTransaction(originalOrder, user, txn)); - } - - /** - * Validates a merch order. An order is considered valid if all the below are true: - * - all the ordered item options exist within the database - * - the ordered item options were placed for non-hidden items - * - the user wouldn't reach monthly or lifetime limits for any item if this order is placed - * - the requested item options are in stock - * - the user has enough credits to place the order - */ - private async validateOrderInTransaction(originalOrder: MerchItemOptionAndQuantity[], - user: UserModel, - txn: EntityManager): Promise { - await user.reload(); - const merchItemOptionRepository = Repositories.merchStoreItemOption(txn); - const itemOptionsToOrder = await merchItemOptionRepository.batchFindByUuid(originalOrder.map((oi) => oi.option)); - if (itemOptionsToOrder.size !== originalOrder.length) { - const requestedItems = originalOrder.map((oi) => oi.option); - const foundItems = Array.from(itemOptionsToOrder.values()) - .filter((o) => !o.item.hidden) - .map((o) => o.uuid); - const missingItems = difference(requestedItems, foundItems); - throw new NotFoundError(`The following items were not found: ${missingItems}`); - } - - // Checks that hidden items were not ordered - const hiddenItems = Array.from(itemOptionsToOrder.values()) - .filter((o) => o.item.hidden) - .map((o) => o.uuid); - - if (hiddenItems.length !== 0) { - throw new UserError(`Not allowed to order: ${hiddenItems}`); - } - - // checks that the user hasn't exceeded monthly/lifetime purchase limits - const merchOrderRepository = Repositories.merchOrder(txn); - const lifetimePurchaseHistory = await merchOrderRepository.getAllOrdersWithItemsForUser(user); - const oneMonthAgo = new Date(moment().subtract(1, 'month').unix()); - const pastMonthPurchaseHistory = lifetimePurchaseHistory.filter((o) => o.orderedAt > oneMonthAgo); - const lifetimeItemOrderCounts = MerchStoreService.countItemOrders(itemOptionsToOrder, lifetimePurchaseHistory); - const pastMonthItemOrderCounts = MerchStoreService.countItemOrders(itemOptionsToOrder, pastMonthPurchaseHistory); - - // aggregate requested quantities by item - const requestedQuantitiesByMerchItem = Array.from(MerchStoreService - .countItemRequestedQuantities(originalOrder, itemOptionsToOrder) - .entries()); - - for (let i = 0; i < requestedQuantitiesByMerchItem.length; i += 1) { - const [uuid, itemWithQuantity] = requestedQuantitiesByMerchItem[i]; - if (!!itemWithQuantity.item.lifetimeLimit - && lifetimeItemOrderCounts.get(uuid) + itemWithQuantity.quantity > itemWithQuantity.item.lifetimeLimit) { - throw new UserError(`This order exceeds the lifetime limit for ${itemWithQuantity.item.itemName}`); - } - if (!!itemWithQuantity.item.monthlyLimit - && pastMonthItemOrderCounts.get(uuid) + itemWithQuantity.quantity > itemWithQuantity.item.monthlyLimit) { - throw new UserError(`This order exceeds the monthly limit for ${itemWithQuantity.item.itemName}`); - } - } - - // checks that enough units of requested item options are in stock - for (let i = 0; i < originalOrder.length; i += 1) { - const optionAndQuantity = originalOrder[i]; - const option = itemOptionsToOrder.get(optionAndQuantity.option); - const quantityRequested = optionAndQuantity.quantity; - if (option.quantity < quantityRequested) { - throw new UserError(`There aren't enough units of ${option.item.itemName} in stock`); - } - } - - // checks that the user has enough credits to place order - const totalCost = MerchStoreService.totalCost(originalOrder, itemOptionsToOrder); - if (user.credits < totalCost) throw new UserError('You don\'t have enough credits for this order'); - } - - /** - * Counts the number of times any MerchandiseItem has been ordered by the user. - * - * An ordered item does not contribute towards an option's count if its order - * has been cancelled AND the item is unfufilled. An ordered item - * whose order has been cancelled but the item is fulfilled still counts towards the count. - */ - private static countItemOrders(itemOptionsToOrder: Map, pastOrders: OrderModel[]): - Map { - const counts = new Map(); - const options = Array.from(itemOptionsToOrder.values()); - for (let o = 0; o < options.length; o += 1) { - counts.set(options[o].item.uuid, 0); - } - const ordersByOrderItem = new Map(); - const orderedItems: OrderItemModel[] = []; - - // go through every OrderItem previously ordered and add to above map/list - for (let o = 0; o < pastOrders.length; o += 1) { - for (let oi = 0; oi < pastOrders[o].items.length; oi += 1) { - const orderItem = pastOrders[o].items[oi]; - ordersByOrderItem.set(orderItem.uuid, pastOrders[o]); - orderedItems.push(orderItem); - } - } - - // count MerchItems based on number of OrderItems previously ordered - for (let i = 0; i < orderedItems.length; i += 1) { - const orderedItem = orderedItems[i]; - const order = ordersByOrderItem.get(orderedItem.uuid); - if (MerchStoreService.doesItemCountTowardsOrderLimits(orderedItem, order)) { - const { uuid: itemUuid } = orderedItem.option.item; - if (counts.has(itemUuid)) { - counts.set(itemUuid, counts.get(itemUuid) + 1); - } - } - } - return counts; - } - - /** - * An item counts towards the order limit if it has either been fulfilled or if - * it's on hold for that customer. This means if the customer's order was cancelled - * and the item is unfulfilled, then the item shouldn't count. - * (having the order cancelled and the item fulfilled would mean the order was - * partially fulfilled then cancelled, which would still count since that item belongs to that user) - */ - private static doesItemCountTowardsOrderLimits(orderItem: OrderItemModel, order: OrderModel) { - return order.status !== OrderStatus.CANCELLED || orderItem.fulfilled; - } - - private static countItemRequestedQuantities(order: MerchItemOptionAndQuantity[], - itemOptions: Map): Map { - const requestedQuantitiesByMerchItem = new Map(); - for (let i = 0; i < order.length; i += 1) { - const option = itemOptions.get(order[i].option); - - const { item } = option; - const quantityRequested = order[i].quantity; - - if (!requestedQuantitiesByMerchItem.has(item.uuid)) { - requestedQuantitiesByMerchItem.set(item.uuid, { - item, - quantity: 0, - }); - } - requestedQuantitiesByMerchItem.get(item.uuid).quantity += quantityRequested; - } - return requestedQuantitiesByMerchItem; - } - - private static totalCost(order: MerchItemOptionAndQuantity[], - itemOptions: Map): number { - return order.reduce((sum, o) => { - const option = itemOptions.get(o.option); - const quantityRequested = o.quantity; - return sum + (option.getPrice() * quantityRequested); - }, 0); - } - public async getCartItems(options: string[]): Promise { return this.transactions.readOnly(async (txn) => { const merchItemOptionRepository = Repositories.merchStoreItemOption(txn); @@ -648,5 +479,4 @@ export default class MerchStoreService { return options.map((option) => itemOptionsByUuid.get(option)); }); } - } diff --git a/tests/controllers/ControllerFactory.ts b/tests/controllers/ControllerFactory.ts index 4f2b84d8..363331b9 100644 --- a/tests/controllers/ControllerFactory.ts +++ b/tests/controllers/ControllerFactory.ts @@ -70,7 +70,7 @@ export class ControllerFactory { public static merchStore(conn: Connection, emailService = new EmailService(), storageService = new StorageService()): MerchStoreController { - const merchStoreService = new MerchStoreService(conn.manager, emailService); + const merchStoreService = new MerchStoreService(conn.manager); const merchOrderService = new MerchOrderService(conn.manager, emailService); return new MerchStoreController(merchStoreService, merchOrderService, storageService); } diff --git a/tests/merchOrder.test.ts b/tests/merchOrder.test.ts index 4c7967fe..5c8e2dc1 100644 --- a/tests/merchOrder.test.ts +++ b/tests/merchOrder.test.ts @@ -1637,6 +1637,62 @@ describe('merch order pickup events', () => { .rejects.toThrow('Cannot change order pickup to an event that starts in less than 2 days'); }); + test('members cannot update their orders\' pickup events if the new pickup event is full', async () => { + const conn = await DatabaseConnection.get(); + const member = UserFactory.fake({ credits: 10000 }); + const item = MerchFactory.fakeItem({ + hidden: false, + monthlyLimit: 100, + }); + const option = MerchFactory.fakeOption({ + item, + quantity: 2, + price: 2000, + }); + const firstPickupEvent = MerchFactory.fakeFutureOrderPickupEvent({ + orderLimit: 1, + }); + + const secondPickupEvent = MerchFactory.fakeFutureOrderPickupEvent({ + orderLimit: 1, + }); + + await new PortalState() + .createUsers(member) + .createMerchItem(item) + .createMerchItemOptions(option) + .createOrderPickupEvents(firstPickupEvent, secondPickupEvent) + .orderMerch(member, [{ option, quantity: 1 }], firstPickupEvent) + .write(); + + const emailService = mock(EmailService); + when(emailService.sendOrderConfirmation(member.email, member.firstName, anything())) + .thenResolve(); + + // place order to secondPickupEvent + const order = [ + { + option: option.uuid, + quantity: 1, + }, + ]; + const placeMerchOrderRequest = { + order, + pickupEvent: secondPickupEvent.uuid, + }; + + const merchController = ControllerFactory.merchStore(conn, instance(emailService)); + const placedOrderResponse = await merchController.placeMerchOrder(placeMerchOrderRequest, member); + const placedOrder = placedOrderResponse.order; + + // attempt to reschedule to firstPickupEvent + const orderParams = { uuid: placedOrder.uuid }; + const newPickupEventParams = { pickupEvent: firstPickupEvent.uuid }; + await expect(merchController.rescheduleOrderPickup(orderParams, newPickupEventParams, member)) + .rejects + .toThrow('This merch pickup event is full! Please choose a different pickup event'); + }); + test('placing an order with a pickup event that has not reached its capacity succeeds', async () => { const conn = await DatabaseConnection.get(); const member = UserFactory.fake({ points: 100 }); @@ -1749,6 +1805,8 @@ describe('merch order pickup events', () => { pickupEvent: pickupEvent.uuid, }; + console.log('PICKUP EVENT', pickupEvent); + await expect(merchController.placeMerchOrder(placeMerchOrderRequest, member)) .rejects .toThrow('This merch pickup event is full! Please choose a different pickup event'); diff --git a/tests/merchStore.test.ts b/tests/merchStore.test.ts index cbaa4bce..1d86fb3d 100644 --- a/tests/merchStore.test.ts +++ b/tests/merchStore.test.ts @@ -619,6 +619,47 @@ describe('merch item edits', () => { expect(getMerchItemResponse.item.lifetimeLimit).toEqual(merchItemEdits.lifetimeLimit); }); + test('merch item collections can be updated', async () => { + const conn = await DatabaseConnection.get(); + const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); + const collection = MerchFactory.fakeCollection(); + const item = MerchFactory.fakeItem(); + + await new PortalState() + .createUsers(admin) + .createMerchCollections(collection) + .createMerchItem(item) + .write(); + + const merchStoreController = ControllerFactory.merchStore(conn); + const params = { uuid: item.uuid }; + + const oldCollection = (await merchStoreController.getOneMerchItem(params, admin)).item.collection; + const newCollectionSize = collection.items.length; + + // update the description and increment the purchase limits + const merchItemEdits: MerchItemEdit = { + description: faker.datatype.hexaDecimal(10), + collection: collection.uuid, + }; + const editMerchItemRequest = { merchandise: merchItemEdits }; + await merchStoreController.editMerchItem(params, editMerchItemRequest, admin); + + const getMerchItemResponse = await merchStoreController.getOneMerchItem(params, admin); + expect(getMerchItemResponse.item.description).toEqual(merchItemEdits.description); + expect(getMerchItemResponse.item.collection.uuid).toEqual(merchItemEdits.collection); + + // test collection update + const oldCollectionParams = { uuid: oldCollection.uuid }; + const getOldMerchCollectionResponse = await merchStoreController.getOneMerchCollection(oldCollectionParams, admin); + const newCollectionParams = { uuid: collection.uuid }; + const getNewMerchCollectionResponse = await merchStoreController.getOneMerchCollection(newCollectionParams, admin); + + expect(getOldMerchCollectionResponse.collection.items.length).toBe(0); + expect(getNewMerchCollectionResponse.collection.items.length).toBe(newCollectionSize + 1); + expect(getNewMerchCollectionResponse.collection.items[0].uuid).toBe(item.uuid); + }); + test('merch item option fields can be updated', async () => { const conn = await DatabaseConnection.get(); const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN });