@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,16 +1,25 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { DocumentTemplatePrivate } from '@stamhoofd/structures';
|
|
2
|
+
import { DocumentTemplate } from '@stamhoofd/models';
|
|
3
|
+
import { assertSort, CountFilteredRequest, DocumentTemplatePrivate, LimitedFilteredRequest, PaginatedResponse, SearchFilterFactory, StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
6
|
+
import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
7
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
8
|
+
import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper.js';
|
|
9
|
+
import { documentTemplateFilterCompilers } from '../../../../sql-filters/document-templates.js';
|
|
10
|
+
import { documentTemplateSorters } from '../../../../sql-sorters/document-templates.js';
|
|
7
11
|
|
|
8
12
|
type Params = Record<string, never>;
|
|
9
|
-
type Query =
|
|
13
|
+
type Query = LimitedFilteredRequest;
|
|
10
14
|
type Body = undefined;
|
|
11
|
-
type ResponseBody = DocumentTemplatePrivate[]
|
|
15
|
+
type ResponseBody = PaginatedResponse<DocumentTemplatePrivate[], LimitedFilteredRequest>;
|
|
16
|
+
|
|
17
|
+
const filterCompilers: SQLFilterDefinitions = documentTemplateFilterCompilers;
|
|
18
|
+
const sorters: SQLSortDefinitions<DocumentTemplate> = documentTemplateSorters;
|
|
12
19
|
|
|
13
20
|
export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
21
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
22
|
+
|
|
14
23
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
15
24
|
if (request.method !== 'GET') {
|
|
16
25
|
return [false];
|
|
@@ -24,7 +33,60 @@ export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
24
33
|
return [false];
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
async
|
|
36
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
37
|
+
const organization = Context.organization!;
|
|
38
|
+
|
|
39
|
+
const templatesTable: string = DocumentTemplate.table;
|
|
40
|
+
|
|
41
|
+
const query = SQL
|
|
42
|
+
.select(SQL.wildcard(templatesTable))
|
|
43
|
+
.from(SQL.table(templatesTable))
|
|
44
|
+
.where('organizationId', organization.id);
|
|
45
|
+
|
|
46
|
+
if (q.filter) {
|
|
47
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (q.search) {
|
|
51
|
+
const searchFilter: StamhoofdFilter | null = getDocumentTemplateSearchFilter(q.search);
|
|
52
|
+
|
|
53
|
+
if (searchFilter) {
|
|
54
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
59
|
+
if (q.pageFilter) {
|
|
60
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
64
|
+
applySQLSorter(query, q.sort, sorters);
|
|
65
|
+
query.limit(q.limit);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return query;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
72
|
+
const query = await this.buildQuery(requestQuery);
|
|
73
|
+
const data = await query.fetch();
|
|
74
|
+
|
|
75
|
+
const templates: DocumentTemplate[] = DocumentTemplate.fromRows(data, DocumentTemplate.table);
|
|
76
|
+
|
|
77
|
+
const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
|
|
78
|
+
request: requestQuery,
|
|
79
|
+
results: templates,
|
|
80
|
+
sorters,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return new PaginatedResponse<DocumentTemplatePrivate[], LimitedFilteredRequest>({
|
|
84
|
+
results: templates.map(t => t.getPrivateStructure()),
|
|
85
|
+
next,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
28
90
|
const organization = await Context.setOrganizationScope();
|
|
29
91
|
await Context.authenticate();
|
|
30
92
|
|
|
@@ -32,19 +94,33 @@ export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
32
94
|
throw Context.auth.error();
|
|
33
95
|
}
|
|
34
96
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
sort: [{
|
|
41
|
-
column: 'createdAt',
|
|
42
|
-
direction: 'ASC',
|
|
43
|
-
}],
|
|
44
|
-
},
|
|
45
|
-
);
|
|
97
|
+
LimitedFilteredRequestHelper.throwIfInvalidLimit({
|
|
98
|
+
request: request.query,
|
|
99
|
+
maxLimit: Context.auth.hasSomePlatformAccess() ? 1000 : 100,
|
|
100
|
+
});
|
|
101
|
+
|
|
46
102
|
return new Response(
|
|
47
|
-
|
|
103
|
+
await GetDocumentTemplatesEndpoint.buildData(request.query),
|
|
48
104
|
);
|
|
49
105
|
}
|
|
50
106
|
}
|
|
107
|
+
|
|
108
|
+
function getDocumentTemplateSearchFilter(search: string | null): StamhoofdFilter | null {
|
|
109
|
+
if (search === null || search === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const numberFilter = SearchFilterFactory.getIntegerFilter(search);
|
|
114
|
+
|
|
115
|
+
if (numberFilter) {
|
|
116
|
+
return {
|
|
117
|
+
year: numberFilter,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
name: {
|
|
123
|
+
$contains: search,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { Endpoint, Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { DocumentTemplate, DocumentTemplateFactory, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { SQL } from '@stamhoofd/sql';
|
|
5
|
+
import { DocumentPrivateSettings, DocumentStatus, DocumentTemplateDefinition, DocumentTemplatePrivate, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
6
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
|
+
import { testServer } from '../../../../../tests/helpers/TestServer.js';
|
|
8
|
+
import { PatchDocumentTemplatesEndpoint } from './PatchDocumentTemplatesEndpoint.js';
|
|
9
|
+
|
|
10
|
+
const baseUrl = `/organization/document-templates`;
|
|
11
|
+
const endpoint = new PatchDocumentTemplatesEndpoint();
|
|
12
|
+
type EndpointType = typeof endpoint;
|
|
13
|
+
type Body = EndpointType extends Endpoint<any, any, infer B, any> ? B : never;
|
|
14
|
+
|
|
15
|
+
describe('Endpoint.PatchDocumentTemplatesEndpoint', () => {
|
|
16
|
+
let period: RegistrationPeriod;
|
|
17
|
+
let organization: Organization;
|
|
18
|
+
let user: User;
|
|
19
|
+
let token: Token;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
period = await new RegistrationPeriodFactory({
|
|
27
|
+
startDate: new Date(2023, 0, 1),
|
|
28
|
+
endDate: new Date(2023, 11, 31),
|
|
29
|
+
}).create();
|
|
30
|
+
|
|
31
|
+
organization = await new OrganizationFactory({ period })
|
|
32
|
+
.create();
|
|
33
|
+
|
|
34
|
+
user = await new UserFactory({
|
|
35
|
+
organization,
|
|
36
|
+
permissions: Permissions.create({
|
|
37
|
+
level: PermissionLevel.Full,
|
|
38
|
+
}),
|
|
39
|
+
}).create();
|
|
40
|
+
|
|
41
|
+
token = await Token.createToken(user);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('put fiscal document', () => {
|
|
45
|
+
it('should throw if already has fiscal document in year', async () => {
|
|
46
|
+
// create existing fiscal document in same year
|
|
47
|
+
await new DocumentTemplateFactory({
|
|
48
|
+
organizationId: organization.id,
|
|
49
|
+
type: 'fiscal',
|
|
50
|
+
groups: [],
|
|
51
|
+
year: 2022,
|
|
52
|
+
}).create();
|
|
53
|
+
|
|
54
|
+
// create new fiscal document in same year
|
|
55
|
+
const arr: Body = new PatchableArray();
|
|
56
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
57
|
+
organizationId: organization.id,
|
|
58
|
+
type: 'fiscal',
|
|
59
|
+
groups: [],
|
|
60
|
+
year: 2022,
|
|
61
|
+
}).createWithoutSave());
|
|
62
|
+
|
|
63
|
+
const newDocument = newDocumentModel.getPrivateStructure();
|
|
64
|
+
arr.addPut(newDocument);
|
|
65
|
+
|
|
66
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
67
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
68
|
+
|
|
69
|
+
await expect(testServer.test(endpoint, request))
|
|
70
|
+
.rejects
|
|
71
|
+
.toThrow(STExpect.errorWithCode('double_fiscal_document'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should not throw if first fiscal document in year', async () => {
|
|
75
|
+
// delete existing docs from organization
|
|
76
|
+
await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
|
|
77
|
+
|
|
78
|
+
const arr: Body = new PatchableArray();
|
|
79
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
80
|
+
organizationId: organization.id,
|
|
81
|
+
type: 'fiscal',
|
|
82
|
+
groups: [],
|
|
83
|
+
year: 2022,
|
|
84
|
+
}).createWithoutSave());
|
|
85
|
+
|
|
86
|
+
const newDocument = newDocumentModel.getPrivateStructure();
|
|
87
|
+
arr.addPut(newDocument);
|
|
88
|
+
|
|
89
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
90
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
91
|
+
|
|
92
|
+
const result = await testServer.test(endpoint, request);
|
|
93
|
+
|
|
94
|
+
expect(result).toBeDefined();
|
|
95
|
+
expect(result.status).toBe(200);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('patch fiscal document', () => {
|
|
100
|
+
describe('change type to fiscal', () => {
|
|
101
|
+
it('should throw if already has fiscal document in year', async () => {
|
|
102
|
+
// create existing fiscal document in same year
|
|
103
|
+
await new DocumentTemplateFactory({
|
|
104
|
+
organizationId: organization.id,
|
|
105
|
+
type: 'fiscal',
|
|
106
|
+
groups: [],
|
|
107
|
+
year: 2022,
|
|
108
|
+
}).create();
|
|
109
|
+
|
|
110
|
+
// create new fiscal document in same year
|
|
111
|
+
const arr: Body = new PatchableArray();
|
|
112
|
+
|
|
113
|
+
// create new participation document in same year
|
|
114
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
115
|
+
organizationId: organization.id,
|
|
116
|
+
type: 'participation',
|
|
117
|
+
groups: [],
|
|
118
|
+
year: 2022,
|
|
119
|
+
}).create());
|
|
120
|
+
|
|
121
|
+
// change type to fiscal
|
|
122
|
+
arr.addPatch(DocumentTemplatePrivate.patch({
|
|
123
|
+
id: newDocumentModel.id,
|
|
124
|
+
privateSettings: DocumentPrivateSettings.patch({
|
|
125
|
+
templateDefinition: DocumentTemplateDefinition.patch({
|
|
126
|
+
type: 'fiscal',
|
|
127
|
+
}),
|
|
128
|
+
}),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
132
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
133
|
+
|
|
134
|
+
await expect(testServer.test(endpoint, request))
|
|
135
|
+
.rejects
|
|
136
|
+
.toThrow(STExpect.errorWithCode('double_fiscal_document'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should not throw if first fiscal document in year', async () => {
|
|
140
|
+
// delete existing docs from organization
|
|
141
|
+
await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
|
|
142
|
+
|
|
143
|
+
// create new fiscal document in same year
|
|
144
|
+
const arr: Body = new PatchableArray();
|
|
145
|
+
|
|
146
|
+
// create new participation document in same year
|
|
147
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
148
|
+
organizationId: organization.id,
|
|
149
|
+
type: 'participation',
|
|
150
|
+
groups: [],
|
|
151
|
+
year: 2022,
|
|
152
|
+
}).create());
|
|
153
|
+
|
|
154
|
+
// change type to fiscal
|
|
155
|
+
arr.addPatch(DocumentTemplatePrivate.patch({
|
|
156
|
+
id: newDocumentModel.id,
|
|
157
|
+
privateSettings: DocumentPrivateSettings.patch({
|
|
158
|
+
templateDefinition: DocumentTemplateDefinition.patch({
|
|
159
|
+
type: 'fiscal',
|
|
160
|
+
}),
|
|
161
|
+
}),
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
165
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
166
|
+
|
|
167
|
+
const result = await testServer.test(endpoint, request);
|
|
168
|
+
|
|
169
|
+
expect(result).toBeDefined();
|
|
170
|
+
expect(result.status).toBe(200);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('change year', () => {
|
|
175
|
+
it('should throw if already has fiscal document in year', async () => {
|
|
176
|
+
// create existing fiscal document in same year
|
|
177
|
+
await new DocumentTemplateFactory({
|
|
178
|
+
organizationId: organization.id,
|
|
179
|
+
type: 'fiscal',
|
|
180
|
+
groups: [],
|
|
181
|
+
year: 2022,
|
|
182
|
+
}).create();
|
|
183
|
+
|
|
184
|
+
// create new fiscal document in same year
|
|
185
|
+
const arr: Body = new PatchableArray();
|
|
186
|
+
|
|
187
|
+
// create new participation document in other year
|
|
188
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
189
|
+
organizationId: organization.id,
|
|
190
|
+
type: 'fiscal',
|
|
191
|
+
groups: [],
|
|
192
|
+
year: 2021,
|
|
193
|
+
}).create());
|
|
194
|
+
|
|
195
|
+
// change year to same year as other existing document
|
|
196
|
+
arr.addPatch(DocumentTemplatePrivate.patch({
|
|
197
|
+
id: newDocumentModel.id,
|
|
198
|
+
year: 2022,
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
202
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
203
|
+
|
|
204
|
+
await expect(testServer.test(endpoint, request))
|
|
205
|
+
.rejects
|
|
206
|
+
.toThrow(STExpect.errorWithCode('double_fiscal_document'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not throw if first fiscal document in year', async () => {
|
|
210
|
+
// delete existing docs from organization
|
|
211
|
+
await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
|
|
212
|
+
|
|
213
|
+
// create new fiscal document in same year
|
|
214
|
+
const arr: Body = new PatchableArray();
|
|
215
|
+
|
|
216
|
+
// create new participation document in other year
|
|
217
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
218
|
+
organizationId: organization.id,
|
|
219
|
+
type: 'fiscal',
|
|
220
|
+
groups: [],
|
|
221
|
+
year: 2021,
|
|
222
|
+
}).create());
|
|
223
|
+
|
|
224
|
+
// change year to same year as other existing document
|
|
225
|
+
arr.addPatch(DocumentTemplatePrivate.patch({
|
|
226
|
+
id: newDocumentModel.id,
|
|
227
|
+
year: 2022,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
231
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
232
|
+
|
|
233
|
+
const result = await testServer.test(endpoint, request);
|
|
234
|
+
|
|
235
|
+
expect(result).toBeDefined();
|
|
236
|
+
expect(result.status).toBe(200);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('do not change type or year of existing fiscal document', () => {
|
|
241
|
+
it('should not throw if multiple fiscal documents in year', async () => {
|
|
242
|
+
// delete existing docs from organization
|
|
243
|
+
await SQL.delete().from(SQL.table(DocumentTemplate.table)).where('organizationId', organization.id);
|
|
244
|
+
|
|
245
|
+
// create existing fiscal document in same year
|
|
246
|
+
await new DocumentTemplateFactory({
|
|
247
|
+
organizationId: organization.id,
|
|
248
|
+
type: 'fiscal',
|
|
249
|
+
groups: [],
|
|
250
|
+
year: 2022,
|
|
251
|
+
}).create();
|
|
252
|
+
|
|
253
|
+
// create new fiscal document in same year
|
|
254
|
+
const arr: Body = new PatchableArray();
|
|
255
|
+
|
|
256
|
+
// create double fiscal document in same year
|
|
257
|
+
const newDocumentModel: DocumentTemplate = (await new DocumentTemplateFactory({
|
|
258
|
+
organizationId: organization.id,
|
|
259
|
+
type: 'fiscal',
|
|
260
|
+
groups: [],
|
|
261
|
+
year: 2022,
|
|
262
|
+
}).create());
|
|
263
|
+
|
|
264
|
+
// change status to published (do not change year or type)
|
|
265
|
+
arr.addPatch(DocumentTemplatePrivate.patch({
|
|
266
|
+
id: newDocumentModel.id,
|
|
267
|
+
status: DocumentStatus.Published,
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
271
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
272
|
+
|
|
273
|
+
const result = await testServer.test(endpoint, request);
|
|
274
|
+
|
|
275
|
+
expect(result).toBeDefined();
|
|
276
|
+
expect(result.status).toBe(200);
|
|
277
|
+
expect(result.body.length).toBe(1);
|
|
278
|
+
expect(result.body[0].status).toBe(DocumentStatus.Published);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { DocumentTemplate
|
|
4
|
+
import { DocumentTemplate } from '@stamhoofd/models';
|
|
5
5
|
import { DocumentTemplatePrivate, PermissionLevel } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
8
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
8
9
|
|
|
9
10
|
type Params = Record<string, never>;
|
|
10
11
|
type Query = undefined;
|
|
@@ -15,7 +16,7 @@ type ResponseBody = DocumentTemplatePrivate[];
|
|
|
15
16
|
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
export class
|
|
19
|
+
export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
19
20
|
bodyDecoder = new PatchableArrayDecoder(DocumentTemplatePrivate as Decoder<DocumentTemplatePrivate>, DocumentTemplatePrivate.patchType() as Decoder<AutoEncoderPatchType<DocumentTemplatePrivate>>, StringDecoder);
|
|
20
21
|
|
|
21
22
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -49,7 +50,19 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
|
|
|
49
50
|
template.status = put.status;
|
|
50
51
|
template.html = put.html;
|
|
51
52
|
template.updatesEnabled = put.updatesEnabled;
|
|
53
|
+
template.year = put.year;
|
|
52
54
|
template.organizationId = organization.id;
|
|
55
|
+
|
|
56
|
+
if (await this.doesYearAlreadyHaveFiscalDocument(template)) {
|
|
57
|
+
throw new SimpleError({
|
|
58
|
+
code: 'double_fiscal_document',
|
|
59
|
+
field: 'year',
|
|
60
|
+
message: 'This year already has a fiscal document',
|
|
61
|
+
human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
|
|
62
|
+
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
await template.save();
|
|
54
67
|
|
|
55
68
|
// todo: Generate documents (maybe in background)
|
|
@@ -65,7 +78,14 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
|
|
|
65
78
|
throw Context.auth.notFoundOrNoAccess($t(`148bfab7-ca0e-4fac-8a0a-302ca7855fc8`));
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
let shouldCheckIfAlreadyHasFiscalDocument = false;
|
|
82
|
+
|
|
68
83
|
if (patch.privateSettings) {
|
|
84
|
+
const patchType = patch.privateSettings.templateDefinition?.type;
|
|
85
|
+
|
|
86
|
+
// only check if type has changed and new type is fiscal
|
|
87
|
+
shouldCheckIfAlreadyHasFiscalDocument = patchType !== undefined && template.privateSettings.templateDefinition.type !== patchType && patchType === 'fiscal';
|
|
88
|
+
|
|
69
89
|
template.privateSettings.patchOrPut(patch.privateSettings);
|
|
70
90
|
}
|
|
71
91
|
|
|
@@ -85,6 +105,23 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
|
|
|
85
105
|
template.html = patch.html;
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
if (patch.year) {
|
|
109
|
+
if (shouldCheckIfAlreadyHasFiscalDocument === false) {
|
|
110
|
+
shouldCheckIfAlreadyHasFiscalDocument = template.year !== patch.year;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
template.year = patch.year;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (shouldCheckIfAlreadyHasFiscalDocument && await this.doesYearAlreadyHaveFiscalDocument(template)) {
|
|
117
|
+
throw new SimpleError({
|
|
118
|
+
code: 'double_fiscal_document',
|
|
119
|
+
field: 'year',
|
|
120
|
+
message: 'This year already has a fiscal document',
|
|
121
|
+
human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
88
125
|
await template.save();
|
|
89
126
|
|
|
90
127
|
// Update documents
|
|
@@ -111,4 +148,20 @@ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body,
|
|
|
111
148
|
updatedTemplates,
|
|
112
149
|
);
|
|
113
150
|
}
|
|
151
|
+
|
|
152
|
+
private async doesYearAlreadyHaveFiscalDocument(template: DocumentTemplate) {
|
|
153
|
+
let query = SQL.select().from(SQL.table(DocumentTemplate.table))
|
|
154
|
+
.where(SQL.column('organizationId'), template.organizationId)
|
|
155
|
+
.where(SQL.column('year'), template.year)
|
|
156
|
+
.where(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal');
|
|
157
|
+
|
|
158
|
+
// id is not set if put
|
|
159
|
+
if (template.id) {
|
|
160
|
+
query = query.where(SQL.column('id'), SQLWhereSign.NotEqual, template.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = await query.limit(1).count();
|
|
164
|
+
|
|
165
|
+
return result > 0;
|
|
166
|
+
}
|
|
114
167
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { XlsxBuiltInNumberFormat, XlsxTransformerColumn, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { Platform } from '@stamhoofd/models';
|
|
3
|
-
import { ExcelExportType, Gender, GroupType, LimitedFilteredRequest, PlatformFamily, PlatformMember, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
3
|
+
import { ExcelExportType, Gender, GroupType, LimitedFilteredRequest, MembershipStatus, PlatformFamily, PlatformMember, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
-
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
|
|
6
|
-
import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint';
|
|
7
|
-
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
|
|
8
|
-
import { Context } from '../helpers/Context';
|
|
9
|
-
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
|
|
5
|
+
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
6
|
+
import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint.js';
|
|
7
|
+
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
|
|
8
|
+
import { Context } from '../helpers/Context.js';
|
|
9
|
+
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
|
|
10
10
|
|
|
11
11
|
export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
|
|
12
12
|
{
|
|
@@ -150,6 +150,16 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
|
|
|
150
150
|
value: object.details.nationalRegisterNumber?.toString() ?? '',
|
|
151
151
|
}),
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
id: 'membership',
|
|
155
|
+
name: $t(`c7d995f1-36a0-446e-9fcf-17ffb69f3f45`),
|
|
156
|
+
width: 20,
|
|
157
|
+
getValue: (member: PlatformMember) => {
|
|
158
|
+
return {
|
|
159
|
+
value: formatMembershipStatus(member.membershipStatus),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
},
|
|
153
163
|
|
|
154
164
|
...XlsxTransformerColumnHelper.creatColumnsForParents(),
|
|
155
165
|
|
|
@@ -260,6 +270,35 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
260
270
|
};
|
|
261
271
|
},
|
|
262
272
|
},
|
|
273
|
+
{
|
|
274
|
+
id: 'outstandingBalance',
|
|
275
|
+
name: $t(`beb45452-dee7-4a7f-956c-e6db06aac20f`),
|
|
276
|
+
width: 30,
|
|
277
|
+
getValue: (v) => {
|
|
278
|
+
return {
|
|
279
|
+
value: v.member.balances.reduce((sum, r) => sum + (r.amountOpen), 0) / 1_0000,
|
|
280
|
+
style: {
|
|
281
|
+
numberFormat: {
|
|
282
|
+
id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'createdAt',
|
|
291
|
+
name: $t('c38e774e-e8ab-4549-b119-4eed380c626c'),
|
|
292
|
+
width: 20,
|
|
293
|
+
getValue: v => ({
|
|
294
|
+
value: v.member.createdAt,
|
|
295
|
+
style: {
|
|
296
|
+
numberFormat: {
|
|
297
|
+
id: XlsxBuiltInNumberFormat.DateSlash,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
},
|
|
263
302
|
|
|
264
303
|
// Registration records
|
|
265
304
|
{
|
|
@@ -340,7 +379,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
340
379
|
},
|
|
341
380
|
}
|
|
342
381
|
: {},
|
|
343
|
-
value: options.
|
|
382
|
+
value: options.map(option => returnAmount ? option.amount : option.option.name).join(', '),
|
|
344
383
|
};
|
|
345
384
|
},
|
|
346
385
|
},
|
|
@@ -427,10 +466,25 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
427
466
|
],
|
|
428
467
|
});
|
|
429
468
|
|
|
430
|
-
function formatGender(gender: Gender) {
|
|
469
|
+
function formatGender(gender: Gender): string {
|
|
431
470
|
switch (gender) {
|
|
432
471
|
case Gender.Male: return $t(`f972abd4-de1e-484b-b7da-ad4c75d37808`);
|
|
433
472
|
case Gender.Female: return $t(`e21f499d-1078-4044-be5d-6693d2636699`);
|
|
434
473
|
default: return $t(`60f13ba4-c6c9-4388-9add-43a996bf6bee`);
|
|
435
474
|
}
|
|
436
475
|
}
|
|
476
|
+
|
|
477
|
+
function formatMembershipStatus(status: MembershipStatus): string {
|
|
478
|
+
switch (status) {
|
|
479
|
+
case MembershipStatus.Trial:
|
|
480
|
+
return $t(`47c7c3c4-9246-40b7-b1e0-2cb408d5f79e`);
|
|
481
|
+
case MembershipStatus.Active:
|
|
482
|
+
return $t(`b56351e9-4847-4a0c-9eec-348d75c794c4`);
|
|
483
|
+
case MembershipStatus.Expiring:
|
|
484
|
+
return $t(`d9858110-37d9-4b4a-8bfb-d76b3cc5ef27`);
|
|
485
|
+
case MembershipStatus.Temporary:
|
|
486
|
+
return $t(`75e62d3c-f348-4104-8a1e-e11e6e7fbe32`);
|
|
487
|
+
case MembershipStatus.Inactive:
|
|
488
|
+
return $t(`1f8620fa-e8a5-4665-99c8-c1907a5b5768`);
|
|
489
|
+
}
|
|
490
|
+
}
|