@stamhoofd/backend 2.108.0 → 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/global/members/GetMembersEndpoint.ts +5 -5
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +8 -7
- 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 +61 -7
- package/src/excel-loaders/registrations.ts +123 -2
- package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
- package/src/helpers/SQLTranslatedString.ts +14 -0
- package/src/helpers/TagHelper.test.ts +9 -9
- 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,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', () => {
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DocumentTemplate, DocumentTemplateFactory } from '@stamhoofd/models';
|
|
2
|
+
import { SQL } from '@stamhoofd/sql';
|
|
3
|
+
import { DocumentStatus } from '@stamhoofd/structures';
|
|
4
|
+
import { initPublishedAtForPublishedDocuments } from './1766150402-document-published-at.js';
|
|
5
|
+
|
|
6
|
+
describe('migration.document-published-at', () => {
|
|
7
|
+
it('should init published at for published documents', async () => {
|
|
8
|
+
// arrange
|
|
9
|
+
// published documents
|
|
10
|
+
const document1 = await createDocument(DocumentStatus.Published, new Date(2022, 0, 1));
|
|
11
|
+
const document2 = await createDocument(DocumentStatus.Published, new Date(2023, 5, 17));
|
|
12
|
+
const document3 = await createDocument(DocumentStatus.Published, new Date(2025, 3, 9));
|
|
13
|
+
|
|
14
|
+
// draft documents
|
|
15
|
+
const document4 = await createDocument(DocumentStatus.Draft, new Date(2022, 0, 1));
|
|
16
|
+
const document5 = await createDocument(DocumentStatus.Draft, new Date(2023, 5, 17));
|
|
17
|
+
const document6 = await createDocument(DocumentStatus.Draft, new Date(2025, 3, 9));
|
|
18
|
+
|
|
19
|
+
// reset published at (save does not update published at automatically)
|
|
20
|
+
await SQL.update(DocumentTemplate.table).set('publishedAt', null).where('id', [document1.id, document2.id, document3.id, document4.id, document5.id, document6.id]).update();
|
|
21
|
+
|
|
22
|
+
// act
|
|
23
|
+
await initPublishedAtForPublishedDocuments();
|
|
24
|
+
|
|
25
|
+
// assert
|
|
26
|
+
const updatedDocument1 = await DocumentTemplate.getByID(document1.id);
|
|
27
|
+
const updatedDocument2 = await DocumentTemplate.getByID(document2.id);
|
|
28
|
+
const updatedDocument3 = await DocumentTemplate.getByID(document3.id);
|
|
29
|
+
|
|
30
|
+
const updatedDocument4 = await DocumentTemplate.getByID(document4.id);
|
|
31
|
+
const updatedDocument5 = await DocumentTemplate.getByID(document5.id);
|
|
32
|
+
const updatedDocument6 = await DocumentTemplate.getByID(document6.id);
|
|
33
|
+
|
|
34
|
+
expect(updatedDocument1?.publishedAt?.getTime()).toEqual(document1.createdAt.getTime());
|
|
35
|
+
expect(updatedDocument2?.publishedAt?.getTime()).toEqual(document2.createdAt.getTime());
|
|
36
|
+
expect(updatedDocument3?.publishedAt?.getTime()).toEqual(document3.createdAt.getTime());
|
|
37
|
+
|
|
38
|
+
expect(updatedDocument4?.publishedAt).toBeNull();
|
|
39
|
+
expect(updatedDocument5?.publishedAt).toBeNull();
|
|
40
|
+
expect(updatedDocument6?.publishedAt).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
async function createDocument(status: DocumentStatus, createdAt: Date) {
|
|
45
|
+
return await new DocumentTemplateFactory({ groups: [], status, createdAt }).create();
|
|
46
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { DocumentTemplate } from '@stamhoofd/models';
|
|
3
|
+
import { SQL } from '@stamhoofd/sql';
|
|
4
|
+
import { DocumentStatus } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
export async function initPublishedAtForPublishedDocuments() {
|
|
7
|
+
await SQL.update(DocumentTemplate.table)
|
|
8
|
+
.set('publishedAt', SQL.column('createdAt'))
|
|
9
|
+
.where('publishedAt', null)
|
|
10
|
+
.where('status', DocumentStatus.Published)
|
|
11
|
+
.update();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default new Migration(async () => {
|
|
15
|
+
process.stdout.write('\n');
|
|
16
|
+
console.log('Start init published at for published documents.');
|
|
17
|
+
await initPublishedAtForPublishedDocuments();
|
|
18
|
+
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
});
|
|
@@ -2,11 +2,11 @@ import createMollieClient, { PaymentStatus as MolliePaymentStatus } from '@molli
|
|
|
2
2
|
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
|
|
3
3
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
4
|
import { AuditLogSource, BalanceItemRelation, BalanceItemStatus, BalanceItemType, PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
5
|
-
import { BuckarooHelper } from '../helpers/BuckarooHelper';
|
|
6
|
-
import { StripeHelper } from '../helpers/StripeHelper';
|
|
7
|
-
import { AuditLogService } from './AuditLogService';
|
|
8
|
-
import { BalanceItemPaymentService } from './BalanceItemPaymentService';
|
|
9
|
-
import { BalanceItemService } from './BalanceItemService';
|
|
5
|
+
import { BuckarooHelper } from '../helpers/BuckarooHelper.js';
|
|
6
|
+
import { StripeHelper } from '../helpers/StripeHelper.js';
|
|
7
|
+
import { AuditLogService } from './AuditLogService.js';
|
|
8
|
+
import { BalanceItemPaymentService } from './BalanceItemPaymentService.js';
|
|
9
|
+
import { BalanceItemService } from './BalanceItemService.js';
|
|
10
10
|
|
|
11
11
|
export const PaymentService = {
|
|
12
12
|
async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
@@ -336,46 +336,28 @@ export const PaymentService = {
|
|
|
336
336
|
* Say the total amount to pay is 15,238 because (e.g. because of VAT). In that case,
|
|
337
337
|
* we'll need to round the payment to 1 cent. That can cause issues in the financial statements because
|
|
338
338
|
* the total amount of balances does not match the total amount received/paid.
|
|
339
|
-
*
|
|
339
|
+
*
|
|
340
340
|
* To fix that, we create an extra balance item with the difference. So the rounding always matches.
|
|
341
|
+
*
|
|
342
|
+
* TODO: update this method to generate a virtual invoice and use the price of the invoice instead of the rounded payment price, so we don't get differences in calculation
|
|
341
343
|
*/
|
|
342
344
|
async round(payment: Payment) {
|
|
343
|
-
if (!payment.organizationId) {
|
|
344
|
-
throw new Error('Cannot round payments without organizationId')
|
|
345
|
-
}
|
|
346
|
-
|
|
347
345
|
const amount = payment.price;
|
|
348
|
-
const rounded = Payment.roundPrice(payment.price)
|
|
346
|
+
const rounded = Payment.roundPrice(payment.price);
|
|
349
347
|
const difference = rounded - amount;
|
|
350
348
|
|
|
351
349
|
if (difference === 0) {
|
|
352
350
|
return;
|
|
353
351
|
}
|
|
354
352
|
|
|
355
|
-
if (difference > 100) {
|
|
356
|
-
throw new Error('Unexpected rounding difference.
|
|
353
|
+
if (difference > 100 || difference < -100) {
|
|
354
|
+
throw new Error('Unexpected rounding difference of ' + difference + ' for payment ' + payment.id);
|
|
357
355
|
}
|
|
358
356
|
|
|
359
|
-
|
|
360
|
-
balanceItem.type = BalanceItemType.Rounding;
|
|
361
|
-
balanceItem.userId = payment.payingUserId;
|
|
362
|
-
balanceItem.payingOrganizationId = payment.payingOrganizationId
|
|
363
|
-
balanceItem.unitPrice = difference;
|
|
364
|
-
balanceItem.pricePaid = 0;
|
|
365
|
-
balanceItem.organizationId = payment.organizationId;
|
|
366
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
367
|
-
await balanceItem.save();
|
|
368
|
-
|
|
369
|
-
// Create balance item payment
|
|
370
|
-
const balanceItemPayment = new BalanceItemPayment();
|
|
371
|
-
balanceItemPayment.organizationId = payment.organizationId;
|
|
372
|
-
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
373
|
-
balanceItemPayment.price = difference;
|
|
374
|
-
balanceItemPayment.paymentId = payment.id;
|
|
375
|
-
await balanceItemPayment.save();
|
|
357
|
+
// payment.roundingAmount = difference;
|
|
376
358
|
|
|
377
359
|
// Change payment total price
|
|
378
|
-
payment.price += difference
|
|
360
|
+
payment.price += difference;
|
|
379
361
|
await payment.save();
|
|
380
|
-
}
|
|
362
|
+
},
|
|
381
363
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createJoinedRelationFilter, SQL, SQLFilterDefinitions, SQLIfNull, SQLValueType } from '@stamhoofd/sql';
|
|
3
3
|
import { FilterWrapperMarker, PermissionLevel, StamhoofdFilter, unwrapFilter } from '@stamhoofd/structures';
|
|
4
|
-
import { Context } from '../helpers/Context';
|
|
5
|
-
import {
|
|
4
|
+
import { Context } from '../helpers/Context.js';
|
|
5
|
+
import { memberCachedBalanceForOrganizationJoin, registrationCachedBalanceJoin } from '../helpers/outstandingBalanceJoin.js';
|
|
6
|
+
import { SQLTranslatedString } from '../helpers/SQLTranslatedString.js';
|
|
7
|
+
import { organizationFilterCompilers } from './organizations.js';
|
|
6
8
|
|
|
7
9
|
async function checkGroupIdFilterAccess(filter: StamhoofdFilter, permissionLevel: PermissionLevel) {
|
|
8
10
|
const groupIds = typeof filter === 'string'
|
|
@@ -77,6 +79,11 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
77
79
|
type: SQLValueType.Number,
|
|
78
80
|
nullable: false,
|
|
79
81
|
}),
|
|
82
|
+
groupPrice: createColumnFilter({
|
|
83
|
+
expression: new SQLTranslatedString(SQL.column('groupPrice'), '$.value.name'),
|
|
84
|
+
type: SQLValueType.String,
|
|
85
|
+
nullable: true,
|
|
86
|
+
}),
|
|
80
87
|
canRegister: createColumnFilter({
|
|
81
88
|
expression: SQL.column('canRegister'),
|
|
82
89
|
type: SQLValueType.Boolean,
|
|
@@ -110,6 +117,21 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
110
117
|
type: SQLValueType.Datetime,
|
|
111
118
|
nullable: true,
|
|
112
119
|
}),
|
|
120
|
+
trialUntil: createColumnFilter({
|
|
121
|
+
expression: SQL.column('registrations', 'trialUntil'),
|
|
122
|
+
type: SQLValueType.Datetime,
|
|
123
|
+
nullable: true,
|
|
124
|
+
}),
|
|
125
|
+
startDate: createColumnFilter({
|
|
126
|
+
expression: SQL.column('registrations', 'startDate'),
|
|
127
|
+
type: SQLValueType.Datetime,
|
|
128
|
+
nullable: true,
|
|
129
|
+
}),
|
|
130
|
+
endDate: createColumnFilter({
|
|
131
|
+
expression: SQL.column('registrations', 'endDate'),
|
|
132
|
+
type: SQLValueType.Datetime,
|
|
133
|
+
nullable: true,
|
|
134
|
+
}),
|
|
113
135
|
group: {
|
|
114
136
|
...baseSQLFilterCompilers,
|
|
115
137
|
id: createColumnFilter({
|
|
@@ -126,7 +148,7 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
126
148
|
nullable: false,
|
|
127
149
|
}),
|
|
128
150
|
name: createColumnFilter({
|
|
129
|
-
expression: SQL.
|
|
151
|
+
expression: SQL.jsonExtract(SQL.column('groups', 'settings'), '$.value.name'),
|
|
130
152
|
type: SQLValueType.JSONString,
|
|
131
153
|
nullable: false,
|
|
132
154
|
}),
|
|
@@ -150,4 +172,29 @@ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
|
|
|
150
172
|
),
|
|
151
173
|
organizationFilterCompilers,
|
|
152
174
|
),
|
|
175
|
+
memberCachedBalance: createJoinedRelationFilter(
|
|
176
|
+
memberCachedBalanceForOrganizationJoin,
|
|
177
|
+
{
|
|
178
|
+
amountOpen: createColumnFilter({
|
|
179
|
+
expression: new SQLIfNull(SQL.column('memberCachedBalance', 'amountOpen'), 0),
|
|
180
|
+
type: SQLValueType.Number,
|
|
181
|
+
nullable: false,
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
),
|
|
185
|
+
registrationCachedBalance: createJoinedRelationFilter(
|
|
186
|
+
registrationCachedBalanceJoin,
|
|
187
|
+
{
|
|
188
|
+
toPay: createColumnFilter({
|
|
189
|
+
expression: new SQLIfNull(SQL.column('registrationCachedBalance', 'toPay'), 0),
|
|
190
|
+
type: SQLValueType.Number,
|
|
191
|
+
nullable: false,
|
|
192
|
+
}),
|
|
193
|
+
price: createColumnFilter({
|
|
194
|
+
expression: new SQLIfNull(SQL.column('registrationCachedBalance', 'price'), 0),
|
|
195
|
+
type: SQLValueType.Number,
|
|
196
|
+
nullable: false,
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
),
|
|
153
200
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const documentTemplateFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createColumnFilter({
|
|
6
|
+
expression: SQL.column('id'),
|
|
7
|
+
type: SQLValueType.String,
|
|
8
|
+
nullable: false,
|
|
9
|
+
}),
|
|
10
|
+
name: createColumnFilter({
|
|
11
|
+
expression: SQL.jsonExtract(SQL.column('settings'), '$.value.name'),
|
|
12
|
+
type: SQLValueType.JSONString,
|
|
13
|
+
nullable: false,
|
|
14
|
+
}),
|
|
15
|
+
year: createColumnFilter({
|
|
16
|
+
expression: SQL.column('year'),
|
|
17
|
+
type: SQLValueType.Number,
|
|
18
|
+
nullable: false,
|
|
19
|
+
}),
|
|
20
|
+
organizationId: createColumnFilter({
|
|
21
|
+
expression: SQL.column('organizationId'),
|
|
22
|
+
type: SQLValueType.String,
|
|
23
|
+
nullable: false,
|
|
24
|
+
}),
|
|
25
|
+
updatedAt: createColumnFilter({
|
|
26
|
+
expression: SQL.column('updatedAt'),
|
|
27
|
+
type: SQLValueType.Datetime,
|
|
28
|
+
nullable: false,
|
|
29
|
+
}),
|
|
30
|
+
createdAt: createColumnFilter({
|
|
31
|
+
expression: SQL.column('createdAt'),
|
|
32
|
+
type: SQLValueType.Datetime,
|
|
33
|
+
nullable: false,
|
|
34
|
+
}),
|
|
35
|
+
status: createColumnFilter({
|
|
36
|
+
expression: SQL.column('status'),
|
|
37
|
+
type: SQLValueType.String,
|
|
38
|
+
nullable: false,
|
|
39
|
+
}),
|
|
40
|
+
type: createColumnFilter({
|
|
41
|
+
expression: SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'),
|
|
42
|
+
type: SQLValueType.JSONString,
|
|
43
|
+
nullable: false,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
@@ -8,7 +8,7 @@ export const documentFilterCompilers: SQLFilterDefinitions = {
|
|
|
8
8
|
nullable: false,
|
|
9
9
|
}),
|
|
10
10
|
description: createColumnFilter({
|
|
11
|
-
expression: SQL.
|
|
11
|
+
expression: SQL.jsonExtract(SQL.column('data'), '$.value.description'),
|
|
12
12
|
type: SQLValueType.JSONString,
|
|
13
13
|
nullable: false,
|
|
14
14
|
}),
|
|
@@ -28,7 +28,7 @@ export const eventFilterCompilers: SQLFilterDefinitions = {
|
|
|
28
28
|
nullable: false,
|
|
29
29
|
}),
|
|
30
30
|
'groupIds': createColumnFilter({
|
|
31
|
-
expression: SQL.
|
|
31
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.groups[*].id'),
|
|
32
32
|
type: SQLValueType.JSONArray,
|
|
33
33
|
nullable: true,
|
|
34
34
|
}),
|
|
@@ -43,27 +43,27 @@ export const eventFilterCompilers: SQLFilterDefinitions = {
|
|
|
43
43
|
nullable: false,
|
|
44
44
|
}),
|
|
45
45
|
'defaultAgeGroupIds': createColumnFilter({
|
|
46
|
-
expression: SQL.
|
|
46
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.defaultAgeGroupIds'),
|
|
47
47
|
type: SQLValueType.JSONArray,
|
|
48
48
|
nullable: true,
|
|
49
49
|
}),
|
|
50
50
|
'organizationTagIds': createColumnFilter({
|
|
51
|
-
expression: SQL.
|
|
51
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
52
52
|
type: SQLValueType.JSONArray,
|
|
53
53
|
nullable: true,
|
|
54
54
|
}),
|
|
55
55
|
'minAge': createColumnFilter({
|
|
56
|
-
expression: SQL.
|
|
56
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.minAge'),
|
|
57
57
|
type: SQLValueType.JSONNumber,
|
|
58
58
|
nullable: true,
|
|
59
59
|
}),
|
|
60
60
|
'maxAge': createColumnFilter({
|
|
61
|
-
expression: SQL.
|
|
61
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.maxAge'),
|
|
62
62
|
type: SQLValueType.JSONNumber,
|
|
63
63
|
nullable: true,
|
|
64
64
|
}),
|
|
65
65
|
'meta.visible': createColumnFilter({
|
|
66
|
-
expression: SQL.
|
|
66
|
+
expression: SQL.jsonExtract(SQL.column('meta'), '$.value.visible'),
|
|
67
67
|
type: SQLValueType.JSONBoolean,
|
|
68
68
|
nullable: false,
|
|
69
69
|
}),
|