@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.
Files changed (54) hide show
  1. package/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
  2. package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
  3. package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
  4. package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
  5. package/dist/src/helpers/EmailBuilder.d.ts +27 -1
  6. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  7. package/dist/src/helpers/EmailBuilder.js +270 -25
  8. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  9. package/dist/src/migrations/1756115317-email-deleted-at.sql +2 -0
  10. package/dist/src/migrations/1756293494-registration-period-next-period-id.sql +3 -0
  11. package/dist/src/migrations/1756293495-platform-next-period-id.sql +3 -0
  12. package/dist/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
  13. package/dist/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
  14. package/dist/src/models/Email.d.ts +4 -2
  15. package/dist/src/models/Email.d.ts.map +1 -1
  16. package/dist/src/models/Email.js +160 -36
  17. package/dist/src/models/Email.js.map +1 -1
  18. package/dist/src/models/EmailRecipient.d.ts +7 -1
  19. package/dist/src/models/EmailRecipient.d.ts.map +1 -1
  20. package/dist/src/models/EmailRecipient.js +28 -2
  21. package/dist/src/models/EmailRecipient.js.map +1 -1
  22. package/dist/src/models/Event.d.ts.map +1 -1
  23. package/dist/src/models/Event.js +2 -0
  24. package/dist/src/models/Event.js.map +1 -1
  25. package/dist/src/models/Member.d.ts +1 -1
  26. package/dist/src/models/Member.d.ts.map +1 -1
  27. package/dist/src/models/Member.js +3 -3
  28. package/dist/src/models/Member.js.map +1 -1
  29. package/dist/src/models/Platform.d.ts +1 -0
  30. package/dist/src/models/Platform.d.ts.map +1 -1
  31. package/dist/src/models/Platform.js +5 -0
  32. package/dist/src/models/Platform.js.map +1 -1
  33. package/dist/src/models/RegistrationPeriod.d.ts +4 -1
  34. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  35. package/dist/src/models/RegistrationPeriod.js +32 -7
  36. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  37. package/dist/src/models/User.d.ts.map +1 -1
  38. package/dist/src/models/User.js +5 -1
  39. package/dist/src/models/User.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/factories/RegistrationPeriodFactory.ts +4 -0
  42. package/src/helpers/EmailBuilder.ts +282 -34
  43. package/src/migrations/1756115317-email-deleted-at.sql +2 -0
  44. package/src/migrations/1756293494-registration-period-next-period-id.sql +3 -0
  45. package/src/migrations/1756293495-platform-next-period-id.sql +3 -0
  46. package/src/migrations/1756387016-registration-period-custom-name.sql +2 -0
  47. package/src/migrations/1756391212-email-recipients-duplicate.sql +3 -0
  48. package/src/models/Email.ts +184 -43
  49. package/src/models/EmailRecipient.ts +32 -3
  50. package/src/models/Event.ts +2 -0
  51. package/src/models/Member.ts +3 -3
  52. package/src/models/Platform.ts +4 -0
  53. package/src/models/RegistrationPeriod.ts +32 -7
  54. package/src/models/User.ts +7 -1
@@ -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 (recipientsSet.has(recipient.id)) {
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 (!recipient.email) {
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
- return;
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: this.organizationId ? (await Organization.getByID(this.organizationId))! : null,
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.getStructure().getRecipient();
171
+ return this.getStructureWithoutRelations().getRecipient();
143
172
  }
144
173
  }
@@ -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
@@ -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 getMemberIdsWithRegistrationForUser(user: User): Promise<string[]> {
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.getMemberIdsWithRegistrationForUser(user)));
342
+ return this.getBlobByIds(...(await this.getMemberIdsForUser(user)));
343
343
  }
344
344
 
345
345
  static getRegistrationWithTinyMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithTinyMember {
@@ -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 setPreviousPeriodId() {
78
- const allPeriods = await RegistrationPeriod.where({ organizationId: this.organizationId });
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 (!this.existsInDatabase) {
82
- allPeriods.push(this);
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
- if (period.id === this.id) {
93
- break;
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
- this.previousPeriodId = previousPeriod?.id ?? null;
120
+ // Save all
121
+ for (const period of allPeriods) {
122
+ await period.save();
123
+ }
99
124
  }
100
125
  }
@@ -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 userId = ? WHERE userId = ?';
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