@stamhoofd/backend 2.73.1 → 2.73.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.73.
|
|
3
|
+
"version": "2.73.3",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bwip-js/node": "^4.5.1",
|
|
35
35
|
"@mollie/api-client": "3.7.0",
|
|
36
|
-
"@simonbackx/simple-database": "1.
|
|
36
|
+
"@simonbackx/simple-database": "1.28.0",
|
|
37
37
|
"@simonbackx/simple-encoding": "2.19.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.73.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.73.
|
|
42
|
-
"@stamhoofd/email": "2.73.
|
|
43
|
-
"@stamhoofd/models": "2.73.
|
|
44
|
-
"@stamhoofd/queues": "2.73.
|
|
45
|
-
"@stamhoofd/sql": "2.73.
|
|
46
|
-
"@stamhoofd/structures": "2.73.
|
|
47
|
-
"@stamhoofd/utility": "2.73.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.73.3",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.73.3",
|
|
42
|
+
"@stamhoofd/email": "2.73.3",
|
|
43
|
+
"@stamhoofd/models": "2.73.3",
|
|
44
|
+
"@stamhoofd/queues": "2.73.3",
|
|
45
|
+
"@stamhoofd/sql": "2.73.3",
|
|
46
|
+
"@stamhoofd/structures": "2.73.3",
|
|
47
|
+
"@stamhoofd/utility": "2.73.3",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "00bbe8abcaac99c614bb352efa0fca54241f77d7"
|
|
68
68
|
}
|
|
@@ -14,17 +14,14 @@ const bootAt = new Date();
|
|
|
14
14
|
async function balanceEmails() {
|
|
15
15
|
// Do not run within 30 minutes after boot to avoid creating multiple email models for emails that failed to send
|
|
16
16
|
if (bootAt.getTime() > new Date().getTime() - 1000 * 60 * 30 && STAMHOOFD.environment !== 'development') {
|
|
17
|
-
console.log('Boot time is too recent, skipping.');
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
if (lastFullRun.getTime() > new Date().getTime() - 1000 * 60 * 60 * 12
|
|
22
|
-
console.log('Already ran today, skipping.');
|
|
20
|
+
if (lastFullRun.getTime() > new Date().getTime() - 1000 * 60 * 60 * 12) {
|
|
23
21
|
return;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
if ((new Date().getHours() > 10 || new Date().getHours() < 6) && STAMHOOFD.environment !== 'development') {
|
|
27
|
-
console.log('Not between 6 and 10 AM, skipping.');
|
|
28
25
|
return;
|
|
29
26
|
}
|
|
30
27
|
|
|
@@ -46,12 +43,13 @@ async function balanceEmails() {
|
|
|
46
43
|
continue;
|
|
47
44
|
}
|
|
48
45
|
|
|
46
|
+
const enabledForOrganizations = organization.privateMeta.featureFlags.includes('organization-receivable-balances') && Object.keys(organization.privateMeta.balanceNotificationSettings.organizationContactsFilter).includes('meta');
|
|
47
|
+
|
|
49
48
|
const selectedEmailAddress = organization.privateMeta.balanceNotificationSettings.emailId ? organization.privateMeta.emails.find(e => e.id === organization.privateMeta.balanceNotificationSettings.emailId) : null;
|
|
50
49
|
const emailAddress = selectedEmailAddress ?? organization.privateMeta.emails.find(e => e.default) ?? null;
|
|
51
50
|
|
|
52
51
|
if (!emailAddress) {
|
|
53
52
|
// No emailadres set
|
|
54
|
-
console.warn('Skipped organization', organization.id, 'because no email address is set');
|
|
55
53
|
continue;
|
|
56
54
|
}
|
|
57
55
|
|
|
@@ -67,43 +65,31 @@ async function balanceEmails() {
|
|
|
67
65
|
reminderEmailCount: 0,
|
|
68
66
|
},
|
|
69
67
|
});
|
|
70
|
-
await sendTemplate({
|
|
71
|
-
objectType: ReceivableBalanceType.organization,
|
|
72
|
-
organization,
|
|
73
|
-
emailAddress,
|
|
74
|
-
systemUser,
|
|
75
|
-
templateType: EmailTemplateType.OrganizationBalanceIncreaseNotification,
|
|
76
|
-
filter: {
|
|
77
|
-
reminderAmountIncreased: true,
|
|
78
|
-
reminderEmailCount: 0,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
const maximumEmailCount = organization.privateMeta.balanceNotificationSettings.maximumReminderEmails;
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
if (maximumEmailCount > 1) {
|
|
69
|
+
if (enabledForOrganizations) {
|
|
85
70
|
await sendTemplate({
|
|
86
|
-
objectType: ReceivableBalanceType.
|
|
71
|
+
objectType: ReceivableBalanceType.organization,
|
|
87
72
|
organization,
|
|
88
73
|
emailAddress,
|
|
89
74
|
systemUser,
|
|
90
|
-
templateType: EmailTemplateType.
|
|
75
|
+
templateType: EmailTemplateType.OrganizationBalanceIncreaseNotification,
|
|
91
76
|
filter: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
reminderEmailCount: { $gt: 0 },
|
|
95
|
-
}, {
|
|
96
|
-
reminderEmailCount: { $lt: maximumEmailCount },
|
|
97
|
-
},
|
|
98
|
-
],
|
|
77
|
+
reminderAmountIncreased: true,
|
|
78
|
+
reminderEmailCount: 0,
|
|
99
79
|
},
|
|
80
|
+
subfilter: organization.privateMeta.balanceNotificationSettings.organizationContactsFilter,
|
|
100
81
|
});
|
|
82
|
+
}
|
|
83
|
+
const maximumEmailCount = organization.privateMeta.balanceNotificationSettings.maximumReminderEmails;
|
|
84
|
+
|
|
85
|
+
// Reminder emails
|
|
86
|
+
if (maximumEmailCount > 1) {
|
|
101
87
|
await sendTemplate({
|
|
102
|
-
objectType: ReceivableBalanceType.
|
|
88
|
+
objectType: ReceivableBalanceType.user,
|
|
103
89
|
organization,
|
|
104
90
|
emailAddress,
|
|
105
91
|
systemUser,
|
|
106
|
-
templateType: EmailTemplateType.
|
|
92
|
+
templateType: EmailTemplateType.UserBalanceReminder,
|
|
107
93
|
filter: {
|
|
108
94
|
$and: [
|
|
109
95
|
{
|
|
@@ -114,14 +100,32 @@ async function balanceEmails() {
|
|
|
114
100
|
],
|
|
115
101
|
},
|
|
116
102
|
});
|
|
103
|
+
|
|
104
|
+
if (enabledForOrganizations) {
|
|
105
|
+
await sendTemplate({
|
|
106
|
+
objectType: ReceivableBalanceType.organization,
|
|
107
|
+
organization,
|
|
108
|
+
emailAddress,
|
|
109
|
+
systemUser,
|
|
110
|
+
templateType: EmailTemplateType.OrganizationBalanceReminder,
|
|
111
|
+
filter: {
|
|
112
|
+
$and: [
|
|
113
|
+
{
|
|
114
|
+
reminderEmailCount: { $gt: 0 },
|
|
115
|
+
}, {
|
|
116
|
+
reminderEmailCount: { $lt: maximumEmailCount },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
subfilter: organization.privateMeta.balanceNotificationSettings.organizationContactsFilter,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
if (savedIterator.isDone) {
|
|
121
127
|
savedIterator = null;
|
|
122
128
|
lastFullRun = new Date();
|
|
123
|
-
|
|
124
|
-
console.log('All done!');
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -132,6 +136,7 @@ async function sendTemplate({
|
|
|
132
136
|
templateType,
|
|
133
137
|
filter,
|
|
134
138
|
objectType,
|
|
139
|
+
subfilter,
|
|
135
140
|
}: {
|
|
136
141
|
objectType: ReceivableBalanceType;
|
|
137
142
|
organization: Organization;
|
|
@@ -139,6 +144,7 @@ async function sendTemplate({
|
|
|
139
144
|
systemUser: User;
|
|
140
145
|
templateType: EmailTemplateType;
|
|
141
146
|
filter: StamhoofdFilter;
|
|
147
|
+
subfilter?: StamhoofdFilter;
|
|
142
148
|
}) {
|
|
143
149
|
// Do not send to persons that already received a similar email before this date
|
|
144
150
|
const weekAgo = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * organization.privateMeta.balanceNotificationSettings.minimumDaysBetween); // 5 instead of 7 so the email received is on another working day
|
|
@@ -181,6 +187,7 @@ async function sendTemplate({
|
|
|
181
187
|
],
|
|
182
188
|
|
|
183
189
|
},
|
|
190
|
+
subfilter,
|
|
184
191
|
}),
|
|
185
192
|
],
|
|
186
193
|
});
|
|
@@ -202,13 +209,16 @@ async function sendTemplate({
|
|
|
202
209
|
console.log('No recipients found for organization', organization.name, organization.id);
|
|
203
210
|
}
|
|
204
211
|
else {
|
|
212
|
+
const now = new Date();
|
|
213
|
+
now.setMilliseconds(0);
|
|
214
|
+
|
|
205
215
|
// Set last balance amount for all these recipients
|
|
206
216
|
for await (const batch of EmailRecipient.select().where('emailId', upToDate.id).limit(100).allBatched()) {
|
|
207
217
|
const balanceItemIds = batch.flatMap(b => b.objectId ? [b.objectId] : []);
|
|
208
218
|
|
|
209
219
|
console.log('Marking balances as reminded...');
|
|
210
220
|
await CachedBalance.update()
|
|
211
|
-
.set('lastReminderEmail',
|
|
221
|
+
.set('lastReminderEmail', now)
|
|
212
222
|
.set('lastReminderAmountOpen', SQL.column('amountOpen'))
|
|
213
223
|
.set(
|
|
214
224
|
'reminderEmailCount',
|
|
@@ -221,6 +231,7 @@ async function sendTemplate({
|
|
|
221
231
|
.where('id', balanceItemIds)
|
|
222
232
|
.where('organizationId', organization.id)
|
|
223
233
|
.where('objectType', objectType)
|
|
234
|
+
.where(SQL.where('lastReminderEmail', '<', now).or('lastReminderEmail', null)) // prevent increasing the count multiple times if multiple recipients received the email
|
|
224
235
|
.update();
|
|
225
236
|
}
|
|
226
237
|
}
|
|
@@ -44,6 +44,9 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
44
44
|
const returnedModels: BalanceItem[] = [];
|
|
45
45
|
const updateOutstandingBalance: BalanceItem[] = [];
|
|
46
46
|
|
|
47
|
+
// Tracking changes
|
|
48
|
+
const additionalItems: { memberId: string; organizationId: string }[] = [];
|
|
49
|
+
|
|
47
50
|
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
48
51
|
for (const { put } of request.body.getPuts()) {
|
|
49
52
|
// Create a new balance item
|
|
@@ -141,8 +144,29 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
141
144
|
model.payingOrganizationId = patch.payingOrganizationId;
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
if (patch.
|
|
145
|
-
model.
|
|
147
|
+
if (patch.userId) {
|
|
148
|
+
model.userId = (await this.validateUserId(model, patch.userId)).id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (patch.memberId !== undefined) {
|
|
152
|
+
if (model.memberId) {
|
|
153
|
+
// Also update old member id outstanding balances
|
|
154
|
+
additionalItems.push({ memberId: model.memberId, organizationId: model.organizationId });
|
|
155
|
+
}
|
|
156
|
+
if (patch.memberId === null) {
|
|
157
|
+
if (model.userId === null) {
|
|
158
|
+
throw new SimpleError({
|
|
159
|
+
code: 'invalid_field',
|
|
160
|
+
message: 'No user or member provided',
|
|
161
|
+
human: 'Een verschuild moet altijd aan een lid of gebruiker gelinkt zijn',
|
|
162
|
+
field: 'memberId',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
model.memberId = null;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
model.memberId = (await this.validateMemberId(patch.memberId)).id;
|
|
169
|
+
}
|
|
146
170
|
}
|
|
147
171
|
|
|
148
172
|
if (patch.createdAt) {
|
|
@@ -201,13 +225,13 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
201
225
|
await model.save();
|
|
202
226
|
returnedModels.push(model);
|
|
203
227
|
|
|
204
|
-
if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined) {
|
|
228
|
+
if (patch.unitPrice || patch.amount || patch.status || patch.dueAt !== undefined || patch.memberId || patch.userId) {
|
|
205
229
|
updateOutstandingBalance.push(model);
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
});
|
|
209
233
|
|
|
210
|
-
await BalanceItem.updateOutstanding(updateOutstandingBalance);
|
|
234
|
+
await BalanceItem.updateOutstanding(updateOutstandingBalance, additionalItems);
|
|
211
235
|
|
|
212
236
|
// Reallocate
|
|
213
237
|
await BalanceItemService.reallocate(updateOutstandingBalance, organization.id);
|
|
@@ -283,6 +283,12 @@ export class AuthenticatedStructures {
|
|
|
283
283
|
|
|
284
284
|
static async userWithMembers(user: User): Promise<UserWithMembers> {
|
|
285
285
|
const members = await Member.getMembersWithRegistrationForUser(user);
|
|
286
|
+
const filtered: MemberWithRegistrations[] = [];
|
|
287
|
+
for (const member of members) {
|
|
288
|
+
if (await Context.auth.canAccessMember(member, PermissionLevel.Read)) {
|
|
289
|
+
filtered.push(member);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
286
292
|
|
|
287
293
|
return UserWithMembers.create({
|
|
288
294
|
...user,
|
|
@@ -290,7 +296,7 @@ export class AuthenticatedStructures {
|
|
|
290
296
|
hasPassword: user.hasPasswordBasedAccount(),
|
|
291
297
|
|
|
292
298
|
// Always include the current context organization - because it is possible we switch organization and we don't want to refetch every time
|
|
293
|
-
members: await this.membersBlob(
|
|
299
|
+
members: await this.membersBlob(filtered, true, user),
|
|
294
300
|
});
|
|
295
301
|
}
|
|
296
302
|
|
package/src/helpers/Context.ts
CHANGED
|
@@ -136,6 +136,15 @@ export class ContextInstance {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
async checkFeatureFlag(flag: string): Promise<boolean> {
|
|
140
|
+
const platform = await Platform.getSharedStruct();
|
|
141
|
+
if (platform.config.featureFlags.includes(flag)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const organization = this.organization;
|
|
145
|
+
return organization?.privateMeta?.featureFlags.includes(flag) ?? false;
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
/**
|
|
140
149
|
* Require organization scope if userMode is not platform
|
|
141
150
|
*/
|