@stamhoofd/backend 2.17.3 → 2.18.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/package.json +5 -5
- package/src/crons/setup-steps.ts +9 -0
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +6 -14
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +66 -4
- package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +6 -7
- package/src/endpoints/organization/dashboard/billing/GetDetailedBillingStatusEndpoint.ts +78 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +6 -2
- package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +78 -0
- package/src/excel-loaders/payments.ts +36 -4
- package/src/helpers/AuthenticatedStructures.ts +6 -7
- package/src/helpers/SetupStepsUpdater.ts +210 -0
- package/src/seeds/1724076679-setup-steps.ts +16 -0
- package/src/sql-filters/members.ts +2 -0
- package/src/sql-sorters/members.ts +25 -14
- package/src/sql-sorters/organizations.ts +8 -0
- package/src/sql-sorters/payments.ts +8 -0
- package/src/endpoints/admin/invoices/GetInvoicesCountEndpoint.ts +0 -47
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +0 -185
- package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +0 -424
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +0 -67
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Organization,
|
|
3
|
+
OrganizationRegistrationPeriod,
|
|
4
|
+
Platform,
|
|
5
|
+
} from "@stamhoofd/models";
|
|
6
|
+
import { QueueHandler } from "@stamhoofd/queues";
|
|
7
|
+
import {
|
|
8
|
+
PlatformPremiseType,
|
|
9
|
+
Platform as PlatformStruct,
|
|
10
|
+
SetupStepType,
|
|
11
|
+
SetupSteps,
|
|
12
|
+
} from "@stamhoofd/structures";
|
|
13
|
+
|
|
14
|
+
type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void;
|
|
15
|
+
|
|
16
|
+
export class SetupStepUpdater {
|
|
17
|
+
private static readonly STEP_TYPE_OPERATIONS: Record<
|
|
18
|
+
SetupStepType,
|
|
19
|
+
SetupStepOperation
|
|
20
|
+
> = {
|
|
21
|
+
[SetupStepType.Groups]: this.updateStepGroups,
|
|
22
|
+
[SetupStepType.Premises]: this.updateStepPremises,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
static async updateSetupStepsForAllOrganizationsInCurrentPeriod({
|
|
26
|
+
batchSize, premiseTypes
|
|
27
|
+
}: { batchSize?: number, premiseTypes?: PlatformPremiseType[] } = {}) {
|
|
28
|
+
const tag = "updateSetupStepsForAllOrganizationsInCurrentPeriod";
|
|
29
|
+
QueueHandler.cancel(tag);
|
|
30
|
+
|
|
31
|
+
await QueueHandler.schedule(tag, async () => {
|
|
32
|
+
const platform = (await Platform.getSharedPrivateStruct()).clone();
|
|
33
|
+
if(premiseTypes) {
|
|
34
|
+
platform.config.premiseTypes = premiseTypes;
|
|
35
|
+
}
|
|
36
|
+
const periodId = platform.period.id;
|
|
37
|
+
|
|
38
|
+
let lastId = "";
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
const organizationRegistrationPeriods =
|
|
42
|
+
await OrganizationRegistrationPeriod.where(
|
|
43
|
+
{
|
|
44
|
+
id: { sign: ">", value: lastId },
|
|
45
|
+
periodId: periodId,
|
|
46
|
+
},
|
|
47
|
+
{ limit: batchSize ?? 10, sort: ["id"] }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (organizationRegistrationPeriods.length === 0) {
|
|
51
|
+
lastId = "";
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const organizationPeriodMap = new Map(
|
|
56
|
+
organizationRegistrationPeriods.map((period) => {
|
|
57
|
+
return [period.organizationId, period];
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const organizations = await Organization.getByIDs(
|
|
62
|
+
...organizationPeriodMap.keys()
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
for (const organization of organizations) {
|
|
66
|
+
const organizationId = organization.id;
|
|
67
|
+
const organizationRegistrationPeriod =
|
|
68
|
+
organizationPeriodMap.get(organizationId);
|
|
69
|
+
|
|
70
|
+
if (!organizationRegistrationPeriod) {
|
|
71
|
+
console.error(
|
|
72
|
+
`[FLAG-MOMENT] organizationRegistrationPeriod not found for organization with id ${organizationId}`
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
"[FLAG-MOMENT] checking flag moments for " +
|
|
79
|
+
organizationId
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await SetupStepUpdater.updateFor(
|
|
83
|
+
organizationRegistrationPeriod,
|
|
84
|
+
platform,
|
|
85
|
+
organization
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
lastId =
|
|
90
|
+
organizationRegistrationPeriods[
|
|
91
|
+
organizationRegistrationPeriods.length - 1
|
|
92
|
+
].id;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static async updateForOrganization(
|
|
98
|
+
organization: Organization,
|
|
99
|
+
{
|
|
100
|
+
platform,
|
|
101
|
+
organizationRegistrationPeriod,
|
|
102
|
+
}: {
|
|
103
|
+
platform?: PlatformStruct;
|
|
104
|
+
organizationRegistrationPeriod?: OrganizationRegistrationPeriod;
|
|
105
|
+
} = {}
|
|
106
|
+
) {
|
|
107
|
+
if (!platform) {
|
|
108
|
+
platform = await Platform.getSharedPrivateStruct();
|
|
109
|
+
if (!platform) {
|
|
110
|
+
console.error("No platform not found");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!organizationRegistrationPeriod) {
|
|
116
|
+
const periodId = platform.period.id;
|
|
117
|
+
organizationRegistrationPeriod = (
|
|
118
|
+
await OrganizationRegistrationPeriod.where({
|
|
119
|
+
organizationId: organization.id,
|
|
120
|
+
periodId: periodId,
|
|
121
|
+
})
|
|
122
|
+
)[0];
|
|
123
|
+
|
|
124
|
+
if (!organizationRegistrationPeriod) {
|
|
125
|
+
console.error(
|
|
126
|
+
`OrganizationRegistrationPeriod with organizationId ${organization.id} and periodId ${periodId} not found`
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this.updateFor(
|
|
133
|
+
organizationRegistrationPeriod,
|
|
134
|
+
platform,
|
|
135
|
+
organization
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static async updateFor(
|
|
140
|
+
organizationRegistrationPeriod: OrganizationRegistrationPeriod,
|
|
141
|
+
platform: PlatformStruct,
|
|
142
|
+
organization: Organization
|
|
143
|
+
) {
|
|
144
|
+
const setupSteps = organizationRegistrationPeriod.setupSteps;
|
|
145
|
+
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
147
|
+
for (const stepType of Object.values(SetupStepType)) {
|
|
148
|
+
console.log(`[STEP TYPE] ${stepType}`);
|
|
149
|
+
const operation = this.STEP_TYPE_OPERATIONS[stepType];
|
|
150
|
+
operation(setupSteps, organization, platform);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await organizationRegistrationPeriod.save();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static updateStepPremises(
|
|
157
|
+
setupSteps: SetupSteps,
|
|
158
|
+
organization: Organization,
|
|
159
|
+
platform: PlatformStruct
|
|
160
|
+
) {
|
|
161
|
+
let totalSteps = 0;
|
|
162
|
+
let finishedSteps = 0;
|
|
163
|
+
|
|
164
|
+
const premiseTypes = platform.config.premiseTypes;
|
|
165
|
+
|
|
166
|
+
for (const premiseType of premiseTypes) {
|
|
167
|
+
const { min, max } = premiseType;
|
|
168
|
+
if (min === null && max === null) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
totalSteps++;
|
|
173
|
+
|
|
174
|
+
const premiseTypeId = premiseType.id;
|
|
175
|
+
let totalPremisesOfThisType = 0;
|
|
176
|
+
|
|
177
|
+
for (const premise of organization.privateMeta.premises) {
|
|
178
|
+
if (premise.premiseTypeIds.includes(premiseTypeId)) {
|
|
179
|
+
totalPremisesOfThisType++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (max !== null && totalPremisesOfThisType > max) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (min !== null && totalPremisesOfThisType < min) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
finishedSteps++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setupSteps.update(SetupStepType.Premises, {
|
|
195
|
+
totalSteps,
|
|
196
|
+
finishedSteps,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static updateStepGroups(
|
|
201
|
+
setupSteps: SetupSteps,
|
|
202
|
+
_organization: Organization,
|
|
203
|
+
_platform: PlatformStruct
|
|
204
|
+
) {
|
|
205
|
+
setupSteps.update(SetupStepType.Groups, {
|
|
206
|
+
totalSteps: 0,
|
|
207
|
+
finishedSteps: 0,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { SetupStepUpdater } from '../helpers/SetupStepsUpdater';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment == "test") {
|
|
6
|
+
console.log("skipped in tests")
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if(STAMHOOFD.userMode !== "platform") {
|
|
11
|
+
console.log("skipped seed setup-steps because usermode not platform")
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod();
|
|
16
|
+
})
|
|
@@ -9,6 +9,8 @@ import { registrationFilterCompilers } from "./registrations";
|
|
|
9
9
|
export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
10
10
|
...baseSQLFilterCompilers,
|
|
11
11
|
id: createSQLColumnFilterCompiler('id'),
|
|
12
|
+
firstName: createSQLColumnFilterCompiler('firstName'),
|
|
13
|
+
lastName: createSQLColumnFilterCompiler('lastName'),
|
|
12
14
|
name: createSQLExpressionFilterCompiler(
|
|
13
15
|
new SQLConcat(
|
|
14
16
|
SQL.column('firstName'),
|
|
@@ -3,6 +3,14 @@ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from "@stamh
|
|
|
3
3
|
import { Formatter } from "@stamhoofd/utility"
|
|
4
4
|
|
|
5
5
|
export const memberSorters: SQLSortDefinitions<MemberWithRegistrations> = {
|
|
6
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
7
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
8
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
9
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
10
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
11
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
12
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
13
|
+
|
|
6
14
|
'id': {
|
|
7
15
|
getValue(a) {
|
|
8
16
|
return a.id
|
|
@@ -14,22 +22,26 @@ export const memberSorters: SQLSortDefinitions<MemberWithRegistrations> = {
|
|
|
14
22
|
})
|
|
15
23
|
}
|
|
16
24
|
},
|
|
17
|
-
'
|
|
25
|
+
'firstName': {
|
|
26
|
+
getValue(a) {
|
|
27
|
+
return a.firstName
|
|
28
|
+
},
|
|
29
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
30
|
+
return new SQLOrderBy({
|
|
31
|
+
column: SQL.column('firstName'),
|
|
32
|
+
direction
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
'lastName': {
|
|
18
37
|
getValue(a) {
|
|
19
|
-
|
|
20
|
-
return a.firstName + ' ' + a.lastName
|
|
38
|
+
return a.lastName
|
|
21
39
|
},
|
|
22
40
|
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
23
|
-
return SQLOrderBy
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}),
|
|
28
|
-
new SQLOrderBy({
|
|
29
|
-
column: SQL.column('lastName'),
|
|
30
|
-
direction
|
|
31
|
-
})
|
|
32
|
-
])
|
|
41
|
+
return new SQLOrderBy({
|
|
42
|
+
column: SQL.column('lastName'),
|
|
43
|
+
direction
|
|
44
|
+
})
|
|
33
45
|
}
|
|
34
46
|
},
|
|
35
47
|
'birthDay': {
|
|
@@ -43,5 +55,4 @@ export const memberSorters: SQLSortDefinitions<MemberWithRegistrations> = {
|
|
|
43
55
|
})
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
|
-
// Note: never add mapped sortings, that should happen in the frontend -> e.g. map age to birthDay
|
|
47
58
|
}
|
|
@@ -2,6 +2,14 @@ import { Organization } from "@stamhoofd/models"
|
|
|
2
2
|
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from "@stamhoofd/sql"
|
|
3
3
|
|
|
4
4
|
export const organizationSorters: SQLSortDefinitions<Organization> = {
|
|
5
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
6
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
7
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
8
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
9
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
10
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
11
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
12
|
+
|
|
5
13
|
'id': {
|
|
6
14
|
getValue(a) {
|
|
7
15
|
return a.id
|
|
@@ -3,6 +3,14 @@ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from "@stamh
|
|
|
3
3
|
import { Formatter } from "@stamhoofd/utility"
|
|
4
4
|
|
|
5
5
|
export const paymentSorters: SQLSortDefinitions<Payment> = {
|
|
6
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
7
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
8
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
9
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
10
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
11
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
12
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
13
|
+
|
|
6
14
|
'id': {
|
|
7
15
|
getValue(a) {
|
|
8
16
|
return a.id
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
-
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
-
|
|
5
|
-
import { Context } from '../../../helpers/Context';
|
|
6
|
-
import { GetInvoicesEndpoint } from './GetInvoicesEndpoint';
|
|
7
|
-
|
|
8
|
-
type Params = Record<string, never>;
|
|
9
|
-
type Query = CountFilteredRequest;
|
|
10
|
-
type Body = undefined;
|
|
11
|
-
type ResponseBody = CountResponse;
|
|
12
|
-
|
|
13
|
-
export class GetInvoicesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
-
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>
|
|
15
|
-
|
|
16
|
-
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
-
if (request.method != "GET") {
|
|
18
|
-
return [false];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const params = Endpoint.parseParameters(request.url, "/admin/invoices/count", {});
|
|
22
|
-
|
|
23
|
-
if (params) {
|
|
24
|
-
return [true, params as Params];
|
|
25
|
-
}
|
|
26
|
-
return [false];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
-
await Context.authenticate()
|
|
31
|
-
|
|
32
|
-
if (!Context.auth.hasPlatformFullAccess()) {
|
|
33
|
-
throw Context.auth.error()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const query = GetInvoicesEndpoint.buildQuery(request.query)
|
|
37
|
-
|
|
38
|
-
const count = await query
|
|
39
|
-
.count();
|
|
40
|
-
|
|
41
|
-
return new Response(
|
|
42
|
-
CountResponse.create({
|
|
43
|
-
count
|
|
44
|
-
})
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
-
import { Decoder } from '@simonbackx/simple-encoding';
|
|
3
|
-
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import { Organization, Payment, STInvoice } from '@stamhoofd/models';
|
|
6
|
-
import { SQL, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLExpressionFilterCompiler } from "@stamhoofd/sql";
|
|
7
|
-
import { CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, Payment as PaymentStruct, STInvoicePrivate, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
|
|
8
|
-
|
|
9
|
-
import { Context } from '../../../helpers/Context';
|
|
10
|
-
|
|
11
|
-
type Params = Record<string, never>;
|
|
12
|
-
type Query = LimitedFilteredRequest;
|
|
13
|
-
type Body = undefined;
|
|
14
|
-
type ResponseBody = PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>
|
|
15
|
-
|
|
16
|
-
export const filterCompilers: SQLFilterDefinitions = {
|
|
17
|
-
...baseSQLFilterCompilers,
|
|
18
|
-
id: createSQLExpressionFilterCompiler(
|
|
19
|
-
SQL.column('stamhoofd_invoices', 'id')
|
|
20
|
-
),
|
|
21
|
-
number: createSQLExpressionFilterCompiler(
|
|
22
|
-
SQL.column('stamhoofd_invoices', 'number')
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const sorters: SQLSortDefinitions<STInvoice> = {
|
|
27
|
-
'id': {
|
|
28
|
-
getValue(a) {
|
|
29
|
-
return a.id
|
|
30
|
-
},
|
|
31
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
32
|
-
return new SQLOrderBy({
|
|
33
|
-
column: SQL.column('id'),
|
|
34
|
-
direction
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
'number': {
|
|
39
|
-
getValue(a) {
|
|
40
|
-
return a.number
|
|
41
|
-
},
|
|
42
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
43
|
-
return new SQLOrderBy({
|
|
44
|
-
column: SQL.column('number'),
|
|
45
|
-
direction
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
52
|
-
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
|
|
53
|
-
|
|
54
|
-
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
55
|
-
if (request.method != "GET") {
|
|
56
|
-
return [false];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const params = Endpoint.parseParameters(request.url, "/admin/invoices", {});
|
|
60
|
-
|
|
61
|
-
if (params) {
|
|
62
|
-
return [true, params as Params];
|
|
63
|
-
}
|
|
64
|
-
return [false];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
|
|
68
|
-
const query = SQL
|
|
69
|
-
.select(
|
|
70
|
-
SQL.wildcard('stamhoofd_invoices')
|
|
71
|
-
)
|
|
72
|
-
.from(
|
|
73
|
-
SQL.table('stamhoofd_invoices')
|
|
74
|
-
)
|
|
75
|
-
.whereNot(SQL.column('stamhoofd_invoices', 'number'), null);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (q.filter) {
|
|
79
|
-
query.where(compileToSQLFilter(q.filter, filterCompilers))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (q.search) {
|
|
83
|
-
let searchFilter: StamhoofdFilter|null = null
|
|
84
|
-
|
|
85
|
-
// todo: auto detect e-mailaddresses and search on admins
|
|
86
|
-
searchFilter = {
|
|
87
|
-
name: {
|
|
88
|
-
$contains: q.search
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (searchFilter) {
|
|
93
|
-
query.where(compileToSQLFilter(searchFilter, filterCompilers))
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (q instanceof LimitedFilteredRequest) {
|
|
98
|
-
if (q.pageFilter) {
|
|
99
|
-
query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
query.orderBy(compileToSQLSorter(q.sort, sorters))
|
|
103
|
-
query.limit(q.limit)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return query
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
110
|
-
await Context.authenticate()
|
|
111
|
-
|
|
112
|
-
if (!Context.auth.hasPlatformFullAccess()) {
|
|
113
|
-
throw Context.auth.error()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
117
|
-
|
|
118
|
-
if (request.query.limit > maxLimit) {
|
|
119
|
-
throw new SimpleError({
|
|
120
|
-
code: 'invalid_field',
|
|
121
|
-
field: 'limit',
|
|
122
|
-
message: 'Limit can not be more than ' + maxLimit
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (request.query.limit < 1) {
|
|
127
|
-
throw new SimpleError({
|
|
128
|
-
code: 'invalid_field',
|
|
129
|
-
field: 'limit',
|
|
130
|
-
message: 'Limit can not be less than 1'
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const data = await GetInvoicesEndpoint.buildQuery(request.query).fetch()
|
|
135
|
-
const invoices = STInvoice.fromRows(data, 'stamhoofd_invoices');
|
|
136
|
-
|
|
137
|
-
let next: LimitedFilteredRequest|undefined;
|
|
138
|
-
|
|
139
|
-
if (invoices.length >= request.query.limit) {
|
|
140
|
-
const lastObject = invoices[invoices.length - 1];
|
|
141
|
-
const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
|
|
142
|
-
|
|
143
|
-
next = new LimitedFilteredRequest({
|
|
144
|
-
filter: request.query.filter,
|
|
145
|
-
pageFilter: nextFilter,
|
|
146
|
-
sort: request.query.sort,
|
|
147
|
-
limit: request.query.limit,
|
|
148
|
-
search: request.query.search
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
|
|
152
|
-
console.error('Found infinite loading loop for', request.query);
|
|
153
|
-
next = undefined;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Get payments + organizations
|
|
158
|
-
const paymentIds = invoices.flatMap(i => i.paymentId ? [i.paymentId] : [])
|
|
159
|
-
const organizationIds = invoices.flatMap(i => i.organizationId ? [i.organizationId] : [])
|
|
160
|
-
|
|
161
|
-
const payments = await Payment.getByIDs(...paymentIds)
|
|
162
|
-
const organizations = await Organization.getByIDs(...organizationIds)
|
|
163
|
-
|
|
164
|
-
const structures: STInvoicePrivate[] = []
|
|
165
|
-
for (const invoice of invoices) {
|
|
166
|
-
const payment = payments.find(p => p.id === invoice.paymentId)
|
|
167
|
-
const organization = organizations.find(p => p.id === invoice.organizationId)
|
|
168
|
-
structures.push(
|
|
169
|
-
STInvoicePrivate.create({
|
|
170
|
-
...invoice,
|
|
171
|
-
payment: payment ? PaymentStruct.create(payment) : null,
|
|
172
|
-
organization: organization ? organization.getBaseStructure() : undefined,
|
|
173
|
-
settlement: payment?.settlement ?? null,
|
|
174
|
-
})
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return new Response(
|
|
179
|
-
new PaginatedResponse<STInvoicePrivate[], LimitedFilteredRequest>({
|
|
180
|
-
results: structures,
|
|
181
|
-
next
|
|
182
|
-
})
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
}
|