@stamhoofd/models 2.93.0 → 2.95.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/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
- package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
- package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
- package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts +27 -1
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +270 -25
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/migrations/1756115317-email-deleted-at.sql +2 -0
- package/dist/src/migrations/1756293494-registration-period-next-period-id.sql +3 -0
- package/dist/src/migrations/1756293495-platform-next-period-id.sql +3 -0
- package/dist/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
- package/dist/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
- package/dist/src/models/Email.d.ts +4 -2
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +160 -36
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/EmailRecipient.d.ts +7 -1
- package/dist/src/models/EmailRecipient.d.ts.map +1 -1
- package/dist/src/models/EmailRecipient.js +28 -2
- package/dist/src/models/EmailRecipient.js.map +1 -1
- package/dist/src/models/Event.d.ts.map +1 -1
- package/dist/src/models/Event.js +2 -0
- package/dist/src/models/Event.js.map +1 -1
- package/dist/src/models/Member.d.ts +1 -1
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +3 -3
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/Platform.d.ts +1 -0
- package/dist/src/models/Platform.d.ts.map +1 -1
- package/dist/src/models/Platform.js +5 -0
- package/dist/src/models/Platform.js.map +1 -1
- package/dist/src/models/RegistrationPeriod.d.ts +4 -1
- package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
- package/dist/src/models/RegistrationPeriod.js +32 -7
- package/dist/src/models/RegistrationPeriod.js.map +1 -1
- package/dist/src/models/User.d.ts.map +1 -1
- package/dist/src/models/User.js +5 -1
- package/dist/src/models/User.js.map +1 -1
- package/package.json +2 -2
- package/src/factories/RegistrationPeriodFactory.ts +4 -0
- package/src/helpers/EmailBuilder.ts +282 -34
- package/src/migrations/1756115317-email-deleted-at.sql +2 -0
- package/src/migrations/1756293494-registration-period-next-period-id.sql +3 -0
- package/src/migrations/1756293495-platform-next-period-id.sql +3 -0
- package/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
- package/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
- package/src/models/Email.ts +184 -43
- package/src/models/EmailRecipient.ts +32 -3
- package/src/models/Event.ts +2 -0
- package/src/models/Member.ts +3 -3
- package/src/models/Platform.ts +4 -0
- package/src/models/RegistrationPeriod.ts +32 -7
- package/src/models/User.ts +7 -1
package/src/models/Email.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { column } from '@simonbackx/simple-database';
|
|
2
|
-
import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse, SortItemDirection, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { User as UserStruct, EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, EmailWithRecipients, getExampleRecipient, isSoftEmailRecipientError, LimitedFilteredRequest, PaginatedResponse, Replacement, SortItemDirection, StamhoofdFilter, BaseOrganization } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
@@ -8,10 +8,11 @@ import { I18n } from '@stamhoofd/backend-i18n';
|
|
|
8
8
|
import { Email as EmailClass, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
9
9
|
import { isAbortedError, QueueHandler, QueueHandlerOptions } from '@stamhoofd/queues';
|
|
10
10
|
import { QueryableModel, readDynamicSQLExpression, SQL, SQLAlias, SQLCalculation, SQLCount, SQLPlusSign, SQLSelectAs, SQLWhereSign } from '@stamhoofd/sql';
|
|
11
|
-
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
|
|
11
|
+
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder, mergeReplacementsIfEqual, removeUnusedReplacements, stripRecipientReplacementsForWebDisplay, stripSensitiveRecipientReplacements } from '../helpers/EmailBuilder';
|
|
12
12
|
import { EmailRecipient } from './EmailRecipient';
|
|
13
13
|
import { EmailTemplate } from './EmailTemplate';
|
|
14
14
|
import { Organization } from './Organization';
|
|
15
|
+
import { User } from './User';
|
|
15
16
|
|
|
16
17
|
function errorToSimpleErrors(e: unknown) {
|
|
17
18
|
if (isSimpleErrors(e)) {
|
|
@@ -31,16 +32,6 @@ function errorToSimpleErrors(e: unknown) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export function isSoftEmailRecipientError(error: SimpleErrors) {
|
|
35
|
-
if (error.hasCode('email_skipped_unsubscribed')) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
if (error.hasCode('email_skipped_duplicate_recipient')) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
35
|
export class Email extends QueryableModel {
|
|
45
36
|
static table = 'emails';
|
|
46
37
|
|
|
@@ -178,6 +169,12 @@ export class Email extends QueryableModel {
|
|
|
178
169
|
})
|
|
179
170
|
sentAt: Date | null = null;
|
|
180
171
|
|
|
172
|
+
@column({
|
|
173
|
+
type: 'datetime',
|
|
174
|
+
nullable: true,
|
|
175
|
+
})
|
|
176
|
+
deletedAt: Date | null = null;
|
|
177
|
+
|
|
181
178
|
@column({
|
|
182
179
|
type: 'datetime', beforeSave(old?: any) {
|
|
183
180
|
if (old !== undefined) {
|
|
@@ -240,7 +237,7 @@ export class Email extends QueryableModel {
|
|
|
240
237
|
});
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
if (this.recipientsErrors !== null && this.recipientsStatus !== EmailRecipientsStatus.Created) {
|
|
240
|
+
if (this.status === EmailStatus.Draft && this.recipientsErrors !== null && this.recipientsStatus !== EmailRecipientsStatus.Created) {
|
|
244
241
|
throw new SimpleError({
|
|
245
242
|
code: 'invalid_recipients',
|
|
246
243
|
message: 'Failed to build recipients (count)',
|
|
@@ -248,6 +245,14 @@ export class Email extends QueryableModel {
|
|
|
248
245
|
});
|
|
249
246
|
}
|
|
250
247
|
|
|
248
|
+
if (this.deletedAt) {
|
|
249
|
+
throw new SimpleError({
|
|
250
|
+
code: 'invalid_state',
|
|
251
|
+
message: 'Email is deleted',
|
|
252
|
+
human: $t(`Deze e-mail is verwijderd en kan niet verzonden worden.`),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
251
256
|
this.validateAttachments();
|
|
252
257
|
}
|
|
253
258
|
|
|
@@ -522,6 +527,10 @@ export class Email extends QueryableModel {
|
|
|
522
527
|
if (upToDate.status === EmailStatus.Draft) {
|
|
523
528
|
upToDate.status = EmailStatus.Queued;
|
|
524
529
|
}
|
|
530
|
+
if (upToDate.status === EmailStatus.Failed) {
|
|
531
|
+
// Retry failed email
|
|
532
|
+
upToDate.status = EmailStatus.Queued;
|
|
533
|
+
}
|
|
525
534
|
await upToDate.save();
|
|
526
535
|
});
|
|
527
536
|
if (waitForSending) {
|
|
@@ -683,7 +692,6 @@ export class Email extends QueryableModel {
|
|
|
683
692
|
// Start actually sending in batches of recipients that are not yet sent
|
|
684
693
|
let idPointer = '';
|
|
685
694
|
const batchSize = 100;
|
|
686
|
-
const recipientsSet = new Set<string>();
|
|
687
695
|
let isSavingStatus = false;
|
|
688
696
|
let lastStatusSave = new Date();
|
|
689
697
|
|
|
@@ -740,10 +748,6 @@ export class Email extends QueryableModel {
|
|
|
740
748
|
idPointer = recipient.id;
|
|
741
749
|
|
|
742
750
|
if (recipient.sentAt) {
|
|
743
|
-
// Already sent
|
|
744
|
-
if (recipient.email) {
|
|
745
|
-
recipientsSet.add(recipient.email);
|
|
746
|
-
}
|
|
747
751
|
succeededCount += 1;
|
|
748
752
|
await saveStatus();
|
|
749
753
|
skipped++;
|
|
@@ -755,29 +759,10 @@ export class Email extends QueryableModel {
|
|
|
755
759
|
continue;
|
|
756
760
|
}
|
|
757
761
|
|
|
758
|
-
if (
|
|
759
|
-
console.error('Found duplicate recipient while sending email', recipient.id);
|
|
760
|
-
softFailedCount += 1;
|
|
761
|
-
|
|
762
|
-
recipient.failCount += 1;
|
|
763
|
-
recipient.failErrorMessage = 'Duplicate recipient';
|
|
764
|
-
recipient.failError = new SimpleErrors(
|
|
765
|
-
new SimpleError({
|
|
766
|
-
code: 'email_skipped_duplicate_recipient',
|
|
767
|
-
message: 'Duplicate recipient',
|
|
768
|
-
human: $t('Dit e-mailadres staat meerdere keren tussen de ontvangers en werd daarom overgeslagen'),
|
|
769
|
-
}),
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
773
|
-
recipient.lastFailedAt = new Date();
|
|
774
|
-
await recipient.save();
|
|
775
|
-
|
|
776
|
-
await saveStatus();
|
|
762
|
+
if (recipient.duplicateOfRecipientId) {
|
|
777
763
|
skipped++;
|
|
778
764
|
continue;
|
|
779
765
|
}
|
|
780
|
-
recipientsSet.add(recipient.email);
|
|
781
766
|
|
|
782
767
|
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
|
783
768
|
const promise = new Promise<void>((resolve) => {
|
|
@@ -1009,12 +994,15 @@ export class Email extends QueryableModel {
|
|
|
1009
994
|
|
|
1010
995
|
abort.throwIfAborted();
|
|
1011
996
|
|
|
997
|
+
console.log('Building recipients for email', id);
|
|
998
|
+
|
|
1012
999
|
// If it is already creating -> something went wrong (e.g. server restart) and we can safely try again
|
|
1013
1000
|
|
|
1014
1001
|
upToDate.recipientsStatus = EmailRecipientsStatus.Creating;
|
|
1015
1002
|
await upToDate.save();
|
|
1016
1003
|
|
|
1017
1004
|
const membersSet = new Set<string>();
|
|
1005
|
+
const emailsSet = new Set<string>();
|
|
1018
1006
|
|
|
1019
1007
|
let count = 0;
|
|
1020
1008
|
let countWithoutEmail = 0;
|
|
@@ -1054,6 +1042,39 @@ export class Email extends QueryableModel {
|
|
|
1054
1042
|
continue;
|
|
1055
1043
|
}
|
|
1056
1044
|
|
|
1045
|
+
item.replacements = removeUnusedReplacements(upToDate.html ?? '', item.replacements);
|
|
1046
|
+
|
|
1047
|
+
let duplicateOfRecipientId: string | null = null;
|
|
1048
|
+
if (item.email && emailsSet.has(item.email)) {
|
|
1049
|
+
console.log('Found duplicate email recipient', item.email);
|
|
1050
|
+
|
|
1051
|
+
// Try to merge
|
|
1052
|
+
const existing = await EmailRecipient.select()
|
|
1053
|
+
.where('emailId', upToDate.id)
|
|
1054
|
+
.where('email', item.email)
|
|
1055
|
+
.fetch();
|
|
1056
|
+
|
|
1057
|
+
for (const other of existing) {
|
|
1058
|
+
const merged = mergeReplacementsIfEqual(other.replacements, item.replacements);
|
|
1059
|
+
if (merged !== false) {
|
|
1060
|
+
console.log('Found duplicate email recipient', item.email, other.id);
|
|
1061
|
+
duplicateOfRecipientId = other.id;
|
|
1062
|
+
|
|
1063
|
+
other.replacements = merged;
|
|
1064
|
+
other.firstName = other.firstName || item.firstName;
|
|
1065
|
+
other.lastName = other.lastName || item.lastName;
|
|
1066
|
+
await other.save();
|
|
1067
|
+
|
|
1068
|
+
item.replacements = merged;
|
|
1069
|
+
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
console.log('Could not merge duplicate email recipient', item.email, other.id, 'keeping both', other.replacements, item.replacements);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1057
1078
|
const recipient = new EmailRecipient();
|
|
1058
1079
|
recipient.emailType = upToDate.emailType;
|
|
1059
1080
|
recipient.objectId = item.objectId;
|
|
@@ -1065,6 +1086,7 @@ export class Email extends QueryableModel {
|
|
|
1065
1086
|
recipient.memberId = item.memberId ?? null;
|
|
1066
1087
|
recipient.userId = item.userId ?? null;
|
|
1067
1088
|
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1089
|
+
recipient.duplicateOfRecipientId = duplicateOfRecipientId;
|
|
1068
1090
|
|
|
1069
1091
|
await recipient.save();
|
|
1070
1092
|
|
|
@@ -1072,7 +1094,11 @@ export class Email extends QueryableModel {
|
|
|
1072
1094
|
membersSet.add(recipient.memberId);
|
|
1073
1095
|
}
|
|
1074
1096
|
|
|
1075
|
-
if (
|
|
1097
|
+
if (recipient.email) {
|
|
1098
|
+
emailsSet.add(recipient.email);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!recipient.email || duplicateOfRecipientId) {
|
|
1076
1102
|
countWithoutEmail += 1;
|
|
1077
1103
|
}
|
|
1078
1104
|
else {
|
|
@@ -1146,13 +1172,21 @@ export class Email extends QueryableModel {
|
|
|
1146
1172
|
|
|
1147
1173
|
for (const item of response.results) {
|
|
1148
1174
|
const recipient = new EmailRecipient();
|
|
1175
|
+
recipient.emailType = upToDate.emailType;
|
|
1149
1176
|
recipient.emailId = upToDate.id;
|
|
1177
|
+
recipient.objectId = item.objectId;
|
|
1150
1178
|
recipient.email = item.email;
|
|
1151
1179
|
recipient.firstName = item.firstName;
|
|
1152
1180
|
recipient.lastName = item.lastName;
|
|
1153
1181
|
recipient.replacements = item.replacements;
|
|
1182
|
+
recipient.memberId = item.memberId ?? null;
|
|
1183
|
+
recipient.userId = item.userId ?? null;
|
|
1184
|
+
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1154
1185
|
await recipient.save();
|
|
1155
|
-
|
|
1186
|
+
|
|
1187
|
+
if (recipient.email || recipient.userId) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1156
1190
|
}
|
|
1157
1191
|
|
|
1158
1192
|
request = null;
|
|
@@ -1175,12 +1209,13 @@ export class Email extends QueryableModel {
|
|
|
1175
1209
|
async getPreviewStructure() {
|
|
1176
1210
|
const emailRecipient = await EmailRecipient.select()
|
|
1177
1211
|
.where('emailId', this.id)
|
|
1212
|
+
.where('email', '!=', null)
|
|
1178
1213
|
.first(false);
|
|
1179
1214
|
|
|
1180
1215
|
let recipientRow: EmailRecipientStruct | undefined;
|
|
1181
1216
|
|
|
1182
1217
|
if (emailRecipient) {
|
|
1183
|
-
recipientRow = emailRecipient.getStructure();
|
|
1218
|
+
recipientRow = await emailRecipient.getStructure();
|
|
1184
1219
|
}
|
|
1185
1220
|
|
|
1186
1221
|
if (!recipientRow) {
|
|
@@ -1188,18 +1223,124 @@ export class Email extends QueryableModel {
|
|
|
1188
1223
|
}
|
|
1189
1224
|
|
|
1190
1225
|
const virtualRecipient = recipientRow.getRecipient();
|
|
1226
|
+
const organization = this.organizationId ? (await Organization.getByID(this.organizationId))! : null;
|
|
1191
1227
|
|
|
1192
1228
|
await fillRecipientReplacements(virtualRecipient, {
|
|
1193
|
-
organization
|
|
1229
|
+
organization,
|
|
1194
1230
|
from: this.getFromAddress(),
|
|
1195
1231
|
replyTo: null,
|
|
1232
|
+
forPreview: true,
|
|
1233
|
+
forceRefresh: !this.sentAt,
|
|
1196
1234
|
});
|
|
1197
|
-
|
|
1198
1235
|
recipientRow.replacements = virtualRecipient.replacements;
|
|
1199
1236
|
|
|
1237
|
+
let user: UserStruct | null = null;
|
|
1238
|
+
if (this.userId) {
|
|
1239
|
+
const u = await User.getByID(this.userId);
|
|
1240
|
+
if (u) {
|
|
1241
|
+
user = u.getStructure();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
let organizationStruct: BaseOrganization | null = null;
|
|
1246
|
+
if (organization) {
|
|
1247
|
+
organizationStruct = organization.getBaseStructure();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1200
1250
|
return EmailPreview.create({
|
|
1201
1251
|
...this,
|
|
1252
|
+
user,
|
|
1253
|
+
organization: organizationStruct,
|
|
1202
1254
|
exampleRecipient: recipientRow,
|
|
1203
1255
|
});
|
|
1204
1256
|
}
|
|
1257
|
+
|
|
1258
|
+
async getStructureForUser(user: User, memberIds: string[]) {
|
|
1259
|
+
const emailRecipients = await EmailRecipient.select()
|
|
1260
|
+
.where('emailId', this.id)
|
|
1261
|
+
.where('memberId', memberIds)
|
|
1262
|
+
.fetch();
|
|
1263
|
+
const organization = this.organizationId ? (await Organization.getByID(this.organizationId))! : null;
|
|
1264
|
+
|
|
1265
|
+
const recipientsMap: Map<string, EmailRecipient> = new Map();
|
|
1266
|
+
for (const memberId of memberIds) {
|
|
1267
|
+
const preferred = emailRecipients.find(e => e.memberId === memberId && (e.userId === user.id || e.email === user.email));
|
|
1268
|
+
if (preferred) {
|
|
1269
|
+
recipientsMap.set(preferred.duplicateOfRecipientId ?? preferred.id, preferred);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const byMember = emailRecipients.find(e => e.memberId === memberId && e.userId === null && e.email === null);
|
|
1274
|
+
if (byMember) {
|
|
1275
|
+
recipientsMap.set(byMember.duplicateOfRecipientId ?? byMember.id, byMember);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const anyData = emailRecipients.find(e => e.memberId === memberId);
|
|
1279
|
+
if (anyData) {
|
|
1280
|
+
recipientsMap.set(anyData.duplicateOfRecipientId ?? anyData.id, anyData);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Remove duplicates that are marked as the same recipient
|
|
1286
|
+
const cleanedRecipients: EmailRecipient[] = [...recipientsMap.values()];
|
|
1287
|
+
const structures = await EmailRecipient.getStructures(cleanedRecipients);
|
|
1288
|
+
|
|
1289
|
+
const virtualRecipients = structures.map((struct) => {
|
|
1290
|
+
const recipient = struct.getRecipient();
|
|
1291
|
+
|
|
1292
|
+
return {
|
|
1293
|
+
struct,
|
|
1294
|
+
recipient,
|
|
1295
|
+
};
|
|
1296
|
+
});
|
|
1297
|
+
for (const { struct, recipient } of virtualRecipients) {
|
|
1298
|
+
if (!(struct.userId === user.id || struct.email === user.email) && !((struct.userId === null && struct.email === null))) {
|
|
1299
|
+
stripSensitiveRecipientReplacements(recipient, {
|
|
1300
|
+
organization,
|
|
1301
|
+
willFill: true,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
recipient.email = user.email;
|
|
1306
|
+
recipient.userId = user.id;
|
|
1307
|
+
|
|
1308
|
+
// We always refresh the data when we display it on the web (so everything is up to date)
|
|
1309
|
+
await fillRecipientReplacements(recipient, {
|
|
1310
|
+
organization,
|
|
1311
|
+
from: this.getFromAddress(),
|
|
1312
|
+
replyTo: null,
|
|
1313
|
+
forPreview: false,
|
|
1314
|
+
forceRefresh: true,
|
|
1315
|
+
});
|
|
1316
|
+
stripRecipientReplacementsForWebDisplay(recipient, {
|
|
1317
|
+
organization,
|
|
1318
|
+
});
|
|
1319
|
+
struct.replacements = recipient.replacements;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let organizationStruct: BaseOrganization | null = null;
|
|
1323
|
+
if (organization) {
|
|
1324
|
+
organizationStruct = organization.getBaseStructure();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return EmailWithRecipients.create({
|
|
1328
|
+
...this,
|
|
1329
|
+
organization: organizationStruct,
|
|
1330
|
+
recipients: structures,
|
|
1331
|
+
|
|
1332
|
+
// Remove private-like data
|
|
1333
|
+
softBouncesCount: 0,
|
|
1334
|
+
failedCount: 0,
|
|
1335
|
+
emailErrors: null,
|
|
1336
|
+
recipientsErrors: null,
|
|
1337
|
+
succeededCount: 1,
|
|
1338
|
+
emailRecipientsCount: 1,
|
|
1339
|
+
hardBouncesCount: 0,
|
|
1340
|
+
spamComplaintsCount: 0,
|
|
1341
|
+
recipientFilter: EmailRecipientFilter.create({}),
|
|
1342
|
+
membersCount: 1,
|
|
1343
|
+
otherRecipientsCount: 0,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1205
1346
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { column } from '@simonbackx/simple-database';
|
|
2
|
-
import { EmailRecipient as EmailRecipientStruct, Recipient, Replacement } from '@stamhoofd/structures';
|
|
2
|
+
import { EmailRecipient as EmailRecipientStruct, Recipient, Replacement, TinyMember } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { QueryableModel } from '@stamhoofd/sql';
|
|
7
7
|
import { SimpleErrors } from '@simonbackx/simple-errors';
|
|
8
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
+
import { Member } from './Member';
|
|
8
10
|
|
|
9
11
|
export class EmailRecipient extends QueryableModel {
|
|
10
12
|
static table = 'email_recipients';
|
|
@@ -70,6 +72,12 @@ export class EmailRecipient extends QueryableModel {
|
|
|
70
72
|
@column({ type: 'string', nullable: true })
|
|
71
73
|
userId: string | null = null;
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Won't get send if it is a duplicate of another email recipient.
|
|
77
|
+
*/
|
|
78
|
+
@column({ type: 'string', nullable: true })
|
|
79
|
+
duplicateOfRecipientId: string | null = null;
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* Set when the email was send, but we received a hard bounce for this specific email
|
|
75
83
|
* Contains the full output we received in the bounce
|
|
@@ -134,11 +142,32 @@ export class EmailRecipient extends QueryableModel {
|
|
|
134
142
|
})
|
|
135
143
|
updatedAt: Date;
|
|
136
144
|
|
|
137
|
-
getStructure() {
|
|
145
|
+
async getStructure() {
|
|
146
|
+
return (await EmailRecipient.getStructures([this]))[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getStructureWithoutRelations() {
|
|
138
150
|
return EmailRecipientStruct.create(this);
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
static async getStructures(models: EmailRecipient[]) {
|
|
154
|
+
const memberIds = Formatter.uniqueArray(models.map(m => m.memberId).filter(m => m) as string[]);
|
|
155
|
+
const members = await Member.getByIDs(...memberIds);
|
|
156
|
+
return models.map((m) => {
|
|
157
|
+
const struct = EmailRecipientStruct.create(m);
|
|
158
|
+
|
|
159
|
+
if (m.memberId) {
|
|
160
|
+
const member = members.find(me => me.id === m.memberId);
|
|
161
|
+
if (member) {
|
|
162
|
+
struct.member = TinyMember.create(member);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return struct;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
141
170
|
getRecipient() {
|
|
142
|
-
return this.
|
|
171
|
+
return this.getStructureWithoutRelations().getRecipient();
|
|
143
172
|
}
|
|
144
173
|
}
|
package/src/models/Event.ts
CHANGED
|
@@ -87,6 +87,8 @@ export class Event extends QueryableModel {
|
|
|
87
87
|
group.settings.requireGroupIds = this.meta.groups?.map(g => g.id) ?? [];
|
|
88
88
|
group.settings.startDate = this.startDate;
|
|
89
89
|
group.settings.endDate = this.endDate;
|
|
90
|
+
group.settings.minAge = this.meta.minAge ?? null;
|
|
91
|
+
group.settings.maxAge = this.meta.maxAge ?? null;
|
|
90
92
|
|
|
91
93
|
if (group.type === GroupType.EventRegistration) {
|
|
92
94
|
// Don't change the name of the waiting list
|
package/src/models/Member.ts
CHANGED
|
@@ -315,7 +315,7 @@ export class Member extends QueryableModel {
|
|
|
315
315
|
/**
|
|
316
316
|
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
317
317
|
*/
|
|
318
|
-
static async
|
|
318
|
+
static async getMemberIdsForUser(user: User): Promise<string[]> {
|
|
319
319
|
const query = SQL
|
|
320
320
|
.select('id')
|
|
321
321
|
.from(Member.table)
|
|
@@ -332,14 +332,14 @@ export class Member extends QueryableModel {
|
|
|
332
332
|
);
|
|
333
333
|
|
|
334
334
|
const data = await query.fetch();
|
|
335
|
-
return data.map(r => r.members.id as string);
|
|
335
|
+
return Formatter.uniqueArray(data.map(r => r.members.id as string));
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
/**
|
|
339
339
|
* Fetch all members with their corresponding (valid) registrations or waiting lists and payments
|
|
340
340
|
*/
|
|
341
341
|
static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithRegistrations[]> {
|
|
342
|
-
return this.getBlobByIds(...(await this.
|
|
342
|
+
return this.getBlobByIds(...(await this.getMemberIdsForUser(user)));
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
static getRegistrationWithTinyMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithTinyMember {
|
package/src/models/Platform.ts
CHANGED
|
@@ -25,6 +25,9 @@ export class Platform extends QueryableModel {
|
|
|
25
25
|
@column({ type: 'string', nullable: true })
|
|
26
26
|
previousPeriodId: string | null = null;
|
|
27
27
|
|
|
28
|
+
@column({ type: 'string', nullable: true })
|
|
29
|
+
nextPeriodId: string | null = null;
|
|
30
|
+
|
|
28
31
|
@column({ type: 'string', nullable: true })
|
|
29
32
|
membershipOrganizationId: string | null = null;
|
|
30
33
|
|
|
@@ -56,6 +59,7 @@ export class Platform extends QueryableModel {
|
|
|
56
59
|
async setPreviousPeriodId() {
|
|
57
60
|
const period = await RegistrationPeriod.getByID(this.periodId);
|
|
58
61
|
this.previousPeriodId = period?.previousPeriodId ?? null;
|
|
62
|
+
this.nextPeriodId = period?.nextPeriodId ?? null;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
static async getSharedPrivateStruct(): Promise<PlatformStruct & { privateConfig: PlatformPrivateConfig }> {
|
|
@@ -13,9 +13,15 @@ export class RegistrationPeriod extends QueryableModel {
|
|
|
13
13
|
})
|
|
14
14
|
id!: string;
|
|
15
15
|
|
|
16
|
+
@column({ type: 'string', nullable: true })
|
|
17
|
+
customName: string | null = null;
|
|
18
|
+
|
|
16
19
|
@column({ type: 'string', nullable: true })
|
|
17
20
|
previousPeriodId: string | null = null;
|
|
18
21
|
|
|
22
|
+
@column({ type: 'string', nullable: true })
|
|
23
|
+
nextPeriodId: string | null = null;
|
|
24
|
+
|
|
19
25
|
@column({ type: 'string', nullable: true })
|
|
20
26
|
organizationId: string | null = null;
|
|
21
27
|
|
|
@@ -74,12 +80,25 @@ export class RegistrationPeriod extends QueryableModel {
|
|
|
74
80
|
return RegistrationPeriod.fromRow(result[this.table]) ?? null;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
async
|
|
78
|
-
|
|
83
|
+
async updatePreviousNextPeriods() {
|
|
84
|
+
return await RegistrationPeriod.updatePreviousNextPeriods(this.organizationId, this);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static async updatePreviousNextPeriods(organizationId: string | null, existingReference?: RegistrationPeriod) {
|
|
88
|
+
const allPeriods = await RegistrationPeriod.select().where('organizationId', organizationId).fetch();
|
|
79
89
|
|
|
80
90
|
// Include self if not yet in database
|
|
81
|
-
if (
|
|
82
|
-
|
|
91
|
+
if (existingReference) {
|
|
92
|
+
if (!existingReference.existsInDatabase) {
|
|
93
|
+
allPeriods.push(existingReference);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Replace self with this
|
|
97
|
+
const index = allPeriods.findIndex(p => p.id === existingReference.id);
|
|
98
|
+
if (index !== -1) {
|
|
99
|
+
allPeriods[index] = existingReference;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
// Sort by start date
|
|
@@ -89,12 +108,18 @@ export class RegistrationPeriod extends QueryableModel {
|
|
|
89
108
|
let previousPeriod: RegistrationPeriod | null = null;
|
|
90
109
|
|
|
91
110
|
for (const period of allPeriods) {
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
period.previousPeriodId = previousPeriod?.id ?? null;
|
|
112
|
+
period.nextPeriodId = null;
|
|
113
|
+
|
|
114
|
+
if (previousPeriod) {
|
|
115
|
+
previousPeriod.nextPeriodId = period.id;
|
|
94
116
|
}
|
|
95
117
|
previousPeriod = period;
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
// Save all
|
|
121
|
+
for (const period of allPeriods) {
|
|
122
|
+
await period.save();
|
|
123
|
+
}
|
|
99
124
|
}
|
|
100
125
|
}
|
package/src/models/User.ts
CHANGED
|
@@ -228,13 +228,19 @@ export class User extends QueryableModel {
|
|
|
228
228
|
await Database.update(query, [this.id, other.id]);
|
|
229
229
|
|
|
230
230
|
// Update payments
|
|
231
|
-
const query2 = 'UPDATE payments SET
|
|
231
|
+
const query2 = 'UPDATE payments SET payingUserId = ? WHERE payingUserId = ?';
|
|
232
232
|
await Database.update(query2, [this.id, other.id]);
|
|
233
233
|
|
|
234
234
|
// Update orders
|
|
235
235
|
const query3 = 'UPDATE webshop_orders SET userId = ? WHERE userId = ?';
|
|
236
236
|
await Database.update(query3, [this.id, other.id]);
|
|
237
237
|
|
|
238
|
+
// Update emails
|
|
239
|
+
await Database.update('UPDATE emails SET userId = ? WHERE userId = ?', [this.id, other.id]);
|
|
240
|
+
|
|
241
|
+
// Update email recipients
|
|
242
|
+
await Database.update('UPDATE email_recipients SET userId = ? WHERE userId = ?', [this.id, other.id]);
|
|
243
|
+
|
|
238
244
|
await other.delete();
|
|
239
245
|
}
|
|
240
246
|
|