@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.
Files changed (29) hide show
  1. package/package.json +10 -10
  2. package/src/crons/{clear-excel-cache.test.ts → clearExcelCache.test.ts} +1 -1
  3. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +18 -0
  4. package/src/crons.ts +8 -2
  5. package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +116 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +24 -20
  7. package/src/endpoints/global/events/PatchEventsEndpoint.ts +10 -0
  8. package/src/endpoints/global/registration/{GetUserDetailedBillingStatusEndpoint.ts → GetUserDetailedPayableBalanceEndpoint.ts} +10 -8
  9. package/src/endpoints/global/registration/{GetUserBillingStatusEndpoint.ts → GetUserPayableBalanceEndpoint.ts} +13 -11
  10. package/src/endpoints/organization/dashboard/billing/{GetOrganizationDetailedBillingStatusEndpoint.ts → GetOrganizationDetailedPayableBalanceEndpoint.ts} +12 -16
  11. package/src/endpoints/organization/dashboard/billing/{GetOrganizationBillingStatusEndpoint.ts → GetOrganizationPayableBalanceEndpoint.ts} +8 -6
  12. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +1 -0
  13. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +98 -0
  14. package/src/endpoints/organization/dashboard/{cached-outstanding-balance/GetCachedOutstandingBalanceCountEndpoint.ts → receivable-balances/GetReceivableBalancesCountEndpoint.ts} +4 -4
  15. package/src/endpoints/organization/dashboard/{cached-outstanding-balance/GetCachedOutstandingBalanceEndpoint.ts → receivable-balances/GetReceivableBalancesEndpoint.ts} +18 -14
  16. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +57 -0
  17. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +123 -13
  18. package/src/helpers/AdminPermissionChecker.ts +11 -2
  19. package/src/helpers/AuthenticatedStructures.ts +79 -18
  20. package/src/helpers/FlagMomentCleanup.ts +68 -0
  21. package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
  22. package/src/helpers/OrganizationCharger.ts +46 -0
  23. package/src/helpers/StripeHelper.ts +35 -10
  24. package/src/sql-filters/orders.ts +26 -0
  25. package/src/sql-filters/{cached-outstanding-balance.ts → receivable-balances.ts} +1 -1
  26. package/src/sql-sorters/orders.ts +47 -0
  27. package/src/sql-sorters/{cached-outstanding-balance.ts → receivable-balances.ts} +2 -2
  28. /package/src/crons/{clear-excel-cache.ts → clearExcelCache.ts} +0 -0
  29. /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 { GetCachedOutstandingBalanceEndpoint } from './GetCachedOutstandingBalanceEndpoint';
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 GetCachedOutstandingBalanceCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
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, '/cached-outstanding-balance/count', {});
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 GetCachedOutstandingBalanceEndpoint.buildQuery(request.query);
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 { CachedOutstandingBalance } from '@stamhoofd/models';
4
+ import { CachedBalance } from '@stamhoofd/models';
5
5
  import { compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
- import { CachedOutstandingBalance as CachedOutstandingBalanceStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
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 { cachedOutstandingBalanceFilterCompilers } from '../../../../sql-filters/cached-outstanding-balance';
11
- import { cachedOutstandingBalanceSorters } from '../../../../sql-sorters/cached-outstanding-balance';
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<CachedOutstandingBalanceStruct[], LimitedFilteredRequest>;
16
+ type ResponseBody = PaginatedResponse<ReceivableBalanceStruct[], LimitedFilteredRequest>;
17
17
 
18
- const sorters = cachedOutstandingBalanceSorters;
19
- const filterCompilers = cachedOutstandingBalanceFilterCompilers;
18
+ const sorters = receivableBalanceSorters;
19
+ const filterCompilers = receivableBalanceFilterCompilers;
20
20
 
21
- export class GetCachedOutstandingBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
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, '/cached-outstanding-balance', {});
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 = CachedOutstandingBalance
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 GetCachedOutstandingBalanceEndpoint.buildQuery(requestQuery);
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<CachedOutstandingBalanceStruct[], LimitedFilteredRequest>({
111
- results: await AuthenticatedStructures.cachedOutstandingBalances(data),
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 GetCachedOutstandingBalanceEndpoint.buildData(request.query),
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 { Order, Webshop } from '@stamhoofd/models';
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 = WebshopOrdersQuery;
14
+ type Query = LimitedFilteredRequest;
10
15
  type Body = undefined;
11
- type ResponseBody = PaginatedResponse<PrivateOrder[], Query>;
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 = WebshopOrdersQuery as Decoder<WebshopOrdersQuery>;
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
- async handle(_: DecodedRequest<Params, Query, Body>): Promise<Response<ResponseBody>> {
30
- await Promise.resolve();
31
- throw new Error('Not implemented');
32
- /* const organization = await Context.setOrganizationScope();
33
- await Context.authenticate()
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
- const webshop = await Webshop.getByID(request.params.id)
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("Je hebt geen toegang tot de bestellingen van deze webshop")
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, CachedOutstandingBalance, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
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 CachedOutstandingBalance.getForObjects([member.id]);
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 { CachedOutstandingBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
3
- import { AccessRight, CachedOutstandingBalanceObject, CachedOutstandingBalanceObjectContact, CachedOutstandingBalance as CachedOutstandingBalanceStruct, CachedOutstandingBalanceType, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
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 cachedOutstandingBalances(balances: CachedOutstandingBalance[]): Promise<CachedOutstandingBalanceStruct[]> {
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 === CachedOutstandingBalanceType.organization).map(b => b.objectId));
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 result: CachedOutstandingBalanceStruct[] = [];
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
- const organization = organizationStructs.find(o => o.id == balance.objectId) ?? null;
440
- let thisAdmins: User[] = [];
441
- if (organization) {
442
- thisAdmins = admins.filter(a => a.permissions && a.permissions.forOrganization(organization)?.hasAccessRight(AccessRight.OrganizationFinanceDirector));
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 = CachedOutstandingBalanceStruct.create({
512
+ const struct = ReceivableBalanceStruct.create({
446
513
  ...balance,
447
- object: CachedOutstandingBalanceObject.create({
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
+ }