@stamhoofd/backend 2.107.3 → 2.109.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/index.ts +2 -2
- package/package.json +20 -20
- package/src/crons/disable-auto-update-documents.test.ts +164 -0
- package/src/crons/disable-auto-update-documents.ts +82 -0
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +5 -5
- package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +87 -0
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +2 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +14 -11
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
- package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
- package/src/excel-loaders/members.ts +62 -8
- package/src/excel-loaders/registrations.ts +180 -9
- package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
- package/src/helpers/MemberCharger.ts +16 -4
- package/src/helpers/SQLTranslatedString.ts +14 -0
- package/src/helpers/TagHelper.test.ts +9 -9
- package/src/helpers/fetchToAsyncIterator.ts +1 -1
- package/src/helpers/outstandingBalanceJoin.ts +49 -0
- package/src/seeds/1765896674-document-update-year.test.ts +179 -0
- package/src/seeds/1765896674-document-update-year.ts +75 -0
- package/src/seeds/1766150402-document-published-at.test.ts +46 -0
- package/src/seeds/1766150402-document-published-at.ts +20 -0
- package/src/services/PaymentService.ts +14 -32
- package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
- package/src/sql-filters/document-templates.ts +45 -0
- package/src/sql-filters/documents.ts +1 -1
- package/src/sql-filters/events.ts +6 -6
- package/src/sql-filters/groups.ts +7 -6
- package/src/sql-filters/members.ts +31 -26
- package/src/sql-filters/orders.ts +16 -16
- package/src/sql-filters/organizations.ts +11 -11
- package/src/sql-filters/payments.ts +10 -10
- package/src/sql-filters/registrations.ts +14 -6
- package/src/sql-sorters/document-templates.ts +79 -0
- package/src/sql-sorters/documents.ts +1 -1
- package/src/sql-sorters/members.ts +22 -0
- package/src/sql-sorters/orders.ts +5 -5
- package/src/sql-sorters/organizations.ts +3 -3
- package/src/sql-sorters/registrations.ts +186 -15
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { XlsxBuiltInNumberFormat, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { Platform } from '@stamhoofd/models';
|
|
3
|
-
import { ExcelExportType, LimitedFilteredRequest, PlatformMember, PlatformRegistration, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
4
|
-
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
|
|
5
|
-
import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint';
|
|
6
|
-
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
|
|
7
|
-
import { Context } from '../helpers/Context';
|
|
8
|
-
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
|
|
9
|
-
import { baseMemberColumns } from './members';
|
|
3
|
+
import { ExcelExportType, getGroupTypeName, LimitedFilteredRequest, PlatformMember, PlatformRegistration, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
4
|
+
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
5
|
+
import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
|
|
6
|
+
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
|
|
7
|
+
import { Context } from '../helpers/Context.js';
|
|
8
|
+
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
|
|
9
|
+
import { baseMemberColumns } from './members.js';
|
|
10
10
|
|
|
11
11
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
12
12
|
const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
@@ -22,7 +22,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
|
22
22
|
}),
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
id: '
|
|
25
|
+
id: 'priceName',
|
|
26
26
|
name: $t(`dcc53f25-f0e9-4e3e-9f4f-e8cfa4e88755`),
|
|
27
27
|
width: 30,
|
|
28
28
|
getValue: (registration: PlatformRegistration) => {
|
|
@@ -31,6 +31,167 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
|
31
31
|
};
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
id: 'price',
|
|
36
|
+
name: $t(`dcc53f25-f0e9-4e3e-9f4f-e8cfa4e88755`),
|
|
37
|
+
width: 30,
|
|
38
|
+
getValue: (registration: PlatformRegistration) => {
|
|
39
|
+
return {
|
|
40
|
+
value: registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPaid + r.amountPending), 0) / 1_0000,
|
|
41
|
+
style: {
|
|
42
|
+
numberFormat: {
|
|
43
|
+
id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'toPay',
|
|
51
|
+
width: 30,
|
|
52
|
+
name: $t(`3a97e6cb-012d-4007-9c54-49d3e5b72909`),
|
|
53
|
+
getValue: (registration: PlatformRegistration) => {
|
|
54
|
+
return {
|
|
55
|
+
value: registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPending), 0) / 1_0000,
|
|
56
|
+
style: {
|
|
57
|
+
numberFormat: {
|
|
58
|
+
id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'outstandingBalance',
|
|
67
|
+
name: $t(`beb45452-dee7-4a7f-956c-e6db06aac20f`),
|
|
68
|
+
width: 30,
|
|
69
|
+
getValue: (v) => {
|
|
70
|
+
return {
|
|
71
|
+
value: v.member.member.balances.reduce((sum, r) => sum + (r.amountOpen), 0) / 1_0000,
|
|
72
|
+
style: {
|
|
73
|
+
numberFormat: {
|
|
74
|
+
id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'registeredAt',
|
|
83
|
+
name: $t(`8bec8990-4632-40b0-93f3-f27a3f2ddbdb`),
|
|
84
|
+
width: 20,
|
|
85
|
+
getValue: (registration: PlatformRegistration) => ({
|
|
86
|
+
value: registration.registeredAt,
|
|
87
|
+
style: {
|
|
88
|
+
numberFormat: {
|
|
89
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'startDate',
|
|
96
|
+
name: $t(`bbe0af99-b574-4719-a505-ca2285fa86e4`),
|
|
97
|
+
width: 20,
|
|
98
|
+
getValue: (registration: PlatformRegistration) => ({
|
|
99
|
+
value: registration.startDate,
|
|
100
|
+
style: {
|
|
101
|
+
numberFormat: {
|
|
102
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'endDate',
|
|
109
|
+
name: $t(`aef10d71-39c4-4cdb-8252-5fd31781abd8`),
|
|
110
|
+
width: 20,
|
|
111
|
+
getValue: (registration: PlatformRegistration) => ({
|
|
112
|
+
value: registration.endDate,
|
|
113
|
+
style: {
|
|
114
|
+
numberFormat: {
|
|
115
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'createdAt',
|
|
122
|
+
name: $t('63a86cdf-8a76-4e8c-9073-4f0b8970e808'),
|
|
123
|
+
width: 20,
|
|
124
|
+
getValue: (registration: PlatformRegistration) => ({
|
|
125
|
+
value: registration.member.member.createdAt,
|
|
126
|
+
style: {
|
|
127
|
+
numberFormat: {
|
|
128
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'organization',
|
|
135
|
+
name: $t('2f325358-6e2f-418c-9fea-31a14abbc17a'),
|
|
136
|
+
width: 40,
|
|
137
|
+
getValue: (registration: PlatformRegistration) => {
|
|
138
|
+
const organization = registration.member.family.getOrganization(registration.group.organizationId);
|
|
139
|
+
return ({
|
|
140
|
+
value: organization?.name ?? $t('836c2cd3-32a3-43f2-b09c-600170fcd9cb'),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'uri',
|
|
146
|
+
name: $t('9d283cbb-7ba2-4a16-88ec-ff0c19f39674'),
|
|
147
|
+
width: 40,
|
|
148
|
+
getValue: (registration: PlatformRegistration) => {
|
|
149
|
+
const organization = registration.member.family.getOrganization(registration.group.organizationId);
|
|
150
|
+
return ({
|
|
151
|
+
value: organization?.uri ?? $t('836c2cd3-32a3-43f2-b09c-600170fcd9cb'),
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'groupRegistration',
|
|
157
|
+
name: $t('7289b10e-a284-40ea-bc57-8287c6566a82'),
|
|
158
|
+
width: 40,
|
|
159
|
+
getValue: (registration: PlatformRegistration) => {
|
|
160
|
+
let value: string;
|
|
161
|
+
|
|
162
|
+
if (registration.payingOrganizationId) {
|
|
163
|
+
const organization = registration.member.organizations.find(o => o.id === registration.payingOrganizationId);
|
|
164
|
+
value = organization ? organization.name : $t(`bd1e59c8-3d4c-4097-ab35-0ce7b20d0e50`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
value = $t(`b8b730fb-f1a3-4c13-8ec4-0aebe08a1449`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return ({
|
|
171
|
+
value,
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'trialUntil',
|
|
177
|
+
name: $t(`1f2e9d09-717b-4c17-9bbe-dce3f3dcbff0`),
|
|
178
|
+
width: 40,
|
|
179
|
+
getValue: (registration: PlatformRegistration) => {
|
|
180
|
+
let value: Date | null = null;
|
|
181
|
+
if (registration.trialUntil && registration.trialUntil > new Date()) {
|
|
182
|
+
value = new Date(registration.trialUntil.getTime());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
value,
|
|
187
|
+
style: {
|
|
188
|
+
numberFormat: {
|
|
189
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
},
|
|
34
195
|
// option menu
|
|
35
196
|
{
|
|
36
197
|
match(id) {
|
|
@@ -71,7 +232,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
|
71
232
|
},
|
|
72
233
|
}
|
|
73
234
|
: {},
|
|
74
|
-
value: options.
|
|
235
|
+
value: options.map(option => returnAmount ? option.amount : option.option.name).join(', '),
|
|
75
236
|
};
|
|
76
237
|
},
|
|
77
238
|
}];
|
|
@@ -178,6 +339,16 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
|
|
|
178
339
|
};
|
|
179
340
|
},
|
|
180
341
|
},
|
|
342
|
+
{
|
|
343
|
+
id: 'group.type',
|
|
344
|
+
name: $t('4fda497f-b2d8-43ef-b08c-a3e4e0b472b4'),
|
|
345
|
+
width: 20,
|
|
346
|
+
getValue: (registration: PlatformRegistration) => {
|
|
347
|
+
return {
|
|
348
|
+
value: getGroupTypeName(registration.group.type),
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
},
|
|
181
352
|
],
|
|
182
353
|
};
|
|
183
354
|
|
|
@@ -46,4 +46,28 @@ export class LimitedFilteredRequestHelper {
|
|
|
46
46
|
|
|
47
47
|
return next;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
static fixInfiniteLoadingLoopWithTransform<R, T>({ request, results, sorters, transformer }: { request: LimitedFilteredRequest; results: R[]; sorters: SQLSortDefinitions<T>; transformer: (r: R) => T }): LimitedFilteredRequest | undefined {
|
|
51
|
+
let next: LimitedFilteredRequest | undefined;
|
|
52
|
+
|
|
53
|
+
if (results.length >= request.limit) {
|
|
54
|
+
const lastObject = transformer(results[results.length - 1]);
|
|
55
|
+
const nextFilter = getSortFilter(lastObject, sorters, request.sort);
|
|
56
|
+
|
|
57
|
+
next = new LimitedFilteredRequest({
|
|
58
|
+
filter: request.filter,
|
|
59
|
+
pageFilter: nextFilter,
|
|
60
|
+
sort: request.sort,
|
|
61
|
+
limit: request.limit,
|
|
62
|
+
search: request.search,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(request.pageFilter)) {
|
|
66
|
+
console.error('Found infinite loading loop for', request);
|
|
67
|
+
next = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
49
73
|
}
|
|
@@ -3,17 +3,29 @@ import { BalanceItemType, MemberWithRegistrationsBlob } from '@stamhoofd/structu
|
|
|
3
3
|
|
|
4
4
|
export class MemberCharger {
|
|
5
5
|
static async chargeMany({ chargingOrganizationId, membersToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; membersToCharge: MemberWithRegistrationsBlob[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
|
|
6
|
-
|
|
6
|
+
await Promise.all(membersToCharge.map(memberToCharge => MemberCharger.charge({
|
|
7
7
|
price,
|
|
8
8
|
amount,
|
|
9
9
|
description,
|
|
10
10
|
chargingOrganizationId,
|
|
11
|
-
|
|
11
|
+
memberToCharge,
|
|
12
12
|
dueAt,
|
|
13
13
|
createdAt,
|
|
14
|
-
}));
|
|
14
|
+
})));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async charge({ chargingOrganizationId, memberToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; memberToCharge: MemberWithRegistrationsBlob; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
|
|
18
|
+
const balanceItem = MemberCharger.createBalanceItem({
|
|
19
|
+
price,
|
|
20
|
+
amount,
|
|
21
|
+
description,
|
|
22
|
+
chargingOrganizationId,
|
|
23
|
+
memberBeingCharged: memberToCharge,
|
|
24
|
+
dueAt,
|
|
25
|
+
createdAt,
|
|
26
|
+
});
|
|
15
27
|
|
|
16
|
-
await
|
|
28
|
+
await balanceItem.save();
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
private static createBalanceItem({ price, amount, description, chargingOrganizationId, memberBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; memberBeingCharged: MemberWithRegistrationsBlob; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SQLColumnExpression, SQLExpression, SQLExpressionOptions, SQLQuery, SQLTranslatedStringHelper } from '@stamhoofd/sql';
|
|
2
|
+
import { Language } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export class SQLTranslatedString implements SQLExpression {
|
|
5
|
+
private helper: SQLTranslatedStringHelper;
|
|
6
|
+
|
|
7
|
+
constructor(columnExpression: SQLColumnExpression, path: string) {
|
|
8
|
+
this.helper = new SQLTranslatedStringHelper(columnExpression, path, () => Language.English);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getSQL(options?: SQLExpressionOptions): SQLQuery {
|
|
12
|
+
return this.helper.getSQL(options);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OrganizationTag } from '@stamhoofd/structures';
|
|
2
|
-
import { TagHelper } from './TagHelper';
|
|
2
|
+
import { TagHelper } from './TagHelper.js';
|
|
3
3
|
|
|
4
4
|
// todo: move tests for methods of shared package to shared package
|
|
5
5
|
describe('TagHelper', () => {
|
|
@@ -123,13 +123,13 @@ describe('TagHelper', () => {
|
|
|
123
123
|
|
|
124
124
|
// assert
|
|
125
125
|
expect(result).toHaveLength(6);
|
|
126
|
-
expect(result).
|
|
127
|
-
expect(result).
|
|
128
|
-
expect(result).
|
|
129
|
-
expect(result).
|
|
130
|
-
expect(result).
|
|
131
|
-
expect(result).
|
|
132
|
-
expect(result).not.
|
|
126
|
+
expect(result).toContain('id5');
|
|
127
|
+
expect(result).toContain('id3');
|
|
128
|
+
expect(result).toContain('id4');
|
|
129
|
+
expect(result).toContain('id6');
|
|
130
|
+
expect(result).toContain('id7');
|
|
131
|
+
expect(result).toContain('id0');
|
|
132
|
+
expect(result).not.toContain('unknownTagId');
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
@@ -199,7 +199,7 @@ describe('TagHelper', () => {
|
|
|
199
199
|
// assert
|
|
200
200
|
expect(tag5.childTags).toHaveLength(0);
|
|
201
201
|
expect(tag7.childTags).toHaveLength(3);
|
|
202
|
-
expect(tag7.childTags).not.
|
|
202
|
+
expect(tag7.childTags).not.toContain('doesNotExist1');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it('should return array of tags in the correct order', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IPaginatedResponse, LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
2
|
-
import { FileSignService } from '../services/FileSignService';
|
|
2
|
+
import { FileSignService } from '../services/FileSignService.js';
|
|
3
3
|
|
|
4
4
|
export function fetchToAsyncIterator<T>(
|
|
5
5
|
initialFilter: LimitedFilteredRequest,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CachedBalance, Registration } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLAlias, SQLCalculation, SQLNamedExpression, SQLPlusSign, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
|
|
3
|
+
|
|
4
|
+
export const memberCachedBalanceForOrganizationJoin = SQL.leftJoin(
|
|
5
|
+
SQL.select('objectId', 'organizationId',
|
|
6
|
+
new SQLSelectAs(
|
|
7
|
+
new SQLSum(
|
|
8
|
+
SQL.column('amountOpen'),
|
|
9
|
+
),
|
|
10
|
+
new SQLAlias('amountOpen'),
|
|
11
|
+
),
|
|
12
|
+
)
|
|
13
|
+
.from(CachedBalance.table)
|
|
14
|
+
.where(SQL.column(CachedBalance.table, 'objectType'), 'member')
|
|
15
|
+
.groupBy(SQL.column(CachedBalance.table, 'objectId'), SQL.column(CachedBalance.table, 'organizationId')).as('memberCachedBalance') as SQLNamedExpression, 'memberCachedBalance',
|
|
16
|
+
)
|
|
17
|
+
.where(SQL.column('objectId'), SQL.column(Registration.table, 'memberId'))
|
|
18
|
+
.andWhere(SQL.column('organizationId'), SQL.column(Registration.table, 'organizationId'));
|
|
19
|
+
|
|
20
|
+
export const registrationCachedBalanceJoin = SQL.leftJoin(
|
|
21
|
+
SQL.select('objectId', 'organizationId',
|
|
22
|
+
new SQLSelectAs(
|
|
23
|
+
new SQLSum(
|
|
24
|
+
new SQLCalculation(
|
|
25
|
+
SQL.column('amountOpen'),
|
|
26
|
+
new SQLPlusSign(),
|
|
27
|
+
SQL.column('amountPending'),
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
new SQLAlias('toPay'),
|
|
31
|
+
),
|
|
32
|
+
new SQLSelectAs(
|
|
33
|
+
new SQLSum(
|
|
34
|
+
new SQLCalculation(
|
|
35
|
+
SQL.column('amountOpen'),
|
|
36
|
+
new SQLPlusSign(),
|
|
37
|
+
SQL.column('amountPaid'),
|
|
38
|
+
new SQLPlusSign(),
|
|
39
|
+
SQL.column('amountPending'),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
new SQLAlias('price'),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
.from(CachedBalance.table)
|
|
46
|
+
.where(SQL.column(CachedBalance.table, 'objectType'), 'registration')
|
|
47
|
+
.groupBy(SQL.column(CachedBalance.table, 'objectId'), SQL.column(CachedBalance.table, 'organizationId')).as('registrationCachedBalance') as SQLNamedExpression, 'registrationCachedBalance',
|
|
48
|
+
)
|
|
49
|
+
.where(SQL.column('objectId'), SQL.column(Registration.table, 'id'));
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { DocumentTemplate, DocumentTemplateFactory, GroupFactory, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory } from '@stamhoofd/models';
|
|
2
|
+
import { migrateDocumentYears } from './1765896674-document-update-year.js';
|
|
3
|
+
|
|
4
|
+
describe('migration.document-update-year', () => {
|
|
5
|
+
describe('should use most frequent year of groups', () => {
|
|
6
|
+
test('groups with date in period', async () => {
|
|
7
|
+
const organization = await new OrganizationFactory({
|
|
8
|
+
}).create();
|
|
9
|
+
|
|
10
|
+
const period1 = await new RegistrationPeriodFactory({
|
|
11
|
+
startDate: new Date(2021, 0, 1),
|
|
12
|
+
endDate: new Date(2021, 11, 31),
|
|
13
|
+
organization,
|
|
14
|
+
}).create();
|
|
15
|
+
|
|
16
|
+
organization.periodId = period1.id;
|
|
17
|
+
await organization.save();
|
|
18
|
+
|
|
19
|
+
const period2 = await new RegistrationPeriodFactory({
|
|
20
|
+
startDate: new Date(2019, 0, 1),
|
|
21
|
+
endDate: new Date(2019, 11, 31),
|
|
22
|
+
organization,
|
|
23
|
+
}).create();
|
|
24
|
+
|
|
25
|
+
const createGroup = async (startDate: Date, endDate: Date, period: RegistrationPeriod) => {
|
|
26
|
+
const group = await new GroupFactory({ organization, period }).create();
|
|
27
|
+
group.settings.startDate = startDate;
|
|
28
|
+
group.settings.endDate = endDate;
|
|
29
|
+
await group.save();
|
|
30
|
+
return group;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// groups
|
|
34
|
+
const group1 = await createGroup(new Date(2021, 0, 1), new Date(2021, 11, 31), period1);
|
|
35
|
+
const group2 = await createGroup(new Date(2021, 0, 1), new Date(2021, 11, 31), period1);
|
|
36
|
+
const group3 = await createGroup(new Date(2019, 0, 1), new Date(2019, 11, 31), period2);
|
|
37
|
+
|
|
38
|
+
// document created in 2021
|
|
39
|
+
const document1 = await new DocumentTemplateFactory({
|
|
40
|
+
groups: [group1, group2, group3],
|
|
41
|
+
year: 0,
|
|
42
|
+
}).create();
|
|
43
|
+
|
|
44
|
+
document1.createdAt = new Date(2022, 0, 1);
|
|
45
|
+
await document1.save();
|
|
46
|
+
|
|
47
|
+
// document created in 2020
|
|
48
|
+
const document2 = await new DocumentTemplateFactory({
|
|
49
|
+
groups: [group1, group2, group3],
|
|
50
|
+
year: 0,
|
|
51
|
+
}).create();
|
|
52
|
+
|
|
53
|
+
document2.createdAt = new Date(2021, 0, 1);
|
|
54
|
+
await document2.save();
|
|
55
|
+
|
|
56
|
+
// document created in 2019
|
|
57
|
+
const document3 = await new DocumentTemplateFactory({
|
|
58
|
+
groups: [group1, group2, group3],
|
|
59
|
+
year: 0,
|
|
60
|
+
}).create();
|
|
61
|
+
|
|
62
|
+
document3.createdAt = new Date(2020, 0, 1);
|
|
63
|
+
await document3.save();
|
|
64
|
+
|
|
65
|
+
// act
|
|
66
|
+
await migrateDocumentYears();
|
|
67
|
+
|
|
68
|
+
// assert
|
|
69
|
+
const updatedDocument1 = await DocumentTemplate.getByID(document1.id);
|
|
70
|
+
const updatedDocument2 = await DocumentTemplate.getByID(document2.id);
|
|
71
|
+
const updatedDocument3 = await DocumentTemplate.getByID(document3.id);
|
|
72
|
+
|
|
73
|
+
// take most frequent year and prefer date of document creation
|
|
74
|
+
expect(updatedDocument1?.year).toBe(2021);
|
|
75
|
+
expect(updatedDocument2?.year).toBe(2021);
|
|
76
|
+
expect(updatedDocument3?.year).toBe(2021);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('groups overlapping period', async () => {
|
|
80
|
+
const organization = await new OrganizationFactory({
|
|
81
|
+
}).create();
|
|
82
|
+
|
|
83
|
+
const period1 = await new RegistrationPeriodFactory({
|
|
84
|
+
startDate: new Date(2021, 0, 1),
|
|
85
|
+
endDate: new Date(2021, 11, 31),
|
|
86
|
+
organization,
|
|
87
|
+
}).create();
|
|
88
|
+
|
|
89
|
+
organization.periodId = period1.id;
|
|
90
|
+
await organization.save();
|
|
91
|
+
|
|
92
|
+
const period2 = await new RegistrationPeriodFactory({
|
|
93
|
+
startDate: new Date(2020, 0, 1),
|
|
94
|
+
endDate: new Date(2020, 11, 31),
|
|
95
|
+
organization,
|
|
96
|
+
}).create();
|
|
97
|
+
|
|
98
|
+
const period3 = await new RegistrationPeriodFactory({
|
|
99
|
+
startDate: new Date(2019, 0, 1),
|
|
100
|
+
endDate: new Date(2019, 11, 31),
|
|
101
|
+
organization,
|
|
102
|
+
}).create();
|
|
103
|
+
|
|
104
|
+
const createGroup = async (startDate: Date, endDate: Date, period: RegistrationPeriod) => {
|
|
105
|
+
const group = await new GroupFactory({ organization, period }).create();
|
|
106
|
+
group.settings.startDate = startDate;
|
|
107
|
+
group.settings.endDate = endDate;
|
|
108
|
+
await group.save();
|
|
109
|
+
return group;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// groups
|
|
113
|
+
const group1 = await createGroup(new Date(2020, 6, 1), new Date(2021, 5, 30), period1);
|
|
114
|
+
const group2 = await createGroup(new Date(2020, 7, 5), new Date(2021, 4, 3), period1);
|
|
115
|
+
const group3 = await createGroup(new Date(2018, 5, 1), new Date(2019, 10, 14), period3);
|
|
116
|
+
|
|
117
|
+
// document created in 2021
|
|
118
|
+
const document1 = await new DocumentTemplateFactory({
|
|
119
|
+
groups: [group1, group2, group3],
|
|
120
|
+
year: 0,
|
|
121
|
+
}).create();
|
|
122
|
+
|
|
123
|
+
document1.createdAt = new Date(2022, 0, 1);
|
|
124
|
+
await document1.save();
|
|
125
|
+
|
|
126
|
+
// document created in 2020
|
|
127
|
+
const document2 = await new DocumentTemplateFactory({
|
|
128
|
+
groups: [group1, group2, group3],
|
|
129
|
+
year: 0,
|
|
130
|
+
}).create();
|
|
131
|
+
|
|
132
|
+
document2.createdAt = new Date(2021, 0, 1);
|
|
133
|
+
await document2.save();
|
|
134
|
+
|
|
135
|
+
// document created in 2019
|
|
136
|
+
const document3 = await new DocumentTemplateFactory({
|
|
137
|
+
groups: [group1, group2, group3],
|
|
138
|
+
year: 0,
|
|
139
|
+
}).create();
|
|
140
|
+
|
|
141
|
+
document3.createdAt = new Date(2020, 0, 1);
|
|
142
|
+
await document3.save();
|
|
143
|
+
|
|
144
|
+
// act
|
|
145
|
+
await migrateDocumentYears();
|
|
146
|
+
|
|
147
|
+
// assert
|
|
148
|
+
const updatedDocument1 = await DocumentTemplate.getByID(document1.id);
|
|
149
|
+
const updatedDocument2 = await DocumentTemplate.getByID(document2.id);
|
|
150
|
+
const updatedDocument3 = await DocumentTemplate.getByID(document3.id);
|
|
151
|
+
|
|
152
|
+
// take most frequent year and prefer date of document creation
|
|
153
|
+
expect(updatedDocument1?.year).toBe(2021);
|
|
154
|
+
// should take 2020 because document was created in 2020
|
|
155
|
+
expect(updatedDocument2?.year).toBe(2020);
|
|
156
|
+
expect(updatedDocument3?.year).toBe(2021);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('no groups should use year before creation', async () => {
|
|
161
|
+
// create document
|
|
162
|
+
const document = await new DocumentTemplateFactory({
|
|
163
|
+
groups: [],
|
|
164
|
+
year: 0,
|
|
165
|
+
}).create();
|
|
166
|
+
|
|
167
|
+
document.createdAt = new Date(2025, 0, 1);
|
|
168
|
+
await document.save();
|
|
169
|
+
|
|
170
|
+
// act
|
|
171
|
+
await migrateDocumentYears();
|
|
172
|
+
|
|
173
|
+
// assert
|
|
174
|
+
const updatedDocument = await DocumentTemplate.getByID(document.id);
|
|
175
|
+
|
|
176
|
+
// year should be year of creation
|
|
177
|
+
expect(updatedDocument?.year).toBe(2024);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { DocumentTemplate, Group } from '@stamhoofd/models';
|
|
3
|
+
import { SQL } from '@stamhoofd/sql';
|
|
4
|
+
|
|
5
|
+
export async function migrateDocumentYears() {
|
|
6
|
+
let c = 0;
|
|
7
|
+
const totalDocuments = await DocumentTemplate.select().count();
|
|
8
|
+
|
|
9
|
+
for await (const document of DocumentTemplate.select().all()) {
|
|
10
|
+
c++;
|
|
11
|
+
if (c % 1000 === 0) {
|
|
12
|
+
console.log('Processed', c, 'of', totalDocuments);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// check
|
|
16
|
+
if (!document.year) {
|
|
17
|
+
// by default use the year before creation
|
|
18
|
+
let year: number = document.createdAt.getFullYear() - 1;
|
|
19
|
+
|
|
20
|
+
const groups = document.privateSettings.groups;
|
|
21
|
+
const groupIds: string[] = [...new Set(groups.map(g => g.group.id))];
|
|
22
|
+
|
|
23
|
+
if (groupIds.length > 0) {
|
|
24
|
+
const query = Group.select().where(SQL.where('id', groupIds));
|
|
25
|
+
const groupModels = await query.fetch();
|
|
26
|
+
|
|
27
|
+
// count the number of groups per year
|
|
28
|
+
const yearMap = new Map<number, number>();
|
|
29
|
+
|
|
30
|
+
for (const group of groupModels) {
|
|
31
|
+
const startYear = group.settings.startDate.getFullYear();
|
|
32
|
+
const endYear = group.settings.endDate.getFullYear();
|
|
33
|
+
|
|
34
|
+
for (let y = startYear; y <= endYear; y++) {
|
|
35
|
+
const count = yearMap.get(y) ?? 0;
|
|
36
|
+
yearMap.set(y, count + 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// find the year with the highest count
|
|
41
|
+
let topYear = 0;
|
|
42
|
+
let topCount = 0;
|
|
43
|
+
const yearBeforeCreation = document.createdAt.getFullYear() - 1;
|
|
44
|
+
|
|
45
|
+
for (const [year, count] of yearMap) {
|
|
46
|
+
if (count > topCount
|
|
47
|
+
// prefer the year before creation
|
|
48
|
+
|| (count === topCount && year === yearBeforeCreation)
|
|
49
|
+
// next prefer the most recent year
|
|
50
|
+
|| (topYear !== yearBeforeCreation && year > topYear)
|
|
51
|
+
) {
|
|
52
|
+
topYear = year;
|
|
53
|
+
topCount = count;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// use the year with the highest count
|
|
58
|
+
if (topCount > 0) {
|
|
59
|
+
year = topYear;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
document.year = year;
|
|
64
|
+
await document.save();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default new Migration(async () => {
|
|
70
|
+
process.stdout.write('\n');
|
|
71
|
+
console.log('Start updating year of document templates.');
|
|
72
|
+
await migrateDocumentYears();
|
|
73
|
+
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
});
|