@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,926 @@
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 { Organization, OrganizationFactory, StripeAccount, Ticket, Token, UserFactory, Webshop, WebshopFactory } from "@stamhoofd/models";
7
+ import { Cart, CartItem, CartReservedSeat, Customer, OrderData, OrderStatus, PaymentConfiguration, PaymentMethod, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, Product, ProductType, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, WebshopMetaData, WebshopPrivateMetaData, WebshopTicketType } from "@stamhoofd/structures";
8
+
9
+ import { PatchWebshopOrdersEndpoint } from "../../src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint";
10
+ import { PlaceOrderEndpoint } from '../../src/endpoints/organization/webshops/PlaceOrderEndpoint';
11
+ import { StripeMocker } from "../helpers/StripeMocker";
12
+ import { testServer } from "../helpers/TestServer";
13
+
14
+ const customer = Customer.create({
15
+ firstName: 'John',
16
+ lastName: 'Doe',
17
+ email: 'john@example.com',
18
+ phone: '+32412345678'
19
+ });
20
+
21
+ function mapTicketChangedAmount(ticket: Ticket) {
22
+ return {
23
+ id: ticket.id,
24
+ seat: ticket.seat,
25
+ originalSeat: ticket.originalSeat,
26
+ orderId: ticket.orderId,
27
+ itemId: ticket.itemId,
28
+ index: ticket.index,
29
+ secret: ticket.secret,
30
+ }
31
+ }
32
+
33
+ function mapTicketCreation(ticket: Ticket) {
34
+ return {
35
+ seat: ticket.seat,
36
+ originalSeat: ticket.originalSeat,
37
+ itemId: ticket.itemId,
38
+ index: ticket.index,
39
+ total: ticket.total
40
+ }
41
+ }
42
+
43
+ describe("E2E.Tickets", () => {
44
+ // Test endpoint
45
+ const endpoint = new PlaceOrderEndpoint();
46
+ const patchWebshopOrdersEndpoint = new PatchWebshopOrdersEndpoint();
47
+
48
+ let organization: Organization;
49
+ let webshop: Webshop;
50
+ let ticket1: Product;
51
+ let ticket2: Product;
52
+ let seatProduct: Product;
53
+ let seatingPlan: SeatingPlan;
54
+ let stripeMocker: StripeMocker
55
+ let stripeAccount: StripeAccount
56
+ let token: Token;
57
+
58
+ beforeAll(async () => {
59
+ stripeMocker = new StripeMocker();
60
+ stripeMocker.start();
61
+ organization = await new OrganizationFactory({}).create()
62
+ stripeAccount = await stripeMocker.createStripeAccount(organization.id);
63
+
64
+ const user = await new UserFactory({
65
+ organization,
66
+ permissions: Permissions.create({
67
+ level: PermissionLevel.Full
68
+ })
69
+ }).create()
70
+ token = await Token.createToken(user)
71
+ });
72
+
73
+ afterAll(() => {
74
+ stripeMocker.stop();
75
+ });
76
+
77
+ beforeEach(async () => {
78
+ stripeMocker.reset();
79
+ let meta = WebshopMetaData.patch({});
80
+
81
+ ticket1 = Product.create({
82
+ name: 'ticket1',
83
+ type: ProductType.Ticket
84
+ })
85
+
86
+ ticket2 = Product.create({
87
+ name: 'ticket2',
88
+ type: ProductType.Ticket
89
+ })
90
+
91
+ seatingPlan = SeatingPlan.create({
92
+ name: 'Testzaal',
93
+ sections: [
94
+ SeatingPlanSection.create({
95
+ rows: [
96
+ SeatingPlanRow.create({
97
+ label: 'A',
98
+ seats: [
99
+ SeatingPlanSeat.create({
100
+ label: '1'
101
+ }),
102
+ SeatingPlanSeat.create({
103
+ label: '2'
104
+ }),
105
+ SeatingPlanSeat.create({
106
+ label: '3'
107
+ }),
108
+ SeatingPlanSeat.create({
109
+ label: '4'
110
+ })
111
+ ]
112
+ }),
113
+ SeatingPlanRow.create({
114
+ label: 'B',
115
+ seats: [
116
+ SeatingPlanSeat.create({
117
+ label: '1'
118
+ }),
119
+ SeatingPlanSeat.create({
120
+ label: '2'
121
+ }),
122
+ SeatingPlanSeat.create({
123
+ label: '3'
124
+ }),
125
+ SeatingPlanSeat.create({
126
+ label: '4'
127
+ })
128
+ ]
129
+ })
130
+ ]
131
+ })
132
+ ]
133
+ })
134
+ meta.seatingPlans.addPut(seatingPlan)
135
+
136
+ seatProduct = Product.create({
137
+ name: 'seatProduct',
138
+ type: ProductType.Ticket,
139
+ seatingPlanId: seatingPlan.id
140
+ })
141
+
142
+ const paymentConfigurationPatch = PaymentConfiguration.patch({
143
+ transferSettings: TransferSettings.create({
144
+ iban: 'BE56587127952688' // = random IBAN
145
+ }),
146
+ })
147
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.PointOfSale)
148
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.Transfer)
149
+ paymentConfigurationPatch.paymentMethods.addPut(PaymentMethod.Bancontact)
150
+
151
+ const privatePaymentConfiguration = PrivatePaymentConfiguration.patch({
152
+ stripeAccountId: stripeAccount.id
153
+ })
154
+
155
+ meta = meta.patch({
156
+ paymentConfiguration: paymentConfigurationPatch,
157
+ ticketType: WebshopTicketType.Tickets
158
+ })
159
+
160
+ const privateMeta = WebshopPrivateMetaData.patch({
161
+ paymentConfiguration: privatePaymentConfiguration
162
+ })
163
+
164
+ webshop = await new WebshopFactory({
165
+ organizationId: organization.id,
166
+ name: 'Test webshop',
167
+ meta,
168
+ privateMeta,
169
+ products: [ticket1, ticket2, seatProduct]
170
+ }).create()
171
+ });
172
+
173
+ test("POS payments create tickets", async () => {
174
+ const item1 = CartItem.create({
175
+ product: ticket1,
176
+ productPrice: ticket1.prices[0],
177
+ amount: 5
178
+ });
179
+
180
+ const item2 = CartItem.create({
181
+ product: ticket2,
182
+ productPrice: ticket2.prices[0],
183
+ amount: 2
184
+ });
185
+
186
+ const orderData = OrderData.create({
187
+ paymentMethod: PaymentMethod.PointOfSale,
188
+ cart: Cart.create({
189
+ items: [
190
+ item1,
191
+ item2
192
+ ]
193
+ }),
194
+ customer
195
+ })
196
+
197
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
198
+
199
+ const response = await testServer.test(endpoint, r);
200
+ expect(response.body).toBeDefined();
201
+ const order = response.body.order;
202
+
203
+ // Check tickets
204
+ const tickets = await Ticket.where({orderId: order.id});
205
+ expect(tickets).toHaveLength(7);
206
+
207
+ const item1Tickets = tickets.filter(t => t.itemId === item1.id);
208
+ const item2Tickets = tickets.filter(t => t.itemId === item2.id);
209
+
210
+ expect(item1Tickets).toHaveLength(5);
211
+ expect(item2Tickets).toHaveLength(2);
212
+
213
+ // Check indexes present
214
+ expect(item1Tickets.map(i => i.index).sort()).toEqual([1, 2, 3, 4, 5])
215
+ expect(item2Tickets.map(i => i.index).sort()).toEqual([1, 2])
216
+
217
+ // Check total present
218
+ expect(item1Tickets.map(i => i.total).sort()).toEqual([5, 5, 5, 5, 5])
219
+ expect(item2Tickets.map(i => i.total).sort()).toEqual([2, 2])
220
+
221
+ expect(tickets.map(mapTicketCreation)).toIncludeSameMembers([
222
+ {
223
+ seat: null,
224
+ originalSeat: null,
225
+ itemId: item1.id,
226
+ index: 1,
227
+ total: 5
228
+ },
229
+ {
230
+ seat: null,
231
+ originalSeat: null,
232
+ itemId: item1.id,
233
+ index: 2,
234
+ total: 5
235
+ },
236
+ {
237
+ seat: null,
238
+ originalSeat: null,
239
+ itemId: item1.id,
240
+ index: 3,
241
+ total: 5
242
+ },
243
+ {
244
+ seat: null,
245
+ originalSeat: null,
246
+ itemId: item1.id,
247
+ index: 4,
248
+ total: 5
249
+ },
250
+ {
251
+ seat: null,
252
+ originalSeat: null,
253
+ itemId: item1.id,
254
+ index: 5,
255
+ total: 5
256
+ },
257
+ {
258
+ seat: null,
259
+ originalSeat: null,
260
+ itemId: item2.id,
261
+ index: 1,
262
+ total: 2
263
+ },
264
+ {
265
+ seat: null,
266
+ originalSeat: null,
267
+ itemId: item2.id,
268
+ index: 2,
269
+ total: 2
270
+ }
271
+ ])
272
+ });
273
+
274
+ test("Adding new items keeps all tickets in place", async () => {
275
+ const item1 = CartItem.create({
276
+ product: ticket1,
277
+ productPrice: ticket1.prices[0],
278
+ amount: 5
279
+ });
280
+
281
+ const item2 = CartItem.create({
282
+ product: ticket2,
283
+ productPrice: ticket2.prices[0],
284
+ amount: 2
285
+ });
286
+
287
+ const orderData = OrderData.create({
288
+ paymentMethod: PaymentMethod.PointOfSale,
289
+ cart: Cart.create({
290
+ items: [
291
+ item1,
292
+ item2
293
+ ]
294
+ }),
295
+ customer
296
+ })
297
+
298
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
299
+
300
+ const response = await testServer.test(endpoint, r);
301
+ expect(response.body).toBeDefined();
302
+ const order = response.body.order;
303
+
304
+ // Check tickets
305
+ const tickets = await Ticket.where({orderId: order.id});
306
+ expect(tickets).toHaveLength(7);
307
+
308
+ // Now add an extra item
309
+
310
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
311
+
312
+ const cartPatch = Cart.patch({})
313
+ cartPatch.items.addPatch(CartItem.patch({
314
+ id: item1.id,
315
+ amount: 7
316
+ }));
317
+
318
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
319
+ patchArray.addPatch(orderPatch);
320
+
321
+ // Send a patch
322
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
323
+ r2.headers.authorization = "Bearer " + token.accessToken
324
+
325
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
326
+
327
+ const ticketsAfter = await Ticket.where({orderId: order.id});
328
+ expect(ticketsAfter).toHaveLength(9);
329
+
330
+ // Didn't change old tickets:
331
+ expect(ticketsAfter.map(mapTicketChangedAmount)).toIncludeAllMembers(tickets.map(mapTicketChangedAmount));
332
+
333
+ // Added 2 new items with index 6 and 7
334
+ const newTickets = ticketsAfter.filter(t => !tickets.find(tt => tt.id === t.id))
335
+ expect(newTickets).toHaveLength(2);
336
+
337
+ expect(newTickets.map(mapTicketCreation)).toIncludeSameMembers([
338
+ {
339
+ seat: null,
340
+ originalSeat: null,
341
+ itemId: item1.id,
342
+ index: 6,
343
+ total: 7
344
+ },
345
+ {
346
+ seat: null,
347
+ originalSeat: null,
348
+ itemId: item1.id,
349
+ index: 7,
350
+ total: 7
351
+ }
352
+ ])
353
+
354
+ expect(ticketsAfter.map(mapTicketCreation)).toIncludeSameMembers([
355
+ {
356
+ seat: null,
357
+ originalSeat: null,
358
+ itemId: item1.id,
359
+ index: 1,
360
+ total: 7
361
+ },
362
+ {
363
+ seat: null,
364
+ originalSeat: null,
365
+ itemId: item1.id,
366
+ index: 2,
367
+ total: 7
368
+ },
369
+ {
370
+ seat: null,
371
+ originalSeat: null,
372
+ itemId: item1.id,
373
+ index: 3,
374
+ total: 7
375
+ },
376
+ {
377
+ seat: null,
378
+ originalSeat: null,
379
+ itemId: item1.id,
380
+ index: 4,
381
+ total: 7
382
+ },
383
+ {
384
+ seat: null,
385
+ originalSeat: null,
386
+ itemId: item1.id,
387
+ index: 5,
388
+ total: 7
389
+ },
390
+ {
391
+ seat: null,
392
+ originalSeat: null,
393
+ itemId: item1.id,
394
+ index: 6,
395
+ total: 7
396
+ },
397
+ {
398
+ seat: null,
399
+ originalSeat: null,
400
+ itemId: item1.id,
401
+ index: 7,
402
+ total: 7
403
+ },
404
+ {
405
+ seat: null,
406
+ originalSeat: null,
407
+ itemId: item2.id,
408
+ index: 1,
409
+ total: 2
410
+ },
411
+ {
412
+ seat: null,
413
+ originalSeat: null,
414
+ itemId: item2.id,
415
+ index: 2,
416
+ total: 2
417
+ }
418
+ ])
419
+
420
+ });
421
+
422
+ test("Deleting items deletes tickets", async () => {
423
+ const item1 = CartItem.create({
424
+ product: ticket1,
425
+ productPrice: ticket1.prices[0],
426
+ amount: 5
427
+ });
428
+
429
+ const item2 = CartItem.create({
430
+ product: ticket2,
431
+ productPrice: ticket2.prices[0],
432
+ amount: 2
433
+ });
434
+
435
+ const orderData = OrderData.create({
436
+ paymentMethod: PaymentMethod.PointOfSale,
437
+ cart: Cart.create({
438
+ items: [
439
+ item1,
440
+ item2
441
+ ]
442
+ }),
443
+ customer
444
+ })
445
+
446
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
447
+
448
+ const response = await testServer.test(endpoint, r);
449
+ expect(response.body).toBeDefined();
450
+ const order = response.body.order;
451
+
452
+ // Check tickets
453
+ const tickets = await Ticket.where({orderId: order.id});
454
+ expect(tickets).toHaveLength(7);
455
+
456
+ // Now add an extra item
457
+
458
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
459
+
460
+ const cartPatch = Cart.patch({})
461
+ cartPatch.items.addDelete(item2.id);
462
+
463
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
464
+ patchArray.addPatch(orderPatch);
465
+
466
+ // Send a patch
467
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
468
+ r2.headers.authorization = "Bearer " + token.accessToken
469
+
470
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
471
+
472
+ const ticketsAfter = await Ticket.where({orderId: order.id});
473
+ expect(ticketsAfter).toHaveLength(7);
474
+ const deletedTickets = ticketsAfter.filter(t => t.isDeleted)
475
+ const remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
476
+ expect(deletedTickets).toHaveLength(2);
477
+ expect(remainingTickets).toHaveLength(5);
478
+
479
+ // Didn't change old tickets:
480
+ expect(deletedTickets.map(mapTicketChangedAmount)).toIncludeSameMembers(tickets.filter(t => t.itemId === item2.id).map(mapTicketChangedAmount));
481
+ expect(remainingTickets.map(mapTicketChangedAmount)).toIncludeSameMembers(tickets.filter(t => t.itemId === item1.id).map(mapTicketChangedAmount));
482
+ });
483
+
484
+ test("Deleting an order deletes tickets", async () => {
485
+ const item1 = CartItem.create({
486
+ product: ticket1,
487
+ productPrice: ticket1.prices[0],
488
+ amount: 5
489
+ });
490
+
491
+ const item2 = CartItem.create({
492
+ product: ticket2,
493
+ productPrice: ticket2.prices[0],
494
+ amount: 2
495
+ });
496
+
497
+ const orderData = OrderData.create({
498
+ paymentMethod: PaymentMethod.PointOfSale,
499
+ cart: Cart.create({
500
+ items: [
501
+ item1,
502
+ item2
503
+ ]
504
+ }),
505
+ customer
506
+ })
507
+
508
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
509
+
510
+ const response = await testServer.test(endpoint, r);
511
+ expect(response.body).toBeDefined();
512
+ const order = response.body.order;
513
+
514
+ // Check tickets
515
+ const tickets = await Ticket.where({orderId: order.id});
516
+ expect(tickets).toHaveLength(7);
517
+
518
+ // Now add an extra item
519
+
520
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
521
+
522
+ const orderPatch = PrivateOrder.patch({id: order.id, status: OrderStatus.Deleted});
523
+ patchArray.addPatch(orderPatch);
524
+
525
+ // Send a patch
526
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
527
+ r2.headers.authorization = "Bearer " + token.accessToken
528
+
529
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
530
+
531
+ const ticketsAfter = await Ticket.where({orderId: order.id});
532
+ expect(ticketsAfter).toHaveLength(7);
533
+
534
+ const deletedTickets = ticketsAfter.filter(t => t.isDeleted)
535
+ const remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
536
+ expect(deletedTickets).toHaveLength(7);
537
+ expect(remainingTickets).toHaveLength(0);
538
+ });
539
+
540
+ test("Reducing amount deletes tickets and reuses them again when added again", async () => {
541
+ const item1 = CartItem.create({
542
+ product: ticket1,
543
+ productPrice: ticket1.prices[0],
544
+ amount: 5
545
+ });
546
+
547
+ const item2 = CartItem.create({
548
+ product: ticket2,
549
+ productPrice: ticket2.prices[0],
550
+ amount: 2
551
+ });
552
+
553
+ const orderData = OrderData.create({
554
+ paymentMethod: PaymentMethod.PointOfSale,
555
+ cart: Cart.create({
556
+ items: [
557
+ item1,
558
+ item2
559
+ ]
560
+ }),
561
+ customer
562
+ })
563
+
564
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
565
+
566
+ const response = await testServer.test(endpoint, r);
567
+ expect(response.body).toBeDefined();
568
+ const order = response.body.order;
569
+
570
+ // Check tickets
571
+ const tickets = await Ticket.where({orderId: order.id});
572
+ expect(tickets).toHaveLength(7);
573
+
574
+ // Now add an extra item
575
+ {
576
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
577
+
578
+ const cartPatch = Cart.patch({})
579
+ cartPatch.items.addPatch(CartItem.patch({
580
+ id: item1.id,
581
+ amount: 1
582
+ }));
583
+
584
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
585
+ patchArray.addPatch(orderPatch);
586
+
587
+ // Send a patch
588
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
589
+ r2.headers.authorization = "Bearer " + token.accessToken
590
+
591
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
592
+ }
593
+
594
+ let ticketsAfter = await Ticket.where({orderId: order.id});
595
+ expect(ticketsAfter).toHaveLength(7);
596
+
597
+ let deletedTickets = ticketsAfter.filter(t => t.isDeleted)
598
+ let remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
599
+ expect(deletedTickets).toHaveLength(4);
600
+ expect(remainingTickets).toHaveLength(3);
601
+
602
+ // Didn't change tickets
603
+ expect(ticketsAfter.map(mapTicketChangedAmount)).toIncludeSameMembers(tickets.map(mapTicketChangedAmount));
604
+
605
+ // Now add an extra item
606
+
607
+ {
608
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
609
+ const cartPatch = Cart.patch({})
610
+ cartPatch.items.addPatch(CartItem.patch({
611
+ id: item1.id,
612
+ amount: 5
613
+ }));
614
+
615
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
616
+ patchArray.addPatch(orderPatch);
617
+
618
+ // Send a patch
619
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
620
+ r2.headers.authorization = "Bearer " + token.accessToken
621
+
622
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
623
+ }
624
+
625
+ ticketsAfter = await Ticket.where({orderId: order.id});
626
+ expect(ticketsAfter).toHaveLength(7);
627
+
628
+ deletedTickets = ticketsAfter.filter(t => t.isDeleted)
629
+ remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
630
+ expect(deletedTickets).toHaveLength(0);
631
+ expect(remainingTickets).toHaveLength(7);
632
+
633
+ // Didn't change tickets
634
+ expect(ticketsAfter.map(mapTicketChangedAmount)).toIncludeSameMembers(tickets.map(mapTicketChangedAmount));
635
+ });
636
+
637
+ test("Seats are assigned to each ticket as expected", async () => {
638
+ const item1 = CartItem.create({
639
+ product: ticket1,
640
+ productPrice: ticket1.prices[0],
641
+ amount: 3,
642
+ seats: [
643
+ CartReservedSeat.create({
644
+ section: seatingPlan.sections[0].id,
645
+ row: 'A',
646
+ seat: '1'
647
+ }),
648
+ CartReservedSeat.create({
649
+ section: seatingPlan.sections[0].id,
650
+ row: 'A',
651
+ seat: '2'
652
+ }),
653
+ CartReservedSeat.create({
654
+ section: seatingPlan.sections[0].id,
655
+ row: 'A',
656
+ seat: '3'
657
+ })
658
+ ]
659
+ });
660
+
661
+ const orderData = OrderData.create({
662
+ paymentMethod: PaymentMethod.PointOfSale,
663
+ cart: Cart.create({
664
+ items: [
665
+ item1
666
+ ]
667
+ }),
668
+ customer
669
+ })
670
+
671
+ const r = Request.buildJson("POST", `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
672
+
673
+ const response = await testServer.test(endpoint, r);
674
+ expect(response.body).toBeDefined();
675
+ const order = response.body.order;
676
+
677
+ // Check tickets
678
+ const tickets = await Ticket.where({orderId: order.id});
679
+ expect(tickets).toHaveLength(3);
680
+
681
+ expect(tickets.map(mapTicketCreation)).toIncludeSameMembers([
682
+ {
683
+ itemId: item1.id,
684
+ index: 1,
685
+ seat: item1.seats[0],
686
+ originalSeat: item1.seats[0],
687
+ total: 3,
688
+ },
689
+ {
690
+ itemId: item1.id,
691
+ index: 2,
692
+ seat: item1.seats[1],
693
+ originalSeat: item1.seats[1],
694
+ total: 3
695
+ },
696
+ {
697
+ itemId: item1.id,
698
+ index: 3,
699
+ seat: item1.seats[2],
700
+ originalSeat: item1.seats[2],
701
+ total: 3
702
+ }
703
+ ])
704
+
705
+ // Now move a seat
706
+ {
707
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
708
+
709
+ const cartPatch = Cart.patch({})
710
+ cartPatch.items.addPatch(CartItem.patch({
711
+ id: item1.id,
712
+ seats: [
713
+ CartReservedSeat.create({
714
+ section: seatingPlan.sections[0].id,
715
+ row: 'A',
716
+ seat: '2'
717
+ }),
718
+ CartReservedSeat.create({
719
+ section: seatingPlan.sections[0].id,
720
+ row: 'A',
721
+ seat: '3'
722
+ }),
723
+ CartReservedSeat.create({
724
+ section: seatingPlan.sections[0].id,
725
+ row: 'A',
726
+ seat: '4'
727
+ }),
728
+ ]
729
+ }));
730
+
731
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
732
+ patchArray.addPatch(orderPatch);
733
+
734
+ // Send a patch
735
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
736
+ r2.headers.authorization = "Bearer " + token.accessToken
737
+
738
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
739
+ }
740
+
741
+ let ticketsAfter = await Ticket.where({orderId: order.id});
742
+ expect(ticketsAfter).toHaveLength(3);
743
+
744
+ let deletedTickets = ticketsAfter.filter(t => t.isDeleted)
745
+ let remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
746
+ expect(deletedTickets).toHaveLength(0);
747
+ expect(remainingTickets).toHaveLength(3);
748
+
749
+ expect(ticketsAfter.map(mapTicketChangedAmount)).toIncludeSameMembers([
750
+ {
751
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 1)!),
752
+ index: 3,
753
+ seat: CartReservedSeat.create({
754
+ section: seatingPlan.sections[0].id,
755
+ row: 'A',
756
+ seat: '4'
757
+ }),
758
+ originalSeat: item1.seats[0]
759
+ },
760
+ {
761
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 2)!),
762
+ index: 1,
763
+ seat: item1.seats[1],
764
+ originalSeat: item1.seats[1]
765
+ },
766
+ {
767
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 3)!),
768
+ index: 2,
769
+ seat: item1.seats[2],
770
+ originalSeat: item1.seats[2]
771
+ }
772
+ ])
773
+
774
+ // Move it back and also delete a different seat
775
+ {
776
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
777
+
778
+ const cartPatch = Cart.patch({})
779
+ cartPatch.items.addPatch(CartItem.patch({
780
+ id: item1.id,
781
+ amount: 2,
782
+ seats: [
783
+ CartReservedSeat.create({
784
+ section: seatingPlan.sections[0].id,
785
+ row: 'A',
786
+ seat: '2'
787
+ }),
788
+ CartReservedSeat.create({
789
+ section: seatingPlan.sections[0].id,
790
+ row: 'A',
791
+ seat: '1'
792
+ }),
793
+ ]
794
+ }));
795
+
796
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
797
+ patchArray.addPatch(orderPatch);
798
+
799
+ // Send a patch
800
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
801
+ r2.headers.authorization = "Bearer " + token.accessToken
802
+
803
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
804
+ }
805
+
806
+ ticketsAfter = await Ticket.where({orderId: order.id});
807
+ expect(ticketsAfter).toHaveLength(3);
808
+
809
+ deletedTickets = ticketsAfter.filter(t => t.isDeleted)
810
+ remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
811
+ expect(deletedTickets).toHaveLength(1);
812
+ expect(remainingTickets).toHaveLength(2);
813
+
814
+ // Note we correctly reused the tickets here
815
+ expect(remainingTickets.map(mapTicketChangedAmount)).toIncludeSameMembers([
816
+ {
817
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 1)!),
818
+ index: 2
819
+ },
820
+ {
821
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 2)!),
822
+ index: 1
823
+ }
824
+ ])
825
+
826
+ expect(deletedTickets.map(mapTicketChangedAmount)).toIncludeSameMembers([
827
+ {
828
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 3)!),
829
+ index: 2
830
+ }
831
+ ])
832
+
833
+ // Add two total new different seats
834
+ {
835
+ const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
836
+
837
+ const cartPatch = Cart.patch({})
838
+ cartPatch.items.addPatch(CartItem.patch({
839
+ id: item1.id,
840
+ amount: 4,
841
+ seats: [
842
+ CartReservedSeat.create({
843
+ section: seatingPlan.sections[0].id,
844
+ row: 'A',
845
+ seat: '2'
846
+ }),
847
+ CartReservedSeat.create({
848
+ section: seatingPlan.sections[0].id,
849
+ row: 'A',
850
+ seat: '1'
851
+ }),
852
+ CartReservedSeat.create({
853
+ section: seatingPlan.sections[0].id,
854
+ row: 'B',
855
+ seat: '1'
856
+ }),
857
+ CartReservedSeat.create({
858
+ section: seatingPlan.sections[0].id,
859
+ row: 'B',
860
+ seat: '2'
861
+ }),
862
+ ]
863
+ }));
864
+
865
+ const orderPatch = PrivateOrder.patch({id: order.id, data: OrderData.patch({cart: cartPatch})});
866
+ patchArray.addPatch(orderPatch);
867
+
868
+ // Send a patch
869
+ const r2 = Request.buildJson("PATCH", `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
870
+ r2.headers.authorization = "Bearer " + token.accessToken
871
+
872
+ await testServer.test(patchWebshopOrdersEndpoint, r2);
873
+ }
874
+
875
+ ticketsAfter = await Ticket.where({orderId: order.id});
876
+ expect(ticketsAfter).toHaveLength(4);
877
+
878
+ deletedTickets = ticketsAfter.filter(t => t.isDeleted)
879
+ remainingTickets = ticketsAfter.filter(t => !t.isDeleted)
880
+ expect(deletedTickets).toHaveLength(0);
881
+ expect(remainingTickets).toHaveLength(4);
882
+
883
+ // Note we correctly reused the tickets here
884
+ expect(remainingTickets.map(mapTicketChangedAmount)).toIncludeAllMembers([
885
+ {
886
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 1)!),
887
+ index: 2
888
+ },
889
+ {
890
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 2)!),
891
+ index: 1
892
+ },
893
+ {
894
+ ...mapTicketChangedAmount(tickets.find(t => t.index === 3)!),
895
+ index: 3,
896
+ seat: CartReservedSeat.create({
897
+ section: seatingPlan.sections[0].id,
898
+ row: 'B',
899
+ seat: '1'
900
+ })
901
+ }
902
+ ])
903
+
904
+ // One new one
905
+ expect(remainingTickets.map(mapTicketCreation)).toIncludeAllMembers([
906
+ {
907
+ // new one!
908
+ index: 4,
909
+ itemId: item1.id,
910
+ total: 4,
911
+
912
+ seat: CartReservedSeat.create({
913
+ section: seatingPlan.sections[0].id,
914
+ row: 'B',
915
+ seat: '2'
916
+ }),
917
+ originalSeat: CartReservedSeat.create({
918
+ section: seatingPlan.sections[0].id,
919
+ row: 'B',
920
+ seat: '2'
921
+ })
922
+ }
923
+ ]);
924
+
925
+ });
926
+ });