@stamhoofd/backend 2.43.3 → 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/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/helpers/AdminPermissionChecker.ts +11 -2
- package/src/helpers/AuthenticatedStructures.ts +56 -18
- package/src/helpers/FlagMomentCleanup.ts +68 -0
- package/src/helpers/OrganizationCharger.ts +46 -0
- package/src/helpers/StripeHelper.ts +35 -10
- package/src/sql-filters/orders.ts +3 -4
- package/src/sql-filters/{cached-outstanding-balance.ts → receivable-balances.ts} +1 -1
- package/src/sql-sorters/orders.ts +3 -14
- 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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -3,14 +3,14 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
3
3
|
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Context } from '../../../../helpers/Context';
|
|
6
|
-
import {
|
|
6
|
+
import { GetReceivableBalancesEndpoint } from './GetReceivableBalancesEndpoint';
|
|
7
7
|
|
|
8
8
|
type Params = Record<string, never>;
|
|
9
9
|
type Query = CountFilteredRequest;
|
|
10
10
|
type Body = undefined;
|
|
11
11
|
type ResponseBody = CountResponse;
|
|
12
12
|
|
|
13
|
-
export class
|
|
13
|
+
export class GetReceivableBalancesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
14
|
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
15
15
|
|
|
16
16
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -18,7 +18,7 @@ export class GetCachedOutstandingBalanceCountEndpoint extends Endpoint<Params, Q
|
|
|
18
18
|
return [false];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const params = Endpoint.parseParameters(request.url, '/
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, '/receivable-balances/count', {});
|
|
22
22
|
|
|
23
23
|
if (params) {
|
|
24
24
|
return [true, params as Params];
|
|
@@ -29,7 +29,7 @@ export class GetCachedOutstandingBalanceCountEndpoint extends Endpoint<Params, Q
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
30
|
await Context.setOrganizationScope();
|
|
31
31
|
await Context.authenticate();
|
|
32
|
-
const query = await
|
|
32
|
+
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
33
33
|
|
|
34
34
|
const count = await query
|
|
35
35
|
.count();
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { Decoder } 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 {
|
|
4
|
+
import { CachedBalance } from '@stamhoofd/models';
|
|
5
5
|
import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
|
|
6
|
-
import {
|
|
6
|
+
import { ReceivableBalance as ReceivableBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
9
9
|
import { Context } from '../../../../helpers/Context';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { receivableBalanceFilterCompilers } from '../../../../sql-filters/receivable-balances';
|
|
11
|
+
import { receivableBalanceSorters } from '../../../../sql-sorters/receivable-balances';
|
|
12
12
|
|
|
13
13
|
type Params = Record<string, never>;
|
|
14
14
|
type Query = LimitedFilteredRequest;
|
|
15
15
|
type Body = undefined;
|
|
16
|
-
type ResponseBody = PaginatedResponse<
|
|
16
|
+
type ResponseBody = PaginatedResponse<ReceivableBalanceStruct[], LimitedFilteredRequest>;
|
|
17
17
|
|
|
18
|
-
const sorters =
|
|
19
|
-
const filterCompilers =
|
|
18
|
+
const sorters = receivableBalanceSorters;
|
|
19
|
+
const filterCompilers = receivableBalanceFilterCompilers;
|
|
20
20
|
|
|
21
|
-
export class
|
|
21
|
+
export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
22
22
|
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
23
23
|
|
|
24
24
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
@@ -26,7 +26,7 @@ export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query,
|
|
|
26
26
|
return [false];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const params = Endpoint.parseParameters(request.url, '/
|
|
29
|
+
const params = Endpoint.parseParameters(request.url, '/receivable-balances', {});
|
|
30
30
|
|
|
31
31
|
if (params) {
|
|
32
32
|
return [true, params as Params];
|
|
@@ -48,9 +48,13 @@ export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query,
|
|
|
48
48
|
|
|
49
49
|
scopeFilter = {
|
|
50
50
|
organizationId: organization.id,
|
|
51
|
+
$or: {
|
|
52
|
+
amount: { $neq: 0 },
|
|
53
|
+
amountPending: { $neq: 0 },
|
|
54
|
+
},
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
const query =
|
|
57
|
+
const query = CachedBalance
|
|
54
58
|
.select();
|
|
55
59
|
|
|
56
60
|
if (scopeFilter) {
|
|
@@ -82,7 +86,7 @@ export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query,
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
85
|
-
const query = await
|
|
89
|
+
const query = await GetReceivableBalancesEndpoint.buildQuery(requestQuery);
|
|
86
90
|
const data = await query.fetch();
|
|
87
91
|
|
|
88
92
|
// todo: Create objects from data
|
|
@@ -107,8 +111,8 @@ export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query,
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
return new PaginatedResponse<
|
|
111
|
-
results: await AuthenticatedStructures.
|
|
114
|
+
return new PaginatedResponse<ReceivableBalanceStruct[], LimitedFilteredRequest>({
|
|
115
|
+
results: await AuthenticatedStructures.receivableBalances(data),
|
|
112
116
|
next,
|
|
113
117
|
});
|
|
114
118
|
}
|
|
@@ -136,7 +140,7 @@ export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query,
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
return new Response(
|
|
139
|
-
await
|
|
143
|
+
await GetReceivableBalancesEndpoint.buildData(request.query),
|
|
140
144
|
);
|
|
141
145
|
}
|
|
142
146
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
-
import { BalanceItem,
|
|
3
|
+
import { BalanceItem, CachedBalance, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
|
|
@@ -259,7 +259,7 @@ export class AdminPermissionChecker {
|
|
|
259
259
|
return true;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const cachedBalance = await
|
|
262
|
+
const cachedBalance = await CachedBalance.getForObjects([member.id]);
|
|
263
263
|
if (cachedBalance.length === 0 || (cachedBalance[0].amount === 0 && cachedBalance[0].amountPending === 0)) {
|
|
264
264
|
return true;
|
|
265
265
|
}
|
|
@@ -398,6 +398,15 @@ export class AdminPermissionChecker {
|
|
|
398
398
|
orders: Order[];
|
|
399
399
|
},
|
|
400
400
|
): Promise<boolean> {
|
|
401
|
+
// These balance items are out of scope - but we do have access to them
|
|
402
|
+
for (const balanceItem of balanceItems) {
|
|
403
|
+
if (balanceItem.payingOrganizationId && this.checkScope(balanceItem.payingOrganizationId)) {
|
|
404
|
+
if (await this.canManagePayments(balanceItem.payingOrganizationId)) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
401
410
|
for (const balanceItem of balanceItems) {
|
|
402
411
|
if (!this.checkScope(balanceItem.organizationId)) {
|
|
403
412
|
// Invalid scope
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import {
|
|
3
|
-
import { AccessRight,
|
|
2
|
+
import { CachedBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { AccessRight, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateOrder, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { Context } from './Context';
|
|
@@ -446,34 +446,72 @@ export class AuthenticatedStructures {
|
|
|
446
446
|
return result;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
static async
|
|
449
|
+
static async receivableBalance(balance: CachedBalance): Promise<ReceivableBalanceStruct> {
|
|
450
|
+
return (await this.receivableBalances([balance]))[0];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static async receivableBalances(balances: CachedBalance[]): Promise<ReceivableBalanceStruct[]> {
|
|
450
454
|
if (balances.length === 0) {
|
|
451
455
|
return [];
|
|
452
456
|
}
|
|
453
457
|
|
|
454
|
-
const organizationIds = Formatter.uniqueArray(balances.filter(b => b.objectType ===
|
|
458
|
+
const organizationIds = Formatter.uniqueArray(balances.filter(b => b.objectType === ReceivableBalanceType.organization).map(b => b.objectId));
|
|
455
459
|
const organizations = organizationIds.length > 0 ? await Organization.getByIDs(...organizationIds) : [];
|
|
456
460
|
const admins = await User.getAdmins(organizationIds, { verified: true });
|
|
457
|
-
|
|
458
461
|
const organizationStructs = await this.organizations(organizations);
|
|
459
462
|
|
|
460
|
-
const
|
|
463
|
+
const memberIds = Formatter.uniqueArray(balances.filter(b => b.objectType === ReceivableBalanceType.member).map(b => b.objectId));
|
|
464
|
+
const members = memberIds.length > 0 ? await Member.getBlobByIds(...memberIds) : [];
|
|
465
|
+
|
|
466
|
+
const result: ReceivableBalanceStruct[] = [];
|
|
461
467
|
for (const balance of balances) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
468
|
+
let object = ReceivableBalanceObject.create({
|
|
469
|
+
id: balance.objectId,
|
|
470
|
+
name: 'Onbekend',
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (balance.objectType === ReceivableBalanceType.organization) {
|
|
474
|
+
const organization = organizationStructs.find(o => o.id == balance.objectId) ?? null;
|
|
475
|
+
if (organization) {
|
|
476
|
+
const thisAdmins = admins.filter(a => a.permissions && a.permissions.forOrganization(organization)?.hasAccessRight(AccessRight.OrganizationFinanceDirector));
|
|
477
|
+
object = ReceivableBalanceObject.create({
|
|
478
|
+
id: balance.objectId,
|
|
479
|
+
name: organization.name,
|
|
480
|
+
contacts: thisAdmins.map(a => ReceivableBalanceObjectContact.create({
|
|
481
|
+
firstName: a.firstName ?? '',
|
|
482
|
+
lastName: a.lastName ?? '',
|
|
483
|
+
emails: [a.email],
|
|
484
|
+
})),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (balance.objectType === ReceivableBalanceType.member) {
|
|
489
|
+
const member = members.find(m => m.id === balance.objectId) ?? null;
|
|
490
|
+
if (member) {
|
|
491
|
+
object = ReceivableBalanceObject.create({
|
|
492
|
+
id: balance.objectId,
|
|
493
|
+
name: member.details.name,
|
|
494
|
+
contacts: [
|
|
495
|
+
ReceivableBalanceObjectContact.create({
|
|
496
|
+
firstName: member.details.firstName ?? '',
|
|
497
|
+
lastName: member.details.lastName ?? '',
|
|
498
|
+
emails: member.details.getMemberEmails(),
|
|
499
|
+
}),
|
|
500
|
+
...member.users.filter(u => !member.details.getMemberEmails().includes(u.email)).map((a) => {
|
|
501
|
+
return ReceivableBalanceObjectContact.create({
|
|
502
|
+
firstName: a.firstName ?? '',
|
|
503
|
+
lastName: a.lastName ?? '',
|
|
504
|
+
emails: [a.email],
|
|
505
|
+
});
|
|
506
|
+
}),
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
}
|
|
466
510
|
}
|
|
467
511
|
|
|
468
|
-
const struct =
|
|
512
|
+
const struct = ReceivableBalanceStruct.create({
|
|
469
513
|
...balance,
|
|
470
|
-
object
|
|
471
|
-
name: organization?.name ?? 'Onbekend',
|
|
472
|
-
contacts: thisAdmins.map(a => CachedOutstandingBalanceObjectContact.create({
|
|
473
|
-
name: a.name ?? '',
|
|
474
|
-
emails: [a.email],
|
|
475
|
-
})),
|
|
476
|
-
}),
|
|
514
|
+
object,
|
|
477
515
|
});
|
|
478
516
|
|
|
479
517
|
result.push(struct);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Group, MemberResponsibilityRecord, Platform, Registration } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLWhereExists, SQLWhereSign } from '@stamhoofd/sql';
|
|
3
|
+
import { GroupType } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export class FlagMomentCleanup {
|
|
6
|
+
/**
|
|
7
|
+
* End functions of old members who have no active registration for the current period.
|
|
8
|
+
*/
|
|
9
|
+
static async endFunctionsOfUsersWithoutRegistration() {
|
|
10
|
+
console.log('Start cleanup functions');
|
|
11
|
+
const responsibilitiesToEnd = await this.getActiveMemberResponsibilityRecordsForOrganizationWithoutRegistrationInCurrentPeriod();
|
|
12
|
+
|
|
13
|
+
const now = new Date();
|
|
14
|
+
|
|
15
|
+
await Promise.all(responsibilitiesToEnd.map(async (responsibility) => {
|
|
16
|
+
responsibility.endDate = now;
|
|
17
|
+
await responsibility.save();
|
|
18
|
+
console.log(`Ended responsibility with id ${responsibility.id}`);
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static async getActiveMemberResponsibilityRecordsForOrganizationWithoutRegistrationInCurrentPeriod() {
|
|
23
|
+
const currentPeriodId = (await Platform.getShared()).periodId;
|
|
24
|
+
|
|
25
|
+
const now = new Date();
|
|
26
|
+
|
|
27
|
+
return await MemberResponsibilityRecord.select()
|
|
28
|
+
.whereNot('organizationId', null)
|
|
29
|
+
.where(
|
|
30
|
+
SQL.where('startDate', SQLWhereSign.LessEqual, now)
|
|
31
|
+
.or('startDate', null),
|
|
32
|
+
)
|
|
33
|
+
.where(
|
|
34
|
+
SQL.where('endDate', SQLWhereSign.GreaterEqual, now)
|
|
35
|
+
.or('endDate', null),
|
|
36
|
+
)
|
|
37
|
+
.whereNot(
|
|
38
|
+
new SQLWhereExists(
|
|
39
|
+
SQL.select()
|
|
40
|
+
.from(Registration.table)
|
|
41
|
+
.join(
|
|
42
|
+
SQL.innerJoin(SQL.table(Group.table))
|
|
43
|
+
.where(
|
|
44
|
+
SQL.column(Group.table, 'id'),
|
|
45
|
+
SQL.column(Registration.table, 'groupId'),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
.where(
|
|
49
|
+
SQL.column(Registration.table, 'memberId'),
|
|
50
|
+
SQL.column(MemberResponsibilityRecord.table, 'memberId'),
|
|
51
|
+
).where(
|
|
52
|
+
SQL.column(Registration.table, 'organizationId'),
|
|
53
|
+
SQL.column(MemberResponsibilityRecord.table, 'organizationId'),
|
|
54
|
+
).where(
|
|
55
|
+
SQL.column(Registration.table, 'periodId'),
|
|
56
|
+
currentPeriodId,
|
|
57
|
+
).where(
|
|
58
|
+
SQL.column(Registration.table, 'deactivatedAt'),
|
|
59
|
+
null,
|
|
60
|
+
).where(
|
|
61
|
+
SQL.column(Group.table, 'type'),
|
|
62
|
+
GroupType.Membership,
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
.fetch();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { BalanceItem, Platform } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItemType, Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export class OrganizationCharger {
|
|
6
|
+
static async chargeFromPlatform(args: { organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
|
|
7
|
+
const platform = await Platform.getShared();
|
|
8
|
+
|
|
9
|
+
const chargeVia = platform.membershipOrganizationId;
|
|
10
|
+
|
|
11
|
+
if (!chargeVia) {
|
|
12
|
+
throw new SimpleError({
|
|
13
|
+
code: 'missing_membership_organization',
|
|
14
|
+
message: 'Missing membershipOrganizationId',
|
|
15
|
+
human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await OrganizationCharger.chargeMany({ chargingOrganizationId: chargeVia, ...args });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static async chargeMany({ chargingOrganizationId, organizationsToCharge, price, amount, description }: { chargingOrganizationId: string; organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
|
|
23
|
+
const balanceItems = organizationsToCharge.map(organizationBeingCharged => OrganizationCharger.createBalanceItem({
|
|
24
|
+
price,
|
|
25
|
+
amount,
|
|
26
|
+
description,
|
|
27
|
+
chargingOrganizationId,
|
|
28
|
+
organizationBeingCharged,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
|
|
32
|
+
await BalanceItem.updateOutstanding(balanceItems);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct }): BalanceItem {
|
|
36
|
+
const balanceItem = new BalanceItem();
|
|
37
|
+
balanceItem.unitPrice = price;
|
|
38
|
+
balanceItem.amount = amount ?? 1;
|
|
39
|
+
balanceItem.description = description;
|
|
40
|
+
balanceItem.type = BalanceItemType.Other;
|
|
41
|
+
balanceItem.payingOrganizationId = organizationBeingCharged.id;
|
|
42
|
+
balanceItem.organizationId = chargingOrganizationId;
|
|
43
|
+
|
|
44
|
+
return balanceItem;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -27,19 +27,29 @@ export class StripeHelper {
|
|
|
27
27
|
|
|
28
28
|
if (charge.payment_method_details?.bancontact) {
|
|
29
29
|
if (charge.payment_method_details.bancontact.iban_last4) {
|
|
30
|
-
payment.iban = '
|
|
30
|
+
payment.iban = '•••• ' + charge.payment_method_details.bancontact.iban_last4;
|
|
31
31
|
}
|
|
32
32
|
payment.ibanName = charge.payment_method_details.bancontact.verified_name;
|
|
33
33
|
}
|
|
34
34
|
if (charge.payment_method_details?.ideal) {
|
|
35
35
|
if (charge.payment_method_details.ideal.iban_last4) {
|
|
36
|
-
payment.iban = '
|
|
36
|
+
payment.iban = '•••• ' + charge.payment_method_details.ideal.iban_last4;
|
|
37
37
|
}
|
|
38
38
|
payment.ibanName = charge.payment_method_details.ideal.verified_name;
|
|
39
39
|
}
|
|
40
40
|
if (charge.payment_method_details?.card) {
|
|
41
41
|
if (charge.payment_method_details.card.last4) {
|
|
42
|
-
payment.iban = '
|
|
42
|
+
payment.iban = '•••• ' + charge.payment_method_details.card.last4;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (charge.payment_method_details?.sepa_debit) {
|
|
46
|
+
if (charge.payment_method_details.sepa_debit.last4) {
|
|
47
|
+
if (charge.payment_method_details.sepa_debit.country === 'BE') {
|
|
48
|
+
payment.iban = charge.payment_method_details.sepa_debit.country + '•• ' + charge.payment_method_details.sepa_debit.bank_code + '• •••• ' + charge.payment_method_details.sepa_debit.last4;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
payment.iban = '•••• ' + charge.payment_method_details.sepa_debit.last4;
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
await payment.save();
|
|
@@ -139,7 +149,7 @@ export class StripeHelper {
|
|
|
139
149
|
|
|
140
150
|
console.log('session', session);
|
|
141
151
|
|
|
142
|
-
if (session.
|
|
152
|
+
if (session.payment_status === 'paid') {
|
|
143
153
|
// This is a direct charge
|
|
144
154
|
const payment_intent = session.payment_intent;
|
|
145
155
|
if (payment_intent !== null && typeof payment_intent !== 'string') {
|
|
@@ -158,7 +168,7 @@ export class StripeHelper {
|
|
|
158
168
|
// Cancel the session
|
|
159
169
|
const session = await stripe.checkout.sessions.expire(model.stripeSessionId);
|
|
160
170
|
|
|
161
|
-
if (session.
|
|
171
|
+
if (session.payment_status === 'paid') {
|
|
162
172
|
return PaymentStatus.Succeeded;
|
|
163
173
|
}
|
|
164
174
|
if (session.status === 'expired') {
|
|
@@ -166,6 +176,11 @@ export class StripeHelper {
|
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
178
|
|
|
179
|
+
if (session.status === 'complete') {
|
|
180
|
+
// Small difference to detect if the payment is almost done
|
|
181
|
+
return PaymentStatus.Pending;
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
return PaymentStatus.Created;
|
|
170
185
|
}
|
|
171
186
|
|
|
@@ -231,10 +246,18 @@ export class StripeHelper {
|
|
|
231
246
|
// Bancontact or iDEAL: use payment intends
|
|
232
247
|
if (payment.method === PaymentMethod.Bancontact || payment.method === PaymentMethod.iDEAL) {
|
|
233
248
|
const paymentMethod = await stripe.paymentMethods.create({
|
|
234
|
-
type: payment.method.toLowerCase() as 'bancontact',
|
|
249
|
+
type: payment.method.toLowerCase() as 'bancontact' | 'ideal',
|
|
235
250
|
billing_details: {
|
|
236
|
-
name: customer
|
|
237
|
-
email: customer.email,
|
|
251
|
+
name: payment.customer?.dynamicName || (customer.name.length > 2 ? customer.name : 'Onbekend'),
|
|
252
|
+
email: payment.customer?.dynamicEmail || customer.email,
|
|
253
|
+
address: payment.customer?.company?.address
|
|
254
|
+
? {
|
|
255
|
+
city: payment.customer.company.address.city,
|
|
256
|
+
country: payment.customer.company.address.country,
|
|
257
|
+
line1: payment.customer.company.address.street + ' ' + payment.customer.company.address.number,
|
|
258
|
+
postal_code: payment.customer.company.address.postalCode,
|
|
259
|
+
}
|
|
260
|
+
: undefined,
|
|
238
261
|
},
|
|
239
262
|
});
|
|
240
263
|
|
|
@@ -315,7 +338,7 @@ export class StripeHelper {
|
|
|
315
338
|
mode: 'payment',
|
|
316
339
|
success_url: redirectUrl,
|
|
317
340
|
cancel_url: cancelUrl,
|
|
318
|
-
payment_method_types: ['card'],
|
|
341
|
+
payment_method_types: payment.method === PaymentMethod.DirectDebit ? ['sepa_debit'] : ['card'],
|
|
319
342
|
line_items: stripeLineItems,
|
|
320
343
|
currency: 'eur',
|
|
321
344
|
locale: i18n.language as 'nl',
|
|
@@ -329,8 +352,10 @@ export class StripeHelper {
|
|
|
329
352
|
: undefined,
|
|
330
353
|
metadata: fullMetadata,
|
|
331
354
|
statement_descriptor: Formatter.slug(statementDescriptor).substring(0, 22).toUpperCase(),
|
|
355
|
+
|
|
332
356
|
},
|
|
333
|
-
customer_email: customer.email,
|
|
357
|
+
customer_email: payment.customer?.dynamicEmail || customer.email,
|
|
358
|
+
customer_creation: 'if_required',
|
|
334
359
|
metadata: fullMetadata,
|
|
335
360
|
expires_at: Math.floor(Date.now() / 1000) + 30 * 60, // Expire in 30 minutes
|
|
336
361
|
});
|
|
@@ -2,10 +2,9 @@ import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQLFilterDefinit
|
|
|
2
2
|
|
|
3
3
|
export const orderFilterCompilers: SQLFilterDefinitions = {
|
|
4
4
|
...baseSQLFilterCompilers,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'#': createSQLColumnFilterCompiler('number'),
|
|
5
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
6
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
7
|
+
number: createSQLColumnFilterCompiler('number'),
|
|
9
8
|
// 'startDate': createSQLColumnFilterCompiler('startDate'),
|
|
10
9
|
// 'endDate': createSQLColumnFilterCompiler('endDate'),
|
|
11
10
|
// 'groupIds': createSQLExpressionFilterCompiler(
|
|
@@ -3,7 +3,7 @@ import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterComp
|
|
|
3
3
|
/**
|
|
4
4
|
* Defines how to filter cached balance items in the database from StamhoofdFilter objects
|
|
5
5
|
*/
|
|
6
|
-
export const
|
|
6
|
+
export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
|
|
7
7
|
...baseSQLFilterCompilers,
|
|
8
8
|
id: createSQLColumnFilterCompiler('id'),
|
|
9
9
|
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
@@ -11,7 +11,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
|
|
|
11
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
12
|
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
number: {
|
|
15
15
|
getValue(a) {
|
|
16
16
|
return a.number;
|
|
17
17
|
},
|
|
@@ -22,7 +22,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
|
|
|
22
22
|
});
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
|
-
|
|
25
|
+
id: {
|
|
26
26
|
getValue(a) {
|
|
27
27
|
return a.id;
|
|
28
28
|
},
|
|
@@ -33,18 +33,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
|
|
|
33
33
|
});
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
-
|
|
37
|
-
getValue(a) {
|
|
38
|
-
return a.id;
|
|
39
|
-
},
|
|
40
|
-
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
41
|
-
return new SQLOrderBy({
|
|
42
|
-
column: SQL.column('name'),
|
|
43
|
-
direction,
|
|
44
|
-
});
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
'createdAt': {
|
|
36
|
+
createdAt: {
|
|
48
37
|
getValue(a) {
|
|
49
38
|
return Formatter.dateTimeIso(a.createdAt);
|
|
50
39
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CachedBalance } from '@stamhoofd/models';
|
|
2
2
|
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const receivableBalanceSorters: SQLSortDefinitions<CachedBalance> = {
|
|
5
5
|
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
6
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
7
|
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
File without changes
|
|
File without changes
|