@stamhoofd/backend 2.83.5 → 2.84.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/index.ts +19 -4
- package/package.json +18 -14
- package/src/crons/amazon-ses.ts +26 -5
- package/src/crons/balance-emails.ts +18 -17
- package/src/email-recipient-loaders/registrations.ts +87 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
- package/src/endpoints/global/files/UploadFile.ts +11 -16
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +233 -232
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +153 -0
- package/src/helpers/AdminPermissionChecker.ts +65 -37
- package/src/helpers/AuthenticatedStructures.ts +43 -3
- package/src/helpers/Context.ts +29 -1
- package/src/helpers/GlobalHelper.ts +3 -1
- package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
- package/src/helpers/GroupedThrottledQueue.ts +108 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
- package/src/helpers/MemberCharger.ts +0 -5
- package/src/helpers/MembershipCharger.ts +3 -9
- package/src/helpers/OrganizationCharger.ts +0 -5
- package/src/helpers/ThrottledQueue.test.ts +194 -0
- package/src/helpers/ThrottledQueue.ts +145 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/services/BalanceItemPaymentService.ts +1 -33
- package/src/services/BalanceItemService.ts +167 -48
- package/src/services/FileSignService.ts +18 -13
- package/src/services/MemberRecordStore.ts +28 -19
- package/src/services/PaymentReallocationService.test.ts +25 -14
- package/src/services/PaymentReallocationService.ts +29 -10
- package/src/services/PaymentService.ts +4 -16
- package/src/services/PlatformMembershipService.ts +8 -4
- package/src/services/RegistrationService.ts +66 -2
- package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
- package/src/sql-filters/groups.ts +67 -0
- package/src/sql-filters/members.ts +33 -58
- package/src/sql-filters/organization-registration-periods.ts +8 -0
- package/src/sql-filters/registration-periods.ts +8 -0
- package/src/sql-filters/registrations.ts +11 -22
- package/src/sql-sorters/groups.ts +24 -0
- package/src/sql-sorters/organization-registration-periods.ts +24 -0
- package/src/sql-sorters/registration-periods.ts +47 -0
- package/src/sql-sorters/registrations.ts +77 -0
- package/tests/actions/patchOrganizationMember.ts +27 -0
- package/tests/actions/patchPaymentStatus.ts +45 -0
- package/tests/actions/patchUserMember.ts +27 -0
- package/tests/assertions/assertBalances.ts +49 -0
- package/tests/e2e/api-rate-limits.test.ts +5 -5
- package/tests/e2e/bundle-discounts.test.ts +4060 -0
- package/tests/e2e/charge-members.test.ts +27 -24
- package/tests/e2e/documents.test.ts +398 -0
- package/tests/e2e/register.test.ts +292 -312
- package/tests/helpers/PayconiqMocker.ts +55 -0
- package/tests/init/index.ts +5 -0
- package/tests/init/initAdmin.ts +14 -0
- package/tests/init/initBundleDiscount.ts +47 -0
- package/tests/init/initPayconiq.ts +9 -0
- package/tests/init/initPlatformAdmin.ts +13 -0
- package/tests/init/initStripe.ts +21 -0
- package/tests/jest.setup.ts +29 -0
- package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
|
@@ -1,57 +1,181 @@
|
|
|
1
|
-
import { BalanceItem, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
|
|
2
|
-
import { AuditLogSource, BalanceItemStatus, OrderStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
1
|
+
import { BalanceItem, CachedBalance, Document, MemberUser, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogSource, BalanceItemStatus, BalanceItemType, OrderStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
3
4
|
import { AuditLogService } from './AuditLogService';
|
|
4
|
-
import { RegistrationService } from './RegistrationService';
|
|
5
5
|
import { PaymentReallocationService } from './PaymentReallocationService';
|
|
6
|
-
import {
|
|
6
|
+
import { RegistrationService } from './RegistrationService';
|
|
7
|
+
import { Model } from '@simonbackx/simple-database';
|
|
8
|
+
import { GroupedThrottledQueue } from '../helpers/GroupedThrottledQueue';
|
|
9
|
+
import { ThrottledQueue } from '../helpers/ThrottledQueue';
|
|
10
|
+
|
|
11
|
+
const memberUpdateQueue = new GroupedThrottledQueue(async (organizationId: string, memberIds: string[]) => {
|
|
12
|
+
await CachedBalance.updateForMembers(organizationId, memberIds);
|
|
13
|
+
|
|
14
|
+
for (const memberId of memberIds) {
|
|
15
|
+
await PaymentReallocationService.reallocate(organizationId, memberId, ReceivableBalanceType.member);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (memberIds.length) {
|
|
19
|
+
// Now also include the userIds of the members
|
|
20
|
+
const userMemberIds = (await MemberUser.select().where('membersId', memberIds).fetch()).map(m => m.usersId);
|
|
21
|
+
userUpdateQueue.addItems(organizationId, userMemberIds);
|
|
22
|
+
}
|
|
23
|
+
}, { maxDelay: 10_000 });
|
|
24
|
+
|
|
25
|
+
const userUpdateQueue = new GroupedThrottledQueue(async (organizationId: string, userIds: string[]) => {
|
|
26
|
+
await CachedBalance.updateForUsers(organizationId, userIds);
|
|
27
|
+
|
|
28
|
+
for (const userId of userIds) {
|
|
29
|
+
await PaymentReallocationService.reallocate(organizationId, userId, ReceivableBalanceType.user);
|
|
30
|
+
}
|
|
31
|
+
}, { maxDelay: 10_000 });
|
|
32
|
+
|
|
33
|
+
const organizationUpdateQueue = new GroupedThrottledQueue(async (organizationId: string, organizationIds: string[]) => {
|
|
34
|
+
await CachedBalance.updateForOrganizations(organizationId, organizationIds);
|
|
35
|
+
|
|
36
|
+
for (const payingOrganizationId of organizationIds) {
|
|
37
|
+
await PaymentReallocationService.reallocate(organizationId, payingOrganizationId, ReceivableBalanceType.organization);
|
|
38
|
+
}
|
|
39
|
+
}, { maxDelay: 60_000 });
|
|
40
|
+
|
|
41
|
+
export const registrationUpdateQueue = new GroupedThrottledQueue(async (organizationId: string, registrationIds: string[]) => {
|
|
42
|
+
await CachedBalance.updateForRegistrations(organizationId, registrationIds);
|
|
43
|
+
await Document.updateForRegistrations(registrationIds, organizationId);
|
|
44
|
+
}, { maxDelay: 10_000 });
|
|
45
|
+
|
|
46
|
+
const bundleDiscountsUpdateQueue = new ThrottledQueue(async (registrationIds: string[]) => {
|
|
47
|
+
for (const registrationId of registrationIds) {
|
|
48
|
+
await RegistrationService.updateDiscounts(registrationId);
|
|
49
|
+
}
|
|
50
|
+
}, { maxDelay: 10_000 });
|
|
7
51
|
|
|
8
52
|
export const BalanceItemService = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (balanceItem.pricePaid <= 0) {
|
|
53
|
+
listening: false,
|
|
54
|
+
|
|
55
|
+
listen() {
|
|
56
|
+
if (this.listening) {
|
|
14
57
|
return;
|
|
15
58
|
}
|
|
59
|
+
this.listening = true;
|
|
60
|
+
Model.modelEventBus.addListener(this, async (event) => {
|
|
61
|
+
if (!(event.model instanceof BalanceItem)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
16
64
|
|
|
17
|
-
|
|
65
|
+
if (event.type === 'created' || event.type === 'deleted') {
|
|
66
|
+
await this.scheduleUpdate(event.model);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
18
69
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
70
|
+
// Check changed:
|
|
71
|
+
// status, unitPrice, dueAt, amount
|
|
72
|
+
if (
|
|
73
|
+
'status' in event.changedFields
|
|
74
|
+
|| 'unitPrice' in event.changedFields
|
|
75
|
+
|| 'dueAt' in event.changedFields
|
|
76
|
+
|| 'amount' in event.changedFields
|
|
77
|
+
|| 'memberId' in event.changedFields
|
|
78
|
+
|| 'userId' in event.changedFields
|
|
79
|
+
|| 'payingOrganizationId' in event.changedFields
|
|
80
|
+
|| 'registrationId' in event.changedFields
|
|
81
|
+
) {
|
|
82
|
+
await this.scheduleUpdate(event.model);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Schedule an update for the balance item:
|
|
89
|
+
* - Updates cached outstanding balances for members, users, organizations and registrations
|
|
90
|
+
*
|
|
91
|
+
* Does not execute the update immediately, but schedules it to be run in the background.
|
|
92
|
+
*/
|
|
93
|
+
async scheduleUpdate(item: BalanceItem) {
|
|
94
|
+
await this.scheduleUpdates([item]);
|
|
95
|
+
|
|
96
|
+
// Todo: optimize
|
|
97
|
+
if (item.type === BalanceItemType.RegistrationBundleDiscount) {
|
|
98
|
+
// Save the applied discount to the related registration
|
|
99
|
+
if (item.registrationId) {
|
|
100
|
+
bundleDiscountsUpdateQueue.addItem(item.registrationId);
|
|
101
|
+
}
|
|
22
102
|
}
|
|
103
|
+
},
|
|
23
104
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Call this when a payment or payment balance items have changed.
|
|
107
|
+
* It will also call updateOutstanding automatically, so no need to call that separately again
|
|
108
|
+
*/
|
|
109
|
+
async updatePaidAndPending(items: BalanceItem[]) {
|
|
110
|
+
console.log('updatePaidAndPending for', items.length, 'items');
|
|
111
|
+
await BalanceItem.updatePricePaid(items.map(i => i.id));
|
|
112
|
+
await this.scheduleUpdates(items);
|
|
113
|
+
},
|
|
31
114
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
115
|
+
/**
|
|
116
|
+
* In some situations we need immediate updates
|
|
117
|
+
*/
|
|
118
|
+
async flushRegistrationDiscountsCache() {
|
|
119
|
+
await bundleDiscountsUpdateQueue.flushAndWait();
|
|
120
|
+
},
|
|
35
121
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Make sure all the pending changes for cached balances are run
|
|
124
|
+
*/
|
|
125
|
+
async flushCaches(organizationId: string) {
|
|
126
|
+
await memberUpdateQueue.flushGroupAndWait(organizationId);
|
|
127
|
+
await userUpdateQueue.flushGroupAndWait(organizationId);
|
|
128
|
+
await organizationUpdateQueue.flushGroupAndWait(organizationId);
|
|
129
|
+
await registrationUpdateQueue.flushGroupAndWait(organizationId);
|
|
130
|
+
await bundleDiscountsUpdateQueue.flushAndWait();
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async flushAll() {
|
|
134
|
+
await memberUpdateQueue.flushAndWait();
|
|
135
|
+
await userUpdateQueue.flushAndWait();
|
|
136
|
+
await organizationUpdateQueue.flushAndWait();
|
|
137
|
+
await registrationUpdateQueue.flushAndWait();
|
|
138
|
+
await bundleDiscountsUpdateQueue.flushAndWait();
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Update how many every object in the system owes or needs to be reimbursed
|
|
143
|
+
* and also updates the pricePaid/pricePending cached values in Balance items and members
|
|
144
|
+
*/
|
|
145
|
+
async scheduleUpdates(items: BalanceItem[]) {
|
|
146
|
+
console.log('Schedule outstanding balance for', items.length, 'items');
|
|
147
|
+
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
if (item.memberId) {
|
|
150
|
+
memberUpdateQueue.addItem(item.organizationId, item.memberId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (item.userId) {
|
|
154
|
+
userUpdateQueue.addItem(item.organizationId, item.userId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (item.payingOrganizationId) {
|
|
158
|
+
organizationUpdateQueue.addItem(item.organizationId, item.payingOrganizationId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (item.registrationId) {
|
|
162
|
+
registrationUpdateQueue.addItem(item.organizationId, item.registrationId);
|
|
41
163
|
}
|
|
42
164
|
}
|
|
43
165
|
},
|
|
44
166
|
|
|
45
167
|
async markDue(balanceItem: BalanceItem) {
|
|
46
168
|
if (balanceItem.status === BalanceItemStatus.Hidden) {
|
|
47
|
-
|
|
169
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
170
|
+
await balanceItem.save();
|
|
48
171
|
}
|
|
49
172
|
|
|
50
173
|
// status and pricePaid changes are handled inside balanceitempayment
|
|
51
174
|
if (balanceItem.dependingBalanceItemId) {
|
|
52
175
|
const depending = await BalanceItem.getByID(balanceItem.dependingBalanceItemId);
|
|
53
176
|
if (depending && depending.status === BalanceItemStatus.Hidden) {
|
|
54
|
-
|
|
177
|
+
depending.status = BalanceItemStatus.Due;
|
|
178
|
+
await balanceItem.save();
|
|
55
179
|
}
|
|
56
180
|
}
|
|
57
181
|
},
|
|
@@ -59,13 +183,22 @@ export const BalanceItemService = {
|
|
|
59
183
|
async markPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
|
|
60
184
|
await this.markDue(balanceItem);
|
|
61
185
|
|
|
186
|
+
if (balanceItem.paidAt) {
|
|
187
|
+
// Already ran side effects
|
|
188
|
+
// If we for example deleted a related order or registration - and we still have the balance item, mark it as paid again, we don't want to reactivate the order or registration
|
|
189
|
+
await this.markUpdated(balanceItem, payment, organization);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
62
193
|
// It is possible this balance item was earlier paid
|
|
63
194
|
// and later the regigstration / order has been canceled and it became a negative balance item - which as some point has been reembursed and marked as 'paid'
|
|
64
195
|
// in that case, we should be careful not to mark the registration as valid again
|
|
65
196
|
|
|
66
197
|
// If registration
|
|
67
198
|
if (balanceItem.registrationId) {
|
|
68
|
-
|
|
199
|
+
if (balanceItem.type === BalanceItemType.Registration) {
|
|
200
|
+
await RegistrationService.markValid(balanceItem.registrationId);
|
|
201
|
+
}
|
|
69
202
|
}
|
|
70
203
|
|
|
71
204
|
// If order
|
|
@@ -85,24 +218,9 @@ export const BalanceItemService = {
|
|
|
85
218
|
}
|
|
86
219
|
}
|
|
87
220
|
}
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
async reallocate(balanceItems: BalanceItem[], organizationId: string) {
|
|
91
|
-
const memberIds = Formatter.uniqueArray(balanceItems.map(b => b.memberId).filter(b => b !== null));
|
|
92
|
-
const payingOrganizationIds = Formatter.uniqueArray(balanceItems.map(b => b.payingOrganizationId).filter(b => b !== null));
|
|
93
|
-
const userIds = Formatter.uniqueArray(balanceItems.map(b => b.userId).filter(b => b !== null));
|
|
94
|
-
|
|
95
|
-
for (const memberId of memberIds) {
|
|
96
|
-
await PaymentReallocationService.reallocate(organizationId, memberId, ReceivableBalanceType.member);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (const payingOrganizationId of payingOrganizationIds) {
|
|
100
|
-
await PaymentReallocationService.reallocate(organizationId, payingOrganizationId, ReceivableBalanceType.organization);
|
|
101
|
-
}
|
|
102
221
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
222
|
+
balanceItem.paidAt = new Date();
|
|
223
|
+
await balanceItem.save();
|
|
106
224
|
},
|
|
107
225
|
|
|
108
226
|
async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
@@ -124,6 +242,7 @@ export const BalanceItemService = {
|
|
|
124
242
|
if (balanceItem.orderId) {
|
|
125
243
|
const order = await Order.getByID(balanceItem.orderId);
|
|
126
244
|
if (order) {
|
|
245
|
+
// This is safe to run multiple times. Doesn't have side effects
|
|
127
246
|
await order.undoPaid(payment, organization);
|
|
128
247
|
}
|
|
129
248
|
}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; // ES Modules import
|
|
2
|
+
import {
|
|
3
|
+
getSignedUrl,
|
|
4
|
+
} from '@aws-sdk/s3-request-presigner';
|
|
1
5
|
import { DecodedRequest, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
7
|
import { File } from '@stamhoofd/structures';
|
|
4
|
-
import AWS from 'aws-sdk';
|
|
5
|
-
import * as jose from 'jose';
|
|
6
8
|
import chalk from 'chalk';
|
|
9
|
+
import * as jose from 'jose';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* This service creates signed urls for valid files
|
|
10
13
|
*/
|
|
11
14
|
export class FileSignService {
|
|
12
|
-
static s3
|
|
13
|
-
endpoint: STAMHOOFD.SPACES_ENDPOINT,
|
|
14
|
-
accessKeyId: STAMHOOFD.SPACES_KEY,
|
|
15
|
-
secretAccessKey: STAMHOOFD.SPACES_SECRET,
|
|
16
|
-
});
|
|
15
|
+
static s3: S3Client;
|
|
17
16
|
|
|
18
17
|
static async load() {
|
|
18
|
+
this.s3 = new S3Client({
|
|
19
|
+
forcePathStyle: false, // Configures to use subdomain/virtual calling format.
|
|
20
|
+
endpoint: 'https://' + STAMHOOFD.SPACES_ENDPOINT,
|
|
21
|
+
credentials: {
|
|
22
|
+
accessKeyId: STAMHOOFD.SPACES_KEY,
|
|
23
|
+
secretAccessKey: STAMHOOFD.SPACES_SECRET,
|
|
24
|
+
},
|
|
25
|
+
region: 'eu-west-1', // Not used, but required by the S3Client
|
|
26
|
+
});
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* Note the algorithm is only used for signing. For verification the algorithm inside the public keys are used
|
|
21
30
|
*/
|
|
@@ -96,20 +105,16 @@ export class FileSignService {
|
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
static async withSignedUrl(file: File, duration = 60 * 60) {
|
|
99
|
-
console.log('Generating signed url:', file.id);
|
|
100
|
-
|
|
101
108
|
if (file.signedUrl) {
|
|
102
109
|
console.error('Warning: file already signed');
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
try {
|
|
106
|
-
const
|
|
113
|
+
const command = new GetObjectCommand({
|
|
107
114
|
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
108
115
|
Key: file.path,
|
|
109
|
-
Expires: duration,
|
|
110
116
|
});
|
|
111
|
-
|
|
112
|
-
console.log('Signed url:', url);
|
|
117
|
+
const url = await getSignedUrl(this.s3, command, { expiresIn: duration });
|
|
113
118
|
|
|
114
119
|
return new File({
|
|
115
120
|
...file,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
2
|
import { Organization, Platform } from '@stamhoofd/models';
|
|
3
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
4
|
import { RecordSettings } from '@stamhoofd/structures';
|
|
4
5
|
|
|
5
6
|
export type RecordCacheEntry = { record: RecordSettings; rootCategoryId: string; organizationId: string | null };
|
|
@@ -118,34 +119,42 @@ export class MemberRecordStore {
|
|
|
118
119
|
static async loadAll() {
|
|
119
120
|
this.cache = new Map<string, RecordCacheEntry>();
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.cache.set(record.id, {
|
|
126
|
-
record: record.clone(),
|
|
127
|
-
organizationId: null,
|
|
128
|
-
rootCategoryId: recordCategory.id,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
122
|
+
// We use a queue here so we can abort this long running task properly
|
|
123
|
+
// + can await it when shutting down the server or tests
|
|
124
|
+
await QueueHandler.schedule('MemberRecordStore.loadAll', async ({ abort }) => {
|
|
125
|
+
this.cache = new Map<string, RecordCacheEntry>();
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
for (const recordCategory of
|
|
127
|
+
const platform = await Platform.getShared();
|
|
128
|
+
for (const recordCategory of platform.config.recordsConfiguration.recordCategories) {
|
|
135
129
|
for (const record of recordCategory.getAllRecords()) {
|
|
136
|
-
if (this.cache.has(record.id)) {
|
|
137
|
-
console.error(`Duplicate record id ${record.id} found in organization ${organization.id} for record ${record.name} (${record.id}) in ${recordCategory.name}`);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
130
|
// Add to cache
|
|
141
131
|
this.cache.set(record.id, {
|
|
142
132
|
record: record.clone(),
|
|
143
|
-
organizationId:
|
|
133
|
+
organizationId: null,
|
|
144
134
|
rootCategoryId: recordCategory.id,
|
|
145
135
|
});
|
|
146
136
|
}
|
|
147
137
|
}
|
|
148
|
-
|
|
138
|
+
abort.throwIfAborted();
|
|
139
|
+
|
|
140
|
+
for await (const organization of Organization.select().all()) {
|
|
141
|
+
for (const recordCategory of organization.meta.recordsConfiguration.recordCategories) {
|
|
142
|
+
for (const record of recordCategory.getAllRecords()) {
|
|
143
|
+
if (this.cache.has(record.id)) {
|
|
144
|
+
console.error(`Duplicate record id ${record.id} found in organization ${organization.id} for record ${record.name} (${record.id}) in ${recordCategory.name}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Add to cache
|
|
148
|
+
this.cache.set(record.id, {
|
|
149
|
+
record: record.clone(),
|
|
150
|
+
organizationId: organization.id,
|
|
151
|
+
rootCategoryId: recordCategory.id,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
abort.throwIfAborted();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
static async getRecord(id: string): Promise<RecordCacheEntry | null> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BalanceItem, BalanceItemPayment, MemberFactory, Organization, OrganizationFactory, Payment } from '@stamhoofd/models';
|
|
2
|
-
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, PaymentMethod, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
2
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, PaymentMethod, PaymentStatus, ReceivableBalanceType, TranslatedString } from '@stamhoofd/structures';
|
|
3
3
|
import { PaymentReallocationService } from './PaymentReallocationService';
|
|
4
|
+
import { BalanceItemService } from './BalanceItemService';
|
|
4
5
|
|
|
5
6
|
let sharedOrganization: Organization | undefined;
|
|
6
7
|
|
|
@@ -46,7 +47,7 @@ async function createItem(options: {
|
|
|
46
47
|
for (const [type, id] of Object.entries(options.relations)) {
|
|
47
48
|
b.relations.set(type as BalanceItemRelationType, BalanceItemRelation.create({
|
|
48
49
|
id,
|
|
49
|
-
name: 'Test ' + id,
|
|
50
|
+
name: new TranslatedString('Test ' + id),
|
|
50
51
|
}));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -100,7 +101,7 @@ async function createItem(options: {
|
|
|
100
101
|
await balanceItemPayment.save();
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
await
|
|
104
|
+
await BalanceItemService.updatePaidAndPending([b]);
|
|
104
105
|
const balance = (await BalanceItem.getByID(b.id))!;
|
|
105
106
|
|
|
106
107
|
await expectItem(balance, {
|
|
@@ -116,7 +117,7 @@ async function createItem(options: {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
async function expectItem(b: BalanceItem, options: { pricePaid?: number; priceOpen?: number; pricePending?: number; paid?: number[]; pending?: number[]; failed?: number[] }) {
|
|
119
|
-
await
|
|
120
|
+
await BalanceItemService.updatePaidAndPending([b]);
|
|
120
121
|
b = (await BalanceItem.getByID(b.id))!;
|
|
121
122
|
|
|
122
123
|
const loaded = await BalanceItem.loadPayments([b]);
|
|
@@ -460,7 +461,7 @@ describe('PaymentReallocationService', () => {
|
|
|
460
461
|
});
|
|
461
462
|
});
|
|
462
463
|
|
|
463
|
-
it('Balances with different relations should create a reallocation payment', async () => {
|
|
464
|
+
it.skip('Balances with different relations should create a reallocation payment', async () => {
|
|
464
465
|
const memberId = (await getMember()).id;
|
|
465
466
|
const b1 = await createItem({
|
|
466
467
|
unitPrice: 30 * 100,
|
|
@@ -507,7 +508,7 @@ describe('PaymentReallocationService', () => {
|
|
|
507
508
|
});
|
|
508
509
|
});
|
|
509
510
|
|
|
510
|
-
it('Balances with different relations should create a reallocation payment with 3 items', async () => {
|
|
511
|
+
it.skip('Balances with different relations should create a reallocation payment with 3 items', async () => {
|
|
511
512
|
const memberId = (await getMember()).id;
|
|
512
513
|
const b1 = await createItem({
|
|
513
514
|
unitPrice: 45 * 100,
|
|
@@ -567,7 +568,7 @@ describe('PaymentReallocationService', () => {
|
|
|
567
568
|
});
|
|
568
569
|
});
|
|
569
570
|
|
|
570
|
-
it('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer most similar item', async () => {
|
|
571
|
+
it.skip('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer most similar item', async () => {
|
|
571
572
|
const memberId = (await getMember()).id;
|
|
572
573
|
const b1 = await createItem({
|
|
573
574
|
unitPrice: 40 * 100,
|
|
@@ -631,7 +632,7 @@ describe('PaymentReallocationService', () => {
|
|
|
631
632
|
/**
|
|
632
633
|
* Note: if this one fails randomly, it might because it isn't working stable enough and doesn't fulfil the requirements
|
|
633
634
|
*/
|
|
634
|
-
it('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer largest amount', async () => {
|
|
635
|
+
it.skip('Balances with different relations should create a reallocation payment with 3 items and remaining open should prefer largest amount', async () => {
|
|
635
636
|
const memberId = (await getMember()).id;
|
|
636
637
|
const b1 = await createItem({
|
|
637
638
|
unitPrice: 40 * 100,
|
|
@@ -686,7 +687,7 @@ describe('PaymentReallocationService', () => {
|
|
|
686
687
|
});
|
|
687
688
|
});
|
|
688
689
|
|
|
689
|
-
it('Balances
|
|
690
|
+
it('Balances due in the future should not be reallocated', async () => {
|
|
690
691
|
const memberId = (await getMember()).id;
|
|
691
692
|
const b1 = await createItem({
|
|
692
693
|
unitPrice: 40 * 100,
|
|
@@ -710,6 +711,11 @@ describe('PaymentReallocationService', () => {
|
|
|
710
711
|
objectId: memberId,
|
|
711
712
|
// This is due later, so it should be the last one to be paid
|
|
712
713
|
dueAt: new Date('2050-01-01'),
|
|
714
|
+
relations: {
|
|
715
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
716
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
717
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
718
|
+
},
|
|
713
719
|
});
|
|
714
720
|
|
|
715
721
|
const b3 = await createItem({
|
|
@@ -718,6 +724,11 @@ describe('PaymentReallocationService', () => {
|
|
|
718
724
|
paid: [],
|
|
719
725
|
priceOpen: 15 * 100, // This adds internal assert
|
|
720
726
|
objectId: memberId,
|
|
727
|
+
relations: {
|
|
728
|
+
[BalanceItemRelationType.Group]: 'group1',
|
|
729
|
+
[BalanceItemRelationType.GroupPrice]: 'defaultprice',
|
|
730
|
+
[BalanceItemRelationType.Member]: 'member1',
|
|
731
|
+
},
|
|
721
732
|
});
|
|
722
733
|
|
|
723
734
|
await PaymentReallocationService.reallocate(
|
|
@@ -728,17 +739,17 @@ describe('PaymentReallocationService', () => {
|
|
|
728
739
|
|
|
729
740
|
// Check if the balance items are now equal
|
|
730
741
|
await expectItem(b1, {
|
|
731
|
-
priceOpen:
|
|
732
|
-
paid: [
|
|
742
|
+
priceOpen: -25_00, // Still paid 25 too much
|
|
743
|
+
paid: [25_00],
|
|
733
744
|
});
|
|
734
745
|
|
|
735
746
|
await expectItem(b2, {
|
|
736
|
-
priceOpen:
|
|
737
|
-
paid: [
|
|
747
|
+
priceOpen: 30 * 100,
|
|
748
|
+
paid: [],
|
|
738
749
|
});
|
|
739
750
|
|
|
740
751
|
await expectItem(b3, {
|
|
741
|
-
priceOpen: 0 * 100,
|
|
752
|
+
priceOpen: 0 * 100, // Paid with canceled balance item that was already paid and should be refunded
|
|
742
753
|
paid: [15 * 100],
|
|
743
754
|
});
|
|
744
755
|
});
|
|
@@ -2,6 +2,7 @@ import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamho
|
|
|
2
2
|
import { SQL } from '@stamhoofd/sql';
|
|
3
3
|
import { BalanceItemStatus, doBalanceItemRelationsMatch, PaymentMethod, PaymentStatus, PaymentType, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
4
|
import { Sorter } from '@stamhoofd/utility';
|
|
5
|
+
import { BalanceItemService } from './BalanceItemService';
|
|
5
6
|
|
|
6
7
|
type BalanceItemWithRemaining = {
|
|
7
8
|
balanceItem: BalanceItem;
|
|
@@ -30,12 +31,12 @@ export const PaymentReallocationService = {
|
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
let balanceItems = await CachedBalance.balanceForObjects(organizationId, [objectId], type);
|
|
34
|
+
let balanceItems = (await CachedBalance.balanceForObjects(organizationId, [objectId], type)).filter(b => b.isAfterDueDate);
|
|
34
35
|
|
|
35
36
|
const didMerge: BalanceItem[] = [];
|
|
36
37
|
|
|
37
38
|
// First try to merge balance items that are the same and have canceled variants
|
|
38
|
-
for (const balanceItem of balanceItems) {
|
|
39
|
+
/* for (const balanceItem of balanceItems) {
|
|
39
40
|
if (balanceItem.status !== BalanceItemStatus.Due) {
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
@@ -46,20 +47,20 @@ export const PaymentReallocationService = {
|
|
|
46
47
|
// Not possible to merge into one: there are 2 due items
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
49
|
-
const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
|
|
50
|
+
const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status !== BalanceItemStatus.Due && (b.pricePaid !== 0 || b.pricePending !== 0) && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
|
|
50
51
|
|
|
51
52
|
if (similarCanceledItems.length) {
|
|
52
53
|
await this.mergeBalanceItems(balanceItem, similarCanceledItems);
|
|
53
54
|
didMerge.push(balanceItem, ...similarCanceledItems);
|
|
54
55
|
}
|
|
55
|
-
}
|
|
56
|
+
} */
|
|
56
57
|
|
|
57
58
|
if (didMerge.length) {
|
|
58
59
|
// Update outstanding
|
|
59
|
-
await
|
|
60
|
+
await BalanceItemService.updatePaidAndPending(didMerge);
|
|
60
61
|
|
|
61
62
|
// Reload balance items
|
|
62
|
-
balanceItems = await CachedBalance.balanceForObjects(organizationId, [objectId], type);
|
|
63
|
+
balanceItems = (await CachedBalance.balanceForObjects(organizationId, [objectId], type)).filter(b => b.isAfterDueDate);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// The algorithm:
|
|
@@ -93,6 +94,13 @@ export const PaymentReallocationService = {
|
|
|
93
94
|
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
94
95
|
},
|
|
95
96
|
},
|
|
97
|
+
{
|
|
98
|
+
// Priority 1: same relation ids, same amount
|
|
99
|
+
alterPayments: false,
|
|
100
|
+
match: (negativeItem, p) => {
|
|
101
|
+
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
96
104
|
{
|
|
97
105
|
// Priority 2: same relation ids, different amount
|
|
98
106
|
alterPayments: true,
|
|
@@ -101,9 +109,19 @@ export const PaymentReallocationService = {
|
|
|
101
109
|
},
|
|
102
110
|
},
|
|
103
111
|
{
|
|
112
|
+
// Priority 2: same relation ids, different amount
|
|
113
|
+
alterPayments: false,
|
|
114
|
+
match: (negativeItem, p) => {
|
|
115
|
+
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// For now I would skip the next priorties because merging these often creates a lot of confusion
|
|
120
|
+
/* {
|
|
104
121
|
// Priority 3: same type, one mismatching relation id
|
|
105
122
|
alterPayments: false,
|
|
106
123
|
match: (negativeItem, p) => {
|
|
124
|
+
// todo: maybe do allow this, but only if the amount is the same
|
|
107
125
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 1);
|
|
108
126
|
},
|
|
109
127
|
},
|
|
@@ -111,6 +129,7 @@ export const PaymentReallocationService = {
|
|
|
111
129
|
// Priority 4: same type, two mismatching relation ids
|
|
112
130
|
alterPayments: false,
|
|
113
131
|
match: (negativeItem, p) => {
|
|
132
|
+
// todo: maybe do allow this, but only if the amount is the same
|
|
114
133
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 2);
|
|
115
134
|
},
|
|
116
135
|
},
|
|
@@ -120,8 +139,8 @@ export const PaymentReallocationService = {
|
|
|
120
139
|
match: (negativeItem, p) => {
|
|
121
140
|
return p.balanceItem.type === negativeItem.balanceItem.type && p.remaining === -negativeItem.remaining;
|
|
122
141
|
},
|
|
123
|
-
},
|
|
124
|
-
{
|
|
142
|
+
}, */
|
|
143
|
+
/* {
|
|
125
144
|
// Priority 6: same type
|
|
126
145
|
alterPayments: false,
|
|
127
146
|
match: (negativeItem, p) => {
|
|
@@ -134,7 +153,7 @@ export const PaymentReallocationService = {
|
|
|
134
153
|
match: () => {
|
|
135
154
|
return true;
|
|
136
155
|
},
|
|
137
|
-
},
|
|
156
|
+
}, */
|
|
138
157
|
];
|
|
139
158
|
|
|
140
159
|
for (const matchMethod of matchMethods) {
|
|
@@ -205,7 +224,7 @@ export const PaymentReallocationService = {
|
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
// Update outstanding
|
|
208
|
-
await
|
|
227
|
+
await BalanceItemService.updatePaidAndPending([
|
|
209
228
|
...negativeItems.filter(n => n.remaining !== n.balanceItem.priceOpen).map(n => n.balanceItem),
|
|
210
229
|
...positiveItems.filter(p => p.remaining !== p.balanceItem.priceOpen).map(p => p.balanceItem),
|
|
211
230
|
]);
|