@stamhoofd/backend 2.129.1 → 2.129.3
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/package.json +17 -17
- package/src/crons/stripe-invoices.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.ts +6 -5
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +95 -76
- package/src/helpers/AdminPermissionChecker.ts +13 -11
- package/src/helpers/MemberMerger.test.ts +134 -0
- package/src/helpers/MemberMerger.ts +6 -0
- package/src/helpers/MemberUserSyncer.test.ts +1 -1
- package/src/helpers/MemberUserSyncer.ts +2 -1
- package/src/services/MollieService.ts +1 -1
- package/src/services/SSOService.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.129.
|
|
3
|
+
"version": "2.129.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
58
58
|
"@simonbackx/simple-errors": "1.5.0",
|
|
59
59
|
"@simonbackx/simple-logging": "1.0.1",
|
|
60
|
-
"@stamhoofd/backend-env": "2.129.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.129.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.129.
|
|
63
|
-
"@stamhoofd/crons": "2.129.
|
|
64
|
-
"@stamhoofd/email": "2.129.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.129.
|
|
66
|
-
"@stamhoofd/logging": "2.129.
|
|
67
|
-
"@stamhoofd/models": "2.129.
|
|
68
|
-
"@stamhoofd/object-differ": "2.129.
|
|
69
|
-
"@stamhoofd/queues": "2.129.
|
|
70
|
-
"@stamhoofd/sql": "2.129.
|
|
71
|
-
"@stamhoofd/structures": "2.129.
|
|
72
|
-
"@stamhoofd/types": "2.129.
|
|
73
|
-
"@stamhoofd/utility": "2.129.
|
|
60
|
+
"@stamhoofd/backend-env": "2.129.3",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.129.3",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.129.3",
|
|
63
|
+
"@stamhoofd/crons": "2.129.3",
|
|
64
|
+
"@stamhoofd/email": "2.129.3",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.129.3",
|
|
66
|
+
"@stamhoofd/logging": "2.129.3",
|
|
67
|
+
"@stamhoofd/models": "2.129.3",
|
|
68
|
+
"@stamhoofd/object-differ": "2.129.3",
|
|
69
|
+
"@stamhoofd/queues": "2.129.3",
|
|
70
|
+
"@stamhoofd/sql": "2.129.3",
|
|
71
|
+
"@stamhoofd/structures": "2.129.3",
|
|
72
|
+
"@stamhoofd/types": "2.129.3",
|
|
73
|
+
"@stamhoofd/utility": "2.129.3",
|
|
74
74
|
"archiver": "7.0.1",
|
|
75
75
|
"axios": "1.16.0",
|
|
76
76
|
"base-x": "3.0.11",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"stripe": "16.12.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stamhoofd/test-utils": "2.129.
|
|
94
|
+
"@stamhoofd/test-utils": "2.129.3",
|
|
95
95
|
"@types/cookie": "0.6.0",
|
|
96
96
|
"@types/luxon": "3.7.1",
|
|
97
97
|
"@types/mailparser": "3.4.6",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "982b7912b141d0149eb2ad89cd2120e073251e66"
|
|
111
111
|
}
|
|
@@ -44,6 +44,6 @@ async function createStripeInvoices() {
|
|
|
44
44
|
const invoicer = new StripeInvoicer({
|
|
45
45
|
secretKey: STAMHOOFD.STRIPE_SECRET_KEY,
|
|
46
46
|
});
|
|
47
|
-
await invoicer.generateAllInvoices(membershipOrganization, { forceLast: Formatter.dateIso(today) === '2026-07-
|
|
47
|
+
await invoicer.generateAllInvoices(membershipOrganization, { forceLast: Formatter.dateIso(today) === '2026-07-04' });
|
|
48
48
|
lastStripeInvoice = new Date();
|
|
49
49
|
}
|
|
@@ -44,8 +44,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
44
44
|
const organization = Context.organization;
|
|
45
45
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (organization && STAMHOOFD.userMode === 'organization' && await Context.auth.hasFullAccess(organization.id)) {
|
|
48
|
+
// Don't add any scoping. Organization id filter is added automatically.
|
|
49
|
+
} else if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
|
|
50
|
+
// First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
|
|
51
|
+
|
|
49
52
|
if (!organization) {
|
|
50
53
|
const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
|
|
51
54
|
if (tags !== 'all' && tags.length === 0) {
|
|
@@ -69,9 +72,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
69
72
|
},
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (organization) {
|
|
75
|
+
} else {
|
|
75
76
|
// Add organization scope filter
|
|
76
77
|
if (await Context.auth.canAccessAllMembers(organization.id, permissionLevel)) {
|
|
77
78
|
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
@@ -7,7 +7,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
7
7
|
import type { MemberWithUsersRegistrationsAndGroups, Registration } from '@stamhoofd/models';
|
|
8
8
|
import { AuditLog, BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, Organization, Platform, RateLimiter, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
9
9
|
import type { MemberResponsibility, MembersBlob } from '@stamhoofd/structures';
|
|
10
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, EmergencyContact, GroupType, MemberDetails, MemberWithRegistrationsBlob, Parent, PermissionLevel, PlatformMembershipTypeBehaviour, SetupStepType } from '@stamhoofd/structures';
|
|
10
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, BooleanStatus, EmergencyContact, GroupType, MemberDetails, MemberWithRegistrationsBlob, Parent, PermissionLevel, PlatformMembershipTypeBehaviour, SetupStepType } from '@stamhoofd/structures';
|
|
11
11
|
import { Formatter } from '@stamhoofd/utility';
|
|
12
12
|
|
|
13
13
|
import { Email } from '@stamhoofd/email';
|
|
@@ -1110,6 +1110,15 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
1110
1110
|
// Merge data
|
|
1111
1111
|
// NOTE: We use mergeTwoMembers instead of mergeMultipleMembers, because we should never safe 'member' , because that one does not exist in the database
|
|
1112
1112
|
await mergeTwoMembers(duplicate, member);
|
|
1113
|
+
|
|
1114
|
+
// Make sure the user keeps access to this member
|
|
1115
|
+
// const user = Context.user;
|
|
1116
|
+
|
|
1117
|
+
// const { userEmails, parentEmails } = MemberUserSyncer.getMemberAccessEmails(duplicate.details);
|
|
1118
|
+
// if (user && !duplicate.details.calculatedParentsHaveAccess && parentEmails.includes(user.email.toLocaleLowerCase()) && !userEmails.includes(user.email.toLocaleLowerCase())) {
|
|
1119
|
+
// console.log('Setting parents have access to true to maintain access for parent that used security code');
|
|
1120
|
+
// duplicate.details.parentsHaveAccess = BooleanStatus.create({ value: true });
|
|
1121
|
+
// }
|
|
1113
1122
|
return duplicate;
|
|
1114
1123
|
}
|
|
1115
1124
|
}
|
|
@@ -159,6 +159,10 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
159
159
|
.setMaxExecutionTime(15 * 1000)
|
|
160
160
|
.where('registeredAt', '!=', null);
|
|
161
161
|
|
|
162
|
+
if (organization && STAMHOOFD.userMode === 'organization') {
|
|
163
|
+
query.where('organizationId', organization.id);
|
|
164
|
+
}
|
|
165
|
+
|
|
162
166
|
if (scopeFilter) {
|
|
163
167
|
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
164
168
|
}
|
package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { PaymentService } from '../../../../services/PaymentService.js';
|
|
|
12
12
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
13
13
|
import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
|
|
14
14
|
import { BalanceItemService } from '../../../../services/BalanceItemService.js';
|
|
15
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
15
16
|
|
|
16
17
|
type Params = Record<string, never>;
|
|
17
18
|
type Query = CountFilteredRequest;
|
|
@@ -37,98 +38,116 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
|
|
|
37
38
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
39
|
const sellingOrganization = await Context.setOrganizationScope();
|
|
39
40
|
const { user } = await Context.authenticate();
|
|
40
|
-
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
message: 'Cannot charge a cached balance from a different organization',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
// todo
|
|
50
|
-
const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
|
|
42
|
+
if (!await Context.auth.canManageFinances(sellingOrganization.id)) {
|
|
43
|
+
throw Context.auth.error();
|
|
44
|
+
}
|
|
45
|
+
const queueId = 'charge-receivable-balances/' + sellingOrganization.id;
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
if (QueueHandler.isRunning(queueId)) {
|
|
48
|
+
throw new SimpleError({
|
|
49
|
+
code: 'charge_pending',
|
|
50
|
+
message: 'Already pending charge',
|
|
51
|
+
human: $t('Er is al een aanrekening bezig, even geduld voor je een tweede aanrekening aanrekend.'),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const i of items) {
|
|
60
|
-
map.set(i, i.priceOpen);
|
|
61
|
-
total += i.priceOpen;
|
|
62
|
-
}
|
|
63
|
-
if (total <= 0) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
55
|
+
await QueueHandler.schedule(queueId, async () => {
|
|
56
|
+
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
for await (const cachedBalance of query.all()) {
|
|
59
|
+
if (cachedBalance.organizationId !== sellingOrganization.id) {
|
|
60
|
+
throw new SimpleError({
|
|
61
|
+
code: 'wrong_organization',
|
|
62
|
+
message: 'Cannot charge a cached balance from a different organization',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// todo
|
|
66
|
+
const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
|
|
67
|
+
|
|
68
|
+
if (items.length === 0) {
|
|
69
|
+
console.log('Nothing to charge for', cachedBalance.id);
|
|
73
70
|
continue;
|
|
74
71
|
}
|
|
75
|
-
payingOrganization = p;
|
|
76
|
-
}
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
const map: Map<BalanceItem, number> = new Map();
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const i of items) {
|
|
76
|
+
map.set(i, i.priceOpen);
|
|
77
|
+
total += i.priceOpen;
|
|
78
|
+
}
|
|
79
|
+
if (total <= 0) {
|
|
82
80
|
continue;
|
|
83
81
|
}
|
|
84
|
-
customerUser = p;
|
|
85
|
-
}
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
let payingOrganization: Organization | null = null;
|
|
84
|
+
let customerUser: User | null = null;
|
|
85
|
+
if (cachedBalance.objectType === ReceivableBalanceType.organization) {
|
|
86
|
+
const p = await Organization.getByID(cachedBalance.objectId);
|
|
87
|
+
if (!p || !(await Context.auth.hasFullAccess(p))) {
|
|
88
|
+
console.error('Unexpected missing paying organization id', cachedBalance);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
payingOrganization = p;
|
|
92
|
+
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
|
|
95
|
+
const p = await User.getByID(cachedBalance.objectId);
|
|
96
|
+
if (!p || !(Context.auth.checkScope(p.organizationId))) {
|
|
97
|
+
console.error('Unexpected missing customer user id', cachedBalance);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
customerUser = p;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mandates = await PaymentMandateService.getMandates({
|
|
104
|
+
sellingOrganization,
|
|
105
|
+
user: customerUser,
|
|
106
|
+
payingOrganization,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mandate = mandates.find(m => m.isDefault);
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
if (!mandate) {
|
|
96
112
|
// Not possible
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
console.error('No mandates found for', cachedBalance.id);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
100
116
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
customerUser = customerUser ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null);
|
|
118
|
+
const customer = PaymentCustomer.create({
|
|
119
|
+
firstName: customerUser?.firstName,
|
|
120
|
+
lastName: customerUser?.lastName,
|
|
121
|
+
email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
|
|
122
|
+
company: payingOrganization?.defaultCompanies[0],
|
|
123
|
+
});
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
await PaymentService.createPayment({
|
|
126
|
+
balanceItems: map,
|
|
127
|
+
checkout: {
|
|
128
|
+
paymentMethod: PaymentMethod.Unknown,
|
|
129
|
+
totalPrice: null,
|
|
130
|
+
customer,
|
|
131
|
+
cancelUrl: null,
|
|
132
|
+
redirectUrl: null,
|
|
133
|
+
},
|
|
134
|
+
user: customerUser,
|
|
135
|
+
adminUserId: user.id,
|
|
136
|
+
organization: sellingOrganization,
|
|
137
|
+
payingOrganization,
|
|
138
|
+
serviceFeeType: 'system',
|
|
139
|
+
createMandate: null,
|
|
140
|
+
useMandate: mandate,
|
|
141
|
+
paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
|
|
142
|
+
privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Clear cache while looping, to update the table while we are charging
|
|
146
|
+
// Make sure the user can refresh data and see the updated cached amounts
|
|
147
|
+
await BalanceItemService.flushCaches(sellingOrganization.id);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
129
150
|
|
|
130
|
-
// Make sure the user can refresh data and see the updated cached amounts
|
|
131
|
-
await BalanceItemService.flushCaches(sellingOrganization.id);
|
|
132
151
|
return new Response(undefined, 201);
|
|
133
152
|
}
|
|
134
153
|
}
|
|
@@ -486,9 +486,21 @@ export class AdminPermissionChecker {
|
|
|
486
486
|
* Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
|
|
487
487
|
*/
|
|
488
488
|
async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember: boolean | MemberWithUsersRegistrationsAndGroups = true) {
|
|
489
|
+
const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
|
|
490
|
+
|
|
491
|
+
if (!organizationPermissions) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
|
|
496
|
+
// Only full permissions; because non-full doesn't have access to other periods
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Non full admin logic
|
|
489
501
|
if (registration.deactivatedAt || !registration.registeredAt) {
|
|
490
502
|
if (!checkMember) {
|
|
491
|
-
// We can't grant access to a member because of a deactivated registration
|
|
503
|
+
// We can't grant access to a member because of a deactivated registration - unless full permission
|
|
492
504
|
return false;
|
|
493
505
|
}
|
|
494
506
|
|
|
@@ -499,16 +511,6 @@ export class AdminPermissionChecker {
|
|
|
499
511
|
}
|
|
500
512
|
}
|
|
501
513
|
|
|
502
|
-
const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
|
|
503
|
-
|
|
504
|
-
if (!organizationPermissions) {
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
|
|
509
|
-
// Only full permissions; because non-full doesn't have access to other periods
|
|
510
|
-
return true;
|
|
511
|
-
}
|
|
512
514
|
const organization = await this.getOrganization(registration.organizationId);
|
|
513
515
|
|
|
514
516
|
if (registration.periodId !== organization.periodId) {
|
|
@@ -778,5 +778,139 @@ describe('member merge', () => {
|
|
|
778
778
|
expect(m1.details.email).toBe('example@example.com');
|
|
779
779
|
expect(m1.details.alternativeEmails).toEqual([]);
|
|
780
780
|
});
|
|
781
|
+
|
|
782
|
+
describe('Parents have access get merged', () => {
|
|
783
|
+
test('False other', () => {
|
|
784
|
+
const m1 = new Member();
|
|
785
|
+
const m2 = new Member();
|
|
786
|
+
|
|
787
|
+
const base = MemberDetails.create({
|
|
788
|
+
email: 'example@example.com',
|
|
789
|
+
firstName: 'John',
|
|
790
|
+
lastName: 'Doe',
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const other = MemberDetails.create({
|
|
794
|
+
email: 'example@example.com',
|
|
795
|
+
firstName: 'John',
|
|
796
|
+
lastName: 'Doe',
|
|
797
|
+
parentsHaveAccess: BooleanStatus.create({ value: false }),
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
m1.details = base.clone();
|
|
801
|
+
m2.details = other.clone();
|
|
802
|
+
|
|
803
|
+
mergeMemberDetails(m1, m2);
|
|
804
|
+
|
|
805
|
+
expect(m1.details.email).toBe('example@example.com');
|
|
806
|
+
expect(m1.details.parentsHaveAccess?.value).toEqual(false);
|
|
807
|
+
});
|
|
808
|
+
test('True other', () => {
|
|
809
|
+
const m1 = new Member();
|
|
810
|
+
const m2 = new Member();
|
|
811
|
+
|
|
812
|
+
const base = MemberDetails.create({
|
|
813
|
+
email: 'example@example.com',
|
|
814
|
+
firstName: 'John',
|
|
815
|
+
lastName: 'Doe',
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const other = MemberDetails.create({
|
|
819
|
+
email: 'example@example.com',
|
|
820
|
+
firstName: 'John',
|
|
821
|
+
lastName: 'Doe',
|
|
822
|
+
parentsHaveAccess: BooleanStatus.create({ value: true }),
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
m1.details = base.clone();
|
|
826
|
+
m2.details = other.clone();
|
|
827
|
+
|
|
828
|
+
mergeMemberDetails(m1, m2);
|
|
829
|
+
|
|
830
|
+
expect(m1.details.email).toBe('example@example.com');
|
|
831
|
+
expect(m1.details.parentsHaveAccess?.value).toEqual(true);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test('Both different other prefer newest other', () => {
|
|
835
|
+
const m1 = new Member();
|
|
836
|
+
const m2 = new Member();
|
|
837
|
+
|
|
838
|
+
const base = MemberDetails.create({
|
|
839
|
+
email: 'example@example.com',
|
|
840
|
+
firstName: 'John',
|
|
841
|
+
lastName: 'Doe',
|
|
842
|
+
parentsHaveAccess: BooleanStatus.create({ value: true, date: new Date(0) }),
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const other = MemberDetails.create({
|
|
846
|
+
email: 'example@example.com',
|
|
847
|
+
firstName: 'John',
|
|
848
|
+
lastName: 'Doe',
|
|
849
|
+
parentsHaveAccess: BooleanStatus.create({ value: false, date: new Date(1) }),
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
m1.details = base.clone();
|
|
853
|
+
m2.details = other.clone();
|
|
854
|
+
|
|
855
|
+
mergeMemberDetails(m1, m2);
|
|
856
|
+
|
|
857
|
+
expect(m1.details.email).toBe('example@example.com');
|
|
858
|
+
expect(m1.details.parentsHaveAccess?.value).toEqual(false);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
test('Both different other prefer newest base', () => {
|
|
862
|
+
const m1 = new Member();
|
|
863
|
+
const m2 = new Member();
|
|
864
|
+
|
|
865
|
+
const base = MemberDetails.create({
|
|
866
|
+
email: 'example@example.com',
|
|
867
|
+
firstName: 'John',
|
|
868
|
+
lastName: 'Doe',
|
|
869
|
+
parentsHaveAccess: BooleanStatus.create({ value: true, date: new Date(1) }),
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const other = MemberDetails.create({
|
|
873
|
+
email: 'example@example.com',
|
|
874
|
+
firstName: 'John',
|
|
875
|
+
lastName: 'Doe',
|
|
876
|
+
parentsHaveAccess: BooleanStatus.create({ value: false, date: new Date(0) }),
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
m1.details = base.clone();
|
|
880
|
+
m2.details = other.clone();
|
|
881
|
+
|
|
882
|
+
mergeMemberDetails(m1, m2);
|
|
883
|
+
|
|
884
|
+
expect(m1.details.email).toBe('example@example.com');
|
|
885
|
+
expect(m1.details.parentsHaveAccess?.value).toEqual(true);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test('Both different other prefer newest base 2', () => {
|
|
889
|
+
const m1 = new Member();
|
|
890
|
+
const m2 = new Member();
|
|
891
|
+
|
|
892
|
+
const base = MemberDetails.create({
|
|
893
|
+
email: 'example@example.com',
|
|
894
|
+
firstName: 'John',
|
|
895
|
+
lastName: 'Doe',
|
|
896
|
+
parentsHaveAccess: BooleanStatus.create({ value: false, date: new Date(1) }),
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const other = MemberDetails.create({
|
|
900
|
+
email: 'example@example.com',
|
|
901
|
+
firstName: 'John',
|
|
902
|
+
lastName: 'Doe',
|
|
903
|
+
parentsHaveAccess: BooleanStatus.create({ value: true, date: new Date(0) }),
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
m1.details = base.clone();
|
|
907
|
+
m2.details = other.clone();
|
|
908
|
+
|
|
909
|
+
mergeMemberDetails(m1, m2);
|
|
910
|
+
|
|
911
|
+
expect(m1.details.email).toBe('example@example.com');
|
|
912
|
+
expect(m1.details.parentsHaveAccess?.value).toEqual(false);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
781
915
|
});
|
|
782
916
|
});
|
|
@@ -306,6 +306,12 @@ export function mergeMemberDetails(base: Member, other: Member): void {
|
|
|
306
306
|
'dataPermissions',
|
|
307
307
|
);
|
|
308
308
|
|
|
309
|
+
mergeBooleanStatusIfBaseNotSet(
|
|
310
|
+
baseDetails,
|
|
311
|
+
otherDetails,
|
|
312
|
+
'parentsHaveAccess',
|
|
313
|
+
);
|
|
314
|
+
|
|
309
315
|
// address
|
|
310
316
|
mergeAddress(baseDetails, otherDetails, baseDetails);
|
|
311
317
|
|
|
@@ -693,7 +693,7 @@ describe('Helpers.MemberUserSyncer', () => {
|
|
|
693
693
|
]);
|
|
694
694
|
});
|
|
695
695
|
|
|
696
|
-
test('Parents are removed if member turns 18', async () => {
|
|
696
|
+
test.skip('Parents are removed if member turns 18', async () => {
|
|
697
697
|
const member = await new MemberFactory({
|
|
698
698
|
details: MemberDetails.create({
|
|
699
699
|
firstName: 'John',
|
|
@@ -132,7 +132,8 @@ export class MemberUserSyncerStatic {
|
|
|
132
132
|
// This makes sure we don't inherit permissions and aren't counted as 'being' the member
|
|
133
133
|
await this.linkUser(user.email, member, true);
|
|
134
134
|
}
|
|
135
|
-
} else if (
|
|
135
|
+
} else if (member.details.parentsHaveAccess?.value === false && parentEmails.includes(user.email.toLocaleLowerCase()) && !userEmails.includes(user.email.toLocaleLowerCase())) {
|
|
136
|
+
// Only remove if manually set to false for now
|
|
136
137
|
await this.unlinkUser(user, member);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
@@ -251,7 +251,7 @@ export class MollieService {
|
|
|
251
251
|
details: PaymentMandateDetails.create({
|
|
252
252
|
name: ('consumerName' in details ? details.consumerName : details.cardHolder) ?? undefined,
|
|
253
253
|
cardNumber: 'cardNumber' in details ? details.cardNumber : null,
|
|
254
|
-
iban: 'consumerAccount' in details ? details.consumerAccount : null,
|
|
254
|
+
iban: 'consumerAccount' in details ? Formatter.iban(details.consumerAccount) : null,
|
|
255
255
|
bic: ('consumerBic' in details ? details.consumerBic : undefined),
|
|
256
256
|
expiryDate: ('cardExpiryDate' in details ? DateTime.fromISO(details.cardExpiryDate, { zone: Formatter.timezone }).toJSDate() : null), // todo: parse date correctly in Brussels timezone!
|
|
257
257
|
brand: ('cardLabel' in details ? details.cardLabel : null),
|
|
@@ -61,6 +61,10 @@ export class SSOService {
|
|
|
61
61
|
this.provider = data.provider;
|
|
62
62
|
this.platform = data.platform;
|
|
63
63
|
this.organization = data.organization ?? null;
|
|
64
|
+
|
|
65
|
+
if (STAMHOOFD.userMode === 'platform' && this.organization) {
|
|
66
|
+
throw new Error('SSO provided by organization disabled in platform mode');
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
static async clearExpiredTokensOrFromUser(userId: string | null = null) {
|
|
@@ -371,7 +375,7 @@ export class SSOService {
|
|
|
371
375
|
if (token) {
|
|
372
376
|
user = await User.getByID(token.userId);
|
|
373
377
|
|
|
374
|
-
if (!user) {
|
|
378
|
+
if (!user || user.organizationId !== (this.organization?.id ?? null)) {
|
|
375
379
|
throw new SimpleError({
|
|
376
380
|
code: 'invalid_user',
|
|
377
381
|
message: 'User not found',
|