@stamhoofd/backend 2.79.8 → 2.80.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/package.json +11 -11
- package/src/audit-logs/ModelLogger.ts +2 -2
- package/src/crons/amazon-ses.ts +215 -227
- package/src/crons/clearExcelCache.test.ts +1 -1
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +105 -0
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +6 -10
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +997 -0
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +19 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +7 -7
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +10 -3
- package/src/helpers/AdminPermissionChecker.ts +3 -3
- package/src/helpers/ForwardHandler.ts +3 -2
- package/src/helpers/MemberCharger.ts +39 -0
- package/src/helpers/OrganizationCharger.ts +9 -20
- package/src/services/EventNotificationService.ts +3 -0
- package/tests/e2e/charge-members.test.ts +429 -0
- package/tests/jest.setup.ts +7 -1
- package/tests/toMatchMap.ts +68 -0
- package/src/services/diff.ts +0 -514
|
@@ -81,7 +81,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
|
|
|
81
81
|
if (index === 0) {
|
|
82
82
|
notification.startDate = model.startDate;
|
|
83
83
|
notification.endDate = model.endDate;
|
|
84
|
-
const period = await RegistrationPeriod.getByDate(
|
|
84
|
+
const period = await RegistrationPeriod.getByDate(model.startDate);
|
|
85
85
|
|
|
86
86
|
if (!period) {
|
|
87
87
|
throw new SimpleError({
|
|
@@ -141,7 +141,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
|
|
|
141
141
|
if (
|
|
142
142
|
notification.status === EventNotificationStatus.Pending
|
|
143
143
|
|| notification.status === EventNotificationStatus.Accepted
|
|
144
|
-
|| (patch.status && patch.status !== EventNotificationStatus.Pending)
|
|
144
|
+
|| (patch.status && (patch.status !== EventNotificationStatus.Pending || (notification.status !== EventNotificationStatus.Draft && notification.status !== EventNotificationStatus.Rejected && notification.status !== EventNotificationStatus.PartiallyAccepted)))
|
|
145
145
|
|| patch.feedbackText !== undefined
|
|
146
146
|
) {
|
|
147
147
|
requiredPermissionLevel = PermissionLevel.Full;
|
|
@@ -187,6 +187,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
|
|
|
187
187
|
// Only allowed if complete
|
|
188
188
|
await this.validateAnswers(notification);
|
|
189
189
|
}
|
|
190
|
+
const previousStatus = notification.status;
|
|
190
191
|
notification.status = patch.status; // checks already happened
|
|
191
192
|
if (patch.status === EventNotificationStatus.Pending) {
|
|
192
193
|
notification.submittedBy = user.id;
|
|
@@ -198,15 +199,30 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
|
|
|
198
199
|
await EventNotificationService.sendReviewerEmail(EmailTemplateType.EventNotificationSubmittedReviewer, notification);
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
if (patch.status === EventNotificationStatus.Accepted) {
|
|
202
|
+
if ((patch.status === EventNotificationStatus.Accepted) && previousStatus !== EventNotificationStatus.Accepted) {
|
|
203
|
+
// Make sure the accepted record answers stay in sync
|
|
204
|
+
notification.acceptedRecordAnswers = notification.recordAnswers;
|
|
205
|
+
|
|
202
206
|
await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationAccepted, notification);
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
if ((patch.status === EventNotificationStatus.PartiallyAccepted) && previousStatus !== EventNotificationStatus.Accepted && previousStatus !== EventNotificationStatus.PartiallyAccepted) {
|
|
210
|
+
// Make sure the accepted record answers stay in sync
|
|
211
|
+
notification.acceptedRecordAnswers = notification.recordAnswers;
|
|
212
|
+
|
|
213
|
+
await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationPartiallyAccepted, notification);
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
if (patch.status === EventNotificationStatus.Rejected) {
|
|
206
217
|
await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationRejected, notification);
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
220
|
|
|
221
|
+
if (notification.status === EventNotificationStatus.Accepted) {
|
|
222
|
+
// Make sure the accepted record answers stay in sync (only for full accepted, since these cannot be changed)
|
|
223
|
+
notification.acceptedRecordAnswers = notification.recordAnswers;
|
|
224
|
+
}
|
|
225
|
+
|
|
210
226
|
await notification.save();
|
|
211
227
|
notifications.push(notification);
|
|
212
228
|
}
|
|
@@ -37,12 +37,12 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
37
37
|
return [false];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
40
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest, permissionLevel: PermissionLevel = PermissionLevel.Read) {
|
|
41
41
|
const organization = Context.organization;
|
|
42
42
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
43
43
|
|
|
44
44
|
if (!organization && !Context.auth.canAccessAllPlatformMembers()) {
|
|
45
|
-
const tags = Context.auth.getPlatformAccessibleOrganizationTags(
|
|
45
|
+
const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
|
|
46
46
|
if (tags !== 'all' && tags.length === 0) {
|
|
47
47
|
throw Context.auth.error();
|
|
48
48
|
}
|
|
@@ -68,14 +68,14 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
68
68
|
|
|
69
69
|
if (organization && !Context.auth.canAccessAllPlatformMembers()) {
|
|
70
70
|
// Add organization scope filter
|
|
71
|
-
const groups = await Context.auth.getAccessibleGroups(organization.id);
|
|
71
|
+
const groups = await Context.auth.getAccessibleGroups(organization.id, permissionLevel);
|
|
72
72
|
|
|
73
73
|
if (groups.length === 0) {
|
|
74
74
|
throw Context.auth.error();
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
if (groups === 'all') {
|
|
78
|
-
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
78
|
+
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
79
79
|
// Can access full history for now
|
|
80
80
|
scopeFilter = {
|
|
81
81
|
registrations: {
|
|
@@ -233,8 +233,8 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
233
233
|
return query;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
237
|
-
const query = await GetMembersEndpoint.buildQuery(requestQuery);
|
|
236
|
+
static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
|
|
237
|
+
const query = await GetMembersEndpoint.buildQuery(requestQuery, permissionLevel);
|
|
238
238
|
let data: SQLResultNamespacedRow[];
|
|
239
239
|
|
|
240
240
|
try {
|
|
@@ -263,7 +263,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
263
263
|
const members = memberIds.map(id => _members.find(m => m.id === id)!);
|
|
264
264
|
|
|
265
265
|
for (const member of members) {
|
|
266
|
-
if (!await Context.auth.canAccessMember(member,
|
|
266
|
+
if (!await Context.auth.canAccessMember(member, permissionLevel)) {
|
|
267
267
|
throw Context.auth.error();
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -133,9 +133,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
133
133
|
value: STAMHOOFD.domains.registrationCname + '.',
|
|
134
134
|
}));
|
|
135
135
|
}
|
|
136
|
-
}
|
|
137
136
|
|
|
138
|
-
if (request.body.mailDomain !== null) {
|
|
139
137
|
let priv: string;
|
|
140
138
|
let pub: string;
|
|
141
139
|
|
|
@@ -158,9 +156,18 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
158
156
|
// DKIM records
|
|
159
157
|
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
160
158
|
type: DNSRecordType.TXT,
|
|
161
|
-
name: '
|
|
159
|
+
name: Formatter.slug(STAMHOOFD.platformName) + '._domainkey.' + organization.privateMeta.pendingMailDomain + '.',
|
|
162
160
|
value: 'v=DKIM1; k=rsa; p=' + pub + '',
|
|
163
161
|
}));
|
|
162
|
+
|
|
163
|
+
// DMARC records
|
|
164
|
+
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
165
|
+
type: DNSRecordType.TXT,
|
|
166
|
+
name: '_dmarc.' + organization.privateMeta.pendingMailDomain + '.',
|
|
167
|
+
value: 'v=DMARC1; p=quarantine; pct=100; sp=quarantine; aspf=r; adkim=r;',
|
|
168
|
+
description: 'Opgelet met het instellen van deze DMARC-record voor je domeinnaam. Mogelijks bestaat er al een record met deze naam, voeg deze dan zeker niet dubbel toe en behoud best de huidige waarde (wel zou aspf en adkim op r moeten staan). De waarde die we voorstellen zorgt voor een sterke beveiliging, maar kan mogelijks problemen veroorzaken als je andere diensten gebruikt die op een onveilige manier emails versturen (zonder DKIM of SPF).',
|
|
169
|
+
optional: true,
|
|
170
|
+
}));
|
|
164
171
|
}
|
|
165
172
|
else {
|
|
166
173
|
if (oldMailDomain) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
|
-
import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct,
|
|
4
|
+
import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
-
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
|
|
7
6
|
import { MemberRecordStore } from '../services/MemberRecordStore';
|
|
7
|
+
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* One class with all the responsabilities of checking permissions to each resource in the system by a given user, possibly in an organization context.
|
|
@@ -1091,7 +1091,7 @@ export class AdminPermissionChecker {
|
|
|
1091
1091
|
}
|
|
1092
1092
|
|
|
1093
1093
|
async getAccessibleGroups(organizationId: string, level: PermissionLevel = PermissionLevel.Read): Promise<string[] | 'all'> {
|
|
1094
|
-
if (await this.hasFullAccess(organizationId)) {
|
|
1094
|
+
if (await this.hasFullAccess(organizationId, level)) {
|
|
1095
1095
|
return 'all';
|
|
1096
1096
|
}
|
|
1097
1097
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
2
|
|
|
3
3
|
import { Email, EmailAddress, EmailInterface, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
4
|
-
import { Organization } from '@stamhoofd/models';
|
|
4
|
+
import { Organization, Platform } from '@stamhoofd/models';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { simpleParser } from 'mailparser';
|
|
7
7
|
|
|
@@ -66,7 +66,8 @@ export class ForwardHandler {
|
|
|
66
66
|
// Send a new e-mail
|
|
67
67
|
let defaultEmail: EmailInterfaceRecipient[] = [Email.getWebmasterToEmail()];
|
|
68
68
|
let organizationEmails: EmailInterfaceRecipient[] = [];
|
|
69
|
-
const
|
|
69
|
+
const platform = await Platform.getShared();
|
|
70
|
+
const extraDescription = 'Dit bericht werd verstuurd naar ' + email + ', en werd automatisch doorgestuurd naar alle beheerders. Stel in ' + platform.config.name + ' de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd.';
|
|
70
71
|
|
|
71
72
|
function doBounce() {
|
|
72
73
|
if (!from) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItemType, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
3
|
+
import { BalanceItemService } from '../services/BalanceItemService';
|
|
4
|
+
|
|
5
|
+
export class MemberCharger {
|
|
6
|
+
static async chargeMany({ chargingOrganizationId, membersToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; membersToCharge: MemberWithRegistrationsBlob[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
|
|
7
|
+
const balanceItems = membersToCharge.map(memberBeingCharged => MemberCharger.createBalanceItem({
|
|
8
|
+
price,
|
|
9
|
+
amount,
|
|
10
|
+
description,
|
|
11
|
+
chargingOrganizationId,
|
|
12
|
+
memberBeingCharged,
|
|
13
|
+
dueAt,
|
|
14
|
+
createdAt,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
|
|
18
|
+
await BalanceItem.updateOutstanding(balanceItems);
|
|
19
|
+
|
|
20
|
+
// Reallocate
|
|
21
|
+
await BalanceItemService.reallocate(balanceItems, chargingOrganizationId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private static createBalanceItem({ price, amount, description, chargingOrganizationId, memberBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; memberBeingCharged: MemberWithRegistrationsBlob; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
|
|
25
|
+
const balanceItem = new BalanceItem();
|
|
26
|
+
balanceItem.unitPrice = price;
|
|
27
|
+
balanceItem.amount = amount ?? 1;
|
|
28
|
+
balanceItem.description = description;
|
|
29
|
+
balanceItem.type = BalanceItemType.Other;
|
|
30
|
+
balanceItem.memberId = memberBeingCharged.id;
|
|
31
|
+
balanceItem.organizationId = chargingOrganizationId;
|
|
32
|
+
balanceItem.dueAt = dueAt;
|
|
33
|
+
if (createdAt !== null) {
|
|
34
|
+
balanceItem.createdAt = createdAt;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return balanceItem;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,32 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BalanceItem, Platform } from '@stamhoofd/models';
|
|
1
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
3
2
|
import { BalanceItemType, Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
4
3
|
import { BalanceItemService } from '../services/BalanceItemService';
|
|
5
4
|
|
|
6
5
|
export class OrganizationCharger {
|
|
7
|
-
static async
|
|
8
|
-
const platform = await Platform.getShared();
|
|
9
|
-
|
|
10
|
-
const chargeVia = platform.membershipOrganizationId;
|
|
11
|
-
|
|
12
|
-
if (!chargeVia) {
|
|
13
|
-
throw new SimpleError({
|
|
14
|
-
code: 'missing_membership_organization',
|
|
15
|
-
message: 'Missing membershipOrganizationId',
|
|
16
|
-
human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd',
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
await OrganizationCharger.chargeMany({ chargingOrganizationId: chargeVia, ...args });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static async chargeMany({ chargingOrganizationId, organizationsToCharge, price, amount, description }: { chargingOrganizationId: string; organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
|
|
6
|
+
static async chargeMany({ chargingOrganizationId, organizationsToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
|
|
24
7
|
const balanceItems = organizationsToCharge.map(organizationBeingCharged => OrganizationCharger.createBalanceItem({
|
|
25
8
|
price,
|
|
26
9
|
amount,
|
|
27
10
|
description,
|
|
28
11
|
chargingOrganizationId,
|
|
29
12
|
organizationBeingCharged,
|
|
13
|
+
dueAt,
|
|
14
|
+
createdAt,
|
|
30
15
|
}));
|
|
31
16
|
|
|
32
17
|
await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
|
|
@@ -36,7 +21,7 @@ export class OrganizationCharger {
|
|
|
36
21
|
await BalanceItemService.reallocate(balanceItems, chargingOrganizationId);
|
|
37
22
|
}
|
|
38
23
|
|
|
39
|
-
private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct }): BalanceItem {
|
|
24
|
+
private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
|
|
40
25
|
const balanceItem = new BalanceItem();
|
|
41
26
|
balanceItem.unitPrice = price;
|
|
42
27
|
balanceItem.amount = amount ?? 1;
|
|
@@ -44,6 +29,10 @@ export class OrganizationCharger {
|
|
|
44
29
|
balanceItem.type = BalanceItemType.Other;
|
|
45
30
|
balanceItem.payingOrganizationId = organizationBeingCharged.id;
|
|
46
31
|
balanceItem.organizationId = chargingOrganizationId;
|
|
32
|
+
balanceItem.dueAt = dueAt;
|
|
33
|
+
if (createdAt !== null) {
|
|
34
|
+
balanceItem.createdAt = createdAt;
|
|
35
|
+
}
|
|
47
36
|
|
|
48
37
|
return balanceItem;
|
|
49
38
|
}
|
|
@@ -171,6 +171,7 @@ export class EventNotificationService {
|
|
|
171
171
|
|
|
172
172
|
static async sendSubmitterEmail(type: EmailTemplateType, notification: EventNotification) {
|
|
173
173
|
if (notification.endDate < new Date()) {
|
|
174
|
+
console.log('Skipped submitter email because it is in the past');
|
|
174
175
|
// Ignore
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
@@ -186,6 +187,8 @@ export class EventNotificationService {
|
|
|
186
187
|
|
|
187
188
|
static async sendReviewerEmail(type: EmailTemplateType, notification: EventNotification) {
|
|
188
189
|
if (notification.endDate < new Date()) {
|
|
190
|
+
console.log('Skipped reviewer email because it is in the past');
|
|
191
|
+
|
|
189
192
|
// Ignore
|
|
190
193
|
return;
|
|
191
194
|
}
|