@stamhoofd/backend 2.64.0 → 2.65.1
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 +10 -10
- package/src/audit-logs/EmailLogger.ts +7 -1
- package/src/audit-logs/ModelLogger.ts +17 -2
- package/src/crons/balance-emails.ts +232 -0
- package/src/crons/index.ts +1 -0
- package/src/email-recipient-loaders/members.ts +14 -4
- package/src/email-recipient-loaders/receivable-balances.ts +29 -15
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +47 -12
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -18
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +15 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +0 -10
- package/src/helpers/EmailResumer.ts +1 -5
- package/src/helpers/MemberUserSyncer.ts +22 -1
- package/src/helpers/MembershipCharger.ts +1 -0
- package/src/helpers/TagHelper.ts +7 -14
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +4 -14
- package/src/seeds/1729253172-update-orders.ts +7 -18
- package/src/seeds/{1726494420-update-cached-outstanding-balance-from-items.ts → 1735577912-update-cached-outstanding-balance-from-items.ts} +1 -14
- package/src/seeds/1736266448-recall-balance-item-price-paid.ts +70 -0
- package/src/services/BalanceItemPaymentService.ts +14 -2
- package/src/services/BalanceItemService.ts +41 -1
- package/src/services/PlatformMembershipService.ts +5 -5
- package/src/sql-filters/members.ts +1 -0
- package/src/sql-filters/receivable-balances.ts +15 -1
- package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +19 -0
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +0 -253
- package/src/helpers/ModelHelper.ts +0 -32
- package/src/seeds/1726055544-balance-item-paid.ts +0 -11
- package/src/seeds/1726055545-balance-item-pending.ts +0 -11
- package/src/seeds/1726494419-update-cached-outstanding-balance.ts +0 -53
- package/src/seeds/1728928973-balance-item-pending.ts +0 -11
- 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.
|
|
3
|
+
"version": "2.65.1",
|
|
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.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.65.1",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.65.1",
|
|
42
|
+
"@stamhoofd/email": "2.65.1",
|
|
43
|
+
"@stamhoofd/models": "2.65.1",
|
|
44
|
+
"@stamhoofd/queues": "2.65.1",
|
|
45
|
+
"@stamhoofd/sql": "2.65.1",
|
|
46
|
+
"@stamhoofd/structures": "2.65.1",
|
|
47
|
+
"@stamhoofd/utility": "2.65.1",
|
|
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": "7817159827eba1f565f35785d1700333ab613aef"
|
|
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
|
-
|
|
117
|
-
|
|
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
|
+
}
|
package/src/crons/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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);
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -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) {
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -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;
|