@stamhoofd/models 2.12.0 → 2.13.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/helpers/MemberMerger.d.ts +13 -0
- package/dist/src/helpers/MemberMerger.d.ts.map +1 -0
- package/dist/src/helpers/MemberMerger.js +328 -0
- package/dist/src/helpers/MemberMerger.js.map +1 -0
- package/dist/src/helpers/MemberMerger.test.d.ts +2 -0
- package/dist/src/helpers/MemberMerger.test.d.ts.map +1 -0
- package/dist/src/helpers/MemberMerger.test.js +605 -0
- package/dist/src/helpers/MemberMerger.test.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/migrations/1723736282-merged-members.sql +19 -0
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +1 -0
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/MergedMember.d.ts +23 -0
- package/dist/src/models/MergedMember.d.ts.map +1 -0
- package/dist/src/models/MergedMember.js +121 -0
- package/dist/src/models/MergedMember.js.map +1 -0
- package/dist/src/models/index.d.ts +1 -0
- package/dist/src/models/index.d.ts.map +1 -1
- package/dist/src/models/index.js +3 -1
- package/dist/src/models/index.js.map +1 -1
- package/package.json +2 -2
- package/src/helpers/MemberMerger.test.ts +719 -0
- package/src/helpers/MemberMerger.ts +497 -0
- package/src/index.ts +1 -0
- package/src/migrations/1723736282-merged-members.sql +19 -0
- package/src/models/Member.ts +2 -1
- package/src/models/MergedMember.ts +112 -0
- 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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -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;
|
package/src/models/Member.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Model, column } from "@simonbackx/simple-database";
|
|
2
|
+
import { MemberDetails } from "@stamhoofd/structures";
|
|
3
|
+
import { Member } from "./";
|
|
4
|
+
|
|
5
|
+
export class MergedMember extends Model {
|
|
6
|
+
static override table = "merged_members";
|
|
7
|
+
|
|
8
|
+
//#region Member columns
|
|
9
|
+
@column({
|
|
10
|
+
primary: true,
|
|
11
|
+
type: "string",
|
|
12
|
+
})
|
|
13
|
+
id: string = "";
|
|
14
|
+
|
|
15
|
+
@column({ type: "string", nullable: true })
|
|
16
|
+
organizationId: string | null = null;
|
|
17
|
+
|
|
18
|
+
@column({
|
|
19
|
+
type: "string",
|
|
20
|
+
})
|
|
21
|
+
firstName: string;
|
|
22
|
+
|
|
23
|
+
@column({
|
|
24
|
+
type: "string",
|
|
25
|
+
})
|
|
26
|
+
lastName: string;
|
|
27
|
+
|
|
28
|
+
@column({
|
|
29
|
+
type: "string",
|
|
30
|
+
nullable: true,
|
|
31
|
+
})
|
|
32
|
+
birthDay: string | null;
|
|
33
|
+
|
|
34
|
+
@column({
|
|
35
|
+
type: "string",
|
|
36
|
+
nullable: true,
|
|
37
|
+
})
|
|
38
|
+
memberNumber: string | null;
|
|
39
|
+
|
|
40
|
+
@column({ type: "json", decoder: MemberDetails })
|
|
41
|
+
details: MemberDetails;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Not yet paid balance
|
|
45
|
+
*/
|
|
46
|
+
@column({ type: "integer" })
|
|
47
|
+
outstandingBalance = 0;
|
|
48
|
+
|
|
49
|
+
@column({
|
|
50
|
+
type: "datetime",
|
|
51
|
+
beforeSave(old?: any) {
|
|
52
|
+
if (old !== undefined) {
|
|
53
|
+
return old;
|
|
54
|
+
}
|
|
55
|
+
const date = new Date();
|
|
56
|
+
date.setMilliseconds(0);
|
|
57
|
+
return date;
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
createdAt: Date;
|
|
61
|
+
|
|
62
|
+
@column({
|
|
63
|
+
type: "datetime",
|
|
64
|
+
beforeSave() {
|
|
65
|
+
const date = new Date();
|
|
66
|
+
date.setMilliseconds(0);
|
|
67
|
+
return date;
|
|
68
|
+
},
|
|
69
|
+
skipUpdate: true,
|
|
70
|
+
})
|
|
71
|
+
updatedAt: Date;
|
|
72
|
+
//#endregion
|
|
73
|
+
|
|
74
|
+
//#region extra columns
|
|
75
|
+
@column({
|
|
76
|
+
type: "string",
|
|
77
|
+
nullable: false,
|
|
78
|
+
})
|
|
79
|
+
mergedToId: string = "";
|
|
80
|
+
|
|
81
|
+
@column({
|
|
82
|
+
type: "datetime",
|
|
83
|
+
beforeSave(old?: any) {
|
|
84
|
+
if (old !== undefined) {
|
|
85
|
+
return old;
|
|
86
|
+
}
|
|
87
|
+
const date = new Date();
|
|
88
|
+
date.setMilliseconds(0);
|
|
89
|
+
return date;
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
mergedAt: Date;
|
|
93
|
+
//#endregion
|
|
94
|
+
|
|
95
|
+
static fromMember(member: Member, mergedToId: string): MergedMember {
|
|
96
|
+
const mergedMember = new MergedMember();
|
|
97
|
+
mergedMember.mergedToId = mergedToId;
|
|
98
|
+
mergedMember.mergedAt = new Date();
|
|
99
|
+
|
|
100
|
+
mergedMember.id = member.id;
|
|
101
|
+
mergedMember.organizationId = member.organizationId;
|
|
102
|
+
mergedMember.firstName = member.firstName;
|
|
103
|
+
mergedMember.lastName = member.lastName;
|
|
104
|
+
mergedMember.birthDay = member.birthDay;
|
|
105
|
+
mergedMember.memberNumber = member.memberNumber;
|
|
106
|
+
mergedMember.outstandingBalance = member.outstandingBalance;
|
|
107
|
+
mergedMember.createdAt = new Date(member.createdAt);
|
|
108
|
+
mergedMember.updatedAt = new Date(member.updatedAt);
|
|
109
|
+
mergedMember.details = member.details.clone();
|
|
110
|
+
return mergedMember;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/models/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export {User} from "./User"
|
|
|
4
4
|
export {Payment} from "./Payment"
|
|
5
5
|
export {Registration} from "./Registration"
|
|
6
6
|
export {Member, RegistrationWithMember, MemberWithRegistrations} from "./Member"
|
|
7
|
+
export {MergedMember} from "./MergedMember"
|
|
7
8
|
|
|
8
9
|
export * from "./EmailVerificationCode"
|
|
9
10
|
|