@stamhoofd/backend 2.17.4 → 2.19.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/ChargeMembershipsEndpoint.ts +49 -0
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +16 -14
- 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 +77 -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/MembershipCharger.ts +126 -0
- package/src/helpers/SetupStepsUpdater.ts +210 -0
- package/src/seeds/1724076679-setup-steps.ts +16 -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,126 @@
|
|
|
1
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
2
|
+
import { BalanceItem, Member, MemberPlatformMembership, Platform } from "@stamhoofd/models";
|
|
3
|
+
import { SQL, SQLOrderBy, SQLWhereSign } from "@stamhoofd/sql";
|
|
4
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemType } from "@stamhoofd/structures";
|
|
5
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
6
|
+
|
|
7
|
+
export const MembershipCharger = {
|
|
8
|
+
async charge() {
|
|
9
|
+
console.log('Charging memberships...')
|
|
10
|
+
|
|
11
|
+
// Loop all
|
|
12
|
+
let lastId = "";
|
|
13
|
+
const platform = await Platform.getShared()
|
|
14
|
+
const chargeVia = platform.membershipOrganizationId
|
|
15
|
+
|
|
16
|
+
if (!chargeVia) {
|
|
17
|
+
throw new SimpleError({
|
|
18
|
+
code: 'missing_membership_organization',
|
|
19
|
+
message: 'Missing membershipOrganizationId',
|
|
20
|
+
human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd'
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getType(id: string) {
|
|
25
|
+
return platform.config.membershipTypes.find(t => t.id === id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let createdCount = 0;
|
|
29
|
+
let createdPrice = 0;
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line no-constant-condition
|
|
32
|
+
while (true) {
|
|
33
|
+
const memberships = await MemberPlatformMembership.select()
|
|
34
|
+
.where('id', SQLWhereSign.Greater, lastId)
|
|
35
|
+
.where('balanceItemId', null)
|
|
36
|
+
.limit(100)
|
|
37
|
+
.orderBy(
|
|
38
|
+
new SQLOrderBy({
|
|
39
|
+
column: SQL.column('id'),
|
|
40
|
+
direction: 'ASC'
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
.fetch();
|
|
44
|
+
|
|
45
|
+
if (memberships.length === 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const memberIds = Formatter.uniqueArray(memberships.map(m => m.memberId))
|
|
50
|
+
const members = await Member.getByIDs(...memberIds)
|
|
51
|
+
const createdBalanceItems: BalanceItem[] = []
|
|
52
|
+
|
|
53
|
+
for (const membership of memberships) {
|
|
54
|
+
// charge
|
|
55
|
+
if (membership.balanceItemId) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const type = getType(membership.membershipTypeId);
|
|
59
|
+
if (!type) {
|
|
60
|
+
console.error('Unknown membership type id ', membership.membershipTypeId)
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (membership.organizationId === chargeVia) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const member = members.find(m => m.id === membership.memberId)
|
|
69
|
+
|
|
70
|
+
if (!member) {
|
|
71
|
+
console.error('Unexpected missing member id ', membership.memberId, 'for membership', membership.id)
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const balanceItem = new BalanceItem();
|
|
76
|
+
balanceItem.unitPrice = membership.price
|
|
77
|
+
balanceItem.amount = 1
|
|
78
|
+
balanceItem.description = Formatter.dateNumber(membership.startDate, true) + " tot " + Formatter.dateNumber(membership.expireDate ?? membership.endDate, true)
|
|
79
|
+
balanceItem.relations = new Map([
|
|
80
|
+
[
|
|
81
|
+
BalanceItemRelationType.Member,
|
|
82
|
+
BalanceItemRelation.create({
|
|
83
|
+
id: member.id,
|
|
84
|
+
name: member.details.name
|
|
85
|
+
})
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
BalanceItemRelationType.MembershipType,
|
|
89
|
+
BalanceItemRelation.create({
|
|
90
|
+
id: type.id,
|
|
91
|
+
name: type.name
|
|
92
|
+
})
|
|
93
|
+
]
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
balanceItem.type = BalanceItemType.PlatformMembership
|
|
97
|
+
balanceItem.organizationId = chargeVia
|
|
98
|
+
balanceItem.payingOrganizationId = membership.organizationId
|
|
99
|
+
|
|
100
|
+
await balanceItem.save();
|
|
101
|
+
membership.balanceItemId = balanceItem.id;
|
|
102
|
+
await membership.save()
|
|
103
|
+
|
|
104
|
+
createdBalanceItems.push(balanceItem)
|
|
105
|
+
|
|
106
|
+
createdCount += 1;
|
|
107
|
+
createdPrice += membership.price
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await BalanceItem.updateOutstanding(createdBalanceItems)
|
|
111
|
+
|
|
112
|
+
if (memberships.length < 100) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const z = lastId;
|
|
117
|
+
lastId = memberships[memberships.length - 1].id;
|
|
118
|
+
|
|
119
|
+
if (lastId === z) {
|
|
120
|
+
throw new Error('Unexpected infinite loop found in MembershipCharger')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('Charged ' + Formatter.integer(createdCount) +' memberships, for a total value of ' + Formatter.price(createdPrice))
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -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
|
+
})
|
|
@@ -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
|
-
}
|