@stamhoofd/backend 2.19.0 → 2.20.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 +1 -1
- package/package.json +5 -5
- package/src/crons.ts +3 -67
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +0 -47
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -16
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +171 -36
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +2 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +28 -32
- package/src/helpers/AdminPermissionChecker.ts +22 -5
- package/src/helpers/AuthenticatedStructures.ts +3 -2
- package/src/helpers/SetupStepsUpdater.ts +115 -13
- package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +0 -153
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +0 -64
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +0 -84
- package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +0 -65
|
@@ -2,19 +2,15 @@ import { createMollieClient } from '@mollie/api-client';
|
|
|
2
2
|
import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
4
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
|
-
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment
|
|
5
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
|
|
6
6
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
|
-
import { PaymentGeneral, PaymentMethod,
|
|
7
|
+
import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
|
|
8
8
|
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
10
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
11
11
|
import { Context } from '../../../helpers/Context';
|
|
12
12
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
13
13
|
|
|
14
|
-
function calculateFee(totalPrice: number, fixed: number, percentageTimes100: number) {
|
|
15
|
-
return Math.round(fixed + Math.max(1, totalPrice * percentageTimes100 / 100 / 100)); // € 0,21 + 0,2%
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
type Params = {id: string};
|
|
19
15
|
class Query extends AutoEncoder {
|
|
20
16
|
@field({ decoder: BooleanDecoder, optional: true })
|
|
@@ -77,7 +73,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
77
73
|
if (payment.status === status) {
|
|
78
74
|
return;
|
|
79
75
|
}
|
|
80
|
-
const wasPaid = payment.paidAt !== null
|
|
76
|
+
// const wasPaid = payment.paidAt !== null
|
|
81
77
|
if (status === PaymentStatus.Succeeded) {
|
|
82
78
|
payment.status = PaymentStatus.Succeeded
|
|
83
79
|
payment.paidAt = new Date()
|
|
@@ -97,31 +93,31 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
97
93
|
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
98
94
|
})
|
|
99
95
|
|
|
100
|
-
if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
96
|
+
//if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
|
|
97
|
+
// // Charge transaction fees
|
|
98
|
+
// let fee = 0
|
|
99
|
+
//
|
|
100
|
+
// if (payment.method === PaymentMethod.iDEAL) {
|
|
101
|
+
// fee = calculateFee(payment.price, 21, 20); // € 0,21 + 0,2%
|
|
102
|
+
// } else if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.Payconiq) {
|
|
103
|
+
// fee = calculateFee(payment.price, 24, 20); // € 0,24 + 0,2%
|
|
104
|
+
// } else {
|
|
105
|
+
// fee = calculateFee(payment.price, 25, 150); // € 0,25 + 1,5%
|
|
106
|
+
// }
|
|
107
|
+
//
|
|
108
|
+
// const name = "Transactiekosten voor "+PaymentMethodHelper.getName(payment.method)
|
|
109
|
+
// const item = STInvoiceItem.create({
|
|
110
|
+
// name,
|
|
111
|
+
// description: "Via Buckaroo",
|
|
112
|
+
// amount: 1,
|
|
113
|
+
// unitPrice: fee,
|
|
114
|
+
// canUseCredits: false
|
|
115
|
+
// })
|
|
116
|
+
// console.log("Scheduling transaction fee charge for ", payment.id, item)
|
|
117
|
+
// await QueueHandler.schedule("billing/invoices-"+organization.id, async () => {
|
|
118
|
+
// await STPendingInvoice.addItems(organization, [item])
|
|
119
|
+
// });
|
|
120
|
+
//}
|
|
125
121
|
return;
|
|
126
122
|
}
|
|
127
123
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
|
|
2
2
|
import { SimpleError } from "@simonbackx/simple-errors"
|
|
3
|
-
import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
3
|
+
import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from "@stamhoofd/models"
|
|
4
4
|
import { AccessRight, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
|
|
5
5
|
import { Formatter } from "@stamhoofd/utility"
|
|
6
6
|
|
|
@@ -75,6 +75,11 @@ export class AdminPermissionChecker {
|
|
|
75
75
|
return result;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
async getOrganizationCurrentPeriod(id: string|Organization): Promise<OrganizationRegistrationPeriod> {
|
|
79
|
+
const organization = await this.getOrganization(id);
|
|
80
|
+
return await organization.getPeriod()
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
error(message?: string): SimpleError {
|
|
79
84
|
return new SimpleError({
|
|
80
85
|
code: "permission_denied",
|
|
@@ -171,7 +176,8 @@ export class AdminPermissionChecker {
|
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
// Check parent categories
|
|
174
|
-
const
|
|
179
|
+
const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
|
|
180
|
+
const parentCategories = group.getParentCategories(organizationPeriod.settings.categories)
|
|
175
181
|
for (const category of parentCategories) {
|
|
176
182
|
if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
|
|
177
183
|
return true
|
|
@@ -677,11 +683,22 @@ export class AdminPermissionChecker {
|
|
|
677
683
|
return false;
|
|
678
684
|
}
|
|
679
685
|
|
|
680
|
-
if (
|
|
681
|
-
return
|
|
686
|
+
if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, category.id, AccessRight.OrganizationCreateGroups)) {
|
|
687
|
+
return true;
|
|
682
688
|
}
|
|
683
689
|
|
|
684
|
-
|
|
690
|
+
// Check parents
|
|
691
|
+
const organization = await this.getOrganization(organizationId)
|
|
692
|
+
const organizationPeriod = await this.getOrganizationCurrentPeriod(organization)
|
|
693
|
+
const parentCategories = category.getParentCategories(organizationPeriod.settings.categories)
|
|
694
|
+
|
|
695
|
+
for (const parentCategory of parentCategories) {
|
|
696
|
+
if (organizationPermissions.hasResourceAccessRight(PermissionsResourceType.GroupCategories, parentCategory.id, AccessRight.OrganizationCreateGroups)) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return false;
|
|
685
702
|
}
|
|
686
703
|
|
|
687
704
|
canUpload() {
|
|
@@ -238,11 +238,12 @@ export class AuthenticatedStructures {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
member.registrations = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false))
|
|
242
242
|
const blob = member.getStructureWithRegistrations()
|
|
243
243
|
memberBlobs.push(
|
|
244
244
|
await Context.auth.filterMemberData(member, blob)
|
|
245
245
|
)
|
|
246
|
+
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
// Load responsibilities
|
|
@@ -268,7 +269,7 @@ export class AuthenticatedStructures {
|
|
|
268
269
|
|
|
269
270
|
return MembersBlob.create({
|
|
270
271
|
members: memberBlobs,
|
|
271
|
-
organizations: await Promise.all([...organizations.values()].map(o => this.organization(o)))
|
|
272
|
+
organizations: await Promise.all([...organizations.values()].filter(o => o.active).map(o => this.organization(o)))
|
|
272
273
|
})
|
|
273
274
|
}
|
|
274
275
|
|
|
@@ -1,38 +1,41 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Group,
|
|
3
|
+
MemberResponsibilityRecord,
|
|
2
4
|
Organization,
|
|
3
5
|
OrganizationRegistrationPeriod,
|
|
4
|
-
Platform
|
|
6
|
+
Platform
|
|
5
7
|
} from "@stamhoofd/models";
|
|
6
8
|
import { QueueHandler } from "@stamhoofd/queues";
|
|
9
|
+
import { SQL, SQLWhereSign } from "@stamhoofd/sql";
|
|
7
10
|
import {
|
|
8
|
-
|
|
11
|
+
MemberResponsibility,
|
|
9
12
|
Platform as PlatformStruct,
|
|
10
13
|
SetupStepType,
|
|
11
|
-
SetupSteps
|
|
14
|
+
SetupSteps
|
|
12
15
|
} from "@stamhoofd/structures";
|
|
13
16
|
|
|
14
|
-
type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void
|
|
17
|
+
type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void | Promise<void>;
|
|
15
18
|
|
|
16
19
|
export class SetupStepUpdater {
|
|
17
20
|
private static readonly STEP_TYPE_OPERATIONS: Record<
|
|
18
21
|
SetupStepType,
|
|
19
22
|
SetupStepOperation
|
|
20
23
|
> = {
|
|
24
|
+
[SetupStepType.Functions]: this.updateStepFunctions,
|
|
25
|
+
[SetupStepType.Companies]: this.updateStepCompanies,
|
|
21
26
|
[SetupStepType.Groups]: this.updateStepGroups,
|
|
22
27
|
[SetupStepType.Premises]: this.updateStepPremises,
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
static async updateSetupStepsForAllOrganizationsInCurrentPeriod({
|
|
26
|
-
batchSize
|
|
27
|
-
}: { batchSize?: number
|
|
31
|
+
batchSize
|
|
32
|
+
}: { batchSize?: number } = {}) {
|
|
28
33
|
const tag = "updateSetupStepsForAllOrganizationsInCurrentPeriod";
|
|
29
34
|
QueueHandler.cancel(tag);
|
|
30
35
|
|
|
31
36
|
await QueueHandler.schedule(tag, async () => {
|
|
32
37
|
const platform = (await Platform.getSharedPrivateStruct()).clone();
|
|
33
|
-
|
|
34
|
-
platform.config.premiseTypes = premiseTypes;
|
|
35
|
-
}
|
|
38
|
+
|
|
36
39
|
const periodId = platform.period.id;
|
|
37
40
|
|
|
38
41
|
let lastId = "";
|
|
@@ -136,7 +139,7 @@ export class SetupStepUpdater {
|
|
|
136
139
|
);
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
static async updateFor(
|
|
142
|
+
private static async updateFor(
|
|
140
143
|
organizationRegistrationPeriod: OrganizationRegistrationPeriod,
|
|
141
144
|
platform: PlatformStruct,
|
|
142
145
|
organization: Organization
|
|
@@ -147,13 +150,13 @@ export class SetupStepUpdater {
|
|
|
147
150
|
for (const stepType of Object.values(SetupStepType)) {
|
|
148
151
|
console.log(`[STEP TYPE] ${stepType}`);
|
|
149
152
|
const operation = this.STEP_TYPE_OPERATIONS[stepType];
|
|
150
|
-
operation(setupSteps, organization, platform);
|
|
153
|
+
await operation(setupSteps, organization, platform);
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
await organizationRegistrationPeriod.save();
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
static updateStepPremises(
|
|
159
|
+
private static updateStepPremises(
|
|
157
160
|
setupSteps: SetupSteps,
|
|
158
161
|
organization: Organization,
|
|
159
162
|
platform: PlatformStruct
|
|
@@ -165,6 +168,8 @@ export class SetupStepUpdater {
|
|
|
165
168
|
|
|
166
169
|
for (const premiseType of premiseTypes) {
|
|
167
170
|
const { min, max } = premiseType;
|
|
171
|
+
|
|
172
|
+
// only add step if premise type has restrictions
|
|
168
173
|
if (min === null && max === null) {
|
|
169
174
|
continue;
|
|
170
175
|
}
|
|
@@ -197,7 +202,7 @@ export class SetupStepUpdater {
|
|
|
197
202
|
});
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
static updateStepGroups(
|
|
205
|
+
private static updateStepGroups(
|
|
201
206
|
setupSteps: SetupSteps,
|
|
202
207
|
_organization: Organization,
|
|
203
208
|
_platform: PlatformStruct
|
|
@@ -207,4 +212,101 @@ export class SetupStepUpdater {
|
|
|
207
212
|
finishedSteps: 0,
|
|
208
213
|
});
|
|
209
214
|
}
|
|
215
|
+
|
|
216
|
+
private static updateStepCompanies(
|
|
217
|
+
setupSteps: SetupSteps,
|
|
218
|
+
_organization: Organization,
|
|
219
|
+
_platform: PlatformStruct
|
|
220
|
+
) {
|
|
221
|
+
setupSteps.update(SetupStepType.Companies, {
|
|
222
|
+
totalSteps: 0,
|
|
223
|
+
finishedSteps: 0,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static async updateStepFunctions(
|
|
228
|
+
setupSteps: SetupSteps,
|
|
229
|
+
organization: Organization,
|
|
230
|
+
platform: PlatformStruct
|
|
231
|
+
) {
|
|
232
|
+
const now = new Date();
|
|
233
|
+
const organizationBasedResponsibilitiesWithRestriction = platform.config.responsibilities
|
|
234
|
+
.filter(r => r.organizationBased && (r.minimumMembers || r.maximumMembers));
|
|
235
|
+
|
|
236
|
+
const responsibilityIds = organizationBasedResponsibilitiesWithRestriction.map(r => r.id);
|
|
237
|
+
|
|
238
|
+
const records = await MemberResponsibilityRecord.select()
|
|
239
|
+
.where('responsibilityId', responsibilityIds)
|
|
240
|
+
.where('organizationId', organization.id)
|
|
241
|
+
.where(SQL.where('endDate', SQLWhereSign.Greater, now).or('endDate', null))
|
|
242
|
+
.fetch();
|
|
243
|
+
|
|
244
|
+
let totalSteps = 0;
|
|
245
|
+
let finishedSteps = 0;
|
|
246
|
+
|
|
247
|
+
const groups = await Group.getAll(organization.id, organization.periodId);
|
|
248
|
+
|
|
249
|
+
const flatResponsibilities: {responsibility: MemberResponsibility, group: Group | null}[] = organizationBasedResponsibilitiesWithRestriction
|
|
250
|
+
.flatMap(responsibility => {
|
|
251
|
+
const defaultAgeGroupIds = responsibility.defaultAgeGroupIds;
|
|
252
|
+
if(defaultAgeGroupIds === null) {
|
|
253
|
+
const item: {responsibility: MemberResponsibility, group: Group | null} = {
|
|
254
|
+
responsibility,
|
|
255
|
+
group: null
|
|
256
|
+
}
|
|
257
|
+
return [item];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return groups
|
|
261
|
+
.filter(g => g.defaultAgeGroupId !== null && defaultAgeGroupIds.includes(g.defaultAgeGroupId))
|
|
262
|
+
.map(group => {
|
|
263
|
+
return {
|
|
264
|
+
responsibility,
|
|
265
|
+
group
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
for(const {responsibility, group} of flatResponsibilities) {
|
|
271
|
+
const { minimumMembers: min, maximumMembers: max } = responsibility;
|
|
272
|
+
|
|
273
|
+
if (min === null && max === null) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
totalSteps++;
|
|
278
|
+
|
|
279
|
+
const responsibilityId = responsibility.id;
|
|
280
|
+
let totalRecordsWithThisResponsibility = 0;
|
|
281
|
+
|
|
282
|
+
if(group === null) {
|
|
283
|
+
for (const record of records) {
|
|
284
|
+
if (record.responsibilityId === responsibilityId) {
|
|
285
|
+
totalRecordsWithThisResponsibility++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
for (const record of records) {
|
|
290
|
+
if (record.responsibilityId === responsibilityId && record.groupId === group.id) {
|
|
291
|
+
totalRecordsWithThisResponsibility++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (max !== null && totalRecordsWithThisResponsibility > max) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (min !== null && totalRecordsWithThisResponsibility < min) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
finishedSteps++;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
setupSteps.update(SetupStepType.Functions, {
|
|
308
|
+
totalSteps,
|
|
309
|
+
finishedSteps,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
210
312
|
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { createMollieClient } from '@mollie/api-client';
|
|
2
|
-
import { AutoEncoder, BooleanDecoder,Decoder,field } from '@simonbackx/simple-encoding';
|
|
3
|
-
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
|
-
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
|
-
import { MolliePayment, Organization } from "@stamhoofd/models";
|
|
6
|
-
import { Payment } from "@stamhoofd/models";
|
|
7
|
-
import { STInvoice } from "@stamhoofd/models";
|
|
8
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
|
-
import { PaymentMethod,PaymentProvider,PaymentStatus, STInvoice as STInvoiceStruct } from "@stamhoofd/structures";
|
|
10
|
-
type Params = {id: string};
|
|
11
|
-
class Query extends AutoEncoder {
|
|
12
|
-
@field({ decoder: BooleanDecoder, optional: true })
|
|
13
|
-
exchange = false
|
|
14
|
-
}
|
|
15
|
-
type Body = undefined
|
|
16
|
-
type ResponseBody = STInvoiceStruct | undefined;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 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
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export class ExchangeSTPaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
23
|
-
queryDecoder = Query as Decoder<Query>
|
|
24
|
-
|
|
25
|
-
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
26
|
-
if (request.method != "POST") {
|
|
27
|
-
return [false];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const params = Endpoint.parseParameters(request.url, "/billing/payments/@id", {id: String});
|
|
31
|
-
|
|
32
|
-
if (params) {
|
|
33
|
-
return [true, params as Params];
|
|
34
|
-
}
|
|
35
|
-
return [false];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
39
|
-
const payment = await Payment.getByID(request.params.id)
|
|
40
|
-
if (!payment) {
|
|
41
|
-
throw new SimpleError({
|
|
42
|
-
code: "",
|
|
43
|
-
message: "Deze link is ongeldig"
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const invoices = await STInvoice.where({ paymentId: payment.id })
|
|
48
|
-
if (invoices.length > 1) {
|
|
49
|
-
console.error("Received more than 1 invoices for the same payment. Danger zone!")
|
|
50
|
-
throw new Error("Unexpected error")
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (invoices.length == 0) {
|
|
54
|
-
console.error("Didn't found and invoice for a given payment!")
|
|
55
|
-
throw new Error("Unexpected error")
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Not method on payment because circular references (not supprted in ts)
|
|
59
|
-
const invoice = invoices[0]
|
|
60
|
-
|
|
61
|
-
if (request.query.exchange) {
|
|
62
|
-
// Don't wait for exchanges
|
|
63
|
-
ExchangeSTPaymentEndpoint.pollStatus(payment, invoice).catch(e => {
|
|
64
|
-
console.error(e)
|
|
65
|
-
})
|
|
66
|
-
return new Response(undefined);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const updatedInvoice = await ExchangeSTPaymentEndpoint.pollStatus(payment, invoice)
|
|
70
|
-
|
|
71
|
-
if (!updatedInvoice) {
|
|
72
|
-
return new Response(undefined);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return new Response(
|
|
76
|
-
await updatedInvoice.getStructure()
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
static async pollStatus(payment: Payment, _invoice: STInvoice): Promise<STInvoice | undefined> {
|
|
81
|
-
// All invoice related logic needs to happen after each ather, not concurrently
|
|
82
|
-
return await QueueHandler.schedule("billing/invoices-"+_invoice.organizationId, async () => {
|
|
83
|
-
|
|
84
|
-
// Get a new copy of the invoice (is required to prevent concurrenty bugs)
|
|
85
|
-
const invoice = await STInvoice.getByID(_invoice.id)
|
|
86
|
-
if (!invoice || invoice.paidAt !== null) {
|
|
87
|
-
return invoice
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if ((payment.provider === PaymentProvider.Mollie || (payment.provider === null && payment.method == PaymentMethod.DirectDebit)) && (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || payment.status == PaymentStatus.Failed)) {
|
|
91
|
-
if (payment.method == PaymentMethod.Bancontact || payment.method == PaymentMethod.iDEAL || payment.method == PaymentMethod.CreditCard || payment.method == PaymentMethod.DirectDebit || payment.method == PaymentMethod.Transfer) {
|
|
92
|
-
// check status via mollie
|
|
93
|
-
const molliePayments = await MolliePayment.where({ paymentId: payment.id}, { limit: 1 })
|
|
94
|
-
if (molliePayments.length == 1) {
|
|
95
|
-
const molliePayment = molliePayments[0]
|
|
96
|
-
// check status
|
|
97
|
-
const apiKey = STAMHOOFD.MOLLIE_API_KEY
|
|
98
|
-
if (apiKey) {
|
|
99
|
-
const mollieClient = createMollieClient({ apiKey });
|
|
100
|
-
const mollieData = await mollieClient.payments.get(molliePayment.mollieId)
|
|
101
|
-
|
|
102
|
-
console.log(mollieData) // log to log files to check issues
|
|
103
|
-
|
|
104
|
-
const details = (mollieData.details as any)
|
|
105
|
-
if (details?.cardNumber) {
|
|
106
|
-
payment.iban = "xxxx xxxx xxxx "+details.cardNumber
|
|
107
|
-
}
|
|
108
|
-
if (details?.cardHolder) {
|
|
109
|
-
payment.ibanName = details.cardHolder
|
|
110
|
-
}
|
|
111
|
-
if (details?.consumerAccount) {
|
|
112
|
-
payment.iban = details.consumerAccount
|
|
113
|
-
}
|
|
114
|
-
if (details?.consumerName) {
|
|
115
|
-
payment.ibanName = details.consumerName
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (mollieData.status == "paid") {
|
|
119
|
-
payment.status = PaymentStatus.Succeeded
|
|
120
|
-
payment.paidAt = new Date()
|
|
121
|
-
await payment.save();
|
|
122
|
-
|
|
123
|
-
await invoice.markPaid()
|
|
124
|
-
|
|
125
|
-
// Save customer id
|
|
126
|
-
if (mollieData.customerId && _invoice.organizationId) {
|
|
127
|
-
const organization = await Organization.getByID(_invoice.organizationId)
|
|
128
|
-
if (organization) {
|
|
129
|
-
organization.serverMeta.mollieCustomerId = mollieData.customerId
|
|
130
|
-
console.log("Saving mollie customer", mollieData.customerId, "for organization", organization.id)
|
|
131
|
-
await organization.save()
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} else if (mollieData.status == "failed" || mollieData.status == "expired" || mollieData.status == "canceled") {
|
|
135
|
-
payment.status = PaymentStatus.Failed
|
|
136
|
-
await payment.save();
|
|
137
|
-
await invoice.markFailed(payment)
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
console.error("Mollie api key is missing for Stamhoofd payments! "+payment.id)
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
console.error("Couldn't find mollie payment for payment "+payment.id)
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
console.error("Payment method not supported for invoice "+invoice.id+" and payment "+payment.id)
|
|
147
|
-
throw new Error("Unsupported payment method for invoices")
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return invoice
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Request } from "@simonbackx/simple-endpoints";
|
|
3
|
-
import { OrganizationFactory, RegisterCodeFactory, STCredit, Token, UserFactory } from "@stamhoofd/models";
|
|
4
|
-
import { PermissionLevel, Permissions } from "@stamhoofd/structures";
|
|
5
|
-
|
|
6
|
-
import { testServer } from "../../../../../tests/helpers/TestServer";
|
|
7
|
-
import { ApplyRegisterCodeEndpoint } from "./ApplyRegisterCodeEndpoint";
|
|
8
|
-
|
|
9
|
-
describe("Endpoint.ApplyRegisterCodeEndpoint", () => {
|
|
10
|
-
// Test endpoint
|
|
11
|
-
const endpoint = new ApplyRegisterCodeEndpoint();
|
|
12
|
-
|
|
13
|
-
test("Cannot apply a register code if not platform admin", async () => {
|
|
14
|
-
const otherOrganization = await new OrganizationFactory({}).create();
|
|
15
|
-
const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
|
|
16
|
-
|
|
17
|
-
const organization = await new OrganizationFactory({}).create();
|
|
18
|
-
const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
|
|
19
|
-
const token = await Token.createToken(user)
|
|
20
|
-
|
|
21
|
-
const r = Request.buildJson(
|
|
22
|
-
"POST",
|
|
23
|
-
"/organization/register-code",
|
|
24
|
-
organization.getApiHost(),
|
|
25
|
-
{
|
|
26
|
-
registerCode: code.code,
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
r.headers.authorization = "Bearer "+token.accessToken
|
|
30
|
-
|
|
31
|
-
await expect(testServer.test(endpoint, r)).rejects.toThrow("You do not have permissions for this action");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("Can apply a register code and apply the discount", async () => {
|
|
35
|
-
const otherOrganization = await new OrganizationFactory({}).create();
|
|
36
|
-
const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
|
|
37
|
-
|
|
38
|
-
const organization = await new OrganizationFactory({}).create();
|
|
39
|
-
const user = await new UserFactory({
|
|
40
|
-
organization,
|
|
41
|
-
globalPermissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
42
|
-
email: 'admin@stamhoofd.be'
|
|
43
|
-
}).create()
|
|
44
|
-
const token = await Token.createToken(user)
|
|
45
|
-
|
|
46
|
-
const r = Request.buildJson(
|
|
47
|
-
"POST",
|
|
48
|
-
"/organization/register-code",
|
|
49
|
-
organization.getApiHost(),
|
|
50
|
-
{
|
|
51
|
-
registerCode: code.code,
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
r.headers.authorization = "Bearer "+token.accessToken
|
|
55
|
-
|
|
56
|
-
const response = await testServer.test(endpoint, r);
|
|
57
|
-
expect(response.body).toBeUndefined();
|
|
58
|
-
|
|
59
|
-
// Check if this organization has an open register code
|
|
60
|
-
const credits = await STCredit.getForOrganization(organization.id);
|
|
61
|
-
expect(credits.length).toBe(1);
|
|
62
|
-
expect(credits[0].change).toBe(code.value);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
-
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
-
import { Email } from '@stamhoofd/email';
|
|
4
|
-
import { RegisterCode, UsedRegisterCode } from '@stamhoofd/models';
|
|
5
|
-
|
|
6
|
-
import { Context } from '../../../../helpers/Context';
|
|
7
|
-
|
|
8
|
-
type Params = Record<string, never>;
|
|
9
|
-
type Query = undefined;
|
|
10
|
-
type ResponseBody = undefined;
|
|
11
|
-
|
|
12
|
-
class Body extends AutoEncoder {
|
|
13
|
-
@field({ decoder: StringDecoder })
|
|
14
|
-
registerCode: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 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
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export class ApplyRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
|
-
bodyDecoder = Body as Decoder<Body>
|
|
23
|
-
|
|
24
|
-
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
25
|
-
if (request.method != "POST") {
|
|
26
|
-
return [false];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const params = Endpoint.parseParameters(request.url, "/organization/register-code", {});
|
|
30
|
-
|
|
31
|
-
if (params) {
|
|
32
|
-
return [true, params as Params];
|
|
33
|
-
}
|
|
34
|
-
return [false];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
|
-
const organization = await Context.setOrganizationScope();
|
|
39
|
-
await Context.authenticate()
|
|
40
|
-
|
|
41
|
-
if (!Context.auth.hasPlatformFullAccess()) {
|
|
42
|
-
throw Context.auth.error()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let code = request.body.registerCode;
|
|
46
|
-
|
|
47
|
-
if (code.startsWith('https:')) {
|
|
48
|
-
try {
|
|
49
|
-
const url = new URL(code);
|
|
50
|
-
const codeParam = url.searchParams.get('code');
|
|
51
|
-
if (codeParam) {
|
|
52
|
-
console.log('Parsed code from URL', codeParam)
|
|
53
|
-
code = codeParam;
|
|
54
|
-
}
|
|
55
|
-
} catch (e) {
|
|
56
|
-
console.error('Tried parsing code as URL but failed', code)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const {models, emails} = await RegisterCode.applyRegisterCode(organization, code)
|
|
61
|
-
|
|
62
|
-
for (const model of models) {
|
|
63
|
-
await model.save();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (const email of emails) {
|
|
67
|
-
Email.sendInternal(email, organization.i18n)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (organization.meta.packages.isPaid) {
|
|
71
|
-
// Already bought something: apply credit to other organization immediately
|
|
72
|
-
const code = await UsedRegisterCode.getFor(organization.id)
|
|
73
|
-
if (code && !code.creditId) {
|
|
74
|
-
console.log("Rewarding code "+code.id+" for payment")
|
|
75
|
-
|
|
76
|
-
// Deze code werd nog niet beloond
|
|
77
|
-
await code.reward()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return new Response(undefined);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|