-
Notifications
You must be signed in to change notification settings - Fork 5
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
Implement API, service, and repository layers for order pickup events #201
Conversation
…es in test factories
Thanks for contributing! If you've made changes to the API's functionality, please make sure to bump the package version—see this guide to semantic versioning for details—and document those changes as appropriate. |
For some reason the commits are a bit messed up, but the diff should be the same, and it'll all get squashed when merged |
I guess since we squash and merge, we can't re-use the same branch for multiple PRs unless we do some supreme git magic |
What do you mean? We can definitely reuse the same branch name after a PR merges but we definitely can't reuse the same branch name for multiple open PRs with different changes. |
Let's keep the tests in this PR so we have some confidence in what we're merging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple questions and let's add the tests to this PR but LGTM.
@@ -188,4 +193,44 @@ export class MerchStoreController { | |||
await this.merchStoreService.updateOrderItems(fulfillOrderRequest.items); | |||
return { error: null }; | |||
} | |||
|
|||
@Get('/pickupEvent/past') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be under the /order
prefix instead? And do we need separate past and future endpoints or just a future endpoint? I could imagine reasons to look at past event events because a member want want to see something related to the event, but I can't think of a good reason to scroll through past pickup events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also nit but thoughts on /pickup
instead of /pickupEvent
? It just looks a little neater but I also prefer /first-second
instead of /firstSecond
because it's a little more readable when looking at requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could imagine reasons to look at past event events because a member want want to see something related to the event, but I can't think of a good reason to scroll through past pickup events.
The reason we have /past
as well is so that at the end of the quarter, an admin can look at all the unfulfilled orders from past events and take action on those. In our case, membership will cancel/refund all orders that haven't been fulfilled by the end of the quarter. This route would serve that functionality, instead of having something like GET /order/past
with query params or something to get past orders that weren't fulfilled since I think orders temporally are tied to pickup events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be under the /order prefix instead? Also nit but thoughts on /pickup instead of /pickupEvent? It just looks a little neater but I also prefer /first-second instead of /firstSecond because it's a little more readable when looking at requests.
Good call. I like the idea of /order/pickup
since pickup events are entities that are tightly related to orders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we have
/past
as well is so that at the end of the quarter, an admin can look at all the unfulfilled orders from past events and take action on those. In our case, membership will cancel/refund all orders that haven't been fulfilled by the end of the quarter. This route would serve that functionality, instead of having something likeGET /order/past
with query params or something to get past orders that weren't fulfilled since I think orders temporally are tied to pickup events.
What about something like /order/unfulfilled
then? That seems more accurate for what the route's intended for. The route would return unfulfilled orders, "unfulfilled" meaning orders with past pickup events that weren't marked as completed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're adding a status
field to orders where status
is one of {PLACED, MISSED, CANCELLED, FULFILLED}
with query params added for status
in GET /order
so I think we can use that for getting all missed orders and taking action. I can remove the /past
endpoint
@Get('/pickupEvent/past') | ||
async getPastPickupEvents(@AuthenticatedUser() user: UserModel): Promise<GetOrderPickupEventsResponse> { | ||
const canSeePickupEventOrders = PermissionsService.canSeePickupEventOrders(user); | ||
const pickupEvents = await this.merchStoreService.getPastPickupEvents(canSeePickupEventOrders); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For these new endpoints, can we start returning the models instead of the response types? And then do the model → response in the controller? We can do a refactor to move the existing endpoints over to this pattern later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, is the reasoning because converting the model to response type is the responsibility of the controller, since it's the one that produces a response? And that it shouldn't be the responsibility of the service layer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah
}); | ||
} | ||
|
||
public async deletePickupEvent(uuid: Uuid): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would deleting a pickup event work? Email the attendees, delete the event from their orders, and then delete the event? Should there be a way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleting a pickup event is only allowed for events that don't have orders attached to it, so it'd only be used if an admin creates a 'faulty' event and wants to delete it before anyone uses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if an admin has to cancel an event for some reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@steets250 any thoughts on this behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think Sumeet's use case is a good idea in the event that we need to cancel a pick up event. Would we want an order status type that reflects the state of an order where it's pickup event (e.g. PICKUP_CANCELLED
) to mark an order as needing to be rescheduled for pickup, or should frontend just check if the pickup event field is empty and allow the user to set a new one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PICKUP_CANCELED
sounds like a good idea to me, so that we can differentiate that case and handle it correctly instead of auto-canceling and -refunding that order at the end of the quarter. Maybe some grace period of a month or end of quarter (whichever comes later) to reschedule?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I think the way membership's doing pick up events is basically until the end of the quarter, so we can just make the users reschedule to any event before the end of the quarter, and if they don't, then membership can just refund those orders in addition to the other missed ones
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to omit this deletePickupEvent
change from this PR and implement it in the next PR (which is for handling order states like MISSED, CANCELLED, etc.
since it seems to be more relevant to that. For now, I'll delete the DELETE /order/pickup
endpoint so that deletion can't be used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tracking in #204
Co-authored-by: Sumeet Bansal <[email protected]>
…ast pickup event route
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes LGTM. Some nits and a potential NPE.
type: 'uuid', | ||
})); | ||
await queryRunner.createForeignKey(TABLE_NAME, new TableForeignKey({ | ||
columnNames: ['pickupEvent'], | ||
referencedTableName: 'OrderPickupEvents', | ||
referencedColumnNames: ['uuid'], | ||
onDelete: 'SET NULL', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think instead of relying on this, we should explicitly set the field to null when it we set the status to PICKUP_EVENT_CANCELED
so it's more obvious what happens during a delete.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem I'm getting when I remove this, is that when I try to delete the order pickup event, I get the 'foreign key violates non-null constraints', even though the fields themselves are { nullable: true }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FAIL tests/merch.test.ts
● Test suite failed to run
QueryFailedError: update or delete on table "OrderPickupEvents" violates foreign key constraint "Orders_pickupEvent_OrderPickupEvents_uuid_fkey" on table "Orders"
at new QueryFailedError (src/error/QueryFailedError.ts:9:9)
at PostgresQueryRunner.<anonymous> (src/driver/postgres/PostgresQueryRunner.ts:228:19)
at step (node_modules/tslib/tslib.js:143:27)
at Object.throw (node_modules/tslib/tslib.js:124:57)
at rejected (node_modules/tslib/tslib.js:115:69)
at runMicrotasks (<anonymous>)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll leave this onDelete behavior, but also manually set the orders' pickup event to be null in the delete service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this PR would leave the delete pickup event endpoint alone for now and get it working in a followup? What I was saying earlier was that in the delete endpoint we should get all the orders for a pickup event, and update the status to PICKUP_CANCELED
and the event to the null, and then delete the pickup event, to avoid violating any fkey constraints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we will handle that in a future PR, but when I removed the onDelete: 'SET NULL'
property, then I was getting the above error. I just realized though, that the error was because of DatabaseConnection.clear()
deleting all tables asynchronously (link below), so when the OrderPickupEvents
table was deleted before the Orders
table, the above error was thrown. I didn't realize the order of the table array mattered, so I'll add a comment on that.
https://github.com/acmucsd/membership-portal/blob/master/tests/data/DatabaseConnection.ts#L28
models/OrderModel.ts
Outdated
// since all orders should either have a pickup event, or no pickup event but with status PICKUP_CANCELLED. | ||
@ManyToOne((type) => OrderPickupEventModel, | ||
(pickupEvent) => pickupEvent.orders, | ||
{ eager: true, onDelete: 'SET NULL' }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here about onDelete: 'SET NULL'
uuid: this.uuid, | ||
title: this.title, | ||
start: this.start, | ||
end: this.end, | ||
description: this.description, | ||
}; | ||
if (canSeeOrders) pickupEvent.orders = this.orders.map((order) => order.getPublicOrder()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we get an order, then that order includes a OrderPickupEventModel
but the OrderPickupEventModel
doesn't include the orders right? So shouldn't this throw some sort of null pointer if we try getting an order? I ran into a similar issue a while back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not quite catching what you're saying here. Can you explain this again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- If you call the
GET /merch/order/:uuid
endpoint, then you find anOrderModel
. - The
OrderModel
haseager: true
set for the pickup event so it includespickupEvent: OrderPickupEventModel
, but theOrderPickupEventModel
that's returned as part of theOrderModel
doesn't haveorders: OrderModel[]
, right? TheOrderPickupEventModel
wouldn't have anorders
field at all since there's no join on that. Soorders
might be null or undefined. - When
OrderModel.getPublicOrder()
's called,OrderPickupEventModel.getPublicPickupEvent()
's also called, right? - So, if
orders
is null or undefined,this.orders.map(...)
throws some sort of exception, wouldn't it?
That's the issue I ran into a while back, where some getPublic
call to one model triggered a getPublic
call to another model that didn't have some other Model[]
.
Side note: should GET /order
be GET /orders
instead? I think so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hope that's clearer, it's kind of a weird case. If it still doesn't make sense, I can pull your branch and test it myself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay got it, thanks for the detail. I think this isn't currently an issue though, because OrderModel.getPublicOrder()
calls OrderPickupEventModel.getPublicPickupEvent()
with canSeeOrders = false
(the default boolean value when nothing is passed), so the this.orders.map()
check wouldn't be ran since it's false.
The only case I can think of where this line would throw potentially an NPE is when getPublicPickupEvent
is called directly on the pickup event that hasn't been queried from the database (i.e., it's been created from the merch factory, because otherwise the orders
join happens), but even then it wouldn't because the merch factory sets orders: []
in the pickup event creation.
services/MerchStoreService.ts
Outdated
|
||
public async createPickupEvent(pickupEvent: OrderPickupEvent): Promise<OrderPickupEventModel> { | ||
return this.transactions.readWrite(async (txn) => { | ||
if (pickupEvent.start >= pickupEvent.end) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this can happen before the transaction.
@@ -86,6 +86,7 @@ export class MerchFactory { | |||
description: faker.lorem.sentences(2), | |||
start, | |||
end, | |||
orders: [], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's not actually part of the table, this shouldn't be needed right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue's that if it's excluded, then the toEqual
check in our test won't pass since every time getFuturePickupEvents
is called, then orders: []
will be set. But the 'expected' models don't have orders: []
if this line is removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏽
const merchStoreController = ControllerFactory.merchStore(conn); | ||
await merchStoreController.createPickupEvent({ pickupEvent }, admin); | ||
|
||
const [persistedPickupEvent] = await conn.manager.find(OrderPickupEventModel, { relations: ['orders'] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use a GET instead of database lookups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only have GET for future pickup events, so using database lookups I think is the only way to actually pull all persisted events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think we should standardize how these tests are operating on our system. My understanding's that these are 'API unit tests', meaning they're testing each API route as an individual unit, and that we'd eventually have integration tests for testing E2E operations like signing up -> verifying -> attending an event -> viewing leaderboard or logging in -> visit merch store -> select few items -> verify cart -> place order -> select pickup event.
If each test is testing an individual unit, then I don't think we should be using any other API routes than the one we are testing, so that a bug in 1 controller wouldn't affect multiple tests, which makes it easier to debug tests. To me it makes sense to use database lookups since those we have full control over what we're querying (which matters in this case), and so that we aren't relying on the implementation of existing lookups which might be buggy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If each test is testing an individual unit, then I don't think we should be using any other API routes than the one we are testing, so that a bug in 1 controller wouldn't affect multiple tests, which makes it easier to debug tests. To me it makes sense to use database lookups since those we have full control over what we're querying (which matters in this case), and so that we aren't relying on the implementation of existing lookups which might be buggy.
Agreed but GETs tend to be so simple I think it's fine and even preferable to use those instead of queries.
Also, I think we should standardize how these tests are operating on our system. My understanding's that these are 'API unit tests', meaning they're testing each API route as an individual unit, and that we'd eventually have integration tests for testing E2E operations like signing up -> verifying -> attending an event -> viewing leaderboard or logging in -> visit merch store -> select few items -> verify cart -> place order -> select pickup event.
Can't we do this by calling the controllers like we do now, except in larger tests that more closely resemble actual user flows? I don't know how much work it'd be to setup some other system that launches the backend app and then makes real HTTP requests, but it doesn't seem worth it based on test coverage. What would that test that our current setup doesn't? Everything outside of the controller methods, so (1) ensuring HTTP requests work, (2) the API routes are valid (e.g. GET /event/:uuid
doesn't "cover" GET /event/future
, where a request to GET /event/future
is evaluated as GET /event/:uuid
where uuid=future
), (3) the routes correctly map to controller methods, (4) request validation.
I think those are all important, but we can assume routing-controllers works as intended for (1) and (3), and I think we can write tests for (2) and (4). (2) maybe by doing something similar to the migrations test, which parses files into ASTs and checks the timestamps in the class names, except the routes test would parse controllers into annotations and verify that no route "covers" any subsequent route using regexes, and (4) by tests that manually validate.
Related to (4), something I've been thinking about wrt API specs and the API client we talked about a while back is if we can have all our controller methods defined as interfaces that the controllers then implement, and one way of doing that would be to accept raw requests in the controller methods and then validate in the method. That'd clean up our method signatures a bit too, since we could do something like
// interface
function editMerchItem(uuid, MerchItemEdit);
// controller
function editMerchItem(@Param uuid: any, merchItemEdit: any) {
CommonValidators.validateUuid(uuid);
MerchStoreValidators.validateMerchItemEdit(merchItemEdit);
}
Don't know if that even works though, and it's not really that relevant to this discussion, except that then our existing tests would also test validation.
tests/merch.test.ts
Outdated
test('POST /order/pickup fails when pickup event start date is later than end date', async () => { | ||
const conn = await DatabaseConnection.get(); | ||
const admin = UserFactory.fake({ accessType: UserAccessType.ADMIN }); | ||
const pickupEvent = MerchFactory.fakeOrderPickupEvent({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: move this to before the controller call.
await ControllerFactory.merchStore(conn) | ||
.editPickupEvent({ uuid: pickupEvent.uuid }, editPickupEventRequest, admin); | ||
|
||
const [persistedPickupEvent] = await conn.manager.find(OrderPickupEventModel, { relations: ['orders'] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here with the GET.
Co-authored-by: Sumeet Bansal <[email protected]>
Co-authored-by: Sumeet Bansal <[email protected]>
Co-authored-by: Sumeet Bansal <[email protected]>
…to null for clarity
…elete: 'SET NULL' set
… deletion in test cleanup)
…acmucsd#201) * Add schema and migration for OrderPickupEvent model * Add pickupEvent field to PlaceOrderRequest and made appropriate changes in test factories * Match OrderPickupEvent request object with model * Add pickupEvent field to validators + update tests accordingly * Update merch test changes to use pickup events * Implement service and repository methods for OrderPickupEvents * Implement API layer for order pickup events * Update repositories/MerchOrderRepository.ts Co-authored-by: Sumeet Bansal <[email protected]> * Rename /store/pickupEvent -> /store/order/pickup API route + remove past pickup event route * Change response type of pickup event services to model * Set OrderModel.pickupEvent not null (for event deletion) * Add OrderPickupEvents table to list of tables to delete from in DatabaseConnection * Write tests for order pickup events API * Temporarily remove DELETE route for pickup events * Clean up merch test param usage Co-authored-by: Sumeet Bansal <[email protected]> * Clean up merch test param usage Co-authored-by: Sumeet Bansal <[email protected]> * Clean up merch test some more Co-authored-by: Sumeet Bansal <[email protected]> * Remove onDelete: SET NULL in favor of manually setting pickup events to null for clarity * Add placeOrder verification for pickup event durations + add back onDelete: 'SET NULL' set * Remove onDelete: SET NULL for real this time (plus fix order of table deletion in test cleanup) * Remove redundant { nullable: true } Co-authored-by: Sumeet Bansal <[email protected]>
Logic implementation of order pickup events. API tests will come in a follow-up PR (to keep this PR size smaller).