@stamhoofd/models 2.12.0 → 2.16.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 (183) hide show
  1. package/dist/src/factories/GroupFactory.js +13 -6
  2. package/dist/src/factories/GroupFactory.js.map +1 -1
  3. package/dist/src/factories/MemberFactory.js +10 -4
  4. package/dist/src/factories/MemberFactory.js.map +1 -1
  5. package/dist/src/factories/OrganizationFactory.js +11 -6
  6. package/dist/src/factories/OrganizationFactory.js.map +1 -1
  7. package/dist/src/factories/ParentFactory.js +2 -3
  8. package/dist/src/factories/ParentFactory.js.map +1 -1
  9. package/dist/src/factories/RecordFactory.js +1 -0
  10. package/dist/src/factories/RecordFactory.js.map +1 -1
  11. package/dist/src/factories/RegisterCodeFactory.js +2 -3
  12. package/dist/src/factories/RegisterCodeFactory.js.map +1 -1
  13. package/dist/src/factories/RegistrationFactory.js +2 -0
  14. package/dist/src/factories/RegistrationFactory.js.map +1 -1
  15. package/dist/src/factories/UserFactory.js +15 -5
  16. package/dist/src/factories/UserFactory.js.map +1 -1
  17. package/dist/src/factories/WebshopFactory.js +7 -3
  18. package/dist/src/factories/WebshopFactory.js.map +1 -1
  19. package/dist/src/helpers/DNSValidator.js +1 -2
  20. package/dist/src/helpers/DNSValidator.js.map +1 -1
  21. package/dist/src/helpers/EmailBuilder.d.ts +23 -5
  22. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  23. package/dist/src/helpers/EmailBuilder.js +73 -21
  24. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  25. package/dist/src/helpers/GroupBuilder.js +2 -2
  26. package/dist/src/helpers/GroupBuilder.js.map +1 -1
  27. package/dist/src/helpers/Handlebars.js +3 -5
  28. package/dist/src/helpers/Handlebars.js.map +1 -1
  29. package/dist/src/helpers/InvoiceBuilder.js +11 -10
  30. package/dist/src/helpers/InvoiceBuilder.js.map +1 -1
  31. package/dist/src/helpers/MemberMerger.d.ts +13 -0
  32. package/dist/src/helpers/MemberMerger.d.ts.map +1 -0
  33. package/dist/src/helpers/MemberMerger.js +328 -0
  34. package/dist/src/helpers/MemberMerger.js.map +1 -0
  35. package/dist/src/helpers/MemberMerger.test.d.ts +2 -0
  36. package/dist/src/helpers/MemberMerger.test.d.ts.map +1 -0
  37. package/dist/src/helpers/MemberMerger.test.js +592 -0
  38. package/dist/src/helpers/MemberMerger.test.js.map +1 -0
  39. package/dist/src/helpers/RateLimiter.d.ts.map +1 -1
  40. package/dist/src/helpers/RateLimiter.js +10 -5
  41. package/dist/src/helpers/RateLimiter.js.map +1 -1
  42. package/dist/src/helpers/WebshopCounter.js +2 -2
  43. package/dist/src/helpers/WebshopCounter.js.map +1 -1
  44. package/dist/src/index.d.ts +1 -0
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/index.js +1 -0
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/migrations/1605262045-import-postcodes.js +10 -2
  49. package/dist/src/migrations/1723736282-merged-members.sql +19 -0
  50. package/dist/src/models/BalanceItem.js +41 -35
  51. package/dist/src/models/BalanceItem.js.map +1 -1
  52. package/dist/src/models/BalanceItemPayment.js +14 -11
  53. package/dist/src/models/BalanceItemPayment.js.map +1 -1
  54. package/dist/src/models/BuckarooPayment.js +5 -2
  55. package/dist/src/models/BuckarooPayment.js.map +1 -1
  56. package/dist/src/models/Document.js +24 -23
  57. package/dist/src/models/Document.js.map +1 -1
  58. package/dist/src/models/DocumentTemplate.js +36 -25
  59. package/dist/src/models/DocumentTemplate.js.map +1 -1
  60. package/dist/src/models/Email.d.ts.map +1 -1
  61. package/dist/src/models/Email.js +36 -38
  62. package/dist/src/models/Email.js.map +1 -1
  63. package/dist/src/models/EmailRecipient.js +15 -13
  64. package/dist/src/models/EmailRecipient.js.map +1 -1
  65. package/dist/src/models/EmailTemplate.d.ts +5 -0
  66. package/dist/src/models/EmailTemplate.d.ts.map +1 -1
  67. package/dist/src/models/EmailTemplate.js +30 -7
  68. package/dist/src/models/EmailTemplate.js.map +1 -1
  69. package/dist/src/models/EmailVerificationCode.js +32 -25
  70. package/dist/src/models/EmailVerificationCode.js.map +1 -1
  71. package/dist/src/models/Event.js +15 -12
  72. package/dist/src/models/Event.js.map +1 -1
  73. package/dist/src/models/Group.js +24 -21
  74. package/dist/src/models/Group.js.map +1 -1
  75. package/dist/src/models/Image.d.ts +0 -1
  76. package/dist/src/models/Image.d.ts.map +1 -1
  77. package/dist/src/models/Image.js +12 -14
  78. package/dist/src/models/Image.js.map +1 -1
  79. package/dist/src/models/Member.d.ts.map +1 -1
  80. package/dist/src/models/Member.js +34 -32
  81. package/dist/src/models/Member.js.map +1 -1
  82. package/dist/src/models/MemberPlatformMembership.js +22 -16
  83. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  84. package/dist/src/models/MemberResponsibilityRecord.js +10 -8
  85. package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
  86. package/dist/src/models/MergedMember.d.ts +23 -0
  87. package/dist/src/models/MergedMember.d.ts.map +1 -0
  88. package/dist/src/models/MergedMember.js +126 -0
  89. package/dist/src/models/MergedMember.js.map +1 -0
  90. package/dist/src/models/MolliePayment.js +5 -2
  91. package/dist/src/models/MolliePayment.js.map +1 -1
  92. package/dist/src/models/MollieToken.js +14 -14
  93. package/dist/src/models/MollieToken.js.map +1 -1
  94. package/dist/src/models/OneTimeToken.js +15 -13
  95. package/dist/src/models/OneTimeToken.js.map +1 -1
  96. package/dist/src/models/Order.d.ts +1 -1
  97. package/dist/src/models/Order.d.ts.map +1 -1
  98. package/dist/src/models/Order.js +42 -52
  99. package/dist/src/models/Order.js.map +1 -1
  100. package/dist/src/models/Organization.d.ts.map +1 -1
  101. package/dist/src/models/Organization.js +64 -66
  102. package/dist/src/models/Organization.js.map +1 -1
  103. package/dist/src/models/OrganizationRegistrationPeriod.js +8 -7
  104. package/dist/src/models/OrganizationRegistrationPeriod.js.map +1 -1
  105. package/dist/src/models/PasswordToken.js +9 -5
  106. package/dist/src/models/PasswordToken.js.map +1 -1
  107. package/dist/src/models/PayconiqPayment.js +12 -14
  108. package/dist/src/models/PayconiqPayment.js.map +1 -1
  109. package/dist/src/models/Payment.d.ts.map +1 -1
  110. package/dist/src/models/Payment.js +41 -35
  111. package/dist/src/models/Payment.js.map +1 -1
  112. package/dist/src/models/Platform.js +8 -10
  113. package/dist/src/models/Platform.js.map +1 -1
  114. package/dist/src/models/RegisterCode.js +12 -9
  115. package/dist/src/models/RegisterCode.js.map +1 -1
  116. package/dist/src/models/Registration.d.ts.map +1 -1
  117. package/dist/src/models/Registration.js +65 -80
  118. package/dist/src/models/Registration.js.map +1 -1
  119. package/dist/src/models/RegistrationPeriod.js +11 -9
  120. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  121. package/dist/src/models/STCredit.js +11 -7
  122. package/dist/src/models/STCredit.js.map +1 -1
  123. package/dist/src/models/STInvoice.js +45 -45
  124. package/dist/src/models/STInvoice.js.map +1 -1
  125. package/dist/src/models/STPackage.d.ts.map +1 -1
  126. package/dist/src/models/STPackage.js +58 -62
  127. package/dist/src/models/STPackage.js.map +1 -1
  128. package/dist/src/models/STPendingInvoice.js +17 -16
  129. package/dist/src/models/STPendingInvoice.js.map +1 -1
  130. package/dist/src/models/StripeAccount.js +15 -14
  131. package/dist/src/models/StripeAccount.js.map +1 -1
  132. package/dist/src/models/StripeCheckoutSession.js +10 -10
  133. package/dist/src/models/StripeCheckoutSession.js.map +1 -1
  134. package/dist/src/models/StripePaymentIntent.js +10 -10
  135. package/dist/src/models/StripePaymentIntent.js.map +1 -1
  136. package/dist/src/models/Ticket.js +46 -32
  137. package/dist/src/models/Ticket.js.map +1 -1
  138. package/dist/src/models/Token.js +12 -5
  139. package/dist/src/models/Token.js.map +1 -1
  140. package/dist/src/models/UsedRegisterCode.js +15 -11
  141. package/dist/src/models/UsedRegisterCode.js.map +1 -1
  142. package/dist/src/models/User.d.ts +2 -1
  143. package/dist/src/models/User.d.ts.map +1 -1
  144. package/dist/src/models/User.js +51 -29
  145. package/dist/src/models/User.js.map +1 -1
  146. package/dist/src/models/UserPermissions.js +11 -8
  147. package/dist/src/models/UserPermissions.js.map +1 -1
  148. package/dist/src/models/Webshop.js +41 -37
  149. package/dist/src/models/Webshop.js.map +1 -1
  150. package/dist/src/models/WebshopDiscountCode.js +13 -9
  151. package/dist/src/models/WebshopDiscountCode.js.map +1 -1
  152. package/dist/src/models/addresses/City.js +9 -8
  153. package/dist/src/models/addresses/City.js.map +1 -1
  154. package/dist/src/models/addresses/PostalCode.js +7 -3
  155. package/dist/src/models/addresses/PostalCode.js.map +1 -1
  156. package/dist/src/models/addresses/Province.js +5 -2
  157. package/dist/src/models/addresses/Province.js.map +1 -1
  158. package/dist/src/models/addresses/Street.js +6 -3
  159. package/dist/src/models/addresses/Street.js.map +1 -1
  160. package/dist/src/models/index.d.ts +1 -0
  161. package/dist/src/models/index.d.ts.map +1 -1
  162. package/dist/src/models/index.js +3 -1
  163. package/dist/src/models/index.js.map +1 -1
  164. package/dist/src/structures/OrganizationServerMetaData.js +30 -27
  165. package/dist/src/structures/OrganizationServerMetaData.js.map +1 -1
  166. package/package.json +3 -3
  167. package/src/helpers/EmailBuilder.ts +95 -17
  168. package/src/helpers/MemberMerger.test.ts +719 -0
  169. package/src/helpers/MemberMerger.ts +497 -0
  170. package/src/helpers/RateLimiter.ts +1 -0
  171. package/src/index.ts +2 -1
  172. package/src/migrations/1723736282-merged-members.sql +19 -0
  173. package/src/models/Email.ts +9 -11
  174. package/src/models/EmailTemplate.ts +20 -1
  175. package/src/models/Member.ts +7 -8
  176. package/src/models/MergedMember.ts +112 -0
  177. package/src/models/Order.ts +11 -21
  178. package/src/models/Organization.ts +17 -26
  179. package/src/models/Payment.ts +1 -0
  180. package/src/models/Registration.ts +17 -45
  181. package/src/models/STPackage.ts +8 -16
  182. package/src/models/User.ts +24 -1
  183. package/src/models/index.ts +1 -0
@@ -0,0 +1,497 @@
1
+ import { Model } from "@simonbackx/simple-database";
2
+ import { SQL } from "@stamhoofd/sql";
3
+ import {
4
+ Address,
5
+ BooleanStatus,
6
+ Gender,
7
+ MemberDetails,
8
+ Parent,
9
+ ParentType,
10
+ RecordAnswer,
11
+ } from "@stamhoofd/structures";
12
+ import { Formatter } from "@stamhoofd/utility";
13
+ import {
14
+ BalanceItem,
15
+ Document,
16
+ Member,
17
+ MemberPlatformMembership,
18
+ MemberResponsibilityRecord,
19
+ MergedMember,
20
+ Registration,
21
+ User,
22
+ } from "../models";
23
+
24
+ export async function mergeMultipleMembers(members: Member[]) {
25
+ const { base, others } = selectBaseMember(members);
26
+
27
+ for (const other of others) {
28
+ await mergeTwoMembers(base, other);
29
+ }
30
+ }
31
+
32
+ export async function findEqualMembers({
33
+ firstName,
34
+ lastName,
35
+ birthDay,
36
+ }: {
37
+ firstName: string;
38
+ lastName: string;
39
+ birthDay: string;
40
+ }): Promise<Member[]> {
41
+ return await Member.where({
42
+ firstName,
43
+ lastName,
44
+ birthDay,
45
+ });
46
+ }
47
+
48
+ async function mergeTwoMembers(base: Member, other: Member): Promise<void> {
49
+ mergeMemberDetails(base, other);
50
+
51
+ await mergeRegistrations(base, other);
52
+ await mergeUsers(base, other);
53
+ await mergeResponsibilities(base, other);
54
+ await mergeBalanceItems(base, other);
55
+ await mergeDocuments(base, other);
56
+ await mergeMemberPlatformMemberships(base, other);
57
+
58
+ await base.save();
59
+ // store other member in merged_member table
60
+ const mergedMember = MergedMember.fromMember(other, base.id);
61
+ await mergedMember.save();
62
+ await other.delete();
63
+ }
64
+
65
+ async function mergeRegistrations(base: Member, other: Member) {
66
+ await mergeModels(base, other, Registration);
67
+ }
68
+
69
+ async function mergeUsers(base: Member, other: Member) {
70
+ await mergeModels(base, other, User);
71
+ }
72
+
73
+ async function mergeResponsibilities(base: Member, other: Member) {
74
+ async function getResponsibilities(memberId: string) {
75
+ const rows = await SQL.select()
76
+ .from(SQL.table(MemberResponsibilityRecord.table))
77
+ .where(SQL.column("memberId"), memberId)
78
+ .fetch();
79
+
80
+ return MemberResponsibilityRecord.fromRows(
81
+ rows,
82
+ MemberResponsibilityRecord.table
83
+ );
84
+ }
85
+
86
+ const otherResponsibilities = await getResponsibilities(other.id);
87
+ const baseResponsibilities = await getResponsibilities(base.id);
88
+
89
+ // Delete duplicate responsibilities where endDate is null -> keep responsibility with oldest start date
90
+ for (const otherResponsibility of otherResponsibilities) {
91
+ // check if equal responsibilities exist
92
+ const otherResponsibilitiesWithoutCurrent =
93
+ otherResponsibilities.filter(
94
+ (o) => o.id !== otherResponsibility.id
95
+ );
96
+ const equalResponsibilities = baseResponsibilities
97
+ .concat(otherResponsibilitiesWithoutCurrent)
98
+ .filter((baseResponsibility) => {
99
+ return (
100
+ baseResponsibility.responsibilityId ===
101
+ otherResponsibility.responsibilityId &&
102
+ baseResponsibility.organizationId ===
103
+ otherResponsibility.organizationId &&
104
+ baseResponsibility.groupId ===
105
+ otherResponsibility.groupId &&
106
+ baseResponsibility.endDate === null &&
107
+ otherResponsibility.endDate === null
108
+ );
109
+ });
110
+
111
+ if (equalResponsibilities.length > 0) {
112
+ const allEqualResponsibilities = [
113
+ ...equalResponsibilities,
114
+ otherResponsibility,
115
+ ]
116
+ // sort on startDate
117
+ .sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
118
+
119
+ const responsibilityWithOldestStartDate =
120
+ allEqualResponsibilities[0];
121
+
122
+ const responsibilitiesToDelete = allEqualResponsibilities.slice(
123
+ 1,
124
+ undefined
125
+ );
126
+
127
+ for (const responsibilityToDelete of responsibilitiesToDelete) {
128
+ const baseIndex = baseResponsibilities.indexOf(
129
+ responsibilityToDelete
130
+ );
131
+ if (baseIndex !== -1) baseResponsibilities.splice(baseIndex, 1);
132
+ else {
133
+ const otherIndex = otherResponsibilities.indexOf(
134
+ responsibilityToDelete
135
+ );
136
+ if (otherIndex !== -1)
137
+ otherResponsibilities.splice(otherIndex, 1);
138
+ }
139
+
140
+ if (responsibilityToDelete.existsInDatabase) {
141
+ await responsibilityToDelete.delete();
142
+ }
143
+ }
144
+
145
+ if (responsibilityWithOldestStartDate.memberId !== base.id) {
146
+ responsibilityWithOldestStartDate.memberId = base.id;
147
+ await responsibilityWithOldestStartDate.save();
148
+ }
149
+ } else {
150
+ otherResponsibility.memberId = base.id;
151
+ await otherResponsibility.save();
152
+ }
153
+ }
154
+ }
155
+
156
+ async function mergeBalanceItems(base: Member, other: Member) {
157
+ await mergeModels(base, other, BalanceItem);
158
+ }
159
+
160
+ async function mergeDocuments(base: Member, other: Member) {
161
+ await mergeModels(base, other, Document);
162
+ }
163
+
164
+ async function mergeMemberPlatformMemberships(base: Member, other: Member) {
165
+ await mergeModels(base, other, MemberPlatformMembership);
166
+ }
167
+
168
+ class ModelWithMemberId extends Model {
169
+ memberId: string | null;
170
+ }
171
+
172
+ async function mergeModels<M extends typeof ModelWithMemberId>(
173
+ base: Member,
174
+ other: Member,
175
+ model: M
176
+ ) {
177
+ const baseId = base.id;
178
+ const otherModels = await model.where({
179
+ memberId: other.id,
180
+ });
181
+
182
+ for (const otherModel of otherModels) {
183
+ otherModel.memberId = baseId;
184
+ await otherModel.save();
185
+ }
186
+ }
187
+
188
+ export function mergeMemberDetails(base: Member, other: Member): void {
189
+ const baseDetails = base.details;
190
+ const otherDetails = other.details;
191
+
192
+ // string details
193
+ mergeStringIfBaseNotSet(baseDetails, otherDetails, "firstName");
194
+ mergeStringIfBaseNotSet(baseDetails, otherDetails, "lastName");
195
+
196
+ mergeStringIfBaseNotSet(baseDetails, otherDetails, "memberNumber");
197
+ mergeStringIfBaseNotSet(baseDetails, otherDetails, "uitpasNumber");
198
+
199
+ // email
200
+ mergeEmail(baseDetails, otherDetails, baseDetails.unverifiedEmails);
201
+
202
+ // phone
203
+ mergePhone(baseDetails, otherDetails, baseDetails);
204
+
205
+ // gender
206
+ if (baseDetails.gender === Gender.Other) {
207
+ baseDetails.gender = otherDetails.gender;
208
+ }
209
+
210
+ // notes
211
+ mergeNotes(baseDetails, otherDetails);
212
+
213
+ // date
214
+ mergeIfBaseNotSet(baseDetails, otherDetails, "birthDay");
215
+
216
+ // boolean status
217
+ mergeBooleanStatusIfBaseNotSet(
218
+ baseDetails,
219
+ otherDetails,
220
+ "requiresFinancialSupport"
221
+ );
222
+
223
+ mergeBooleanStatusIfBaseNotSet(
224
+ baseDetails,
225
+ otherDetails,
226
+ "dataPermissions"
227
+ );
228
+
229
+ // address
230
+ mergeAddress(baseDetails, otherDetails, baseDetails);
231
+
232
+ // parents
233
+ mergeParents(baseDetails, otherDetails);
234
+
235
+ // emergency contacts
236
+ baseDetails.emergencyContacts = baseDetails.emergencyContacts.concat(
237
+ // add contacts that are not yet in the list
238
+ otherDetails.emergencyContacts.filter(
239
+ (otherContact) =>
240
+ !baseDetails.emergencyContacts.some((baseContact) =>
241
+ baseContact.isEqual(otherContact)
242
+ )
243
+ )
244
+ );
245
+
246
+ // review times
247
+ // todo: is this correct?
248
+ baseDetails.reviewTimes.merge(otherDetails.reviewTimes);
249
+
250
+ // answers
251
+ mergeAnswers(baseDetails, otherDetails);
252
+
253
+ // unverified data
254
+ baseDetails.unverifiedEmails = Formatter.uniqueArray(
255
+ baseDetails.unverifiedEmails.concat(
256
+ otherDetails.unverifiedEmails.filter(
257
+ (email) => !isNullOrEmpty(email)
258
+ )
259
+ )
260
+ );
261
+ baseDetails.unverifiedPhones = Formatter.uniqueArray(
262
+ baseDetails.unverifiedPhones.concat(
263
+ otherDetails.unverifiedPhones.filter(
264
+ (phone) => !isNullOrEmpty(phone)
265
+ )
266
+ )
267
+ );
268
+
269
+ // unverified addresses
270
+ for (const address of otherDetails.unverifiedAddresses) {
271
+ if (!baseDetails.unverifiedAddresses.some((a) => a.id === address.id)) {
272
+ baseDetails.unverifiedAddresses.push(address);
273
+ }
274
+ }
275
+ }
276
+
277
+ export function selectBaseMember(members: Member[]): {
278
+ base: Member;
279
+ others: Member[];
280
+ } {
281
+ if (members.length < 2) {
282
+ throw Error("Members array length is less than 2.");
283
+ }
284
+ const sorted = members.sort(
285
+ (m1, m2) => m2.createdAt.getTime() - m1.createdAt.getTime()
286
+ );
287
+
288
+ return { base: sorted[0], others: sorted.slice(1, undefined) };
289
+ }
290
+
291
+ function mergeAnswers(base: MemberDetails, other: MemberDetails) {
292
+ const newAnswers: Map<string, RecordAnswer> = new Map(base.recordAnswers);
293
+ for (const otherAnswer of other.recordAnswers.values()) {
294
+ const otherId = otherAnswer.settings.id;
295
+ const baseAnswer = newAnswers.get(otherId);
296
+
297
+ if (!baseAnswer) {
298
+ newAnswers.set(otherId, otherAnswer);
299
+ } else if (otherAnswer.date >= baseAnswer.date) {
300
+ newAnswers.set(otherId, otherAnswer);
301
+ } else {
302
+ // keep existing, this one is more up-to-date, don't add the other answer
303
+ }
304
+ }
305
+ base.recordAnswers = newAnswers;
306
+ }
307
+
308
+ function mergeNotes(base: MemberDetails, other: MemberDetails) {
309
+ if (base.notes && other.notes) {
310
+ base.notes = `${base.notes}\n${other.notes}`;
311
+ } else if (base.notes) {
312
+ return;
313
+ } else {
314
+ base.notes = other.notes;
315
+ }
316
+ }
317
+
318
+ function mergeParents(base: MemberDetails, other: MemberDetails) {
319
+ const baseParents = base.parents;
320
+ const otherParents = other.parents;
321
+ const parentsToAdd: Parent[] = [];
322
+
323
+ for (const otherParent of otherParents) {
324
+ // equal if same first and last name
325
+ const equalBaseParent = baseParents.find(
326
+ (baseParent) =>
327
+ hasEqualStringValue(baseParent, otherParent, "firstName") &&
328
+ hasEqualStringValue(baseParent, otherParent, "lastName")
329
+ );
330
+
331
+ if (!equalBaseParent) {
332
+ parentsToAdd.push(otherParent);
333
+ continue;
334
+ }
335
+
336
+ mergeParent(equalBaseParent, otherParent, base);
337
+ }
338
+
339
+ base.parents = baseParents.concat(parentsToAdd);
340
+ }
341
+
342
+ function mergeParent(base: Parent, other: Parent, baseDetails: MemberDetails) {
343
+ if (base.type === ParentType.Other) {
344
+ base.type = other.type;
345
+ }
346
+ mergeStringIfBaseNotSet(base, other, "firstName");
347
+ mergeStringIfBaseNotSet(base, other, "lastName");
348
+ // add other emails to alternative emails
349
+ mergeEmail(base, other, base.alternativeEmails);
350
+ mergePhone(base, other, baseDetails);
351
+ mergeAddress(base, other, baseDetails);
352
+ }
353
+
354
+ function mergeEmail(
355
+ base: { email: string | null | undefined },
356
+ other: { email: string | null | undefined },
357
+ alternativeEmails: string[]
358
+ ) {
359
+ const isEmailMerged = mergeStringIfBaseNotSet(base, other, "email");
360
+ const otherEmail = other.email;
361
+ if (!isEmailMerged && !isNullOrEmpty(otherEmail)) {
362
+ if (!alternativeEmails.some((email) => email === otherEmail)) {
363
+ alternativeEmails.push(otherEmail!);
364
+ }
365
+ }
366
+ }
367
+
368
+ function mergePhone(
369
+ base: { phone: string | null | undefined },
370
+ other: { phone: string | null | undefined },
371
+ baseDetails: MemberDetails
372
+ ) {
373
+ const isPhoneMerged = mergeStringIfBaseNotSet(base, other, "phone");
374
+ const otherPhone = other.phone;
375
+ if (!isPhoneMerged && !isNullOrEmpty(otherPhone)) {
376
+ if (
377
+ !baseDetails.unverifiedPhones.some((phone) => phone === otherPhone)
378
+ ) {
379
+ baseDetails.unverifiedPhones.push(otherPhone!);
380
+ }
381
+ }
382
+ }
383
+
384
+ function mergeAddress(
385
+ base: { address: Address | null | undefined },
386
+ other: { address: Address | null | undefined },
387
+ baseDetails: MemberDetails
388
+ ) {
389
+ const baseAddress = base.address;
390
+ const otherAddress = other.address;
391
+
392
+ if (!baseAddress) {
393
+ base.address = otherAddress;
394
+ } else if (otherAddress && baseAddress.id !== otherAddress.id) {
395
+ // add other address to unverified addresses
396
+ if (
397
+ !baseDetails.unverifiedAddresses.some(
398
+ (address) => address.id === otherAddress.id
399
+ )
400
+ ) {
401
+ baseDetails.unverifiedAddresses.push(otherAddress);
402
+ }
403
+ }
404
+ }
405
+
406
+ function mergeStringIfBaseNotSet<T, K extends keyof T>(
407
+ base: T,
408
+ other: T,
409
+ key: K & (T[K] extends string | null | undefined ? K : never)
410
+ ): boolean {
411
+ const baseValue = base[key] as string | null | undefined;
412
+ if (!isNullOrEmpty(baseValue)) {
413
+ return false;
414
+ }
415
+ const otherValue = other[key] as string | null | undefined;
416
+ if (isNullOrEmpty(otherValue)) {
417
+ return false;
418
+ }
419
+
420
+ (base[key] as string | null | undefined) = otherValue;
421
+
422
+ return true;
423
+ }
424
+
425
+ function mergeIfBaseNotSet<T, K extends keyof T>(
426
+ base: T,
427
+ other: T,
428
+ key: K &
429
+ (T[K] extends number | Date | boolean | null | undefined ? K : never)
430
+ ): boolean {
431
+ const baseValue = base[key] as number | Date | boolean | null | undefined;
432
+ if (!(baseValue === null || baseValue === undefined)) return false;
433
+ const otherValue = other[key] as number | Date | boolean | null | undefined;
434
+ if (otherValue === null || otherValue === undefined) return false;
435
+ (base[key] as number | Date | boolean | null | undefined) = otherValue;
436
+ return true;
437
+ }
438
+
439
+ function mergeBooleanStatusIfBaseNotSet<T, K extends keyof T>(
440
+ base: T,
441
+ other: T,
442
+ key: K & (T[K] extends BooleanStatus | null | undefined ? K : never)
443
+ ): boolean {
444
+ const otherValue = other[key] as BooleanStatus | null | undefined;
445
+ if (otherValue === null || otherValue === undefined) return false;
446
+ const baseValue = base[key] as BooleanStatus | null | undefined;
447
+ if (!(baseValue === null || baseValue === undefined)) {
448
+ if (baseValue.date < otherValue.date) {
449
+ (base[key] as BooleanStatus | null | undefined) = otherValue;
450
+ return true;
451
+ }
452
+ return false;
453
+ }
454
+
455
+ (base[key] as BooleanStatus | null | undefined) = otherValue;
456
+ return true;
457
+ }
458
+
459
+ /**
460
+ * Returns true if the values of the key for a and b
461
+ * are not null or undefined
462
+ * and both are equal.
463
+ * @param a
464
+ * @param b
465
+ * @param key
466
+ * @returns
467
+ */
468
+ function hasEqualStringValue<T, K extends keyof T>(
469
+ a: T,
470
+ b: T,
471
+ key: K & (T[K] extends string | null | undefined ? K : never)
472
+ ) {
473
+ return hasValueAndIsEqual(
474
+ a[key] as string | null | undefined,
475
+ b[key] as string | null | undefined
476
+ );
477
+ }
478
+
479
+ function hasValueAndIsEqual(
480
+ a: string | null | undefined,
481
+ b: string | null | undefined
482
+ ): boolean {
483
+ if (isNullOrEmpty(a) || isNullOrEmpty(b)) return false;
484
+ return isStringEqual(a as string, b as string);
485
+ }
486
+
487
+ function isStringEqual(a: string, b: string): boolean {
488
+ return toLowerTrim(a) === toLowerTrim(b);
489
+ }
490
+
491
+ function toLowerTrim(name: string) {
492
+ return name.toLowerCase().trim();
493
+ }
494
+
495
+ function isNullOrEmpty(value: string | null | undefined) {
496
+ return value === null || value === undefined || value.trim() === "";
497
+ }
@@ -42,6 +42,7 @@ export class RateLimitWindow {
42
42
  throw new SimpleError({
43
43
  code: 'rate_limit',
44
44
  message: `Rate limit exceeded (${w} ${amount > 1 ? '('+amount+' added)' : ''} requests in ${Math.round(this.age/1000)}s). Retry after ${retryAfter}s. Check your code and try to reduce the number of (parallel) requests you make. Add waiting periods if needed.`,
45
+ human: `Oeps! Te veel aanvragen. Om spam te vermijden is jouw aanvraag tijdelijk geblokkeerd. Probeer het over ${retryAfter} seconden opnieuw.`,
45
46
  statusCode: 429
46
47
  })
47
48
  }
package/src/index.ts CHANGED
@@ -15,7 +15,8 @@ export * from "./helpers/EmailBuilder"
15
15
  export * from "./helpers/GroupBuilder"
16
16
  export * from "./helpers/RateLimiter"
17
17
  export * from "./helpers/WebshopCounter"
18
+ export * from "./helpers/MemberMerger"
18
19
 
19
20
  // Models
20
21
  export * from "./models"
21
- export * from "./structures/OrganizationServerMetaData"
22
+ export * from "./structures/OrganizationServerMetaData"
@@ -0,0 +1,19 @@
1
+ CREATE TABLE `merged_members` (
2
+ `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
3
+ `firstName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
4
+ `lastName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
5
+ `birthDay` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
6
+ `organizationId` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
7
+ `details` json NOT NULL,
8
+ `createdAt` datetime NOT NULL,
9
+ `updatedAt` datetime NOT NULL,
10
+ `outstandingBalance` int NOT NULL DEFAULT '0',
11
+ `memberNumber` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
12
+ `mergedToId` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
13
+ `mergedAt` datetime NOT NULL,
14
+ PRIMARY KEY (`id`),
15
+ KEY `organizationId` (`organizationId`),
16
+ CONSTRAINT `merged_members_ibfk_1` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
17
+ KEY `mergedToId` (`mergedToId`),
18
+ CONSTRAINT `merged_members_ibfk_2` FOREIGN KEY (`mergedToId`) REFERENCES `members` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
19
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -241,17 +241,15 @@ export class Email extends Model {
241
241
 
242
242
  // eslint-disable-next-line no-constant-condition
243
243
  while (true) {
244
- const q = SQL.select()
245
- .from(SQL.table('email_recipients'))
246
- .where(SQL.column('emailId'), upToDate.id)
247
- .where(SQL.column('sentAt'), null)
248
- .where(SQL.column('id'), SQLWhereSign.Greater, idPointer);
249
-
250
- q.orderBy(SQL.column('id'), 'ASC')
251
- q.limit(batchSize)
244
+ const data = await SQL.select()
245
+ .from('email_recipients')
246
+ .where('emailId', upToDate.id)
247
+ .where('sentAt', null)
248
+ .where('id', SQLWhereSign.Greater, idPointer)
249
+ .orderBy(SQL.column('id'), 'ASC')
250
+ .limit(batchSize)
251
+ .fetch();
252
252
 
253
- const data = await q.fetch();
254
-
255
253
  const recipients = EmailRecipient.fromRows(data, 'email_recipients');
256
254
 
257
255
  if (recipients.length == 0) {
@@ -301,7 +299,7 @@ export class Email extends Model {
301
299
  from,
302
300
  replyTo,
303
301
  subject: upToDate.subject!,
304
- html: upToDate.html,
302
+ html: upToDate.html!,
305
303
  type: "broadcast",
306
304
  callback(error: Error|null ) {
307
305
  callback(error).catch(console.error)
@@ -1,5 +1,6 @@
1
- import { column, Model } from "@simonbackx/simple-database";
1
+ import { column, Model, SQLResultNamespacedRow } from "@simonbackx/simple-database";
2
2
  import { AnyDecoder } from "@simonbackx/simple-encoding";
3
+ import { SQL, SQLSelect } from "@stamhoofd/sql";
3
4
  import { EmailTemplate as EmailTemplateStruct, EmailTemplateType } from "@stamhoofd/structures";
4
5
  import { v4 as uuidv4 } from "uuid";
5
6
 
@@ -65,4 +66,22 @@ export class EmailTemplate extends Model {
65
66
  getStructure() {
66
67
  return EmailTemplateStruct.create(this)
67
68
  }
69
+
70
+ /**
71
+ * Experimental: needs to move to library
72
+ */
73
+ static select() {
74
+ const transformer = (row: SQLResultNamespacedRow): EmailTemplate => {
75
+ const d = (this as typeof EmailTemplate & typeof Model).fromRow(row[this.table] as any) as EmailTemplate|undefined
76
+
77
+ if (!d) {
78
+ throw new Error("EmailTemplate not found")
79
+ }
80
+
81
+ return d;
82
+ }
83
+
84
+ const select = new SQLSelect(transformer, SQL.wildcard())
85
+ return select.from(SQL.table(this.table))
86
+ }
68
87
  }
@@ -18,7 +18,7 @@ export type RegistrationWithMember = Registration & { member: Member }
18
18
  export class Member extends Model {
19
19
  static table = "members"
20
20
 
21
- // Columns
21
+ //#region Columns
22
22
  @column({
23
23
  primary: true, type: "string", beforeSave(value) {
24
24
  return value ?? uuidv4();
@@ -91,6 +91,7 @@ export class Member extends Model {
91
91
  skipUpdate: true
92
92
  })
93
93
  updatedAt: Date
94
+ //#endregion
94
95
 
95
96
  static registrations = new OneToManyRelation(Member, Registration, "registrations", "memberId")
96
97
 
@@ -354,14 +355,12 @@ export class Member extends Model {
354
355
  */
355
356
  static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithRegistrations[]> {
356
357
  const query = SQL
357
- .select(
358
- SQL.column('id')
359
- )
360
- .from(SQL.table(Member.table))
358
+ .select('id')
359
+ .from(Member.table)
361
360
  .join(
362
- SQL.leftJoin(
363
- SQL.table('_members_users')
364
- ).where(
361
+ SQL
362
+ .leftJoin('_members_users')
363
+ .where(
365
364
  SQL.column('_members_users', 'membersId'),
366
365
  SQL.column(Member.table, 'id'),
367
366
  )