@stamhoofd/backend 2.43.2 → 2.44.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 +10 -10
- package/src/crons/{clear-excel-cache.test.ts → clearExcelCache.test.ts} +1 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +18 -0
- package/src/crons.ts +8 -2
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +116 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +24 -20
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +10 -0
- package/src/endpoints/global/registration/{GetUserDetailedBillingStatusEndpoint.ts → GetUserDetailedPayableBalanceEndpoint.ts} +10 -8
- package/src/endpoints/global/registration/{GetUserBillingStatusEndpoint.ts → GetUserPayableBalanceEndpoint.ts} +13 -11
- package/src/endpoints/organization/dashboard/billing/{GetOrganizationDetailedBillingStatusEndpoint.ts → GetOrganizationDetailedPayableBalanceEndpoint.ts} +12 -16
- package/src/endpoints/organization/dashboard/billing/{GetOrganizationBillingStatusEndpoint.ts → GetOrganizationPayableBalanceEndpoint.ts} +8 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +98 -0
- package/src/endpoints/organization/dashboard/{cached-outstanding-balance/GetCachedOutstandingBalanceCountEndpoint.ts → receivable-balances/GetReceivableBalancesCountEndpoint.ts} +4 -4
- package/src/endpoints/organization/dashboard/{cached-outstanding-balance/GetCachedOutstandingBalanceEndpoint.ts → receivable-balances/GetReceivableBalancesEndpoint.ts} +18 -14
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +57 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +123 -13
- package/src/helpers/AdminPermissionChecker.ts +11 -2
- package/src/helpers/AuthenticatedStructures.ts +79 -18
- package/src/helpers/FlagMomentCleanup.ts +68 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
- package/src/helpers/OrganizationCharger.ts +46 -0
- package/src/helpers/StripeHelper.ts +35 -10
- package/src/sql-filters/orders.ts +26 -0
- package/src/sql-filters/{cached-outstanding-balance.ts → receivable-balances.ts} +1 -1
- package/src/sql-sorters/orders.ts +47 -0
- package/src/sql-sorters/{cached-outstanding-balance.ts → receivable-balances.ts} +2 -2
- /package/src/crons/{clear-excel-cache.ts → clearExcelCache.ts} +0 -0
- /package/src/crons/{setup-steps.ts → updateSetupSteps.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.44.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.1",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.44.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.44.0",
|
|
41
|
+
"@stamhoofd/email": "2.44.0",
|
|
42
|
+
"@stamhoofd/models": "2.44.0",
|
|
43
|
+
"@stamhoofd/queues": "2.44.0",
|
|
44
|
+
"@stamhoofd/sql": "2.44.0",
|
|
45
|
+
"@stamhoofd/structures": "2.44.0",
|
|
46
|
+
"@stamhoofd/utility": "2.44.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "^4.0.5",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "1168aea7895e1aba48cd3123b09fc0b09a2b336f"
|
|
64
64
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Dirent } from 'fs';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
-
import { clearExcelCacheHelper } from './
|
|
3
|
+
import { clearExcelCacheHelper } from './clearExcelCache';
|
|
4
4
|
|
|
5
5
|
const testPath = '/Users/user/project/backend/app/api/.cache';
|
|
6
6
|
jest.mock('fs/promises');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FlagMomentCleanup } from '../helpers/FlagMomentCleanup';
|
|
2
|
+
|
|
3
|
+
let lastCleanupYear: number = -1;
|
|
4
|
+
let lastCleanupMonth: number = -1;
|
|
5
|
+
|
|
6
|
+
export async function endFunctionsOfUsersWithoutRegistration() {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const currentYear = now.getFullYear();
|
|
9
|
+
const currentMonth = now.getMonth();
|
|
10
|
+
|
|
11
|
+
if (lastCleanupMonth === currentMonth && currentYear === lastCleanupYear) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await FlagMomentCleanup.endFunctionsOfUsersWithoutRegistration();
|
|
16
|
+
lastCleanupYear = currentYear;
|
|
17
|
+
lastCleanupMonth = currentMonth;
|
|
18
|
+
}
|
package/src/crons.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
|
2
1
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
3
2
|
import { Database } from '@simonbackx/simple-database';
|
|
4
3
|
import { logger, StyledText } from '@simonbackx/simple-logging';
|
|
@@ -9,7 +8,8 @@ import { Formatter, sleep } from '@stamhoofd/utility';
|
|
|
9
8
|
import AWS from 'aws-sdk';
|
|
10
9
|
import { DateTime } from 'luxon';
|
|
11
10
|
|
|
12
|
-
import { clearExcelCache } from './crons/
|
|
11
|
+
import { clearExcelCache } from './crons/clearExcelCache';
|
|
12
|
+
import { endFunctionsOfUsersWithoutRegistration } from './crons/endFunctionsOfUsersWithoutRegistration';
|
|
13
13
|
import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
|
|
14
14
|
import { checkSettlements } from './helpers/CheckSettlements';
|
|
15
15
|
import { ForwardHandler } from './helpers/ForwardHandler';
|
|
@@ -740,6 +740,12 @@ registeredCronJobs.push({
|
|
|
740
740
|
running: false,
|
|
741
741
|
});
|
|
742
742
|
|
|
743
|
+
registeredCronJobs.push({
|
|
744
|
+
name: 'endFunctionsOfUsersWithoutRegistration',
|
|
745
|
+
method: endFunctionsOfUsersWithoutRegistration,
|
|
746
|
+
running: false,
|
|
747
|
+
});
|
|
748
|
+
|
|
743
749
|
async function run(name: string, handler: () => Promise<void>) {
|
|
744
750
|
try {
|
|
745
751
|
await logger.setContext({
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { ChargeOrganizationsRequest, LimitedFilteredRequest, Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
|
+
import { Context } from '../../../helpers/Context';
|
|
8
|
+
import { fetchToAsyncIterator } from '../../../helpers/fetchToAsyncIterator';
|
|
9
|
+
import { OrganizationCharger } from '../../../helpers/OrganizationCharger';
|
|
10
|
+
import { GetOrganizationsEndpoint } from './GetOrganizationsEndpoint';
|
|
11
|
+
|
|
12
|
+
type Params = Record<string, never>;
|
|
13
|
+
type Query = LimitedFilteredRequest;
|
|
14
|
+
type Body = ChargeOrganizationsRequest;
|
|
15
|
+
type ResponseBody = undefined;
|
|
16
|
+
|
|
17
|
+
export class ChargeOrganizationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
19
|
+
bodyDecoder = ChargeOrganizationsRequest as Decoder<ChargeOrganizationsRequest>;
|
|
20
|
+
|
|
21
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
22
|
+
if (request.method !== 'POST') {
|
|
23
|
+
return [false];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const params = Endpoint.parseParameters(request.url, '/admin/charge-organizations', {});
|
|
27
|
+
|
|
28
|
+
if (params) {
|
|
29
|
+
return [true, params as Params];
|
|
30
|
+
}
|
|
31
|
+
return [false];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static throwIfInvalidBody(body: Body) {
|
|
35
|
+
if (!body.description?.trim()?.length) {
|
|
36
|
+
throw new SimpleError({
|
|
37
|
+
code: 'invalid_field',
|
|
38
|
+
message: 'Invalid description',
|
|
39
|
+
human: 'Beschrijving is verplicht',
|
|
40
|
+
field: 'description',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!body.price) {
|
|
45
|
+
throw new SimpleError({
|
|
46
|
+
code: 'invalid_field',
|
|
47
|
+
message: 'Invalid price',
|
|
48
|
+
human: 'Bedrag kan niet 0 zijn',
|
|
49
|
+
field: 'price',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (body.amount === 0) {
|
|
54
|
+
throw new SimpleError({
|
|
55
|
+
code: 'invalid_field',
|
|
56
|
+
message: 'Invalid amount',
|
|
57
|
+
human: 'Aantal kan niet 0 zijn',
|
|
58
|
+
field: 'amount',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (body.organizationId === undefined) {
|
|
63
|
+
throw new SimpleError({
|
|
64
|
+
code: 'invalid_field',
|
|
65
|
+
message: 'Invalid organization id',
|
|
66
|
+
human: 'Organisatie is verplicht',
|
|
67
|
+
field: 'organizationId',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
73
|
+
await Context.authenticate();
|
|
74
|
+
|
|
75
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
76
|
+
throw Context.auth.error();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = request.body;
|
|
80
|
+
ChargeOrganizationsEndpoint.throwIfInvalidBody(body);
|
|
81
|
+
|
|
82
|
+
const queueId = 'charge-organizations';
|
|
83
|
+
|
|
84
|
+
if (QueueHandler.isRunning(queueId)) {
|
|
85
|
+
throw new SimpleError({
|
|
86
|
+
code: 'charge_pending',
|
|
87
|
+
message: 'Charge organizations already pending',
|
|
88
|
+
human: 'Er is al een aanrekening bezig, even geduld.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await QueueHandler.schedule(queueId, async () => {
|
|
93
|
+
const dataGenerator = fetchToAsyncIterator(request.query, {
|
|
94
|
+
fetch: GetOrganizationsEndpoint.buildData,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const organizationId = body.organizationId;
|
|
98
|
+
const chargeOrganizations = organizationId === null
|
|
99
|
+
? OrganizationCharger.chargeFromPlatform
|
|
100
|
+
: (args: { organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) => OrganizationCharger.chargeMany({
|
|
101
|
+
chargingOrganizationId: organizationId,
|
|
102
|
+
...args });
|
|
103
|
+
|
|
104
|
+
for await (const data of dataGenerator) {
|
|
105
|
+
await chargeOrganizations({
|
|
106
|
+
organizationsToCharge: data,
|
|
107
|
+
price: body.price,
|
|
108
|
+
amount: body.amount ?? 1,
|
|
109
|
+
description: body.description,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return new Response(undefined);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -95,12 +95,10 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
95
95
|
return query;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
async
|
|
99
|
-
await Context.authenticate();
|
|
100
|
-
|
|
98
|
+
static async buildData(requestQuery: LimitedFilteredRequest): Promise<PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>> {
|
|
101
99
|
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
102
100
|
|
|
103
|
-
if (
|
|
101
|
+
if (requestQuery.limit > maxLimit) {
|
|
104
102
|
throw new SimpleError({
|
|
105
103
|
code: 'invalid_field',
|
|
106
104
|
field: 'limit',
|
|
@@ -108,7 +106,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
108
106
|
});
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
if (
|
|
109
|
+
if (requestQuery.limit < 1) {
|
|
112
110
|
throw new SimpleError({
|
|
113
111
|
code: 'invalid_field',
|
|
114
112
|
field: 'limit',
|
|
@@ -116,34 +114,40 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
116
114
|
});
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
const data = await GetOrganizationsEndpoint.buildQuery(
|
|
117
|
+
const data = await GetOrganizationsEndpoint.buildQuery(requestQuery).fetch();
|
|
120
118
|
const organizations = Organization.fromRows(data, 'organizations');
|
|
121
119
|
|
|
122
120
|
let next: LimitedFilteredRequest | undefined;
|
|
123
121
|
|
|
124
|
-
if (organizations.length >=
|
|
122
|
+
if (organizations.length >= requestQuery.limit) {
|
|
125
123
|
const lastObject = organizations[organizations.length - 1];
|
|
126
|
-
const nextFilter = getSortFilter(lastObject, sorters,
|
|
124
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
127
125
|
|
|
128
126
|
next = new LimitedFilteredRequest({
|
|
129
|
-
filter:
|
|
127
|
+
filter: requestQuery.filter,
|
|
130
128
|
pageFilter: nextFilter,
|
|
131
|
-
sort:
|
|
132
|
-
limit:
|
|
133
|
-
search:
|
|
129
|
+
sort: requestQuery.sort,
|
|
130
|
+
limit: requestQuery.limit,
|
|
131
|
+
search: requestQuery.search,
|
|
134
132
|
});
|
|
135
133
|
|
|
136
|
-
if (JSON.stringify(nextFilter) === JSON.stringify(
|
|
137
|
-
console.error('Found infinite loading loop for',
|
|
134
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
135
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
138
136
|
next = undefined;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
|
|
142
|
-
return new
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
return new PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>({
|
|
141
|
+
results: await AuthenticatedStructures.organizations(organizations),
|
|
142
|
+
next,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
147
|
+
await Context.authenticate();
|
|
148
|
+
|
|
149
|
+
const paginatedResponse = await GetOrganizationsEndpoint.buildData(request.query);
|
|
150
|
+
|
|
151
|
+
return new Response(paginatedResponse);
|
|
148
152
|
}
|
|
149
153
|
}
|
|
@@ -53,6 +53,11 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
53
53
|
event.organizationId = put.organizationId;
|
|
54
54
|
event.meta = put.meta;
|
|
55
55
|
|
|
56
|
+
if (event.organizationId === null && event.meta.groups !== null) {
|
|
57
|
+
event.meta.groups = null;
|
|
58
|
+
console.error('Removed groups because organizationId is null for new event');
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
if (event.meta.groups && event.meta.groups.length === 0) {
|
|
57
62
|
throw new SimpleError({
|
|
58
63
|
code: 'invalid_field',
|
|
@@ -131,6 +136,11 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
131
136
|
event.organizationId = patch.organizationId;
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
if (event.organizationId === null && event.meta.groups !== null) {
|
|
140
|
+
event.meta.groups = null;
|
|
141
|
+
console.error('Removed groups because organizationId is null for event', event.id);
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
if (event.meta.groups && event.meta.groups.length === 0) {
|
|
135
145
|
throw new SimpleError({
|
|
136
146
|
code: 'invalid_field',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { BalanceItem, Member, Organization, Payment } from '@stamhoofd/models';
|
|
3
|
-
import {
|
|
3
|
+
import { DetailedPayableBalanceCollection, DetailedPayableBalance } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -9,15 +9,17 @@ import { Context } from '../../../helpers/Context';
|
|
|
9
9
|
type Params = Record<string, never>;
|
|
10
10
|
type Query = undefined;
|
|
11
11
|
type Body = undefined;
|
|
12
|
-
type ResponseBody =
|
|
12
|
+
type ResponseBody = DetailedPayableBalanceCollection;
|
|
13
13
|
|
|
14
|
-
export class
|
|
14
|
+
export class GetUserDetailedPayableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
15
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
16
|
if (request.method !== 'GET') {
|
|
17
17
|
return [false];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const params =
|
|
20
|
+
const params = request.getVersion() >= 339
|
|
21
|
+
? Endpoint.parseParameters(request.url, '/user/payable-balance/detailed', {})
|
|
22
|
+
: Endpoint.parseParameters(request.url, '/user/billing/status/detailed', {});
|
|
21
23
|
|
|
22
24
|
if (params) {
|
|
23
25
|
return [true, params as Params];
|
|
@@ -36,7 +38,7 @@ export class GetUserDetailedBilingStatusEndpoint extends Endpoint<Params, Query,
|
|
|
36
38
|
// todo: this is a duplicate query
|
|
37
39
|
const { payments, balanceItemPayments } = await BalanceItem.loadPayments(balanceItemModels);
|
|
38
40
|
|
|
39
|
-
return new Response(await
|
|
41
|
+
return new Response(await GetUserDetailedPayableBalanceEndpoint.getDetailedBillingStatus(balanceItemModels, payments));
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
static async getDetailedBillingStatus(balanceItemModels: BalanceItem[], paymentModels: Payment[]) {
|
|
@@ -47,7 +49,7 @@ export class GetUserDetailedBilingStatusEndpoint extends Endpoint<Params, Query,
|
|
|
47
49
|
|
|
48
50
|
// Group by organization you'll have to pay to
|
|
49
51
|
if (organizationIds.length === 0) {
|
|
50
|
-
return
|
|
52
|
+
return DetailedPayableBalanceCollection.create({});
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// Optimization: prevent fetching the organization we already have
|
|
@@ -70,9 +72,9 @@ export class GetUserDetailedBilingStatusEndpoint extends Endpoint<Params, Query,
|
|
|
70
72
|
const organizations = await AuthenticatedStructures.organizations(organizationModels);
|
|
71
73
|
const payments = await AuthenticatedStructures.paymentsGeneral(paymentModels, false);
|
|
72
74
|
|
|
73
|
-
return
|
|
75
|
+
return DetailedPayableBalanceCollection.create({
|
|
74
76
|
organizations: organizations.map((o) => {
|
|
75
|
-
return
|
|
77
|
+
return DetailedPayableBalance.create({
|
|
76
78
|
organization: o,
|
|
77
79
|
balanceItems: balanceItems.filter(b => b.organizationId == o.id),
|
|
78
80
|
payments: payments.filter(p => p.organizationId === o.id),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CachedBalance, Member, Organization } from '@stamhoofd/models';
|
|
3
|
+
import { PayableBalanceCollection, PayableBalance } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -9,15 +9,17 @@ import { Context } from '../../../helpers/Context';
|
|
|
9
9
|
type Params = Record<string, never>;
|
|
10
10
|
type Query = undefined;
|
|
11
11
|
type Body = undefined;
|
|
12
|
-
type ResponseBody =
|
|
12
|
+
type ResponseBody = PayableBalanceCollection;
|
|
13
13
|
|
|
14
|
-
export class
|
|
14
|
+
export class GetUserPayableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
15
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
16
|
if (request.method !== 'GET') {
|
|
17
17
|
return [false];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const params =
|
|
20
|
+
const params = request.getVersion() >= 339
|
|
21
|
+
? Endpoint.parseParameters(request.url, '/user/payable-balance', {})
|
|
22
|
+
: Endpoint.parseParameters(request.url, '/user/billing/status', {});
|
|
21
23
|
|
|
22
24
|
if (params) {
|
|
23
25
|
return [true, params as Params];
|
|
@@ -31,14 +33,14 @@ export class GetUserBilingStatusEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
31
33
|
|
|
32
34
|
const memberIds = await Member.getMemberIdsWithRegistrationForUser(user);
|
|
33
35
|
|
|
34
|
-
return new Response(await
|
|
36
|
+
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([user.id, ...memberIds], organization));
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
static async getBillingStatusForObjects(objectIds: string[], organization?: Organization | null) {
|
|
38
40
|
// Load cached balances
|
|
39
|
-
const
|
|
41
|
+
const receivableBalances = await CachedBalance.getForObjects(objectIds, organization?.id);
|
|
40
42
|
|
|
41
|
-
const organizationIds = Formatter.uniqueArray(
|
|
43
|
+
const organizationIds = Formatter.uniqueArray(receivableBalances.map(b => b.organizationId));
|
|
42
44
|
|
|
43
45
|
let addOrganization = false;
|
|
44
46
|
const i = organization ? organizationIds.indexOf(organization.id) : -1;
|
|
@@ -55,10 +57,10 @@ export class GetUserBilingStatusEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
55
57
|
|
|
56
58
|
const authenticatedOrganizations = await AuthenticatedStructures.organizations(organizations);
|
|
57
59
|
|
|
58
|
-
const billingStatus =
|
|
60
|
+
const billingStatus = PayableBalanceCollection.create({});
|
|
59
61
|
|
|
60
62
|
for (const organization of authenticatedOrganizations) {
|
|
61
|
-
const items =
|
|
63
|
+
const items = receivableBalances.filter(b => b.organizationId === organization.id);
|
|
62
64
|
|
|
63
65
|
let amount = 0;
|
|
64
66
|
let amountPending = 0;
|
|
@@ -68,7 +70,7 @@ export class GetUserBilingStatusEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
68
70
|
amountPending += item.amountPending;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
billingStatus.organizations.push(
|
|
73
|
+
billingStatus.organizations.push(PayableBalance.create({
|
|
72
74
|
organization,
|
|
73
75
|
amount,
|
|
74
76
|
amountPending,
|
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
2
|
+
import { DetailedPayableBalanceCollection, PaymentStatus } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { BalanceItem, Payment } from '@stamhoofd/models';
|
|
5
5
|
import { SQL } from '@stamhoofd/sql';
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
|
-
import {
|
|
7
|
+
import { GetUserDetailedPayableBalanceEndpoint } from '../../../global/registration/GetUserDetailedPayableBalanceEndpoint';
|
|
8
8
|
|
|
9
9
|
type Params = Record<string, never>;
|
|
10
10
|
type Query = undefined;
|
|
11
|
-
type ResponseBody =
|
|
11
|
+
type ResponseBody = DetailedPayableBalanceCollection;
|
|
12
12
|
type Body = undefined;
|
|
13
13
|
|
|
14
|
-
export class
|
|
14
|
+
export class GetOrganizationDetailedPayableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
15
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
16
|
if (request.method !== 'GET') {
|
|
17
17
|
return [false];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return [false];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const params = Endpoint.parseParameters(request.url, '/organization/billing/status/detailed', {});
|
|
20
|
+
const params = request.getVersion() >= 339
|
|
21
|
+
? Endpoint.parseParameters(request.url, '/organization/payable-balance/detailed', {})
|
|
22
|
+
: (
|
|
23
|
+
request.getVersion() <= 334
|
|
24
|
+
? Endpoint.parseParameters(request.url, '/organization/billing/status', {})
|
|
25
|
+
: Endpoint.parseParameters(request.url, '/organization/billing/status/detailed', {})
|
|
26
|
+
);
|
|
31
27
|
|
|
32
28
|
if (params) {
|
|
33
29
|
return [true, params as Params];
|
|
@@ -53,6 +49,6 @@ export class GetOrganizationDetailedBillingStatusEndpoint extends Endpoint<Param
|
|
|
53
49
|
)
|
|
54
50
|
.fetch();
|
|
55
51
|
|
|
56
|
-
return new Response(await
|
|
52
|
+
return new Response(await GetUserDetailedPayableBalanceEndpoint.getDetailedBillingStatus(balanceItemModels, paymentModels));
|
|
57
53
|
}
|
|
58
54
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
2
|
+
import { PayableBalanceCollection } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { Context } from '../../../../helpers/Context';
|
|
5
|
-
import {
|
|
5
|
+
import { GetUserPayableBalanceEndpoint } from '../../../global/registration/GetUserPayableBalanceEndpoint';
|
|
6
6
|
|
|
7
7
|
type Params = Record<string, never>;
|
|
8
8
|
type Query = undefined;
|
|
9
|
-
type ResponseBody =
|
|
9
|
+
type ResponseBody = PayableBalanceCollection;
|
|
10
10
|
type Body = undefined;
|
|
11
11
|
|
|
12
|
-
export class
|
|
12
|
+
export class GetOrganizationPayableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
13
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
14
14
|
if (request.method !== 'GET') {
|
|
15
15
|
return [false];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const params =
|
|
18
|
+
const params = request.getVersion() >= 339
|
|
19
|
+
? Endpoint.parseParameters(request.url, '/organization/payable-balance', {})
|
|
20
|
+
: Endpoint.parseParameters(request.url, '/organization/billing/status', {});
|
|
19
21
|
|
|
20
22
|
if (params) {
|
|
21
23
|
return [true, params as Params];
|
|
@@ -32,6 +34,6 @@ export class GetDetailedBillingStatusEndpoint extends Endpoint<Params, Query, Bo
|
|
|
32
34
|
throw Context.auth.error();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
return new Response(await
|
|
37
|
+
return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([organization.id], null));
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -9,6 +9,7 @@ type Query = undefined;
|
|
|
9
9
|
type Body = undefined;
|
|
10
10
|
type ResponseBody = BalanceItemWithPayments[];
|
|
11
11
|
|
|
12
|
+
// Rename to ReceiveableBalance
|
|
12
13
|
export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
14
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
14
15
|
if (request.method !== 'GET') {
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { DetailedReceivableBalance, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamhoofd/models';
|
|
5
|
+
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
7
|
+
import { SQL } from '@stamhoofd/sql';
|
|
8
|
+
|
|
9
|
+
type Params = { id: string; type: ReceivableBalanceType };
|
|
10
|
+
type Query = undefined;
|
|
11
|
+
type ResponseBody = DetailedReceivableBalance;
|
|
12
|
+
type Body = undefined;
|
|
13
|
+
|
|
14
|
+
export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
|
+
if (request.method !== 'GET') {
|
|
17
|
+
return [false];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = Endpoint.parseParameters(request.url, '/receivable-balances/@type/@id', {
|
|
21
|
+
type: String,
|
|
22
|
+
id: String,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (params && Object.values(ReceivableBalanceType).includes(params.type as unknown as ReceivableBalanceType)) {
|
|
26
|
+
return [true, params as Params];
|
|
27
|
+
}
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
|
+
const organization = await Context.setOrganizationScope();
|
|
33
|
+
await Context.authenticate();
|
|
34
|
+
|
|
35
|
+
// If the user has permission, we'll also search if he has access to the organization's key
|
|
36
|
+
if (!await Context.auth.canManageFinances(organization.id)) {
|
|
37
|
+
throw Context.auth.error();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
|
|
41
|
+
let paymentModels: Payment[] = [];
|
|
42
|
+
|
|
43
|
+
switch (request.params.type) {
|
|
44
|
+
case ReceivableBalanceType.organization: {
|
|
45
|
+
paymentModels = await Payment.select()
|
|
46
|
+
.where('organizationId', organization.id)
|
|
47
|
+
.where('payingOrganizationId', request.params.id)
|
|
48
|
+
.andWhere(
|
|
49
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
50
|
+
)
|
|
51
|
+
.fetch();
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case ReceivableBalanceType.member: {
|
|
56
|
+
paymentModels = await Payment.select()
|
|
57
|
+
.where('organizationId', organization.id)
|
|
58
|
+
.join(
|
|
59
|
+
SQL.join(BalanceItemPayment.table)
|
|
60
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
61
|
+
)
|
|
62
|
+
.join(
|
|
63
|
+
SQL.join(BalanceItem.table)
|
|
64
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
65
|
+
)
|
|
66
|
+
.where(SQL.column(BalanceItem.table, 'memberId'), request.params.id)
|
|
67
|
+
.andWhere(
|
|
68
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
69
|
+
)
|
|
70
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
71
|
+
.fetch();
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
|
|
77
|
+
const payments = await AuthenticatedStructures.paymentsGeneral(paymentModels, false);
|
|
78
|
+
|
|
79
|
+
const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
|
|
80
|
+
|
|
81
|
+
const created = new CachedBalance();
|
|
82
|
+
created.amount = 0;
|
|
83
|
+
created.amountPending = 0;
|
|
84
|
+
created.organizationId = organization.id;
|
|
85
|
+
created.objectId = request.params.id;
|
|
86
|
+
created.objectType = request.params.type;
|
|
87
|
+
|
|
88
|
+
const base = await AuthenticatedStructures.receivableBalance(balances.length === 1 ? balances[0] : created);
|
|
89
|
+
|
|
90
|
+
return new Response(
|
|
91
|
+
DetailedReceivableBalance.create({
|
|
92
|
+
...base,
|
|
93
|
+
balanceItems,
|
|
94
|
+
payments,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|