@stamhoofd/backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.template.json +63 -0
  2. package/.eslintrc.js +61 -0
  3. package/README.md +40 -0
  4. package/index.ts +172 -0
  5. package/jest.config.js +11 -0
  6. package/migrations.ts +33 -0
  7. package/package.json +48 -0
  8. package/src/crons.ts +845 -0
  9. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
  10. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
  12. package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
  13. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
  14. package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
  15. package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
  17. package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
  18. package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
  19. package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
  20. package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
  21. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
  22. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
  23. package/src/endpoints/auth/SignupEndpoint.ts +107 -0
  24. package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
  25. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
  26. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
  27. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
  28. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
  29. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
  30. package/src/endpoints/global/files/UploadFile.ts +147 -0
  31. package/src/endpoints/global/files/UploadImage.ts +119 -0
  32. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
  33. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
  34. package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
  35. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
  36. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
  37. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
  38. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
  39. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
  40. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
  41. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
  42. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
  43. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
  44. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
  45. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
  46. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
  47. package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
  48. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
  49. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
  50. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
  51. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
  52. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
  53. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
  54. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
  55. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
  56. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
  57. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
  58. package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
  59. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
  60. package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
  61. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
  62. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
  63. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
  64. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
  65. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
  66. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
  67. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
  68. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
  69. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
  70. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
  71. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
  72. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
  73. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
  74. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
  75. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
  76. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
  77. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
  78. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
  79. package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
  80. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
  81. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
  82. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
  83. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
  84. package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
  85. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
  86. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
  87. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
  88. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
  89. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
  90. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
  91. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
  92. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
  93. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
  94. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
  95. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
  96. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
  97. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
  98. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
  99. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
  100. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
  101. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
  102. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
  103. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
  104. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
  105. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
  106. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
  107. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
  108. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
  109. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
  110. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
  111. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
  112. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
  113. package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
  114. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
  115. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
  116. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
  117. package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
  118. package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
  119. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
  120. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
  121. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
  122. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
  123. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
  124. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
  125. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
  126. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
  127. package/src/helpers/AddressValidator.test.ts +40 -0
  128. package/src/helpers/AddressValidator.ts +256 -0
  129. package/src/helpers/AdminPermissionChecker.ts +1031 -0
  130. package/src/helpers/AuthenticatedStructures.ts +158 -0
  131. package/src/helpers/BuckarooHelper.ts +279 -0
  132. package/src/helpers/CheckSettlements.ts +215 -0
  133. package/src/helpers/Context.ts +202 -0
  134. package/src/helpers/CookieHelper.ts +45 -0
  135. package/src/helpers/ForwardHandler.test.ts +216 -0
  136. package/src/helpers/ForwardHandler.ts +140 -0
  137. package/src/helpers/OpenIDConnectHelper.ts +284 -0
  138. package/src/helpers/StripeHelper.ts +293 -0
  139. package/src/helpers/StripePayoutChecker.ts +188 -0
  140. package/src/middleware/ContextMiddleware.ts +16 -0
  141. package/src/migrations/1646578856-validate-addresses.ts +60 -0
  142. package/src/seeds/0000000000-example.ts +13 -0
  143. package/src/seeds/1715028563-user-permissions.ts +52 -0
  144. package/tests/e2e/stock.test.ts +2120 -0
  145. package/tests/e2e/tickets.test.ts +926 -0
  146. package/tests/helpers/StripeMocker.ts +362 -0
  147. package/tests/helpers/TestServer.ts +21 -0
  148. package/tests/jest.global.setup.ts +29 -0
  149. package/tests/jest.setup.ts +59 -0
  150. package/tsconfig.json +42 -0
@@ -0,0 +1,2120 @@
1
+ /* eslint-disable jest/expect-expect */
2
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
3
+ /* eslint-disable jest/no-standalone-expect */
4
+ import { PatchableArray, PatchableArrayAutoEncoder } from "@simonbackx/simple-encoding";
5
+ import { Request } from "@simonbackx/simple-endpoints";
6
+ import { Order, Organization, OrganizationFactory, StripeAccount, Token, UserFactory, Webshop, WebshopFactory } from "@stamhoofd/models";
7
+ import { Address, Cart, CartItem, CartItemOption, CartReservedSeat, Country, Customer, Option, OptionMenu, OrderData, OrderStatus, PaymentConfiguration, PaymentMethod, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, Product, ProductPrice, ProductType, ReservedSeat, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, ValidatedAddress, WebshopDeliveryMethod, WebshopMetaData, WebshopOnSiteMethod, WebshopPrivateMetaData, WebshopTakeoutMethod, WebshopTimeSlot } from "@stamhoofd/structures";
8
+ import { v4 as uuidv4 } from "uuid";
9
+
10
+ import { PatchWebshopOrdersEndpoint } from "../../src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint";
11
+ import { PlaceOrderEndpoint } from '../../src/endpoints/organization/webshops/PlaceOrderEndpoint';
12
+ import { StripeMocker } from "../helpers/StripeMocker";
13
+ import { testServer } from "../helpers/TestServer";
14
+
15
+ const address = Address.create({
16
+ street: 'Demostraat',
17
+ number: '15',
18
+ postalCode: '9000',
19
+ city: 'Gent',
20
+ country: Country.Belgium
21
+ })
22
+
23
+ const customer = Customer.create({
24
+ firstName: 'John',
25
+ lastName: 'Doe',
26
+ email: 'john@example.com',
27
+ phone: '+32412345678'
28
+ });
29
+
30
+ describe("E2E.Stock", () => {
31
+ // Test endpoint
32
+ const endpoint = new PlaceOrderEndpoint();
33
+ const patchWebshopOrdersEndpoint = new PatchWebshopOrdersEndpoint();
34
+
35
+ let organization: Organization;
36
+ let webshop: Webshop;
37
+ let product: Product;
38
+ let seatProduct: Product;
39
+ let personProduct: Product;
40
+ let takeoutMethod: WebshopTakeoutMethod;
41
+ let deliveryMethod: WebshopDeliveryMethod;
42
+ let onSiteMethod: WebshopOnSiteMethod;
43
+ let slot1: WebshopTimeSlot;
44
+ let slot2: WebshopTimeSlot;
45
+ let slot3: WebshopTimeSlot;
46
+ let slot4: WebshopTimeSlot;
47
+
48
+ let productPrice1: ProductPrice;
49
+ let productPrice2: ProductPrice;
50
+ let freeProductPrice: ProductPrice;
51
+ let personProductPrice: ProductPrice;
52
+ let seatProductPrice: ProductPrice;
53
+ let seatingPlan: SeatingPlan;
54
+
55
+ let multipleChoiceOptionMenu: OptionMenu;
56
+ let chooseOneOptionMenu: OptionMenu;
57
+ let checkboxOption1: Option;
58
+ let checkboxOption2: Option;
59
+ let radioOption1: Option;
60
+ let radioOption2: Option;
61
+ let stripeMocker: StripeMocker
62
+ let stripeAccount: StripeAccount
63
+ let token: Token;
64
+
65
+ async function refreshAll() {
66
+ webshop = (await Webshop.getByID(webshop.id))!;
67
+ product = webshop.products.find(p => p.id == product.id)!;
68
+ seatProduct = webshop.products.find(p => p.id == seatProduct.id)!;
69
+ personProduct = webshop.products.find(p => p.id == personProduct.id)!;
70
+ takeoutMethod = webshop.meta.checkoutMethods.find(m => m.id == takeoutMethod.id)! as WebshopTakeoutMethod;
71
+ deliveryMethod = webshop.meta.checkoutMethods.find(m => m.id == deliveryMethod.id)! as WebshopDeliveryMethod;
72
+ onSiteMethod = webshop.meta.checkoutMethods.find(m => m.id == onSiteMethod.id)! as WebshopOnSiteMethod;
73
+ slot1 = takeoutMethod.timeSlots.timeSlots.find(s => s.id == slot1.id)!;
74
+ slot2 = takeoutMethod.timeSlots.timeSlots.find(s => s.id == slot2.id)!;
75
+ slot3 = deliveryMethod.timeSlots.timeSlots.find(s => s.id == slot3.id)!;
76
+ slot4 = onSiteMethod.timeSlots.timeSlots.find(s => s.id == slot4.id)!;
77
+ productPrice1 = product.prices.find(p => p.id == productPrice1.id)!;
78
+ productPrice2 = product.prices.find(p => p.id == productPrice2.id)!;
79
+ freeProductPrice = product.prices.find(p => p.id == freeProductPrice.id)!;
80
+ multipleChoiceOptionMenu = product.optionMenus.find(m => m.id == multipleChoiceOptionMenu.id)!;
81
+ chooseOneOptionMenu = product.optionMenus.find(m => m.id == chooseOneOptionMenu.id)!;
82
+ checkboxOption1 = multipleChoiceOptionMenu.options.find(o => o.id == checkboxOption1.id)!;
83
+ checkboxOption2 = multipleChoiceOptionMenu.options.find(o => o.id == checkboxOption2.id)!;
84
+ radioOption1 = chooseOneOptionMenu.options.find(o => o.id == radioOption1.id)!;
85
+ radioOption2 = chooseOneOptionMenu.options.find(o => o.id == radioOption2.id)!;
86
+ personProductPrice = personProduct.prices.find(p => p.id == personProductPrice.id)!;
87
+ seatProductPrice = seatProduct.prices.find(p => p.id == seatProductPrice.id)!;
88
+ seatingPlan = webshop.meta.seatingPlans.find(s => s.id === seatingPlan.id)!;
89
+ }
90
+
91
+ async function refreshCartItems(orderId: string, cartItems: CartItem[]): Promise<Order> {
92
+ const order = (await Order.getByID(orderId))!;
93
+
94
+ for (const item of cartItems) {
95
+ const i = order.data.cart.items.find(i => i.id == item.id)!;
96
+ if (i) {
97
+ item.set(i);
98
+ }
99
+ }
100
+ return order;
101
+ }
102
+
103
+ async function checkStocks(orderIds: string[], cartItems: CartItem[], excludedCartItems: CartItem[] = []) {
104
+ await refreshAll();
105
+ const orders: Order[] = [];
106
+ for (const orderId of orderIds) {
107
+ orders.push(await refreshCartItems(orderId, [...cartItems, ...excludedCartItems]));
108
+ }
109
+
110
+ const products = [product, seatProduct, personProduct];
111
+ for (const product of products) {
112
+ let used = 0;
113
+ const seats: ReservedSeat[] = []
114
+
115
+ for (const item of cartItems) {
116
+ if (item.product.id == product.id) {
117
+ used += item.amount;
118
+ // All seats should be reserved
119
+ expect(item.reservedSeats).toIncludeSameMembers(item.seats);
120
+ seats.push(...item.seats.map(s => ReservedSeat.create(s)));
121
+ }
122
+ }
123
+ expect(product.usedStock).toBe(used);
124
+ expect(product.reservedSeats.length).toBe(seats.length);
125
+ expect(product.reservedSeats).toIncludeSameMembers(seats);
126
+ }
127
+
128
+ const productPrices = [productPrice1, productPrice2, freeProductPrice, personProductPrice];
129
+ for (const price of productPrices) {
130
+ let used = 0;
131
+ for (const item of cartItems) {
132
+ if (item.productPrice.id == price.id) {
133
+ used += item.amount;
134
+ }
135
+ }
136
+ expect(price.usedStock).toBe(used);
137
+ }
138
+
139
+ const options = [checkboxOption1, checkboxOption2, radioOption1, radioOption2];
140
+ for (const option of options) {
141
+ let used = 0;
142
+ for (const item of cartItems) {
143
+ for (const o of item.options) {
144
+ if (o.option.id == option.id) {
145
+ used += item.amount;
146
+ }
147
+ }
148
+ }
149
+ expect(option.usedStock).toBe(used);
150
+ }
151
+
152
+ // Now check reserved for each item
153
+ for (const item of cartItems) {
154
+ expect(item.reservedAmount).toBe(item.amount);
155
+
156
+ for (const price of productPrices) {
157
+ if (item.productPrice.id == price.id) {
158
+ expect(item.reservedPrices.get(price.id)).toBe(item.amount);
159
+ } else {
160
+ expect(item.reservedPrices.get(price.id) ?? 0).toBe(0);
161
+ }
162
+ }
163
+
164
+ for (const option of options) {
165
+ let reserved = 0;
166
+ for (const o of item.options) {
167
+ if (o.option.id == option.id) {
168
+ reserved += item.amount;
169
+ }
170
+ }
171
+ expect(item.reservedOptions.get(option.id) ?? 0).toBe(reserved);
172
+ }
173
+ }
174
+
175
+ for (const item of excludedCartItems) {
176
+ expect(item.reservedAmount).toBe(0);
177
+ expect(item.reservedSeats.length).toBe(0);
178
+
179
+ for (const price of productPrices) {
180
+ expect(item.reservedPrices.get(price.id) ?? 0).toBe(0);
181
+ }
182
+
183
+ for (const option of options) {
184
+ expect(item.reservedOptions.get(option.id) ?? 0).toBe(0);
185
+ }
186
+ }
187
+
188
+ // Check order stock
189
+ for (const order of orders) {
190
+ const filteredItems = cartItems.filter(i => !!order.data.cart.items.find(c => c.id === i.id))
191
+ let persons = 0;
192
+ for (const item of filteredItems) {
193
+ if (item.product.type === ProductType.Person) {
194
+ persons += item.amount;
195
+ }
196
+ }
197
+ expect(order.data.reservedPersons).toBe(persons);
198
+ expect(order.data.reservedOrder).toBe(filteredItems.length > 0);
199
+ }
200
+
201
+ const timeslots = [slot1, slot2, slot3, slot4];
202
+ for (const slot of timeslots) {
203
+ let ordersCount = 0;
204
+ let personsCount = 0;
205
+
206
+ for (const order of orders) {
207
+ if (order.data.timeSlot?.id === slot.id) {
208
+ let persons = 0;
209
+ const filteredItems = cartItems.filter(i => !!order.data.cart.items.find(c => c.id === i.id))
210
+
211
+ for (const item of filteredItems) {
212
+ if (item.product.type === ProductType.Person) {
213
+ persons += item.amount;
214
+ }
215
+ }
216
+
217
+ ordersCount += filteredItems.length > 0 ? 1 : 0
218
+ personsCount += persons;
219
+ }
220
+ }
221
+
222
+ expect(slot.usedOrders).toBe(ordersCount);
223
+ expect(slot.usedPersons).toBe(personsCount);
224
+ }
225
+
226
+ return orders;
227
+ }
228
+
229
+ async function checkStock(orderId: string, cartItems: CartItem[], excludedCartItems: CartItem[] = []) {
230
+ const otherOrders = (await Order.where({webshopId: webshop.id})).filter(o => o.id !== orderId)
231
+ return (await checkStocks([orderId, ...otherOrders.map(o => o.id)], [...cartItems, ...otherOrders.flatMap(o => o.data.cart.items)], excludedCartItems))[0]
232
+ }
233
+
234
+ /** Allows to change the stock */
235
+ async function saveChanges() {
236
+ // Set products
237
+ webshop = (await Webshop.getByID(webshop.id))!;
238
+ webshop.products = [product, seatProduct, personProduct]
239
+ await webshop.save();
240
+ await refreshAll();
241
+ }
242
+
243
+ beforeAll(async () => {
244
+ stripeMocker = new StripeMocker();
245
+ stripeMocker.start();
246
+ organization = await new OrganizationFactory({}).create()
247
+ stripeAccount = await stripeMocker.createStripeAccount(organization.id);
248
+
249
+ const user = await new UserFactory({
250
+ organization,
251
+ permissions: Permissions.create({
252
+ level: PermissionLevel.Full
253
+ })
254
+ }).create()
255
+ token = await Token.createToken(user)
256
+ });
257
+
258
+ afterAll(() => {
259
+ stripeMocker.stop();
260
+ });
261
+
262
+ beforeEach(async () => {
263
+ stripeMocker.reset();
264
+ let meta = WebshopMetaData.patch({});
265
+
266
+ productPrice1 = ProductPrice.create({
267
+ name: 'productPrice1',
268
+ price: 100,
269
+ stock: 100
270
+ })
271
+
272
+ productPrice2 = ProductPrice.create({
273
+ name: 'productPrice2',
274
+ price: 150,
275
+ stock: 100
276
+ })
277
+
278
+ freeProductPrice = ProductPrice.create({
279
+ name: 'freeProductPrice',
280
+ price: 0,
281
+ stock: 100
282
+ })
283
+
284
+ checkboxOption1 = Option.create({
285
+ name: 'checkboxOption1',
286
+ price: 10,
287
+ stock: 100
288
+ })
289
+
290
+ checkboxOption2 = Option.create({
291
+ name: 'checkboxOption2',
292
+ price: 0,
293
+ stock: 100
294
+ })
295
+
296
+ radioOption1 = Option.create({
297
+ name: 'radioOption1',
298
+ price: 10,
299
+ stock: 100
300
+ })
301
+
302
+ radioOption2 = Option.create({
303
+ name: 'radioOption2',
304
+ price: 0,
305
+ stock: 100
306
+ })
307
+
308
+ multipleChoiceOptionMenu = OptionMenu.create({
309
+ name: 'multipleChoiceOptionMenu',
310
+ multipleChoice: true,
311
+ options: [checkboxOption1, checkboxOption2]
312
+ })
313
+
314
+ chooseOneOptionMenu = OptionMenu.create({
315
+ name: 'chooseOneOptionMenu',
316
+ multipleChoice: false,
317
+ options: [radioOption1, radioOption2]
318
+ })
319
+
320
+ product = Product.create({
321
+ name: 'product',
322
+ stock: 100,
323
+ prices: [productPrice1, productPrice2, freeProductPrice],
324
+ optionMenus: [multipleChoiceOptionMenu, chooseOneOptionMenu]
325
+ })
326
+
327
+ personProduct = Product.create({
328
+ name: 'personProduct',
329
+ type: ProductType.Person,
330
+ stock: 100
331
+ })
332
+ personProductPrice = personProduct.prices[0]
333
+
334
+ seatingPlan = SeatingPlan.create({
335
+ name: 'Testzaal',
336
+ sections: [
337
+ SeatingPlanSection.create({
338
+ rows: [
339
+ SeatingPlanRow.create({
340
+ label: 'A',
341
+ seats: [
342
+ SeatingPlanSeat.create({
343
+ label: '1'
344
+ }),
345
+ SeatingPlanSeat.create({
346
+ label: '2'
347
+ }),
348
+ SeatingPlanSeat.create({
349
+ label: '3'
350
+ }),
351
+ SeatingPlanSeat.create({
352
+ label: '4'
353
+ })
354
+ ]
355
+ }),
356
+ SeatingPlanRow.create({
357
+ label: 'B',
358
+ seats: [
359
+ SeatingPlanSeat.create({
360
+ label: '1'
361
+ }),
362
+ SeatingPlanSeat.create({
363
+ label: '2'
364
+ }),
365
+ SeatingPlanSeat.create({
366
+ label: '3'
367
+ }),
368
+ SeatingPlanSeat.create({
369
+ label: '4'
370
+ })
371
+ ]
372
+ })
373
+ ]
374
+ })
375
+ ]
376
+ })
377
+ meta.seatingPlans.addPut(seatingPlan)
378
+
379
+ seatProduct = Product.create({
380
+ name: 'seatProduct',
381
+ type: ProductType.Ticket,
382
+ seatingPlanId: seatingPlan.id
383
+ })
384
+ seatProductPrice = seatProduct.prices[0]
385
+
386
+ // Takeout
387
+ takeoutMethod = WebshopTakeoutMethod.create({
388
+ name: 'Bakkerij Test',
389
+ address
390
+ })
391
+
392
+ slot1 = WebshopTimeSlot.create({
393
+ date: new Date(),
394
+ maxPersons: 100,
395
+ maxOrders: 100
396
+ });
397
+ takeoutMethod.timeSlots.timeSlots.push(slot1)
398
+
399
+ slot2 = WebshopTimeSlot.create({
400
+ date: new Date(),
401
+ maxPersons: 100,
402
+ maxOrders: 100,
403
+ startTime: 14*60,
404
+ endTime: 15*60
405
+ })
406
+ takeoutMethod.timeSlots.timeSlots.push(slot2)
407
+ meta.checkoutMethods.addPut(takeoutMethod)
408
+
409
+
410
+ // Delivery
411
+ deliveryMethod = WebshopDeliveryMethod.create({
412
+ name: 'Delivery',
413
+ countries: [Country.Belgium]
414
+ })
415
+
416
+ slot3 = WebshopTimeSlot.create({
417
+ date: new Date(),
418
+ maxPersons: 100,
419
+ maxOrders: 100
420
+ });
421
+
422
+ deliveryMethod.timeSlots.timeSlots.push(slot3)
423
+ meta.checkoutMethods.addPut(deliveryMethod)
424
+
425
+ // OnSite
426
+ onSiteMethod = WebshopOnSiteMethod.create({
427
+ name: 'Onsite',
428
+ address
429
+ })
430
+
431
+ slot4 = WebshopTimeSlot.create({
432
+ date: new Date(),
433
+ maxPersons: 100,
434
+ maxOrders: 100
435
+ });
436
+
437
+ onSiteMethod.timeSlots.timeSlots.push(slot4)
438
+ meta.checkoutMethods.addPut(onSiteMethod)
439
+
440
+ const paymentConfigurationPatch = PaymentConfiguration.patch({
441
+ transferSettings: TransferSettings.create({
442
+ iban: 'BE56587127952688' // = random IBAN
443
+ }),
444
+ })
445
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.PointOfSale)
446
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.Transfer)
447
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.Bancontact)
448
+
449
+ const privatePaymentConfiguration = PrivatePaymentConfiguration.patch({
450
+ stripeAccountId: stripeAccount.id
451
+ })
452
+
453
+ meta = meta.patch({
454
+ paymentConfiguration: paymentConfigurationPatch
455
+ })
456
+
457
+ const privateMeta = WebshopPrivateMetaData.patch({
458
+ paymentConfiguration: privatePaymentConfiguration
459
+ })
460
+
461
+ webshop = await new WebshopFactory({
462
+ organizationId: organization.id,
463
+ name: 'Test webshop',
464
+ meta,
465
+ privateMeta,
466
+ products: [product, seatProduct, personProduct]
467
+ }).create()
468
+ });
469
+
470
+ describe('Reserving stock', () => {
471
+ test("Online payments reserve the stock and remain if they succeed", async () => {
472
+ const orderData = OrderData.create({
473
+ paymentMethod: PaymentMethod.Bancontact,
474
+ checkoutMethod: onSiteMethod,
475
+ timeSlot: slot4,
476
+ cart: Cart.create({
477
+ items: [
478
+ CartItem.create({
479
+ product,
480
+ productPrice: productPrice2,
481
+ amount: 5,
482
+ options: [
483
+ CartItemOption.create({
484
+ optionMenu: multipleChoiceOptionMenu,
485
+ option: checkboxOption1
486
+ }),
487
+ CartItemOption.create({
488
+ optionMenu: multipleChoiceOptionMenu,
489
+ option: checkboxOption2
490
+ }),
491
+ CartItemOption.create({
492
+ optionMenu: chooseOneOptionMenu,
493
+ option: radioOption2
494
+ })
495
+ ]
496
+ })
497
+ ]
498
+ }),
499
+ customer
500
+ })
501
+
502
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
503
+
504
+ const response = await testServer.test(endpoint, r);
505
+ expect(response.body).toBeDefined();
506
+ const order = response.body.order;
507
+
508
+ await checkStock(order.id, order.data.cart.items);
509
+
510
+ // Cancel the payment
511
+ await stripeMocker.succeedPayment(stripeMocker.getLastIntent())
512
+
513
+ const updatedOrder = await checkStock(order.id, order.data.cart.items);
514
+ expect(updatedOrder.status).toBe(OrderStatus.Created);
515
+ expect(updatedOrder.number).toBeDefined();
516
+ });
517
+
518
+ test("Online payments do not reserve the stock if creation fails", async () => {
519
+ stripeMocker.forceFailure();
520
+ const orderData = OrderData.create({
521
+ paymentMethod: PaymentMethod.Bancontact,
522
+ checkoutMethod: onSiteMethod,
523
+ timeSlot: slot4,
524
+ cart: Cart.create({
525
+ items: [
526
+ CartItem.create({
527
+ product,
528
+ productPrice: productPrice2,
529
+ amount: 5,
530
+ options: [
531
+ CartItemOption.create({
532
+ optionMenu: multipleChoiceOptionMenu,
533
+ option: checkboxOption1
534
+ }),
535
+ CartItemOption.create({
536
+ optionMenu: multipleChoiceOptionMenu,
537
+ option: checkboxOption2
538
+ }),
539
+ CartItemOption.create({
540
+ optionMenu: chooseOneOptionMenu,
541
+ option: radioOption2
542
+ })
543
+ ]
544
+ })
545
+ ]
546
+ }),
547
+ customer
548
+ })
549
+
550
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
551
+
552
+ await expect(testServer.test(endpoint, r)).toReject();
553
+ await checkStocks([], []);
554
+ });
555
+
556
+ test("Free payments reserve the stock", async () => {
557
+ const orderData = OrderData.create({
558
+ paymentMethod: PaymentMethod.Unknown,
559
+ checkoutMethod: onSiteMethod,
560
+ timeSlot: slot4,
561
+ cart: Cart.create({
562
+ items: [
563
+ CartItem.create({
564
+ product,
565
+ productPrice: freeProductPrice,
566
+ amount: 5,
567
+ options: [
568
+ CartItemOption.create({
569
+ optionMenu: multipleChoiceOptionMenu,
570
+ option: checkboxOption2
571
+ }),
572
+ CartItemOption.create({
573
+ optionMenu: chooseOneOptionMenu,
574
+ option: radioOption2
575
+ })
576
+ ]
577
+ })
578
+ ]
579
+ }),
580
+ customer
581
+ })
582
+
583
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
584
+
585
+ const response = await testServer.test(endpoint, r);
586
+ expect(response.body).toBeDefined();
587
+ const order = response.body.order;
588
+
589
+ await checkStock(order.id, order.data.cart.items);
590
+ });
591
+
592
+ test("Transfer payments reserve the stock", async () => {
593
+ const orderData = OrderData.create({
594
+ paymentMethod: PaymentMethod.Transfer,
595
+ checkoutMethod: onSiteMethod,
596
+ timeSlot: slot4,
597
+ cart: Cart.create({
598
+ items: [
599
+ CartItem.create({
600
+ product,
601
+ productPrice: productPrice2,
602
+ amount: 5,
603
+ options: [
604
+ CartItemOption.create({
605
+ optionMenu: multipleChoiceOptionMenu,
606
+ option: checkboxOption1
607
+ }),
608
+ CartItemOption.create({
609
+ optionMenu: multipleChoiceOptionMenu,
610
+ option: checkboxOption2
611
+ }),
612
+ CartItemOption.create({
613
+ optionMenu: chooseOneOptionMenu,
614
+ option: radioOption2
615
+ })
616
+ ]
617
+ })
618
+ ]
619
+ }),
620
+ customer
621
+ })
622
+
623
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
624
+
625
+ const response = await testServer.test(endpoint, r);
626
+ expect(response.body).toBeDefined();
627
+ const order = response.body.order;
628
+
629
+ await checkStock(order.id, order.data.cart.items);
630
+ });
631
+
632
+ test("Transfer payments do not reserve the stock if iban is missing and throws on validating", async () => {
633
+ webshop.meta.paymentConfiguration.transferSettings.iban = null;
634
+ await webshop.save();
635
+
636
+ const orderData = OrderData.create({
637
+ paymentMethod: PaymentMethod.Transfer,
638
+ checkoutMethod: onSiteMethod,
639
+ timeSlot: slot4,
640
+ cart: Cart.create({
641
+ items: [
642
+ CartItem.create({
643
+ product,
644
+ productPrice: productPrice2,
645
+ amount: 5,
646
+ options: [
647
+ CartItemOption.create({
648
+ optionMenu: multipleChoiceOptionMenu,
649
+ option: checkboxOption1
650
+ }),
651
+ CartItemOption.create({
652
+ optionMenu: multipleChoiceOptionMenu,
653
+ option: checkboxOption2
654
+ }),
655
+ CartItemOption.create({
656
+ optionMenu: chooseOneOptionMenu,
657
+ option: radioOption2
658
+ })
659
+ ]
660
+ })
661
+ ]
662
+ }),
663
+ customer
664
+ })
665
+
666
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
667
+
668
+ await expect(testServer.test(endpoint, r)).rejects.toThrow('Missing IBAN');
669
+ await checkStocks([], []);
670
+ });
671
+
672
+ test("POS payments reserve the stock", async () => {
673
+ const orderData = OrderData.create({
674
+ paymentMethod: PaymentMethod.PointOfSale,
675
+ checkoutMethod: takeoutMethod,
676
+ timeSlot: slot1,
677
+ cart: Cart.create({
678
+ items: [
679
+ CartItem.create({
680
+ product,
681
+ productPrice: productPrice1,
682
+ amount: 5,
683
+ options: [
684
+ CartItemOption.create({
685
+ optionMenu: multipleChoiceOptionMenu,
686
+ option: checkboxOption2
687
+ }),
688
+ CartItemOption.create({
689
+ optionMenu: chooseOneOptionMenu,
690
+ option: radioOption1
691
+ })
692
+ ]
693
+ })
694
+ ]
695
+ }),
696
+ customer
697
+ })
698
+
699
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
700
+
701
+ const response = await testServer.test(endpoint, r);
702
+ expect(response.body).toBeDefined();
703
+ const order = response.body.order;
704
+
705
+ await checkStock(order.id, order.data.cart.items);
706
+ });
707
+
708
+ test("Orders placed by an admin reserve the stock", async () => {
709
+ const orderData = OrderData.create({
710
+ paymentMethod: PaymentMethod.PointOfSale,
711
+ checkoutMethod: takeoutMethod,
712
+ timeSlot: slot1,
713
+ cart: Cart.create({
714
+ items: [
715
+ CartItem.create({
716
+ product,
717
+ productPrice: productPrice1,
718
+ amount: 5,
719
+ options: [
720
+ CartItemOption.create({
721
+ optionMenu: multipleChoiceOptionMenu,
722
+ option: checkboxOption2
723
+ }),
724
+ CartItemOption.create({
725
+ optionMenu: chooseOneOptionMenu,
726
+ option: radioOption1
727
+ })
728
+ ]
729
+ })
730
+ ]
731
+ }),
732
+ customer
733
+ })
734
+
735
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
736
+
737
+ const orderPatch = PrivateOrder.create({
738
+ id: uuidv4(),
739
+ data: orderData,
740
+ webshopId: webshop.id
741
+ });
742
+ patchArray.addPut(orderPatch);
743
+
744
+ // Send a patch
745
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
746
+ r.headers.authorization = "Bearer " + token.accessToken
747
+
748
+ const response = await testServer.test(patchWebshopOrdersEndpoint, r);
749
+ expect(response.body).toBeDefined();
750
+ const order = response.body[0];
751
+ await checkStock(order.id, order.data.cart.items);
752
+ });
753
+
754
+ test("Orders placed by an admin do not reserve the stock if IBAN is missing", async () => {
755
+
756
+ webshop.meta.paymentConfiguration.transferSettings.iban = null;
757
+ await webshop.save();
758
+
759
+ const orderData = OrderData.create({
760
+ paymentMethod: PaymentMethod.Transfer,
761
+ checkoutMethod: takeoutMethod,
762
+ timeSlot: slot1,
763
+ cart: Cart.create({
764
+ items: [
765
+ CartItem.create({
766
+ product,
767
+ productPrice: productPrice1,
768
+ amount: 5,
769
+ options: [
770
+ CartItemOption.create({
771
+ optionMenu: multipleChoiceOptionMenu,
772
+ option: checkboxOption2
773
+ }),
774
+ CartItemOption.create({
775
+ optionMenu: chooseOneOptionMenu,
776
+ option: radioOption1
777
+ })
778
+ ]
779
+ })
780
+ ]
781
+ }),
782
+ customer
783
+ })
784
+
785
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
786
+
787
+ const orderPatch = PrivateOrder.create({
788
+ id: uuidv4(),
789
+ data: orderData,
790
+ webshopId: webshop.id
791
+ });
792
+ patchArray.addPut(orderPatch);
793
+
794
+ // Send a patch
795
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
796
+ r.headers.authorization = "Bearer " + token.accessToken
797
+
798
+ await expect(testServer.test(patchWebshopOrdersEndpoint, r)).rejects.toThrow('Missing IBAN');
799
+ await checkStocks([], []);
800
+ });
801
+
802
+ test("Chosen seats reserve the stock for POS order", async () => {
803
+ const orderData = OrderData.create({
804
+ paymentMethod: PaymentMethod.PointOfSale,
805
+ checkoutMethod: takeoutMethod,
806
+ timeSlot: slot1,
807
+ cart: Cart.create({
808
+ items: [
809
+ CartItem.create({
810
+ product: seatProduct,
811
+ productPrice: seatProductPrice,
812
+ amount: 2,
813
+ seats: [
814
+ CartReservedSeat.create({
815
+ section: seatingPlan.sections[0].id,
816
+ row: 'A',
817
+ seat: '1'
818
+ }),
819
+ CartReservedSeat.create({
820
+ section: seatingPlan.sections[0].id,
821
+ row: 'A',
822
+ seat: '2'
823
+ })
824
+ ]
825
+ })
826
+ ]
827
+ }),
828
+ customer
829
+ })
830
+
831
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
832
+
833
+ const response = await testServer.test(endpoint, r);
834
+ expect(response.body).toBeDefined();
835
+ const order = response.body.order;
836
+
837
+ await checkStock(order.id, order.data.cart.items);
838
+ });
839
+
840
+ test("Chosen seats reserve the stock for an admin order", async () => {
841
+ const orderData = OrderData.create({
842
+ paymentMethod: PaymentMethod.PointOfSale,
843
+ checkoutMethod: takeoutMethod,
844
+ timeSlot: slot1,
845
+ cart: Cart.create({
846
+ items: [
847
+ CartItem.create({
848
+ product: seatProduct,
849
+ productPrice: seatProductPrice,
850
+ amount: 2,
851
+ seats: [
852
+ CartReservedSeat.create({
853
+ section: seatingPlan.sections[0].id,
854
+ row: 'A',
855
+ seat: '1'
856
+ }),
857
+ CartReservedSeat.create({
858
+ section: seatingPlan.sections[0].id,
859
+ row: 'A',
860
+ seat: '2'
861
+ })
862
+ ]
863
+ })
864
+ ]
865
+ }),
866
+ customer
867
+ })
868
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
869
+
870
+ const orderPatch = PrivateOrder.create({
871
+ id: uuidv4(),
872
+ data: orderData,
873
+ webshopId: webshop.id
874
+ });
875
+ patchArray.addPut(orderPatch);
876
+
877
+ // Send a patch
878
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
879
+ r.headers.authorization = "Bearer " + token.accessToken
880
+
881
+ const response = await testServer.test(patchWebshopOrdersEndpoint, r);
882
+ expect(response.body).toBeDefined();
883
+ const order = response.body[0];
884
+ await checkStock(order.id, order.data.cart.items);
885
+ });
886
+
887
+ test("Amount of a cart item should match the amount of chosen seats", async () => {
888
+ const orderData = OrderData.create({
889
+ paymentMethod: PaymentMethod.PointOfSale,
890
+ checkoutMethod: takeoutMethod,
891
+ timeSlot: slot1,
892
+ cart: Cart.create({
893
+ items: [
894
+ CartItem.create({
895
+ product: seatProduct,
896
+ productPrice: seatProductPrice,
897
+ amount: 2,
898
+ seats: [
899
+ CartReservedSeat.create({
900
+ section: seatingPlan.sections[0].id,
901
+ row: 'A',
902
+ seat: '1'
903
+ })
904
+ ]
905
+ })
906
+ ]
907
+ }),
908
+ customer
909
+ })
910
+
911
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
912
+ await expect(testServer.test(endpoint, r)).rejects.toThrow('Invalid seats');
913
+ });
914
+
915
+ test.todo("Amount of persons and orders for a takeout method is calculated correctly");
916
+
917
+ test.todo("Amount of persons and orders for a delivery method is calculated correctly");
918
+
919
+ });
920
+
921
+ describe('Full stock', () => {
922
+ test("Cannot place an order when product stock is full", async () => {
923
+ // Set stock
924
+ product.stock = 2;
925
+ await saveChanges();
926
+
927
+ const orderData = OrderData.create({
928
+ paymentMethod: PaymentMethod.PointOfSale,
929
+ checkoutMethod: takeoutMethod,
930
+ timeSlot: slot1,
931
+ cart: Cart.create({
932
+ items: [
933
+ CartItem.create({
934
+ product,
935
+ productPrice: productPrice1,
936
+ amount: 5,
937
+ options: [
938
+ CartItemOption.create({
939
+ optionMenu: multipleChoiceOptionMenu,
940
+ option: checkboxOption2
941
+ }),
942
+ CartItemOption.create({
943
+ optionMenu: chooseOneOptionMenu,
944
+ option: radioOption1
945
+ })
946
+ ]
947
+ })
948
+ ]
949
+ }),
950
+ customer
951
+ })
952
+
953
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
954
+
955
+ await expect(testServer.test(endpoint, r)).rejects.toThrow('Product unavailable');
956
+ });
957
+
958
+ test("Cannot place an order when product price stock is full", async () => {
959
+ // Set stock
960
+ productPrice1.stock = 2;
961
+ await saveChanges();
962
+
963
+ const orderData = OrderData.create({
964
+ paymentMethod: PaymentMethod.PointOfSale,
965
+ checkoutMethod: takeoutMethod,
966
+ timeSlot: slot1,
967
+ cart: Cart.create({
968
+ items: [
969
+ CartItem.create({
970
+ product,
971
+ productPrice: productPrice1,
972
+ amount: 5,
973
+ options: [
974
+ CartItemOption.create({
975
+ optionMenu: multipleChoiceOptionMenu,
976
+ option: checkboxOption2
977
+ }),
978
+ CartItemOption.create({
979
+ optionMenu: chooseOneOptionMenu,
980
+ option: radioOption1
981
+ })
982
+ ]
983
+ })
984
+ ]
985
+ }),
986
+ customer
987
+ })
988
+
989
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
990
+
991
+ await expect(testServer.test(endpoint, r)).rejects.toHaveProperty('human','Er zijn nog maar 2 stuks van productPrice1 beschikbaar');
992
+ });
993
+
994
+ test("Cannot place an order when option stock is full", async () => {
995
+ // Set stock
996
+ radioOption1.stock = 2;
997
+ await saveChanges();
998
+
999
+ const orderData = OrderData.create({
1000
+ paymentMethod: PaymentMethod.PointOfSale,
1001
+ checkoutMethod: takeoutMethod,
1002
+ timeSlot: slot1,
1003
+ cart: Cart.create({
1004
+ items: [
1005
+ CartItem.create({
1006
+ product,
1007
+ productPrice: productPrice1,
1008
+ amount: 5,
1009
+ options: [
1010
+ CartItemOption.create({
1011
+ optionMenu: multipleChoiceOptionMenu,
1012
+ option: checkboxOption2
1013
+ }),
1014
+ CartItemOption.create({
1015
+ optionMenu: chooseOneOptionMenu,
1016
+ option: radioOption1
1017
+ })
1018
+ ]
1019
+ })
1020
+ ]
1021
+ }),
1022
+ customer
1023
+ })
1024
+
1025
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1026
+
1027
+ await expect(testServer.test(endpoint, r)).rejects.toHaveProperty('human','Er zijn nog maar 2 stuks van radioOption1 beschikbaar');
1028
+ });
1029
+
1030
+ test("Cannot place an order when multiple choice option stock is full", async () => {
1031
+ // Set stock
1032
+ checkboxOption2.stock = 2;
1033
+ await saveChanges();
1034
+
1035
+ const orderData = OrderData.create({
1036
+ paymentMethod: PaymentMethod.PointOfSale,
1037
+ checkoutMethod: takeoutMethod,
1038
+ timeSlot: slot1,
1039
+ cart: Cart.create({
1040
+ items: [
1041
+ CartItem.create({
1042
+ product,
1043
+ productPrice: productPrice1,
1044
+ amount: 5,
1045
+ options: [
1046
+ CartItemOption.create({
1047
+ optionMenu: multipleChoiceOptionMenu,
1048
+ option: checkboxOption2
1049
+ }),
1050
+ CartItemOption.create({
1051
+ optionMenu: chooseOneOptionMenu,
1052
+ option: radioOption1
1053
+ })
1054
+ ]
1055
+ })
1056
+ ]
1057
+ }),
1058
+ customer
1059
+ })
1060
+
1061
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1062
+
1063
+ await expect(testServer.test(endpoint, r)).rejects.toHaveProperty('human','Er zijn nog maar 2 stuks van checkboxOption2 beschikbaar');
1064
+ });
1065
+
1066
+ test.todo("Cannot place an order when takeout persons stock is full");
1067
+
1068
+ test.todo("Cannot place an order when takeout orders stock is full");
1069
+
1070
+ test("Cannot place an order for a reserved seat", async () => {
1071
+ // Set stock
1072
+ seatProduct.reservedSeats = [
1073
+ ReservedSeat.create({
1074
+ section: seatingPlan.sections[0].id,
1075
+ row: 'A',
1076
+ seat: '1'
1077
+ })
1078
+ ]
1079
+ await saveChanges();
1080
+
1081
+ const orderData = OrderData.create({
1082
+ paymentMethod: PaymentMethod.PointOfSale,
1083
+ checkoutMethod: takeoutMethod,
1084
+ timeSlot: slot1,
1085
+ cart: Cart.create({
1086
+ items: [
1087
+ CartItem.create({
1088
+ product: seatProduct,
1089
+ productPrice: seatProductPrice,
1090
+ amount: 2,
1091
+ seats: [
1092
+ CartReservedSeat.create({
1093
+ section: seatingPlan.sections[0].id,
1094
+ row: 'A',
1095
+ seat: '1'
1096
+ }),
1097
+ CartReservedSeat.create({
1098
+ section: seatingPlan.sections[0].id,
1099
+ row: 'A',
1100
+ seat: '2'
1101
+ })
1102
+ ]
1103
+ })
1104
+ ]
1105
+ }),
1106
+ customer
1107
+ })
1108
+
1109
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1110
+ await expect(testServer.test(endpoint, r)).rejects.toThrow('Seats unavailable');
1111
+ });
1112
+
1113
+ test("Admin cannot place an order for a reserved seat", async () => {
1114
+ // Set stock
1115
+ seatProduct.reservedSeats = [
1116
+ ReservedSeat.create({
1117
+ section: seatingPlan.sections[0].id,
1118
+ row: 'A',
1119
+ seat: '1'
1120
+ })
1121
+ ]
1122
+ await saveChanges();
1123
+
1124
+ const orderData = OrderData.create({
1125
+ paymentMethod: PaymentMethod.PointOfSale,
1126
+ checkoutMethod: takeoutMethod,
1127
+ timeSlot: slot1,
1128
+ cart: Cart.create({
1129
+ items: [
1130
+ CartItem.create({
1131
+ product: seatProduct,
1132
+ productPrice: seatProductPrice,
1133
+ amount: 2,
1134
+ seats: [
1135
+ CartReservedSeat.create({
1136
+ section: seatingPlan.sections[0].id,
1137
+ row: 'A',
1138
+ seat: '1'
1139
+ }),
1140
+ CartReservedSeat.create({
1141
+ section: seatingPlan.sections[0].id,
1142
+ row: 'A',
1143
+ seat: '2'
1144
+ })
1145
+ ]
1146
+ })
1147
+ ]
1148
+ }),
1149
+ customer
1150
+ })
1151
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1152
+
1153
+ const orderPatch = PrivateOrder.create({
1154
+ id: uuidv4(),
1155
+ data: orderData,
1156
+ webshopId: webshop.id
1157
+ });
1158
+ patchArray.addPut(orderPatch);
1159
+
1160
+ // Send a patch
1161
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1162
+ r.headers.authorization = "Bearer " + token.accessToken
1163
+ await expect(testServer.test(patchWebshopOrdersEndpoint, r)).rejects.toThrow('Seats unavailable');
1164
+ });
1165
+
1166
+ test.todo("Admin cannot edit an an order to a reserved seat");
1167
+ });
1168
+
1169
+ describe('Cleaning up stock', () => {
1170
+ test("Stock is returned when a payment failed", async () => {
1171
+ const orderData = OrderData.create({
1172
+ paymentMethod: PaymentMethod.Bancontact,
1173
+ checkoutMethod: onSiteMethod,
1174
+ timeSlot: slot4,
1175
+ cart: Cart.create({
1176
+ items: [
1177
+ CartItem.create({
1178
+ product,
1179
+ productPrice: productPrice2,
1180
+ amount: 5,
1181
+ options: [
1182
+ CartItemOption.create({
1183
+ optionMenu: multipleChoiceOptionMenu,
1184
+ option: checkboxOption1
1185
+ }),
1186
+ CartItemOption.create({
1187
+ optionMenu: multipleChoiceOptionMenu,
1188
+ option: checkboxOption2
1189
+ }),
1190
+ CartItemOption.create({
1191
+ optionMenu: chooseOneOptionMenu,
1192
+ option: radioOption2
1193
+ })
1194
+ ]
1195
+ })
1196
+ ]
1197
+ }),
1198
+ customer
1199
+ })
1200
+
1201
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1202
+
1203
+ const response = await testServer.test(endpoint, r);
1204
+ expect(response.body).toBeDefined();
1205
+ const order = response.body.order;
1206
+
1207
+ await checkStock(order.id, order.data.cart.items);
1208
+
1209
+ // Cancel the payment
1210
+ await stripeMocker.failPayment(stripeMocker.getLastIntent())
1211
+
1212
+ const updatedOrder = await checkStock(order.id, [], order.data.cart.items);
1213
+ expect(updatedOrder.status).toBe(OrderStatus.Deleted);
1214
+ });
1215
+ });
1216
+
1217
+ describe('Modifying orders', () => {
1218
+ let order: Order;
1219
+ let baseOrder: Order;
1220
+ let productCartItem: CartItem|undefined;
1221
+ let personCartItem: CartItem|undefined;
1222
+
1223
+ beforeEach(async () => {
1224
+ productCartItem = CartItem.create({
1225
+ product,
1226
+ productPrice: productPrice1,
1227
+ amount: 5,
1228
+ options: [
1229
+ CartItemOption.create({
1230
+ optionMenu: multipleChoiceOptionMenu,
1231
+ option: checkboxOption1
1232
+ }),
1233
+ CartItemOption.create({
1234
+ optionMenu: chooseOneOptionMenu,
1235
+ option: radioOption1
1236
+ })
1237
+ ]
1238
+ })
1239
+
1240
+ personCartItem = CartItem.create({
1241
+ product: personProduct,
1242
+ productPrice: personProductPrice,
1243
+ amount: 2
1244
+ })
1245
+
1246
+ {
1247
+ const orderData = OrderData.create({
1248
+ paymentMethod: PaymentMethod.PointOfSale,
1249
+ checkoutMethod: takeoutMethod,
1250
+ timeSlot: slot1,
1251
+ cart: Cart.create({
1252
+ items: [
1253
+ productCartItem,
1254
+ personCartItem
1255
+ ]
1256
+ }),
1257
+ customer
1258
+ })
1259
+
1260
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1261
+
1262
+ const response = await testServer.test(endpoint, r);
1263
+ expect(response.body).toBeDefined();
1264
+ const orderStruct = response.body.order;
1265
+
1266
+
1267
+ // Now check the stock has changed for the product
1268
+ order = await checkStock(orderStruct.id, [productCartItem, personCartItem]);
1269
+ }
1270
+
1271
+ // Make sure all items in the cart, options etc, have at least a usedStock of 1, to also test they don't decrease when making
1272
+ // changes to roders
1273
+ {
1274
+ const orderData = OrderData.create({
1275
+ paymentMethod: PaymentMethod.PointOfSale,
1276
+ checkoutMethod: takeoutMethod,
1277
+ timeSlot: slot1,
1278
+ cart: Cart.create({
1279
+ items: [
1280
+ CartItem.create({
1281
+ product,
1282
+ productPrice: productPrice1,
1283
+ amount: 1,
1284
+ options: [
1285
+ CartItemOption.create({
1286
+ optionMenu: multipleChoiceOptionMenu,
1287
+ option: checkboxOption1
1288
+ }),
1289
+ CartItemOption.create({
1290
+ optionMenu: chooseOneOptionMenu,
1291
+ option: radioOption1
1292
+ })
1293
+ ]
1294
+ }),
1295
+ CartItem.create({
1296
+ product,
1297
+ productPrice: productPrice2,
1298
+ amount: 1,
1299
+ options: [
1300
+ CartItemOption.create({
1301
+ optionMenu: multipleChoiceOptionMenu,
1302
+ option: checkboxOption2
1303
+ }),
1304
+ CartItemOption.create({
1305
+ optionMenu: chooseOneOptionMenu,
1306
+ option: radioOption2
1307
+ })
1308
+ ]
1309
+ }),
1310
+ CartItem.create({
1311
+ product: personProduct,
1312
+ productPrice: personProductPrice,
1313
+ amount: 1
1314
+ })
1315
+ ]
1316
+ }),
1317
+ customer
1318
+ })
1319
+
1320
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1321
+
1322
+ const response = await testServer.test(endpoint, r);
1323
+ expect(response.body).toBeDefined();
1324
+ const orderStruct = response.body.order;
1325
+
1326
+
1327
+ // Now check the stock has changed for the product
1328
+ const orders = await checkStocks([order.id, orderStruct.id], [productCartItem, personCartItem, ...orderStruct.data.cart.items]);
1329
+ baseOrder = orders.find(o => o.id === orderStruct.id)!
1330
+ }
1331
+ });
1332
+
1333
+ test("Stock is removed when a product is removed or added in two steps", async () => {
1334
+ {
1335
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1336
+
1337
+ const cartPatch = Cart.patch({})
1338
+ cartPatch.items.addDelete(productCartItem!.id)
1339
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1340
+ patchArray.addPatch(orderPatch);
1341
+
1342
+ // Send a patch
1343
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1344
+ r.headers.authorization = "Bearer " + token.accessToken
1345
+
1346
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1347
+
1348
+ await checkStock(order.id, [personCartItem!]);
1349
+ }
1350
+
1351
+ {
1352
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1353
+
1354
+ const cartPatch = Cart.patch({})
1355
+ cartPatch.items.addDelete(personCartItem!.id)
1356
+ const newItem = CartItem.create({
1357
+ product,
1358
+ productPrice: productPrice2,
1359
+ amount: 30,
1360
+ options: [
1361
+ CartItemOption.create({
1362
+ optionMenu: chooseOneOptionMenu,
1363
+ option: radioOption2
1364
+ })
1365
+ ]
1366
+ });
1367
+
1368
+ cartPatch.items.addPut(
1369
+ newItem
1370
+ )
1371
+
1372
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1373
+ patchArray.addPatch(orderPatch);
1374
+
1375
+ // Send a patch
1376
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1377
+ r.headers.authorization = "Bearer " + token.accessToken
1378
+
1379
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1380
+
1381
+ await checkStock(order.id, [newItem]);
1382
+ }
1383
+ });
1384
+
1385
+ test("Stock is removed when a product is removed or added in single step", async () => {
1386
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1387
+
1388
+ const cartPatch = Cart.patch({})
1389
+ cartPatch.items.addDelete(productCartItem!.id)
1390
+ cartPatch.items.addDelete(personCartItem!.id)
1391
+ cartPatch.items.addPut(personCartItem!)
1392
+
1393
+ const newItem = CartItem.create({
1394
+ product,
1395
+ productPrice: productPrice2,
1396
+ amount: 40,
1397
+ options: [
1398
+ CartItemOption.create({
1399
+ optionMenu: chooseOneOptionMenu,
1400
+ option: radioOption2
1401
+ })
1402
+ ]
1403
+ });
1404
+
1405
+ cartPatch.items.addPut(
1406
+ newItem
1407
+ )
1408
+
1409
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1410
+ patchArray.addPatch(orderPatch);
1411
+
1412
+ // Send a patch
1413
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1414
+ r.headers.authorization = "Bearer " + token.accessToken
1415
+
1416
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1417
+
1418
+ order = await checkStock(order.id, [personCartItem!, newItem]);
1419
+ });
1420
+
1421
+ test("Stock is adjusted if product amount is changed", async () => {
1422
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1423
+
1424
+ const cartPatch = Cart.patch({})
1425
+ cartPatch.items.addPatch(CartItem.patch({
1426
+ id: productCartItem!.id,
1427
+ amount: 6
1428
+ }))
1429
+ cartPatch.items.addPatch(CartItem.patch({
1430
+ id: personCartItem!.id,
1431
+ amount: 13
1432
+ }))
1433
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1434
+ patchArray.addPatch(orderPatch);
1435
+
1436
+ // Send a patch
1437
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1438
+ r.headers.authorization = "Bearer " + token.accessToken
1439
+
1440
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1441
+
1442
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1443
+ });
1444
+
1445
+ test("Stock is changed if timeslot is changed", async () => {
1446
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1447
+
1448
+ const orderPatch = PrivateOrder.patch({
1449
+ id: order.id,
1450
+ data: OrderData.patch({
1451
+ timeSlot: slot2
1452
+ })
1453
+ });
1454
+ patchArray.addPatch(orderPatch);
1455
+
1456
+ // Send a patch
1457
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1458
+ r.headers.authorization = "Bearer " + token.accessToken
1459
+
1460
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1461
+
1462
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1463
+ });
1464
+
1465
+ test('Stock is changed if productPrice is changed', async () => {
1466
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1467
+
1468
+ const cartPatch = Cart.patch({})
1469
+ cartPatch.items.addPatch(CartItem.patch({
1470
+ id: productCartItem!.id,
1471
+ productPrice: productPrice2
1472
+ }))
1473
+
1474
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1475
+ patchArray.addPatch(orderPatch);
1476
+
1477
+ // Send a patch
1478
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1479
+ r.headers.authorization = "Bearer " + token.accessToken
1480
+
1481
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1482
+
1483
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1484
+ });
1485
+
1486
+ test('Stock is changed when option is removed', async () => {
1487
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1488
+
1489
+ const cartPatch = Cart.patch({})
1490
+ cartPatch.items.addPatch(CartItem.patch({
1491
+ id: productCartItem!.id,
1492
+ options: [
1493
+ CartItemOption.create({
1494
+ optionMenu: chooseOneOptionMenu,
1495
+ option: radioOption1
1496
+ })
1497
+ ]
1498
+ }))
1499
+
1500
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1501
+ patchArray.addPatch(orderPatch);
1502
+
1503
+ // Send a patch
1504
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1505
+ r.headers.authorization = "Bearer " + token.accessToken
1506
+
1507
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1508
+
1509
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1510
+ });
1511
+
1512
+ test('Stock is changed when option is changed', async () => {
1513
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1514
+
1515
+ const cartPatch = Cart.patch({})
1516
+ cartPatch.items.addPatch(CartItem.patch({
1517
+ id: productCartItem!.id,
1518
+ options: [
1519
+ CartItemOption.create({
1520
+ optionMenu: chooseOneOptionMenu,
1521
+ option: radioOption2
1522
+ }),
1523
+ CartItemOption.create({
1524
+ optionMenu: multipleChoiceOptionMenu,
1525
+ option: checkboxOption2
1526
+ })
1527
+ ]
1528
+ }))
1529
+
1530
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1531
+ patchArray.addPatch(orderPatch);
1532
+
1533
+ // Send a patch
1534
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1535
+ r.headers.authorization = "Bearer " + token.accessToken
1536
+
1537
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1538
+
1539
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1540
+ });
1541
+
1542
+ test('Stock is changed when option is added', async () => {
1543
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1544
+
1545
+ const cartPatch = Cart.patch({})
1546
+ cartPatch.items.addPatch(CartItem.patch({
1547
+ id: productCartItem!.id,
1548
+ options: [
1549
+ CartItemOption.create({
1550
+ optionMenu: chooseOneOptionMenu,
1551
+ option: radioOption1
1552
+ }),
1553
+ CartItemOption.create({
1554
+ optionMenu: multipleChoiceOptionMenu,
1555
+ option: checkboxOption1
1556
+ }),
1557
+ CartItemOption.create({
1558
+ optionMenu: multipleChoiceOptionMenu,
1559
+ option: checkboxOption2
1560
+ })
1561
+ ]
1562
+ }))
1563
+
1564
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1565
+ patchArray.addPatch(orderPatch);
1566
+
1567
+ // Send a patch
1568
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1569
+ r.headers.authorization = "Bearer " + token.accessToken
1570
+
1571
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1572
+
1573
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1574
+ });
1575
+
1576
+ test("Stock is changed if delivery method is changed", async () => {
1577
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1578
+
1579
+ const orderPatch = PrivateOrder.patch({
1580
+ id: order.id,
1581
+ data: OrderData.patch({
1582
+ checkoutMethod: deliveryMethod,
1583
+ timeSlot: slot3,
1584
+ address: ValidatedAddress.create({
1585
+ ...address,
1586
+ cityId: '',
1587
+ parentCityId: null,
1588
+ provinceId: ''
1589
+ })
1590
+ })
1591
+ });
1592
+ patchArray.addPatch(orderPatch);
1593
+
1594
+ // Send a patch
1595
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1596
+ r.headers.authorization = "Bearer " + token.accessToken
1597
+
1598
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1599
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1600
+ });
1601
+
1602
+ test("Stock is returned when an order is canceled and added when uncanceled again", async () => {
1603
+ {
1604
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1605
+
1606
+ const orderPatch = PrivateOrder.patch({
1607
+ id: order.id,
1608
+ status: OrderStatus.Canceled
1609
+ });
1610
+ patchArray.addPatch(orderPatch);
1611
+
1612
+ // Send a patch
1613
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1614
+ r.headers.authorization = "Bearer " + token.accessToken
1615
+
1616
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1617
+
1618
+ await checkStock(order.id, [], [personCartItem!, productCartItem!]);
1619
+ }
1620
+
1621
+ // Uncancel
1622
+ {
1623
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1624
+
1625
+ const orderPatch = PrivateOrder.patch({
1626
+ id: order.id,
1627
+ status: OrderStatus.Created
1628
+ });
1629
+ patchArray.addPatch(orderPatch);
1630
+
1631
+ // Send a patch
1632
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1633
+ r.headers.authorization = "Bearer " + token.accessToken
1634
+
1635
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1636
+
1637
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1638
+ }
1639
+ });
1640
+
1641
+ test("Stock is returned when an order is deleted and added when undeleted", async () => {
1642
+ {
1643
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1644
+
1645
+ const orderPatch = PrivateOrder.patch({
1646
+ id: order.id,
1647
+ status: OrderStatus.Deleted
1648
+ });
1649
+ patchArray.addPatch(orderPatch);
1650
+
1651
+ // Send a patch
1652
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1653
+ r.headers.authorization = "Bearer " + token.accessToken
1654
+
1655
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1656
+
1657
+ await checkStock(order.id, [], [personCartItem!, productCartItem!]);
1658
+ }
1659
+
1660
+ // Undelete
1661
+ {
1662
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1663
+
1664
+ const orderPatch = PrivateOrder.patch({
1665
+ id: order.id,
1666
+ status: OrderStatus.Created
1667
+ });
1668
+ patchArray.addPatch(orderPatch);
1669
+
1670
+ // Send a patch
1671
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1672
+ r.headers.authorization = "Bearer " + token.accessToken
1673
+
1674
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1675
+
1676
+ await checkStock(order.id, [personCartItem!, productCartItem!]);
1677
+ }
1678
+ });
1679
+ });
1680
+
1681
+ describe('Modifying seat orders', () => {
1682
+ let order: Order;
1683
+ let seatCartItem: CartItem|undefined;
1684
+
1685
+ beforeEach(async () => {
1686
+ seatCartItem = CartItem.create({
1687
+ product: seatProduct,
1688
+ productPrice: seatProductPrice,
1689
+ amount: 2,
1690
+ seats: [
1691
+ CartReservedSeat.create({
1692
+ section: seatingPlan.sections[0].id,
1693
+ row: 'A',
1694
+ seat: '1'
1695
+ }),
1696
+ CartReservedSeat.create({
1697
+ section: seatingPlan.sections[0].id,
1698
+ row: 'A',
1699
+ seat: '2'
1700
+ })
1701
+ ]
1702
+ })
1703
+
1704
+ const orderData = OrderData.create({
1705
+ paymentMethod: PaymentMethod.PointOfSale,
1706
+ checkoutMethod: takeoutMethod,
1707
+ timeSlot: slot1,
1708
+ cart: Cart.create({
1709
+ items: [
1710
+ seatCartItem
1711
+ ]
1712
+ }),
1713
+ customer
1714
+ })
1715
+
1716
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1717
+
1718
+ const response = await testServer.test(endpoint, r);
1719
+ expect(response.body).toBeDefined();
1720
+ const orderStruct = response.body.order;
1721
+
1722
+ // Now check the stock has changed for the product
1723
+ order = await checkStock(orderStruct.id, [seatCartItem]);
1724
+ });
1725
+
1726
+ test("Stock is removed when a product is removed and another is added", async () => {
1727
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1728
+
1729
+ const cartPatch = Cart.patch({})
1730
+ cartPatch.items.addDelete(seatCartItem!.id)
1731
+
1732
+ const newItem = CartItem.create({
1733
+ product: seatProduct,
1734
+ productPrice: seatProductPrice,
1735
+ amount: 1,
1736
+ seats: [
1737
+ CartReservedSeat.create({
1738
+ section: seatingPlan.sections[0].id,
1739
+ row: 'B',
1740
+ seat: '1'
1741
+ })
1742
+ ]
1743
+ });
1744
+
1745
+ cartPatch.items.addPut(
1746
+ newItem
1747
+ )
1748
+
1749
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1750
+ patchArray.addPatch(orderPatch);
1751
+
1752
+ // Send a patch
1753
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1754
+ r.headers.authorization = "Bearer " + token.accessToken
1755
+
1756
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1757
+
1758
+ await checkStock(order.id, [newItem]);
1759
+
1760
+ });
1761
+
1762
+ test("Stock is adjusted if extra seat is selected", async () => {
1763
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1764
+
1765
+ const cartPatch = Cart.patch({})
1766
+ const c = CartItem.patch({
1767
+ id: seatCartItem!.id,
1768
+ amount: 3,
1769
+ seats: [
1770
+ CartReservedSeat.create({
1771
+ section: seatingPlan.sections[0].id,
1772
+ row: 'A',
1773
+ seat: '1'
1774
+ }),
1775
+ CartReservedSeat.create({
1776
+ section: seatingPlan.sections[0].id,
1777
+ row: 'A',
1778
+ seat: '2'
1779
+ }),
1780
+ CartReservedSeat.create({
1781
+ section: seatingPlan.sections[0].id,
1782
+ row: 'A',
1783
+ seat: '3'
1784
+ })
1785
+ ]
1786
+ });
1787
+ cartPatch.items.addPatch(c)
1788
+
1789
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1790
+ patchArray.addPatch(orderPatch);
1791
+
1792
+ // Send a patch
1793
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1794
+ r.headers.authorization = "Bearer " + token.accessToken
1795
+
1796
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1797
+
1798
+ await checkStock(order.id, [seatCartItem!]);
1799
+ });
1800
+
1801
+ test("Reserved seats are changed when seats are moved", async () => {
1802
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1803
+
1804
+ const cartPatch = Cart.patch({})
1805
+ cartPatch.items.addPatch(CartItem.patch({
1806
+ id: seatCartItem?.id,
1807
+ seats: [
1808
+ CartReservedSeat.create({
1809
+ section: seatingPlan.sections[0].id,
1810
+ row: 'B',
1811
+ seat: '1'
1812
+ }),
1813
+ CartReservedSeat.create({
1814
+ section: seatingPlan.sections[0].id,
1815
+ row: 'B',
1816
+ seat: '2'
1817
+ })
1818
+ ]
1819
+ }))
1820
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
1821
+ patchArray.addPatch(orderPatch);
1822
+
1823
+ // Send a patch
1824
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1825
+ r.headers.authorization = "Bearer " + token.accessToken
1826
+
1827
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1828
+
1829
+ await checkStock(order.id, [seatCartItem!]);
1830
+ });
1831
+
1832
+ test("Stock is returned when an order is canceled and added when uncanceled again", async () => {
1833
+ {
1834
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1835
+
1836
+ const orderPatch = PrivateOrder.patch({
1837
+ id: order.id,
1838
+ status: OrderStatus.Canceled
1839
+ });
1840
+ patchArray.addPatch(orderPatch);
1841
+
1842
+ // Send a patch
1843
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1844
+ r.headers.authorization = "Bearer " + token.accessToken
1845
+
1846
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1847
+
1848
+ await checkStock(order.id, [], [seatCartItem!]);
1849
+ }
1850
+
1851
+ // Uncancel
1852
+ {
1853
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1854
+
1855
+ const orderPatch = PrivateOrder.patch({
1856
+ id: order.id,
1857
+ status: OrderStatus.Created
1858
+ });
1859
+ patchArray.addPatch(orderPatch);
1860
+
1861
+ // Send a patch
1862
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1863
+ r.headers.authorization = "Bearer " + token.accessToken
1864
+
1865
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1866
+
1867
+ await checkStock(order.id, [seatCartItem!]);
1868
+ }
1869
+ });
1870
+
1871
+ test("Stock is returned when an order is deleted and added when undeleted", async () => {
1872
+ {
1873
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1874
+
1875
+ const orderPatch = PrivateOrder.patch({
1876
+ id: order.id,
1877
+ status: OrderStatus.Deleted
1878
+ });
1879
+ patchArray.addPatch(orderPatch);
1880
+
1881
+ // Send a patch
1882
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1883
+ r.headers.authorization = "Bearer " + token.accessToken
1884
+
1885
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1886
+
1887
+ await checkStock(order.id, [], [seatCartItem!]);
1888
+ }
1889
+
1890
+ // Undelete
1891
+ {
1892
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1893
+
1894
+ const orderPatch = PrivateOrder.patch({
1895
+ id: order.id,
1896
+ status: OrderStatus.Created
1897
+ });
1898
+ patchArray.addPatch(orderPatch);
1899
+
1900
+ // Send a patch
1901
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1902
+ r.headers.authorization = "Bearer " + token.accessToken
1903
+
1904
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1905
+
1906
+ await checkStock(order.id, [seatCartItem!]);
1907
+ }
1908
+ });
1909
+
1910
+ test("Correctly handles duplicate seat booking recovery", async () => {
1911
+ {
1912
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1913
+
1914
+ const orderPatch = PrivateOrder.patch({
1915
+ id: order.id,
1916
+ status: OrderStatus.Canceled
1917
+ });
1918
+ patchArray.addPatch(orderPatch);
1919
+
1920
+ // Send a patch
1921
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1922
+ r.headers.authorization = "Bearer " + token.accessToken
1923
+
1924
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1925
+
1926
+ await checkStock(order.id, [], [seatCartItem!]);
1927
+ }
1928
+
1929
+ // Place an order for the same seats
1930
+ const newItem = CartItem.create({
1931
+ product: seatProduct,
1932
+ productPrice: seatProductPrice,
1933
+ amount: 2,
1934
+ seats: [
1935
+ CartReservedSeat.create({
1936
+ section: seatingPlan.sections[0].id,
1937
+ row: 'A',
1938
+ seat: '1'
1939
+ }),
1940
+ CartReservedSeat.create({
1941
+ section: seatingPlan.sections[0].id,
1942
+ row: 'A',
1943
+ seat: '2'
1944
+ })
1945
+ ]
1946
+ })
1947
+
1948
+ const orderData = OrderData.create({
1949
+ paymentMethod: PaymentMethod.PointOfSale,
1950
+ checkoutMethod: takeoutMethod,
1951
+ timeSlot: slot1,
1952
+ cart: Cart.create({
1953
+ items: [
1954
+ newItem
1955
+ ]
1956
+ }),
1957
+ customer
1958
+ })
1959
+ let orders: Order[];
1960
+
1961
+ {
1962
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
1963
+
1964
+ const response = await testServer.test(endpoint, r);
1965
+ expect(response.body).toBeDefined();
1966
+ const orderStruct = response.body.order;
1967
+
1968
+ // Now check the stock has changed for the product
1969
+ orders = await checkStocks([order.id, orderStruct.id], [newItem], [seatCartItem!]);
1970
+ }
1971
+
1972
+ // Uncancel
1973
+ {
1974
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
1975
+
1976
+ const orderPatch = PrivateOrder.patch({
1977
+ id: order.id,
1978
+ status: OrderStatus.Created
1979
+ });
1980
+ patchArray.addPatch(orderPatch);
1981
+
1982
+ // Send a patch
1983
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
1984
+ r.headers.authorization = "Bearer " + token.accessToken
1985
+
1986
+ await testServer.test(patchWebshopOrdersEndpoint, r);
1987
+
1988
+ orders = await checkStocks(orders.map(o => o.id), [newItem, seatCartItem!]);
1989
+ }
1990
+
1991
+ // Now we are in a duplicate seat selected situation.
1992
+ // To recover, move seats of one of the orders
1993
+ // and check in the final result, all seats are still correctly reserved (once)
1994
+
1995
+ // Manual check
1996
+ expect(seatProduct.reservedSeats).toHaveLength(4);
1997
+ expect(seatProduct.reservedSeats).toIncludeSameMembers([
1998
+ ReservedSeat.create({
1999
+ section: seatingPlan.sections[0].id,
2000
+ row: 'A',
2001
+ seat: '1'
2002
+ }),
2003
+ ReservedSeat.create({
2004
+ section: seatingPlan.sections[0].id,
2005
+ row: 'A',
2006
+ seat: '1'
2007
+ }),
2008
+ ReservedSeat.create({
2009
+ section: seatingPlan.sections[0].id,
2010
+ row: 'A',
2011
+ seat: '2'
2012
+ }),
2013
+ ReservedSeat.create({
2014
+ section: seatingPlan.sections[0].id,
2015
+ row: 'A',
2016
+ seat: '2'
2017
+ }),
2018
+ ])
2019
+
2020
+ // Move seats of first order
2021
+ {
2022
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
2023
+
2024
+ const cartPatch = Cart.patch({})
2025
+ cartPatch.items.addPatch(CartItem.patch({
2026
+ id: seatCartItem?.id,
2027
+ seats: [
2028
+ CartReservedSeat.create({
2029
+ section: seatingPlan.sections[0].id,
2030
+ row: 'B',
2031
+ seat: '1'
2032
+ }),
2033
+ CartReservedSeat.create({
2034
+ section: seatingPlan.sections[0].id,
2035
+ row: 'B',
2036
+ seat: '2'
2037
+ })
2038
+ ]
2039
+ }))
2040
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
2041
+ patchArray.addPatch(orderPatch);
2042
+
2043
+ // Send a patch
2044
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
2045
+ r.headers.authorization = "Bearer " + token.accessToken
2046
+
2047
+ await testServer.test(patchWebshopOrdersEndpoint, r);
2048
+
2049
+ await checkStocks(orders.map(o => o.id), [newItem, seatCartItem!]);
2050
+ }
2051
+
2052
+ // Manual check
2053
+ expect(seatProduct.reservedSeats).toHaveLength(4);
2054
+ expect(seatProduct.reservedSeats).toIncludeSameMembers([
2055
+ ReservedSeat.create({
2056
+ section: seatingPlan.sections[0].id,
2057
+ row: 'A',
2058
+ seat: '1'
2059
+ }),
2060
+ ReservedSeat.create({
2061
+ section: seatingPlan.sections[0].id,
2062
+ row: 'B',
2063
+ seat: '1'
2064
+ }),
2065
+ ReservedSeat.create({
2066
+ section: seatingPlan.sections[0].id,
2067
+ row: 'A',
2068
+ seat: '2'
2069
+ }),
2070
+ ReservedSeat.create({
2071
+ section: seatingPlan.sections[0].id,
2072
+ row: 'B',
2073
+ seat: '2'
2074
+ }),
2075
+ ])
2076
+ });
2077
+
2078
+ test("Patching an order triggers an auto recovery of not reserved seats", async () => {
2079
+ // This is required to recover from bugs in stock changes.
2080
+ // Simply patching all orders will fix the stock.
2081
+
2082
+ // Manually remove all reserved seats = caused by a past bug
2083
+ seatProduct.reservedSeats = []
2084
+ await saveChanges();
2085
+
2086
+ {
2087
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
2088
+
2089
+ const orderPatch = PrivateOrder.patch({
2090
+ id: order.id,
2091
+ status: OrderStatus.Completed
2092
+ });
2093
+ patchArray.addPatch(orderPatch);
2094
+
2095
+ // Send a patch
2096
+ const r = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
2097
+ r.headers.authorization = "Bearer " + token.accessToken
2098
+
2099
+ await testServer.test(patchWebshopOrdersEndpoint, r);
2100
+
2101
+ await checkStock(order.id, [seatCartItem!]);
2102
+ }
2103
+
2104
+ // Manual check
2105
+ expect(seatProduct.reservedSeats).toHaveLength(2);
2106
+ expect(seatProduct.reservedSeats).toIncludeSameMembers([
2107
+ ReservedSeat.create({
2108
+ section: seatingPlan.sections[0].id,
2109
+ row: 'A',
2110
+ seat: '1'
2111
+ }),
2112
+ ReservedSeat.create({
2113
+ section: seatingPlan.sections[0].id,
2114
+ row: 'A',
2115
+ seat: '2'
2116
+ }),
2117
+ ])
2118
+ });
2119
+ });
2120
+ });