@stamhoofd/backend 2.64.0 → 2.65.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.
Files changed (31) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +7 -1
  3. package/src/audit-logs/ModelLogger.ts +17 -2
  4. package/src/crons/balance-emails.ts +232 -0
  5. package/src/crons/index.ts +1 -0
  6. package/src/email-recipient-loaders/members.ts +14 -4
  7. package/src/email-recipient-loaders/receivable-balances.ts +29 -15
  8. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +47 -12
  9. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -18
  10. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +15 -1
  11. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +1 -1
  13. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +0 -10
  14. package/src/helpers/EmailResumer.ts +1 -5
  15. package/src/helpers/MemberUserSyncer.ts +22 -1
  16. package/src/helpers/MembershipCharger.ts +1 -0
  17. package/src/helpers/TagHelper.ts +7 -14
  18. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +4 -14
  19. package/src/seeds/1729253172-update-orders.ts +7 -18
  20. package/src/seeds/{1726494420-update-cached-outstanding-balance-from-items.ts → 1735577912-update-cached-outstanding-balance-from-items.ts} +1 -14
  21. package/src/services/PlatformMembershipService.ts +5 -5
  22. package/src/sql-filters/members.ts +1 -0
  23. package/src/sql-filters/receivable-balances.ts +15 -1
  24. package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +19 -0
  25. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +0 -253
  26. package/src/helpers/ModelHelper.ts +0 -32
  27. package/src/seeds/1726055544-balance-item-paid.ts +0 -11
  28. package/src/seeds/1726055545-balance-item-pending.ts +0 -11
  29. package/src/seeds/1726494419-update-cached-outstanding-balance.ts +0 -53
  30. package/src/seeds/1728928973-balance-item-pending.ts +0 -11
  31. package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.64.0",
3
+ "version": "2.65.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -37,14 +37,14 @@
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.64.0",
41
- "@stamhoofd/backend-middleware": "2.64.0",
42
- "@stamhoofd/email": "2.64.0",
43
- "@stamhoofd/models": "2.64.0",
44
- "@stamhoofd/queues": "2.64.0",
45
- "@stamhoofd/sql": "2.64.0",
46
- "@stamhoofd/structures": "2.64.0",
47
- "@stamhoofd/utility": "2.64.0",
40
+ "@stamhoofd/backend-i18n": "2.65.0",
41
+ "@stamhoofd/backend-middleware": "2.65.0",
42
+ "@stamhoofd/email": "2.65.0",
43
+ "@stamhoofd/models": "2.65.0",
44
+ "@stamhoofd/queues": "2.65.0",
45
+ "@stamhoofd/sql": "2.65.0",
46
+ "@stamhoofd/structures": "2.65.0",
47
+ "@stamhoofd/utility": "2.65.0",
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": "80eebbb0b4fb84ac979041c3f8c7762863622367"
67
+ "gitHead": "16bb6a7ce183c964158cee5d1581de6e7575133d"
68
68
  }
@@ -8,6 +8,7 @@ export const EmailLogger = new ModelLogger(Email, {
8
8
  if (event.type === 'deleted') {
9
9
  return;
10
10
  }
11
+
11
12
  let oldStatus = EmailStatus.Draft;
12
13
 
13
14
  if (event.type === 'updated') {
@@ -35,6 +36,11 @@ export const EmailLogger = new ModelLogger(Email, {
35
36
  };
36
37
  }
37
38
 
39
+ if (event.model.emailType) {
40
+ // don't log the scheduled part of automated emails
41
+ return;
42
+ }
43
+
38
44
  return {
39
45
  type: AuditLogType.EmailSending,
40
46
  data: {
@@ -48,7 +54,7 @@ export const EmailLogger = new ModelLogger(Email, {
48
54
  const map = new Map([
49
55
  ['e', AuditLogReplacement.create({
50
56
  id: model.id,
51
- value: model.subject || '',
57
+ value: replaceHtml(model.subject ?? '', options.data.recipient?.replacements ?? []),
52
58
  type: AuditLogReplacementType.Email,
53
59
  })],
54
60
  ['c', AuditLogReplacement.create({
@@ -113,8 +113,23 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
113
113
  try {
114
114
  const context = ContextInstance.optional;
115
115
  const log = new AuditLog();
116
- const settings = AuditLogService.getContext();
117
- const userId = settings?.userId !== undefined ? settings?.userId : (context?.optionalAuth?.user?.id ?? settings?.fallbackUserId ?? null);
116
+ let settings = AuditLogService.getContext();
117
+ let userId = settings?.userId !== undefined ? settings?.userId : (context?.optionalAuth?.user?.id ?? settings?.fallbackUserId ?? null);
118
+
119
+ if (userId === '1') {
120
+ // System user
121
+ userId = null;
122
+
123
+ if (!settings?.source) {
124
+ if (settings) {
125
+ settings.source = AuditLogSource.System;
126
+ }
127
+ else {
128
+ settings = { source: AuditLogSource.System };
129
+ }
130
+ }
131
+ }
132
+
118
133
  log.userId = userId;
119
134
 
120
135
  log.organizationId = context?.organization?.id ?? settings?.fallbackOrganizationId ?? null;
@@ -0,0 +1,232 @@
1
+ import { registerCron } from '@stamhoofd/crons';
2
+ import { CachedBalance, Email, EmailRecipient, Organization, Platform, User } from '@stamhoofd/models';
3
+ import { IterableSQLSelect, readDynamicSQLExpression, SQL, SQLCalculation, SQLPlusSign } from '@stamhoofd/sql';
4
+ import { EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientSubfilter, EmailTemplateType, OrganizationEmail, ReceivableBalanceType, StamhoofdFilter } from '@stamhoofd/structures';
5
+ import { ContextInstance } from '../helpers/Context';
6
+
7
+ registerCron('balanceEmails', balanceEmails);
8
+
9
+ let lastFullRun = new Date(0);
10
+ let savedIterator: IterableSQLSelect<Organization> | null = null;
11
+
12
+ const bootAt = new Date();
13
+
14
+ async function balanceEmails() {
15
+ // Do not run within 30 minutes after boot to avoid creating multiple email models for emails that failed to send
16
+ if (bootAt.getTime() > new Date().getTime() - 1000 * 60 * 30 && STAMHOOFD.environment !== 'development') {
17
+ console.log('Boot time is too recent, skipping.');
18
+ return;
19
+ }
20
+
21
+ if (lastFullRun.getTime() > new Date().getTime() - 1000 * 60 * 60 * 12 && STAMHOOFD.environment !== 'development') {
22
+ console.log('Already ran today, skipping.');
23
+ return;
24
+ }
25
+
26
+ if ((new Date().getHours() > 10 || new Date().getHours() < 6) && STAMHOOFD.environment !== 'development') {
27
+ console.log('Not between 6 and 10 AM, skipping.');
28
+ return;
29
+ }
30
+
31
+ // Get the next x organization to send e-mails for
32
+ if (savedIterator === null) {
33
+ console.log('Starting new iterator');
34
+ savedIterator = Organization.select().limit(10).all();
35
+ }
36
+
37
+ const platform = await Platform.getSharedPrivateStruct();
38
+
39
+ if (!platform.config.featureFlags.includes('balance-emails')) {
40
+ console.log('Feature flag not enabled, skipping.');
41
+ return;
42
+ }
43
+ const systemUser = await User.getSystem();
44
+
45
+ for await (const organization of savedIterator.maxQueries(5)) {
46
+ if (!organization.privateMeta.balanceNotificationSettings.enabled || organization.privateMeta.balanceNotificationSettings.maximumReminderEmails <= 0 || organization.privateMeta.balanceNotificationSettings.minimumDaysBetween <= 0) {
47
+ continue;
48
+ }
49
+
50
+ const selectedEmailAddress = organization.privateMeta.balanceNotificationSettings.emailId ? organization.privateMeta.emails.find(e => e.id === organization.privateMeta.balanceNotificationSettings.emailId) : null;
51
+ const emailAddress = selectedEmailAddress ?? organization.privateMeta.emails.find(e => e.default) ?? null;
52
+
53
+ if (!emailAddress) {
54
+ // No emailadres set
55
+ console.warn('Skipped organization', organization.id, 'because no email address is set');
56
+ continue;
57
+ }
58
+
59
+ // First emails
60
+ await sendTemplate({
61
+ objectType: ReceivableBalanceType.user,
62
+ organization,
63
+ emailAddress,
64
+ systemUser,
65
+ templateType: EmailTemplateType.UserBalanceIncreaseNotification,
66
+ filter: {
67
+ reminderAmountIncreased: true,
68
+ reminderEmailCount: 0,
69
+ },
70
+ });
71
+ await sendTemplate({
72
+ objectType: ReceivableBalanceType.organization,
73
+ organization,
74
+ emailAddress,
75
+ systemUser,
76
+ templateType: EmailTemplateType.OrganizationBalanceIncreaseNotification,
77
+ filter: {
78
+ reminderAmountIncreased: true,
79
+ reminderEmailCount: 0,
80
+ },
81
+ });
82
+ const maximumEmailCount = organization.privateMeta.balanceNotificationSettings.maximumReminderEmails;
83
+
84
+ // Reminder emails
85
+ if (maximumEmailCount > 1) {
86
+ await sendTemplate({
87
+ objectType: ReceivableBalanceType.user,
88
+ organization,
89
+ emailAddress,
90
+ systemUser,
91
+ templateType: EmailTemplateType.UserBalanceReminder,
92
+ filter: {
93
+ $and: [
94
+ {
95
+ reminderEmailCount: { $gt: 0 },
96
+ }, {
97
+ reminderEmailCount: { $lt: maximumEmailCount },
98
+ },
99
+ ],
100
+ },
101
+ });
102
+ await sendTemplate({
103
+ objectType: ReceivableBalanceType.organization,
104
+ organization,
105
+ emailAddress,
106
+ systemUser,
107
+ templateType: EmailTemplateType.OrganizationBalanceReminder,
108
+ filter: {
109
+ $and: [
110
+ {
111
+ reminderEmailCount: { $gt: 0 },
112
+ }, {
113
+ reminderEmailCount: { $lt: maximumEmailCount },
114
+ },
115
+ ],
116
+ },
117
+ });
118
+ }
119
+ }
120
+
121
+ if (savedIterator.isDone) {
122
+ savedIterator = null;
123
+ lastFullRun = new Date();
124
+
125
+ console.log('All done!');
126
+ }
127
+ }
128
+
129
+ async function sendTemplate({
130
+ organization,
131
+ emailAddress,
132
+ systemUser,
133
+ templateType,
134
+ filter,
135
+ objectType,
136
+ }: {
137
+ objectType: ReceivableBalanceType;
138
+ organization: Organization;
139
+ emailAddress: OrganizationEmail;
140
+ systemUser: User;
141
+ templateType: EmailTemplateType;
142
+ filter: StamhoofdFilter;
143
+ }) {
144
+ // Do not send to persons that already received a similar email before this date
145
+ 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
146
+
147
+ const model = new Email();
148
+ model.userId = null; // This is a system e-mail
149
+ model.organizationId = organization?.id ?? null;
150
+ model.emailType = templateType;
151
+
152
+ model.recipientFilter = EmailRecipientFilter.create({
153
+ filters: [
154
+ EmailRecipientSubfilter.create({
155
+ type: EmailRecipientFilterType.ReceivableBalances,
156
+ filter: {
157
+ $and: [
158
+ {
159
+ amountOpen: { $gt: 0 },
160
+ objectType,
161
+ },
162
+ {
163
+ // Never send more than minimumDaysBetween
164
+ $or: [
165
+ { lastReminderEmail: null },
166
+ { lastReminderEmail: { $lt: weekAgo } },
167
+ ],
168
+ },
169
+ filter,
170
+ /* {
171
+ // Do not send if already received any email very recently
172
+ $not: {
173
+ emails: {
174
+ $elemMatch: {
175
+ sentAt: {
176
+ $gt: weekAgo,
177
+ },
178
+ },
179
+ },
180
+ },
181
+ }, */
182
+ ],
183
+
184
+ },
185
+ }),
186
+ ],
187
+ });
188
+
189
+ if (!await model.setFromTemplate(templateType)) {
190
+ console.warn('Skipped organization: email template not found', organization.id);
191
+ return;
192
+ }
193
+
194
+ model.fromAddress = emailAddress.email;
195
+ model.fromName = emailAddress.name ?? organization.name;
196
+
197
+ try {
198
+ const upToDate = await ContextInstance.startForUser(systemUser, organization, async () => {
199
+ return await model.send();
200
+ });
201
+
202
+ if (!upToDate) {
203
+ console.log('No recipients found for organization', organization.name, organization.id);
204
+ }
205
+ else {
206
+ // Set last balance amount for all these recipients
207
+ for await (const batch of EmailRecipient.select().where('emailId', upToDate.id).limit(100).allBatched()) {
208
+ const balanceItemIds = batch.flatMap(b => b.objectId ? [b.objectId] : []);
209
+
210
+ console.log('Marking balances as reminded...');
211
+ await CachedBalance.update()
212
+ .set('lastReminderEmail', new Date())
213
+ .set('lastReminderAmountOpen', SQL.column('amountOpen'))
214
+ .set(
215
+ 'reminderEmailCount',
216
+ new SQLCalculation(
217
+ SQL.column('reminderEmailCount'),
218
+ new SQLPlusSign(),
219
+ readDynamicSQLExpression(1),
220
+ ),
221
+ )
222
+ .where('id', balanceItemIds)
223
+ .where('organizationId', organization.id)
224
+ .where('objectType', objectType)
225
+ .update();
226
+ }
227
+ }
228
+ }
229
+ catch (e) {
230
+ console.error('Error sending email for organization', e, organization.name, organization.id);
231
+ }
232
+ }
@@ -3,3 +3,4 @@ import './clearExcelCache.js';
3
3
  import './endFunctionsOfUsersWithoutRegistration.js';
4
4
  import './postmark.js';
5
5
  import './update-cached-balances.js';
6
+ import './balance-emails.js';
@@ -1,14 +1,24 @@
1
1
  import { Email } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
- import { EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, mergeFilters } from '@stamhoofd/structures';
3
+ import { EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, MembersBlob, PaginatedResponse, mergeFilters } from '@stamhoofd/structures';
4
4
  import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint';
5
5
 
6
+ async function getRecipients(result: PaginatedResponse<MembersBlob, LimitedFilteredRequest>, type: 'member' | 'parents' | 'unverified') {
7
+ const recipients: EmailRecipient[] = [];
8
+
9
+ for (const member of result.results.members) {
10
+ const memberRecipients = member.getEmailRecipients([type]);
11
+ recipients.push(...memberRecipients);
12
+ }
13
+ return recipients;
14
+ }
15
+
6
16
  Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
7
17
  fetch: async (query: LimitedFilteredRequest) => {
8
18
  const result = await GetMembersEndpoint.buildData(query);
9
19
 
10
20
  return new PaginatedResponse({
11
- results: result.results.members.flatMap(m => m.getEmailRecipients(['member'])),
21
+ results: await getRecipients(result, 'member'),
12
22
  next: result.next,
13
23
  });
14
24
  },
@@ -29,7 +39,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
29
39
  const result = await GetMembersEndpoint.buildData(query);
30
40
 
31
41
  return new PaginatedResponse({
32
- results: result.results.members.flatMap(m => m.getEmailRecipients(['parents'])),
42
+ results: await getRecipients(result, 'parents'),
33
43
  next: result.next,
34
44
  });
35
45
  },
@@ -47,7 +57,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.MemberUnverified, {
47
57
  const result = await GetMembersEndpoint.buildData(query);
48
58
 
49
59
  return new PaginatedResponse({
50
- results: result.results.members.flatMap(m => m.getEmailRecipients(['unverified'])),
60
+ results: await getRecipients(result, 'unverified'),
51
61
  next: result.next,
52
62
  });
53
63
  },
@@ -1,7 +1,7 @@
1
- import { Email } from '@stamhoofd/models';
2
- import { receivableBalanceObjectContactInMemoryFilterCompilers, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
3
- import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
1
+ import { CachedBalance, Email } from '@stamhoofd/models';
2
+ import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
4
3
  import { Formatter } from '@stamhoofd/utility';
4
+ import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
5
5
 
6
6
  async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
7
7
  const result = await GetReceivableBalancesEndpoint.buildData(query);
@@ -9,10 +9,19 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
9
9
  // Map all contacts to recipients
10
10
  const compiledFilter = compileToInMemoryFilter(subfilter, receivableBalanceObjectContactInMemoryFilterCompilers);
11
11
 
12
- return new PaginatedResponse({
13
- results: result.results.flatMap((balance) => {
14
- return balance.object.contacts.filter(c => compiledFilter(c)).flatMap((contact) => {
15
- return contact.emails.map(email => EmailRecipient.create({
12
+ // const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
13
+ // const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
14
+
15
+ const recipients: EmailRecipient[] = [];
16
+ for (const balance of result.results) {
17
+ const balanceItemModels = await CachedBalance.balanceForObjects(balance.organizationId, [balance.object.id], balance.objectType, true);
18
+ const balanceItems = balanceItemModels.map(i => i.getStructure());
19
+
20
+ const filteredContacts = balance.object.contacts.filter(c => compiledFilter(c));
21
+ for (const contact of filteredContacts) {
22
+ for (const email of contact.emails) {
23
+ const recipient = EmailRecipient.create({
24
+ objectId: balance.id, // Note: not set member, user or organization id here - should be the queryable balance id
16
25
  firstName: contact.firstName,
17
26
  lastName: contact.lastName,
18
27
  email,
@@ -21,15 +30,15 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
21
30
  token: 'objectName',
22
31
  value: balance.object.name,
23
32
  }),
24
- Replacement.create({
25
- // Deprecated: for backwards compatibility
26
- token: 'organizationName',
27
- value: balance.object.name,
28
- }),
29
33
  Replacement.create({
30
34
  token: 'outstandingBalance',
31
35
  value: Formatter.price(balance.amountOpen),
32
36
  }),
37
+ Replacement.create({
38
+ token: 'balanceTable',
39
+ value: '',
40
+ html: BalanceItemStruct.getDetailsHTMLTable(balanceItems),
41
+ }),
33
42
  ...(contact.meta && contact.meta.url && typeof contact.meta.url === 'string'
34
43
  ? [Replacement.create({
35
44
  token: 'paymentUrl',
@@ -37,9 +46,14 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
37
46
  })]
38
47
  : []),
39
48
  ],
40
- }));
41
- });
42
- }),
49
+ });
50
+ recipients.push(recipient);
51
+ }
52
+ }
53
+ }
54
+
55
+ return new PaginatedResponse({
56
+ results: recipients,
43
57
  next: result.next,
44
58
  });
45
59
  }
@@ -1,5 +1,5 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
2
+ import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum, SQLWhereSign } from '@stamhoofd/sql';
3
3
  import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
4
4
  import { Context } from '../../../helpers/Context';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
@@ -43,6 +43,29 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
43
43
  const platform = await Platform.getShared();
44
44
  const chargeVia = platform.membershipOrganizationId;
45
45
 
46
+ const total = await this.fetchTotal(chargeVia, false);
47
+ const totalTrials = await this.fetchTotal(chargeVia, true);
48
+
49
+ return new Response(
50
+ ChargeMembershipsSummary.create({
51
+ running: false,
52
+ memberships: total.memberships ?? 0,
53
+ members: total.members ?? 0,
54
+ price: total.price ?? 0,
55
+ organizations: total.organizations ?? 0,
56
+ membershipsPerType: await this.fetchPerType(chargeVia),
57
+ trials: ChargeMembershipsTypeSummary.create({
58
+ memberships: totalTrials.memberships ?? 0,
59
+ members: totalTrials.members ?? 0,
60
+ price: totalTrials.price ?? 0,
61
+ organizations: totalTrials.organizations ?? 0,
62
+ }),
63
+ }),
64
+ );
65
+ }
66
+
67
+ async fetchTotal(chargeVia: string | null, trial = false) {
68
+ const noTrial = SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date());
46
69
  const query = SQL
47
70
  .select(
48
71
  new SQLSelectAs(
@@ -81,25 +104,29 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
81
104
  .where('deletedAt', null)
82
105
  .whereNot('organizationId', chargeVia);
83
106
 
107
+ if (!trial) {
108
+ query.where(noTrial);
109
+ }
110
+ else {
111
+ query.whereNot(noTrial);
112
+ }
113
+
84
114
  const result = await query.fetch();
85
115
  const members = result[0]['data']['members'] as number;
86
116
  const memberships = result[0]['data']['memberships'] as number;
87
117
  const organizations = result[0]['data']['organizations'] as number;
88
118
  const price = result[0]['data']['price'] as number;
89
119
 
90
- return new Response(
91
- ChargeMembershipsSummary.create({
92
- running: false,
93
- memberships: memberships ?? 0,
94
- members: members ?? 0,
95
- price: price ?? 0,
96
- organizations: organizations ?? 0,
97
- membershipsPerType: await this.fetchPerType(chargeVia),
98
- }),
99
- );
120
+ return {
121
+ members,
122
+ memberships,
123
+ organizations,
124
+ price,
125
+ };
100
126
  }
101
127
 
102
- async fetchPerType(chargeVia: string | null) {
128
+ async fetchPerType(chargeVia: string | null, trial = false) {
129
+ const trialQ = SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date());
103
130
  const query = SQL
104
131
  .select(
105
132
  SQL.column('member_platform_memberships', 'membershipTypeId'),
@@ -137,11 +164,19 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
137
164
  .from('member_platform_memberships')
138
165
  .where('balanceItemId', null)
139
166
  .where('deletedAt', null)
167
+ .where(trialQ)
140
168
  .whereNot('organizationId', chargeVia)
141
169
  .groupBy(
142
170
  SQL.column('member_platform_memberships', 'membershipTypeId'),
143
171
  );
144
172
 
173
+ if (!trial) {
174
+ query.where(trialQ);
175
+ }
176
+ else {
177
+ query.whereNot(trialQ);
178
+ }
179
+
145
180
  const result = await query.fetch();
146
181
  const membershipsPerType = new Map<string, ChargeMembershipsTypeSummary>();
147
182
 
@@ -88,24 +88,8 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
88
88
  // Check default
89
89
  if (JSON.stringify(model.json).length < 3 && model.recipientFilter.filters[0].type && EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)) {
90
90
  const type = EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type);
91
-
92
- // Most specific template: for specific group
93
- let templates = (await EmailTemplate.where({ type, organizationId: organization?.id ?? null, groupId: null }));
94
-
95
- // Then default
96
- if (templates.length == 0 && organization) {
97
- templates = (await EmailTemplate.where({ type, organizationId: null, groupId: null }));
98
- }
99
-
100
- if (templates.length == 0) {
101
- // No default
102
- }
103
- else {
104
- const defaultTemplate = templates[0];
105
- model.html = defaultTemplate.html;
106
- model.text = defaultTemplate.text;
107
- model.subject = defaultTemplate.subject;
108
- model.json = defaultTemplate.json;
91
+ if (type) {
92
+ await model.setFromTemplate(type);
109
93
  }
110
94
  }
111
95
 
@@ -95,7 +95,7 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
95
95
  : []
96
96
  );
97
97
 
98
- const defaultTemplateTypes = organization ? types.filter(type => EmailTemplateStruct.isSavedEmail(type)) : types;
98
+ const defaultTemplateTypes = organization ? types.filter(type => !EmailTemplateStruct.isSavedEmail(type)) : types;
99
99
  const defaultTemplates = defaultTemplateTypes.length === 0
100
100
  ? []
101
101
  : (await EmailTemplate.where({
@@ -106,6 +106,20 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
106
106
  },
107
107
  }));
108
108
 
109
+ if (organization && (request.query.webshopId || request.query.groupIds)) {
110
+ const orgDefaults = (await EmailTemplate.where({
111
+ organizationId: organization.id,
112
+ webshopId: null,
113
+ groupId: null,
114
+ type: {
115
+ sign: 'IN',
116
+ value: defaultTemplateTypes,
117
+ },
118
+ }));
119
+
120
+ defaultTemplates.unshift(...orgDefaults);
121
+ }
122
+
109
123
  return new Response(templates.concat(defaultTemplates).map(template => EmailTemplateStruct.create(template)));
110
124
  }
111
125
  }
@@ -125,6 +125,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
125
125
  organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles);
126
126
  organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey;
127
127
  organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
128
+ organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
128
129
 
129
130
  if (request.body.privateMeta.mollieProfile !== undefined) {
130
131
  organization.privateMeta.mollieProfile = patchObject(organization.privateMeta.mollieProfile, request.body.privateMeta.mollieProfile);
@@ -37,7 +37,7 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
37
37
  throw Context.auth.error();
38
38
  }
39
39
 
40
- const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
40
+ const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type, true);
41
41
  let paymentModels: Payment[] = [];
42
42
 
43
43
  switch (request.params.type) {
@@ -48,16 +48,6 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
48
48
 
49
49
  scopeFilter = {
50
50
  organizationId: organization.id,
51
- $or: {
52
- amountOpen: { $neq: 0 },
53
- amountPending: { $neq: 0 },
54
- nextDueAt: { $neq: null },
55
- },
56
- $not: {
57
- objectType: {
58
- $in: Context.auth.hasSomePlatformAccess() ? [ReceivableBalanceType.registration] : [ReceivableBalanceType.organization, ReceivableBalanceType.registration],
59
- },
60
- },
61
51
  };
62
52
 
63
53
  const query = CachedBalance
@@ -12,13 +12,9 @@ export async function resumeEmails() {
12
12
  const emails = Email.fromRows(result, Email.table);
13
13
 
14
14
  for (const email of emails) {
15
- if (!email.userId) {
16
- console.warn('Cannot retry sending email because userId is not set - which is required for setting the scope', email.id);
17
- continue;
18
- }
19
15
  console.log('Resuming email that has sending status on boot', email.id);
20
16
 
21
- const user = await User.getByID(email.userId);
17
+ const user = email.userId ? (await User.getByID(email.userId)) : await User.getSystem();
22
18
  if (!user) {
23
19
  console.warn('Cannot retry sending email because user not found', email.id);
24
20
  continue;