@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.
- package/.env.template.json +63 -0
- package/.eslintrc.js +61 -0
- package/README.md +40 -0
- package/index.ts +172 -0
- package/jest.config.js +11 -0
- package/migrations.ts +33 -0
- package/package.json +48 -0
- package/src/crons.ts +845 -0
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
- package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
- package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
- package/src/endpoints/auth/SignupEndpoint.ts +107 -0
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
- package/src/endpoints/global/files/UploadFile.ts +147 -0
- package/src/endpoints/global/files/UploadImage.ts +119 -0
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
- package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
- package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
- package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
- package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
- package/src/helpers/AddressValidator.test.ts +40 -0
- package/src/helpers/AddressValidator.ts +256 -0
- package/src/helpers/AdminPermissionChecker.ts +1031 -0
- package/src/helpers/AuthenticatedStructures.ts +158 -0
- package/src/helpers/BuckarooHelper.ts +279 -0
- package/src/helpers/CheckSettlements.ts +215 -0
- package/src/helpers/Context.ts +202 -0
- package/src/helpers/CookieHelper.ts +45 -0
- package/src/helpers/ForwardHandler.test.ts +216 -0
- package/src/helpers/ForwardHandler.ts +140 -0
- package/src/helpers/OpenIDConnectHelper.ts +284 -0
- package/src/helpers/StripeHelper.ts +293 -0
- package/src/helpers/StripePayoutChecker.ts +188 -0
- package/src/middleware/ContextMiddleware.ts +16 -0
- package/src/migrations/1646578856-validate-addresses.ts +60 -0
- package/src/seeds/0000000000-example.ts +13 -0
- package/src/seeds/1715028563-user-permissions.ts +52 -0
- package/tests/e2e/stock.test.ts +2120 -0
- package/tests/e2e/tickets.test.ts +926 -0
- package/tests/helpers/StripeMocker.ts +362 -0
- package/tests/helpers/TestServer.ts +21 -0
- package/tests/jest.global.setup.ts +29 -0
- package/tests/jest.setup.ts +59 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { PermissionLevel, WebshopUriAvailabilityResponse } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
|
|
8
|
+
type Params = { id: string };
|
|
9
|
+
type Body = undefined;
|
|
10
|
+
class Query extends AutoEncoder {
|
|
11
|
+
@field({ decoder: StringDecoder })
|
|
12
|
+
uri: string;
|
|
13
|
+
}
|
|
14
|
+
type ResponseBody = WebshopUriAvailabilityResponse;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class GetWebshopUriAvailabilityEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
21
|
+
queryDecoder = Query as Decoder<Query>
|
|
22
|
+
|
|
23
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
24
|
+
if (request.method != "GET") {
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/check-uri", { id: String });
|
|
29
|
+
|
|
30
|
+
if (params) {
|
|
31
|
+
return [true, params as Params];
|
|
32
|
+
}
|
|
33
|
+
return [false];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
37
|
+
const organization = await Context.setOrganizationScope();
|
|
38
|
+
await Context.authenticate()
|
|
39
|
+
|
|
40
|
+
// Fast throw first (more in depth checking for patches later)
|
|
41
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
42
|
+
throw Context.auth.error()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
46
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
47
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const q = await Webshop.where({
|
|
51
|
+
uri: request.query.uri,
|
|
52
|
+
id: {
|
|
53
|
+
sign: "!=",
|
|
54
|
+
value: request.params.id
|
|
55
|
+
}
|
|
56
|
+
}, {
|
|
57
|
+
limit: 1,
|
|
58
|
+
select: "id"
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const available = q.length == 0
|
|
62
|
+
|
|
63
|
+
return new Response(
|
|
64
|
+
WebshopUriAvailabilityResponse.create({
|
|
65
|
+
available
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from "@simonbackx/simple-encoding";
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
5
|
+
import { QueueHandler } from "@stamhoofd/queues";
|
|
6
|
+
import { DiscountCode, PermissionLevel } from "@stamhoofd/structures";
|
|
7
|
+
|
|
8
|
+
import { Context } from "../../../../helpers/Context";
|
|
9
|
+
|
|
10
|
+
type Params = { id: string };
|
|
11
|
+
type Query = undefined
|
|
12
|
+
type Body = PatchableArrayAutoEncoder<DiscountCode>
|
|
13
|
+
type ResponseBody = DiscountCode[]
|
|
14
|
+
|
|
15
|
+
export class PatchWebshopDiscountCodesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
16
|
+
bodyDecoder = new PatchableArrayDecoder(DiscountCode as Decoder<DiscountCode>, DiscountCode.patchType() as Decoder<AutoEncoderPatchType<DiscountCode>>, StringDecoder)
|
|
17
|
+
|
|
18
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
19
|
+
if (request.method != "PATCH") {
|
|
20
|
+
return [false];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/discount-codes", { id: String });
|
|
24
|
+
|
|
25
|
+
if (params) {
|
|
26
|
+
return [true, params as Params];
|
|
27
|
+
}
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
|
+
const organization = await Context.setOrganizationScope();
|
|
33
|
+
await Context.authenticate()
|
|
34
|
+
|
|
35
|
+
// Fast throw first (more in depth checking for patches later)
|
|
36
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
37
|
+
throw Context.auth.error()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
41
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
42
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const discountCodes: WebshopDiscountCode[] = []
|
|
46
|
+
|
|
47
|
+
// Updating discoutn codes should happen in the stock queue (because they are also edited when placing orders)
|
|
48
|
+
await QueueHandler.schedule("webshop-stock/"+request.params.id, async () => {
|
|
49
|
+
// TODO: handle order creation here
|
|
50
|
+
for (const put of request.body.getPuts()) {
|
|
51
|
+
const struct = put.put
|
|
52
|
+
const model = new WebshopDiscountCode()
|
|
53
|
+
model.code = struct.code;
|
|
54
|
+
model.description = struct.description
|
|
55
|
+
model.webshopId = webshop.id
|
|
56
|
+
model.organizationId = webshop.organizationId
|
|
57
|
+
model.discounts = struct.discounts
|
|
58
|
+
model.maximumUsage = struct.maximumUsage
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await model.save()
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Duplicate key probably
|
|
64
|
+
if (e.code && e.code == "ER_DUP_ENTRY") {
|
|
65
|
+
throw new SimpleError({
|
|
66
|
+
code: 'used_code',
|
|
67
|
+
message: 'Discount code already in use',
|
|
68
|
+
human: 'Er bestaat al een kortingscode met de code ' + struct.code+', een code moet uniek zijn.'
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
discountCodes.push(model)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const patch of request.body.getPatches()) {
|
|
78
|
+
const model = await WebshopDiscountCode.getByID(patch.id)
|
|
79
|
+
if (!model || model.webshopId !== webshop.id) {
|
|
80
|
+
throw new SimpleError({
|
|
81
|
+
code: "not_found",
|
|
82
|
+
message: "Discount code with id "+patch.id+" does not exist"
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
model.code = patchObject(model.code, patch.code)
|
|
87
|
+
model.description = patchObject(model.description, patch.description)
|
|
88
|
+
model.discounts = patchObject(model.discounts, patch.discounts)
|
|
89
|
+
model.maximumUsage = patchObject(model.maximumUsage, patch.maximumUsage)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await model.save()
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// Duplicate key probably
|
|
95
|
+
if (e.code && e.code == "ER_DUP_ENTRY") {
|
|
96
|
+
throw new SimpleError({
|
|
97
|
+
code: 'used_code',
|
|
98
|
+
message: 'Discount code already in use',
|
|
99
|
+
human: 'Er bestaat al een kortingscode met de code ' + model.code+', een code moet uniek zijn.'
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
discountCodes.push(model)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const id of request.body.getDeletes()) {
|
|
109
|
+
const model = await WebshopDiscountCode.getByID(id)
|
|
110
|
+
if (!model || model.webshopId !== webshop.id) {
|
|
111
|
+
throw new SimpleError({
|
|
112
|
+
code: "not_found",
|
|
113
|
+
message: "Discount code with id "+id+" does not exist"
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await model.delete()
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return new Response(
|
|
122
|
+
discountCodes.map(d => d.getStructure())
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { Token, Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
+
import { PermissionLevel, PrivateWebshop, WebshopPrivateMetaData } from "@stamhoofd/structures";
|
|
7
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
8
|
+
|
|
9
|
+
import { Context } from '../../../../helpers/Context';
|
|
10
|
+
|
|
11
|
+
type Params = { id: string };
|
|
12
|
+
type Query = undefined;
|
|
13
|
+
type Body = AutoEncoderPatchType<PrivateWebshop>;
|
|
14
|
+
type ResponseBody = PrivateWebshop;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class PatchWebshopEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
21
|
+
bodyDecoder = PrivateWebshop.patchType() as Decoder<AutoEncoderPatchType<PrivateWebshop>>
|
|
22
|
+
|
|
23
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
24
|
+
if (request.method != "PATCH") {
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id", { id: String });
|
|
29
|
+
|
|
30
|
+
if (params) {
|
|
31
|
+
return [true, params as Params];
|
|
32
|
+
}
|
|
33
|
+
return [false];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
37
|
+
const organization = await Context.setOrganizationScope();
|
|
38
|
+
await Context.authenticate()
|
|
39
|
+
|
|
40
|
+
// Fast throw first (more in depth checking for patches later)
|
|
41
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
42
|
+
throw Context.auth.error()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Halt all order placement and validation + pause stock updates
|
|
46
|
+
return await QueueHandler.schedule("webshop-stock/"+request.params.id, async () => {
|
|
47
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
48
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
49
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Do all updates
|
|
53
|
+
if (request.body.meta) {
|
|
54
|
+
request.body.meta.domainActive = undefined
|
|
55
|
+
webshop.meta.patchOrPut(request.body.meta)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (request.body.privateMeta) {
|
|
59
|
+
// Prevent editing internal fields
|
|
60
|
+
(request.body.privateMeta as any).dnsRecords = undefined
|
|
61
|
+
webshop.privateMeta.patchOrPut(request.body.privateMeta)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (request.body.products) {
|
|
65
|
+
webshop.products = request.body.products.applyTo(webshop.products)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (request.body.categories) {
|
|
69
|
+
webshop.categories = request.body.categories.applyTo(webshop.categories)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (request.body.domain !== undefined) {
|
|
73
|
+
if (request.body.domain !== null) {
|
|
74
|
+
const cleaned = request.body.domain.toLowerCase().replace(/[^a-zA-Z0-9-.]/g, '');
|
|
75
|
+
|
|
76
|
+
if (cleaned != webshop.domain) {
|
|
77
|
+
webshop.domain = cleaned
|
|
78
|
+
webshop.meta.domainActive = false
|
|
79
|
+
webshop.privateMeta.dnsRecords = WebshopPrivateMetaData.buildDNSRecords(cleaned)
|
|
80
|
+
|
|
81
|
+
// Check if this is a known domain
|
|
82
|
+
const knownWebshops = await Webshop.getByDomainOnly(cleaned)
|
|
83
|
+
|
|
84
|
+
if (knownWebshops.length > 0) {
|
|
85
|
+
const active = !!knownWebshops.find(k => k.meta.domainActive)
|
|
86
|
+
|
|
87
|
+
if (active) {
|
|
88
|
+
const sameOrg = knownWebshops.find(w => w.organizationId === organization.id)
|
|
89
|
+
const otherOrg = knownWebshops.find(w => w.organizationId !== organization.id)
|
|
90
|
+
if (otherOrg && !sameOrg) {
|
|
91
|
+
throw new SimpleError({
|
|
92
|
+
code: "domain_already_used",
|
|
93
|
+
message: "This domain is already used by another organization",
|
|
94
|
+
human: "Deze domeinnaam is al in gebruik door een andere vereniging. Neem contact op met Stamhoofd als je denkt dat je toch toegang zou moeten krijgen.",
|
|
95
|
+
statusCode: 400
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Automatically update the dns records already.
|
|
100
|
+
// This domain was already used, so no risk of making DNS-caches dirty
|
|
101
|
+
console.log("Automatically updating dns records for", cleaned, "during patch")
|
|
102
|
+
await webshop.updateDNSRecords()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cleaned.length < 4 || !cleaned.includes(".")) {
|
|
107
|
+
throw new SimpleError({
|
|
108
|
+
code: "invalid_field",
|
|
109
|
+
message: "Invalid domain",
|
|
110
|
+
human: "Ongeldige domeinnaam",
|
|
111
|
+
field: "customUrl"
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
webshop.domain = null
|
|
117
|
+
webshop.privateMeta.dnsRecords = []
|
|
118
|
+
webshop.meta.domainActive = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (request.body.domainUri !== undefined) {
|
|
123
|
+
if (webshop.domain !== null) {
|
|
124
|
+
webshop.domainUri = request.body.domainUri ?? ""
|
|
125
|
+
|
|
126
|
+
if (webshop.domainUri != Formatter.slug(webshop.domainUri)) {
|
|
127
|
+
throw new SimpleError({
|
|
128
|
+
code: "invalid_field",
|
|
129
|
+
message: "domainUri contains invalid characters",
|
|
130
|
+
human: "Een link mag geen spaties, hoofdletters of speciale tekens bevatten",
|
|
131
|
+
field: "customUrl"
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check exists
|
|
136
|
+
const existing = await Webshop.getByDomain(webshop.domain, webshop.domainUri);
|
|
137
|
+
if (existing !== undefined) {
|
|
138
|
+
throw new SimpleError({
|
|
139
|
+
code: "invalid_domain",
|
|
140
|
+
message: "This domain is already in use",
|
|
141
|
+
human: "Deze link is al in gebruik door een andere webshop: " + existing.meta.name+". Verwijder of pas daar de link eerst aan als je die wilt hergebruiken."
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
webshop.domainUri = null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (request.body.legacyUri !== undefined) {
|
|
150
|
+
// Support editing the legacy uri (e.g. delete it, or for older clients)
|
|
151
|
+
webshop.legacyUri = request.body.legacyUri
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (request.body.uri !== undefined) {
|
|
155
|
+
// Validate
|
|
156
|
+
if (request.body.uri.length == 0) {
|
|
157
|
+
throw new SimpleError({
|
|
158
|
+
code: "invalid_field",
|
|
159
|
+
message: "Uri cannot be empty",
|
|
160
|
+
human: "De link mag niet leeg zijn",
|
|
161
|
+
field: "uri"
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (request.body.uri != Formatter.slug(request.body.uri)) {
|
|
166
|
+
throw new SimpleError({
|
|
167
|
+
code: "invalid_field",
|
|
168
|
+
message: "Uri contains invalid characters",
|
|
169
|
+
human: "Een link mag geen spaties, hoofdletters of speciale tekens bevatten",
|
|
170
|
+
field: "uri"
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
webshop.uri = request.body.uri
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Verify if we still have full access
|
|
178
|
+
if (!await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
179
|
+
throw new SimpleError({
|
|
180
|
+
code: "missing_permissions",
|
|
181
|
+
message: "You cannot restrict your own permissions",
|
|
182
|
+
human: "Je kan je eigen volledige toegang tot deze webshop niet verwijderen (algemeen > toegangsbeheer). Vraag aan een hoofdbeheerder om jouw toegang te verwijderen."
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await webshop.save()
|
|
188
|
+
} catch (e) {
|
|
189
|
+
// Duplicate key probably
|
|
190
|
+
if (e.code && e.code == "ER_DUP_ENTRY") {
|
|
191
|
+
throw new SimpleError({
|
|
192
|
+
code: "invalid_field",
|
|
193
|
+
message: "Uri already in use",
|
|
194
|
+
human: "De link die je hebt gekozen is al in gebruik. Kies een andere.",
|
|
195
|
+
field: "uri"
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return new Response(PrivateWebshop.create(webshop));
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { ArrayDecoder, AutoEncoderPatchType, Data, Decoder, PatchableArray, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
|
+
import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
6
|
+
import { BalanceItemStatus, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment,Webshop as WebshopStruct } from "@stamhoofd/structures";
|
|
7
|
+
|
|
8
|
+
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
|
|
10
|
+
type Params = { id: string };
|
|
11
|
+
type Query = undefined;
|
|
12
|
+
type Body = AutoEncoderPatchType<PrivateOrder>[] | PatchableArrayAutoEncoder<PrivateOrder>
|
|
13
|
+
type ResponseBody = PrivateOrder[]
|
|
14
|
+
|
|
15
|
+
class VersionSpecificDecoder<A, B> implements Decoder<A | B> {
|
|
16
|
+
oldDecoder: Decoder<A>;
|
|
17
|
+
version: number;
|
|
18
|
+
newerDecoder: Decoder<B>;
|
|
19
|
+
|
|
20
|
+
constructor(oldDecoder: Decoder<A>, version: number, newerDecoder: Decoder<B>) {
|
|
21
|
+
this.oldDecoder = oldDecoder;
|
|
22
|
+
this.version = version;
|
|
23
|
+
this.newerDecoder = newerDecoder;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
decode(data: Data): A | B {
|
|
27
|
+
// Set the version of the decoding context of "data"
|
|
28
|
+
const v = data.context.version
|
|
29
|
+
|
|
30
|
+
if (v >= this.version) {
|
|
31
|
+
return this.newerDecoder.decode(data);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.oldDecoder.decode(data);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
39
|
+
bodyDecoder = new VersionSpecificDecoder(
|
|
40
|
+
// Before version 159, accept an array of patches
|
|
41
|
+
new ArrayDecoder(PrivateOrder.patchType() as Decoder<AutoEncoderPatchType<PrivateOrder>>),
|
|
42
|
+
159,
|
|
43
|
+
// After or at version 159, accept a patchable array
|
|
44
|
+
new PatchableArrayDecoder(PrivateOrder as Decoder<PrivateOrder>, PrivateOrder.patchType() as Decoder<AutoEncoderPatchType<PrivateOrder>>, StringDecoder)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
48
|
+
if (request.method != "PATCH") {
|
|
49
|
+
return [false];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/orders", { id: String });
|
|
53
|
+
|
|
54
|
+
if (params) {
|
|
55
|
+
return [true, params as Params];
|
|
56
|
+
}
|
|
57
|
+
return [false];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
61
|
+
const organization = await Context.setOrganizationScope();
|
|
62
|
+
await Context.authenticate()
|
|
63
|
+
|
|
64
|
+
// Fast throw first (more in depth checking for patches later)
|
|
65
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
66
|
+
throw Context.auth.error()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let body: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray()
|
|
70
|
+
|
|
71
|
+
// Migrate old syntax
|
|
72
|
+
if (Array.isArray(request.body)) {
|
|
73
|
+
for (const p of request.body) {
|
|
74
|
+
body.addPatch(p);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
body = request.body
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (body.changes.length == 0) {
|
|
81
|
+
return new Response([]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Need to happen in the queue because we are updating the webshop stock
|
|
85
|
+
const orders = await QueueHandler.schedule("webshop-stock/"+request.params.id, async () => {
|
|
86
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
87
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Write)) {
|
|
88
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const orders = body.getPatches().length > 0 ? await Order.where({
|
|
92
|
+
webshopId: webshop.id,
|
|
93
|
+
id: {
|
|
94
|
+
sign: "IN",
|
|
95
|
+
value: body.getPatches().map(o => o.id)
|
|
96
|
+
}
|
|
97
|
+
}) : []
|
|
98
|
+
|
|
99
|
+
// We use a getter because we need to have an up to date webshop struct
|
|
100
|
+
// otherwise we won't validate orders on the latest webshop with the latest stock information
|
|
101
|
+
const webshopGetter = {
|
|
102
|
+
get struct() {
|
|
103
|
+
return WebshopStruct.create(webshop);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// TODO: handle order creation here
|
|
108
|
+
for (const put of body.getPuts()) {
|
|
109
|
+
const struct = put.put
|
|
110
|
+
const model = new Order()
|
|
111
|
+
model.webshopId = webshop.id
|
|
112
|
+
model.organizationId = webshop.organizationId
|
|
113
|
+
model.status = struct.status
|
|
114
|
+
model.data = struct.data
|
|
115
|
+
|
|
116
|
+
// For now, we don't invalidate tickets, because they will get invalidated at scan time (the order status is checked)
|
|
117
|
+
// This allows you to revalidate a ticket without needing to generate a new one (e.g. when accidentally canceling an order)
|
|
118
|
+
// -> the user doesn't need to download the ticket again
|
|
119
|
+
// + added benefit: we can inform the user that the ticket was canceled, instead of throwing an 'invalid ticket' error
|
|
120
|
+
|
|
121
|
+
if (model.status === OrderStatus.Deleted) {
|
|
122
|
+
model.data.removePersonalData()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const order = model.setRelation(Order.webshop, webshop.setRelation(Webshop.organization, organization))
|
|
126
|
+
|
|
127
|
+
// TODO: validate before updating stock
|
|
128
|
+
order.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await order.updateStock()
|
|
132
|
+
const totalPrice = order.data.totalPrice
|
|
133
|
+
|
|
134
|
+
if (totalPrice == 0) {
|
|
135
|
+
// Force unknown payment method
|
|
136
|
+
order.data.paymentMethod = PaymentMethod.Unknown
|
|
137
|
+
|
|
138
|
+
// Mark this order as paid
|
|
139
|
+
await order.markPaid(null, organization, webshop)
|
|
140
|
+
await order.save()
|
|
141
|
+
} else {
|
|
142
|
+
const payment = new Payment()
|
|
143
|
+
payment.organizationId = organization.id
|
|
144
|
+
payment.method = struct.data.paymentMethod
|
|
145
|
+
payment.status = PaymentStatus.Created
|
|
146
|
+
payment.price = totalPrice
|
|
147
|
+
payment.paidAt = null
|
|
148
|
+
|
|
149
|
+
// Determine the payment provider (always null because no online payments here)
|
|
150
|
+
payment.provider = null
|
|
151
|
+
|
|
152
|
+
await payment.save()
|
|
153
|
+
|
|
154
|
+
order.paymentId = payment.id
|
|
155
|
+
order.setRelation(Order.payment, payment)
|
|
156
|
+
|
|
157
|
+
// Create balance item
|
|
158
|
+
const balanceItem = new BalanceItem();
|
|
159
|
+
balanceItem.orderId = order.id;
|
|
160
|
+
balanceItem.price = totalPrice
|
|
161
|
+
balanceItem.description = webshop.meta.name
|
|
162
|
+
balanceItem.pricePaid = 0
|
|
163
|
+
balanceItem.organizationId = organization.id;
|
|
164
|
+
balanceItem.status = BalanceItemStatus.Pending;
|
|
165
|
+
await balanceItem.save();
|
|
166
|
+
|
|
167
|
+
// Create one balance item payment to pay it in one payment
|
|
168
|
+
const balanceItemPayment = new BalanceItemPayment()
|
|
169
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
170
|
+
balanceItemPayment.paymentId = payment.id;
|
|
171
|
+
balanceItemPayment.organizationId = organization.id;
|
|
172
|
+
balanceItemPayment.price = balanceItem.price;
|
|
173
|
+
await balanceItemPayment.save();
|
|
174
|
+
|
|
175
|
+
if (payment.method == PaymentMethod.Transfer) {
|
|
176
|
+
await order.markValid(payment, [])
|
|
177
|
+
await payment.save()
|
|
178
|
+
await order.save()
|
|
179
|
+
} else if (payment.method == PaymentMethod.PointOfSale) {
|
|
180
|
+
// Not really paid, but needed to create the tickets if needed
|
|
181
|
+
await order.markPaid(payment, organization, webshop)
|
|
182
|
+
await payment.save()
|
|
183
|
+
await order.save()
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error("Unsupported payment method")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
balanceItem.description = order.generateBalanceDescription(webshop)
|
|
189
|
+
await balanceItem.save()
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {
|
|
192
|
+
await order.deleteOrderBecauseOfCreationError()
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
orders.push(order)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const patch of body.getPatches()) {
|
|
200
|
+
const model = orders.find(p => p.id == patch.id)
|
|
201
|
+
if (!model) {
|
|
202
|
+
throw new SimpleError({
|
|
203
|
+
code: "not_found",
|
|
204
|
+
message: "Order with id "+patch.id+" does not exist"
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
const previousToPay = model.totalToPay;
|
|
208
|
+
const previousStatus = model.status
|
|
209
|
+
|
|
210
|
+
model.status = patch.status ?? model.status
|
|
211
|
+
|
|
212
|
+
// For now, we don't invalidate tickets, because they will get invalidated at scan time (the order status is checked)
|
|
213
|
+
// This allows you to revalidate a ticket without needing to generate a new one (e.g. when accidentally canceling an order)
|
|
214
|
+
// -> the user doesn't need to download the ticket again
|
|
215
|
+
// + added benefit: we can inform the user that the ticket was canceled, instead of throwing an 'invalid ticket' error
|
|
216
|
+
|
|
217
|
+
const previousData = model.data.clone()
|
|
218
|
+
if (patch.data) {
|
|
219
|
+
model.data.patchOrPut(patch.data)
|
|
220
|
+
|
|
221
|
+
if (model.status !== OrderStatus.Deleted) {
|
|
222
|
+
// Make sure all data is up to date and validated (= possible corrections happen here too)
|
|
223
|
+
model.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (model.status === OrderStatus.Deleted) {
|
|
228
|
+
model.data.removePersonalData()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (model.status === OrderStatus.Deleted || model.status === OrderStatus.Canceled) {
|
|
232
|
+
model.markUpdated()
|
|
233
|
+
// Cancel payment if still pending
|
|
234
|
+
await BalanceItem.deleteForDeletedOrders([model.id])
|
|
235
|
+
} else {
|
|
236
|
+
if (previousStatus === OrderStatus.Canceled || previousStatus === OrderStatus.Deleted) {
|
|
237
|
+
model.markUpdated()
|
|
238
|
+
// Undo deletion
|
|
239
|
+
await BalanceItem.undoForDeletedOrders([model.id])
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Update balance item prices for this order if price has changed
|
|
244
|
+
if (previousToPay !== model.totalToPay) {
|
|
245
|
+
const items = await BalanceItem.where({ orderId: model.id })
|
|
246
|
+
if (items.length === 1) {
|
|
247
|
+
model.markUpdated()
|
|
248
|
+
items[0].price = model.totalToPay
|
|
249
|
+
items[0].description = model.generateBalanceDescription(webshop)
|
|
250
|
+
items[0].updateStatus();
|
|
251
|
+
await items[0].save()
|
|
252
|
+
} else if (items.length === 0 && model.totalToPay > 0) {
|
|
253
|
+
model.markUpdated()
|
|
254
|
+
const balanceItem = new BalanceItem();
|
|
255
|
+
balanceItem.orderId = model.id;
|
|
256
|
+
balanceItem.price = model.totalToPay
|
|
257
|
+
balanceItem.description = model.generateBalanceDescription(webshop)
|
|
258
|
+
balanceItem.pricePaid = 0
|
|
259
|
+
balanceItem.organizationId = organization.id;
|
|
260
|
+
balanceItem.status = BalanceItemStatus.Pending;
|
|
261
|
+
await balanceItem.save();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await model.save()
|
|
266
|
+
await model.setRelation(Order.webshop, webshop).updateStock(previousData)
|
|
267
|
+
await model.setRelation(Order.webshop, webshop).updateTickets()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const mapped = orders.map(order => order.setRelation(Order.webshop, webshop))
|
|
271
|
+
return mapped
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return new Response(
|
|
275
|
+
await Order.getPrivateStructures(orders)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|