@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
|
@@ -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
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse, PermissionLevel } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Webshop } from '@stamhoofd/models';
|
|
6
|
+
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
import { GetWebshopOrdersEndpoint } from './GetWebshopOrdersEndpoint';
|
|
8
|
+
|
|
9
|
+
type Params = { id: string };
|
|
10
|
+
type Query = CountFilteredRequest;
|
|
11
|
+
type Body = undefined;
|
|
12
|
+
type ResponseBody = CountResponse;
|
|
13
|
+
|
|
14
|
+
export class GetWebshopOrdersCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
15
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
16
|
+
|
|
17
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
18
|
+
if (request.method !== 'GET') {
|
|
19
|
+
return [false];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const params = Endpoint.parseParameters(request.url, '/webshop/@id/orders/count', { id: String });
|
|
23
|
+
|
|
24
|
+
if (params) {
|
|
25
|
+
return [true, params as Params];
|
|
26
|
+
}
|
|
27
|
+
return [false];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
+
const organization = await Context.setOrganizationScope();
|
|
32
|
+
await Context.authenticate();
|
|
33
|
+
|
|
34
|
+
// Fast throw first (more in depth checking for patches later)
|
|
35
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
36
|
+
throw Context.auth.error();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const webshopId = request.params.id;
|
|
40
|
+
|
|
41
|
+
const webshop = await Webshop.getByID(webshopId);
|
|
42
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
|
|
43
|
+
throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot de bestellingen van deze webshop');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const query = GetWebshopOrdersEndpoint.buildQuery(webshopId, request.query);
|
|
47
|
+
|
|
48
|
+
const count = await query
|
|
49
|
+
.count();
|
|
50
|
+
|
|
51
|
+
return new Response(
|
|
52
|
+
CountResponse.create({
|
|
53
|
+
count,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import {
|
|
4
|
-
import { PaginatedResponse, PermissionLevel, PrivateOrder, WebshopOrdersQuery } from '@stamhoofd/structures';
|
|
3
|
+
import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, PrivateOrder, StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
4
|
|
|
5
|
+
import { Order, Webshop } from '@stamhoofd/models';
|
|
6
|
+
import { compileToSQLFilter, compileToSQLSorter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
7
|
+
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
6
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper';
|
|
10
|
+
import { orderFilterCompilers } from '../../../../sql-filters/orders';
|
|
11
|
+
import { orderSorters } from '../../../../sql-sorters/orders';
|
|
7
12
|
|
|
8
13
|
type Params = { id: string };
|
|
9
|
-
type Query =
|
|
14
|
+
type Query = LimitedFilteredRequest;
|
|
10
15
|
type Body = undefined;
|
|
11
|
-
type ResponseBody = PaginatedResponse<PrivateOrder[],
|
|
16
|
+
type ResponseBody = PaginatedResponse<PrivateOrder[], LimitedFilteredRequest>;
|
|
17
|
+
|
|
18
|
+
const filterCompilers: SQLFilterDefinitions = orderFilterCompilers;
|
|
19
|
+
const sorters: SQLSortDefinitions<Order> = orderSorters;
|
|
12
20
|
|
|
13
21
|
export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
-
queryDecoder =
|
|
22
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
15
23
|
|
|
16
24
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
25
|
if (request.method !== 'GET') {
|
|
@@ -26,22 +34,124 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
26
34
|
return [false];
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
static buildQuery(webshopId: string, q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
38
|
+
// todo: filter userId???
|
|
39
|
+
const organization = Context.organization!;
|
|
40
|
+
|
|
41
|
+
if (!webshopId) {
|
|
42
|
+
// todo
|
|
43
|
+
throw new Error();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ordersTable: string = Order.table;
|
|
47
|
+
|
|
48
|
+
const query = SQL
|
|
49
|
+
.select(SQL.wildcard(ordersTable))
|
|
50
|
+
.from(SQL.table(ordersTable))
|
|
51
|
+
// todo: extra check on webshopId to prevent all orders are returned if webshopId is null?
|
|
52
|
+
.where('webshopId', webshopId)
|
|
53
|
+
.where(compileToSQLFilter({
|
|
54
|
+
$or: [
|
|
55
|
+
{
|
|
56
|
+
organizationId: organization.id,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
organizationId: null,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}, filterCompilers));
|
|
63
|
+
|
|
64
|
+
if (q.filter) {
|
|
65
|
+
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (q.search) {
|
|
69
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
70
|
+
|
|
71
|
+
// todo: detect special search patterns and adjust search filter if needed
|
|
72
|
+
searchFilter = {
|
|
73
|
+
name: {
|
|
74
|
+
$contains: q.search,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (searchFilter) {
|
|
79
|
+
query.where(compileToSQLFilter(searchFilter, filterCompilers));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
84
|
+
if (q.pageFilter) {
|
|
85
|
+
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
89
|
+
query.orderBy(compileToSQLSorter(q.sort, sorters));
|
|
90
|
+
query.limit(q.limit);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return query;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static async buildData(webshopId: string, requestQuery: LimitedFilteredRequest) {
|
|
97
|
+
const query = this.buildQuery(webshopId, requestQuery);
|
|
98
|
+
const data = await query.fetch();
|
|
99
|
+
|
|
100
|
+
const orders: Order[] = Order.fromRows(data, Order.table);
|
|
101
|
+
|
|
102
|
+
let next: LimitedFilteredRequest | undefined;
|
|
103
|
+
|
|
104
|
+
if (orders.length >= requestQuery.limit) {
|
|
105
|
+
const lastObject = orders[orders.length - 1];
|
|
106
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
107
|
+
|
|
108
|
+
next = new LimitedFilteredRequest({
|
|
109
|
+
filter: requestQuery.filter,
|
|
110
|
+
pageFilter: nextFilter,
|
|
111
|
+
sort: requestQuery.sort,
|
|
112
|
+
limit: requestQuery.limit,
|
|
113
|
+
search: requestQuery.search,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
117
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
118
|
+
next = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new PaginatedResponse<PrivateOrder[], LimitedFilteredRequest>({
|
|
123
|
+
results: await AuthenticatedStructures.orders(orders),
|
|
124
|
+
next,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async handle(request: DecodedRequest<Params, Query, Body>): Promise<Response<ResponseBody>> {
|
|
129
|
+
const organization = await Context.setOrganizationScope();
|
|
130
|
+
await Context.authenticate();
|
|
34
131
|
|
|
35
132
|
// Fast throw first (more in depth checking for patches later)
|
|
36
133
|
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
37
|
-
throw Context.auth.error()
|
|
134
|
+
throw Context.auth.error();
|
|
38
135
|
}
|
|
39
136
|
|
|
40
|
-
|
|
137
|
+
LimitedFilteredRequestHelper.throwIfInvalidLimit({
|
|
138
|
+
request: request.query,
|
|
139
|
+
maxLimit: Context.auth.hasSomePlatformAccess() ? 1000 : 100,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const webshopId = request.params.id;
|
|
143
|
+
|
|
144
|
+
const webshop = await Webshop.getByID(webshopId);
|
|
41
145
|
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
|
|
42
|
-
throw Context.auth.notFoundOrNoAccess(
|
|
146
|
+
throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot de bestellingen van deze webshop');
|
|
43
147
|
}
|
|
44
148
|
|
|
149
|
+
return new Response(
|
|
150
|
+
await GetWebshopOrdersEndpoint.buildData(webshopId, request.query),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
|
|
45
155
|
let orders: Order[] | undefined = undefined
|
|
46
156
|
const limit = 50
|
|
47
157
|
|
|
@@ -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';
|
|
@@ -423,34 +423,95 @@ export class AuthenticatedStructures {
|
|
|
423
423
|
return result;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
static async
|
|
426
|
+
static async orders(orders: Order[]): Promise<PrivateOrder[]> {
|
|
427
|
+
// Load groups
|
|
428
|
+
// const groupIds = orders.map(e => e.groupId).filter(id => id !== null);
|
|
429
|
+
// const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : [];
|
|
430
|
+
// const groupStructs = await this.groups(groups);
|
|
431
|
+
|
|
432
|
+
const result: PrivateOrder[] = [];
|
|
433
|
+
|
|
434
|
+
for (const order of orders) {
|
|
435
|
+
// const group = groupStructs.find(g => g.id == event.groupId) ?? null;
|
|
436
|
+
|
|
437
|
+
const struct = PrivateOrder.create({
|
|
438
|
+
...order,
|
|
439
|
+
// todo!!!!!
|
|
440
|
+
balanceItems: [],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
result.push(struct);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
|
|
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[]> {
|
|
427
454
|
if (balances.length === 0) {
|
|
428
455
|
return [];
|
|
429
456
|
}
|
|
430
457
|
|
|
431
|
-
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));
|
|
432
459
|
const organizations = organizationIds.length > 0 ? await Organization.getByIDs(...organizationIds) : [];
|
|
433
460
|
const admins = await User.getAdmins(organizationIds, { verified: true });
|
|
434
|
-
|
|
435
461
|
const organizationStructs = await this.organizations(organizations);
|
|
436
462
|
|
|
437
|
-
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[] = [];
|
|
438
467
|
for (const balance of balances) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
}
|
|
443
510
|
}
|
|
444
511
|
|
|
445
|
-
const struct =
|
|
512
|
+
const struct = ReceivableBalanceStruct.create({
|
|
446
513
|
...balance,
|
|
447
|
-
object
|
|
448
|
-
name: organization?.name ?? 'Onbekend',
|
|
449
|
-
contacts: thisAdmins.map(a => CachedOutstandingBalanceObjectContact.create({
|
|
450
|
-
name: a.name ?? '',
|
|
451
|
-
emails: [a.email],
|
|
452
|
-
})),
|
|
453
|
-
}),
|
|
514
|
+
object,
|
|
454
515
|
});
|
|
455
516
|
|
|
456
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,24 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export class LimitedFilteredRequestHelper {
|
|
5
|
+
static throwIfInvalidLimit({ request, maxLimit }: { request: LimitedFilteredRequest; maxLimit: number }) {
|
|
6
|
+
const requestLimit = request.limit;
|
|
7
|
+
|
|
8
|
+
if (requestLimit > maxLimit) {
|
|
9
|
+
throw new SimpleError({
|
|
10
|
+
code: 'invalid_field',
|
|
11
|
+
field: 'limit',
|
|
12
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (requestLimit < 1) {
|
|
17
|
+
throw new SimpleError({
|
|
18
|
+
code: 'invalid_field',
|
|
19
|
+
field: 'limit',
|
|
20
|
+
message: 'Limit can not be less than 1',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -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
|
+
}
|