@stamhoofd/backend 2.73.0 → 2.73.2

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.0",
3
+ "version": "2.73.2",
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.27.0",
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.0",
41
- "@stamhoofd/backend-middleware": "2.73.0",
42
- "@stamhoofd/email": "2.73.0",
43
- "@stamhoofd/models": "2.73.0",
44
- "@stamhoofd/queues": "2.73.0",
45
- "@stamhoofd/sql": "2.73.0",
46
- "@stamhoofd/structures": "2.73.0",
47
- "@stamhoofd/utility": "2.73.0",
40
+ "@stamhoofd/backend-i18n": "2.73.2",
41
+ "@stamhoofd/backend-middleware": "2.73.2",
42
+ "@stamhoofd/email": "2.73.2",
43
+ "@stamhoofd/models": "2.73.2",
44
+ "@stamhoofd/queues": "2.73.2",
45
+ "@stamhoofd/sql": "2.73.2",
46
+ "@stamhoofd/structures": "2.73.2",
47
+ "@stamhoofd/utility": "2.73.2",
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": "fee6c6f01c19d15010807f2bac6a3ab5ee14019e"
67
+ "gitHead": "d19fcea9fa93cf46133a0542848e0d15d0488613"
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 && STAMHOOFD.environment !== 'development') {
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
- // Reminder emails
84
- if (maximumEmailCount > 1) {
69
+ if (enabledForOrganizations) {
85
70
  await sendTemplate({
86
- objectType: ReceivableBalanceType.user,
71
+ objectType: ReceivableBalanceType.organization,
87
72
  organization,
88
73
  emailAddress,
89
74
  systemUser,
90
- templateType: EmailTemplateType.UserBalanceReminder,
75
+ templateType: EmailTemplateType.OrganizationBalanceIncreaseNotification,
91
76
  filter: {
92
- $and: [
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.organization,
88
+ objectType: ReceivableBalanceType.user,
103
89
  organization,
104
90
  emailAddress,
105
91
  systemUser,
106
- templateType: EmailTemplateType.OrganizationBalanceReminder,
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', new Date())
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.memberId) {
145
- model.memberId = (await this.validateMemberId(patch.memberId)).id;
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(members, true, user),
299
+ members: await this.membersBlob(filtered, true, user),
294
300
  });
295
301
  }
296
302
 
@@ -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
  */