@stamhoofd/backend 2.63.0 → 2.65.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 +8 -6
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +7 -1
- package/src/audit-logs/ModelLogger.ts +17 -2
- package/src/crons/balance-emails.ts +232 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/update-cached-balances.ts +39 -0
- package/src/email-recipient-loaders/members.ts +14 -4
- package/src/email-recipient-loaders/receivable-balances.ts +29 -15
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +47 -12
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -18
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +48 -3
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +15 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -2
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +23 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +0 -12
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
- package/src/helpers/AdminPermissionChecker.ts +2 -1
- package/src/helpers/AuthenticatedStructures.ts +73 -3
- package/src/helpers/EmailResumer.ts +1 -5
- package/src/helpers/MemberUserSyncer.ts +22 -1
- package/src/helpers/MembershipCharger.ts +5 -0
- package/src/helpers/OrganizationCharger.ts +4 -0
- package/src/helpers/TagHelper.ts +7 -14
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +4 -14
- package/src/seeds/1729253172-update-orders.ts +7 -18
- package/src/seeds/{1733996431-update-cached-outstanding-balance-from-items.ts → 1734596144-fill-previous-period-id.ts} +21 -6
- package/src/seeds/{1726494420-update-cached-outstanding-balance-from-items.ts → 1735577912-update-cached-outstanding-balance-from-items.ts} +1 -14
- package/src/services/BalanceItemService.ts +22 -3
- package/src/services/PaymentReallocationService.test.ts +746 -0
- package/src/services/PaymentReallocationService.ts +339 -0
- package/src/services/PaymentService.ts +13 -0
- package/src/services/PlatformMembershipService.ts +167 -137
- package/src/sql-filters/members.ts +1 -0
- package/src/sql-filters/receivable-balances.ts +16 -2
- package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +19 -0
- package/src/sql-sorters/receivable-balances.ts +3 -3
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +0 -253
- package/src/helpers/ModelHelper.ts +0 -32
- package/src/seeds/1726055544-balance-item-paid.ts +0 -11
- package/src/seeds/1726055545-balance-item-pending.ts +0 -11
- package/src/seeds/1726494419-update-cached-outstanding-balance.ts +0 -53
- package/src/seeds/1728928973-balance-item-pending.ts +0 -11
|
@@ -2,7 +2,7 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
5
|
+
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
6
6
|
import { GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
@@ -218,7 +218,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
const platform = await Platform.getShared();
|
|
222
221
|
const responsibility = platform.config.responsibilities.find(r => r.id === patchResponsibility.responsibilityId);
|
|
223
222
|
|
|
224
223
|
if (responsibility && !responsibility.organizationBased && !Context.auth.hasPlatformFullAccess()) {
|
|
@@ -261,7 +260,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
261
260
|
throw Context.auth.error('Je hebt niet voldoende rechten om functies van leden aan te passen');
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
const platform = await Platform.getShared();
|
|
265
263
|
const platformResponsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId);
|
|
266
264
|
const org = organization ?? (put.organizationId ? await Organization.getByID(put.organizationId) : null);
|
|
267
265
|
|
|
@@ -414,12 +412,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
414
412
|
// Add platform memberships
|
|
415
413
|
for (const { put } of patch.platformMemberships.getPuts()) {
|
|
416
414
|
if (put.periodId !== platform.periodId) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
415
|
+
const period = await RegistrationPeriod.getByID(put.periodId);
|
|
416
|
+
|
|
417
|
+
if (!period) {
|
|
418
|
+
throw new SimpleError({
|
|
419
|
+
code: 'invalid_field',
|
|
420
|
+
message: 'Invalid period',
|
|
421
|
+
human: Context.i18n.$t(`Je kan geen aansluitingen meer toevoegen in dit werkjaar`),
|
|
422
|
+
field: 'periodId',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (period?.locked) {
|
|
427
|
+
throw new SimpleError({
|
|
428
|
+
code: 'invalid_field',
|
|
429
|
+
message: 'Invalid period',
|
|
430
|
+
human: Context.i18n.$t(`Je kan geen aansluitingen meer toevoegen in {period} (vergrendeld)`, { period: period?.getBaseStructure().name }),
|
|
431
|
+
field: 'periodId',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
423
434
|
}
|
|
424
435
|
|
|
425
436
|
if (organization && put.organizationId !== organization.id) {
|
|
@@ -449,6 +460,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
449
460
|
.where('memberId', member.id)
|
|
450
461
|
.where('membershipTypeId', put.membershipTypeId)
|
|
451
462
|
.where('periodId', put.periodId)
|
|
463
|
+
.where('deletedAt', null)
|
|
452
464
|
.where(
|
|
453
465
|
SQL.where(
|
|
454
466
|
SQL.where('startDate', SQLWhereSign.LessEqual, put.startDate)
|
|
@@ -481,7 +493,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
481
493
|
membership.organizationId = put.organizationId;
|
|
482
494
|
membership.periodId = put.periodId;
|
|
483
495
|
|
|
484
|
-
membership.startDate = put.startDate;
|
|
496
|
+
membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
|
|
485
497
|
membership.endDate = put.endDate;
|
|
486
498
|
membership.expireDate = put.expireDate;
|
|
487
499
|
|
|
@@ -509,12 +521,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
509
521
|
}
|
|
510
522
|
|
|
511
523
|
if (membership.periodId !== platform.periodId) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
524
|
+
const period = await RegistrationPeriod.getByID(membership.periodId);
|
|
525
|
+
|
|
526
|
+
if (!period) {
|
|
527
|
+
throw new SimpleError({
|
|
528
|
+
code: 'invalid_field',
|
|
529
|
+
message: 'Invalid period',
|
|
530
|
+
human: Context.i18n.$t(`Je kan geen aansluitingen meer verwijderen in dit werkjaar`),
|
|
531
|
+
field: 'periodId',
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (period?.locked) {
|
|
536
|
+
throw new SimpleError({
|
|
537
|
+
code: 'invalid_field',
|
|
538
|
+
message: 'Invalid period',
|
|
539
|
+
human: Context.i18n.$t(`Je kan geen aansluitingen meer verwijderen in {period} (vergrendeld)`, { period: period?.getBaseStructure().name }),
|
|
540
|
+
field: 'periodId',
|
|
541
|
+
});
|
|
542
|
+
}
|
|
518
543
|
}
|
|
519
544
|
|
|
520
545
|
if (!membership.canDelete() && !Context.auth.hasPlatformFullAccess()) {
|
|
@@ -157,6 +157,8 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
157
157
|
platform.periodId = period.id;
|
|
158
158
|
shouldUpdateSetupSteps = true;
|
|
159
159
|
shouldMoveToPeriod = period;
|
|
160
|
+
|
|
161
|
+
await platform.setPreviousPeriodId();
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
if (request.body.membershipOrganizationId !== undefined) {
|
|
@@ -62,18 +62,21 @@ export class GetUserPayableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
62
62
|
for (const organization of authenticatedOrganizations) {
|
|
63
63
|
const items = receivableBalances.filter(b => b.organizationId === organization.id);
|
|
64
64
|
|
|
65
|
-
let
|
|
65
|
+
let amountOpen = 0;
|
|
66
66
|
let amountPending = 0;
|
|
67
|
+
let amountPaid = 0;
|
|
67
68
|
|
|
68
69
|
for (const item of items) {
|
|
69
|
-
|
|
70
|
+
amountOpen += item.amountOpen;
|
|
70
71
|
amountPending += item.amountPending;
|
|
72
|
+
amountPaid += item.amountPaid;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
billingStatus.organizations.push(PayableBalance.create({
|
|
74
76
|
organization,
|
|
75
|
-
|
|
77
|
+
amountOpen,
|
|
76
78
|
amountPending,
|
|
79
|
+
amountPaid,
|
|
77
80
|
}));
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -242,7 +242,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// Check if this member is already registered in this group?
|
|
245
|
-
const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle });
|
|
245
|
+
const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle, periodId: group.periodId, registeredAt: { sign: '!=', value: null } });
|
|
246
246
|
|
|
247
247
|
for (const existingRegistration of existingRegistrations) {
|
|
248
248
|
if (item.replaceRegistrations.some(r => r.id === existingRegistration.id)) {
|
|
@@ -263,7 +263,25 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
let reuseRegistration: Registration | null = null;
|
|
267
|
+
|
|
268
|
+
if (item.replaceRegistrations.length === 1) {
|
|
269
|
+
// Try to reuse this specific one
|
|
270
|
+
reuseRegistration = (await Registration.getByID(item.replaceRegistrations[0].id)) ?? null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let startDate = item.calculatedStartDate;
|
|
274
|
+
|
|
275
|
+
if (!reuseRegistration) {
|
|
276
|
+
// Otherwise try to reuse a registration in the same period, for the same group that has been deactived for less than 7 days since the start of the new registration
|
|
277
|
+
reuseRegistration = existingRegistrations.find(r => r.deactivatedAt !== null && r.deactivatedAt.getTime() > startDate.getTime() - 7 * 24 * 60 * 60 * 1000) ?? null;
|
|
278
|
+
|
|
279
|
+
if (reuseRegistration && reuseRegistration.startDate && reuseRegistration.startDate < startDate && reuseRegistration.startDate >= group.settings.startDate && !item.trial) {
|
|
280
|
+
startDate = reuseRegistration.startDate;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const registration = (reuseRegistration ?? new Registration())
|
|
267
285
|
.setRelation(registrationMemberRelation, member as Member)
|
|
268
286
|
.setRelation(Registration.group, group);
|
|
269
287
|
registration.organizationId = organization.id;
|
|
@@ -275,6 +293,20 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
275
293
|
registration.groupPrice = item.groupPrice;
|
|
276
294
|
registration.options = item.options;
|
|
277
295
|
registration.recordAnswers = item.recordAnswers;
|
|
296
|
+
registration.startDate = startDate;
|
|
297
|
+
|
|
298
|
+
// Clear if we are reusing an existing registration
|
|
299
|
+
registration.trialUntil = null;
|
|
300
|
+
registration.pricePaid = 0;
|
|
301
|
+
registration.payingOrganizationId = null;
|
|
302
|
+
|
|
303
|
+
// NOTE: we don't reset deactivatedAt - registeredAt, because those will get reset when markValid is called later on (while keeping the original registeredAt date)
|
|
304
|
+
// registration.deactivatedAt = null;
|
|
305
|
+
// registration.registeredAt = null; // this is required to trigger platform membership updates
|
|
306
|
+
|
|
307
|
+
if (item.trial) {
|
|
308
|
+
registration.trialUntil = item.calculatedTrialUntil;
|
|
309
|
+
}
|
|
278
310
|
|
|
279
311
|
if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
|
|
280
312
|
registration.payingOrganizationId = request.body.asOrganizationId;
|
|
@@ -428,6 +460,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
428
460
|
// Who is responsible for payment?
|
|
429
461
|
balanceItem2.memberId = registration.memberId;
|
|
430
462
|
|
|
463
|
+
if (registration.trialUntil) {
|
|
464
|
+
balanceItem2.dueAt = registration.trialUntil;
|
|
465
|
+
}
|
|
466
|
+
|
|
431
467
|
// If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
|
|
432
468
|
balanceItem2.status = BalanceItemStatus.Hidden;
|
|
433
469
|
await balanceItem2.save();
|
|
@@ -446,6 +482,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
446
482
|
// Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
|
|
447
483
|
balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null;
|
|
448
484
|
|
|
485
|
+
if (registration.trialUntil) {
|
|
486
|
+
balanceItem.dueAt = registration.trialUntil;
|
|
487
|
+
}
|
|
488
|
+
|
|
449
489
|
await balanceItem.save();
|
|
450
490
|
createdBalanceItems.push(balanceItem);
|
|
451
491
|
}
|
|
@@ -613,7 +653,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
613
653
|
const mappedBalanceItems = new Map<BalanceItem, number>();
|
|
614
654
|
|
|
615
655
|
for (const item of createdBalanceItems) {
|
|
616
|
-
|
|
656
|
+
if (item.dueAt === null) {
|
|
657
|
+
mappedBalanceItems.set(item, item.price);
|
|
658
|
+
}
|
|
617
659
|
}
|
|
618
660
|
|
|
619
661
|
for (const item of checkout.cart.balanceItems) {
|
|
@@ -652,6 +694,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
652
694
|
await BalanceItem.updateOutstanding([...createdBalanceItems, ...unrelatedCreatedBalanceItems]);
|
|
653
695
|
}
|
|
654
696
|
|
|
697
|
+
// Reallocate
|
|
698
|
+
await BalanceItemService.reallocate([...createdBalanceItems, ...unrelatedCreatedBalanceItems], organization.id);
|
|
699
|
+
|
|
655
700
|
// Update occupancy
|
|
656
701
|
for (const group of groups) {
|
|
657
702
|
if (registrations.some(r => r.groupId === group.id) || deactivatedRegistrationGroupIds.some(id => id === group.id)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder, patchObject } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { AuditLogType, RegistrationPeriod as RegistrationPeriodStruct } from '@stamhoofd/structures';
|
|
3
|
+
import { AuditLogSource, AuditLogType, RegistrationPeriod as RegistrationPeriodStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
@@ -60,6 +60,7 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
60
60
|
period.locked = put.locked;
|
|
61
61
|
period.settings = put.settings;
|
|
62
62
|
period.organizationId = organization?.id ?? null;
|
|
63
|
+
await period.setPreviousPeriodId();
|
|
63
64
|
|
|
64
65
|
await period.save();
|
|
65
66
|
periods.push(period);
|
|
@@ -91,6 +92,7 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
91
92
|
model.settings = patchObject(model.settings, patch.settings);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
await model.setPreviousPeriodId();
|
|
94
96
|
await model.save();
|
|
95
97
|
|
|
96
98
|
// Schedule patch of all groups in this period
|
|
@@ -108,7 +110,19 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
108
110
|
});
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Get before deleting the model
|
|
114
|
+
const updateWhere = await RegistrationPeriod.where({ previousPeriodId: model.id });
|
|
115
|
+
|
|
116
|
+
// Now delete the model
|
|
111
117
|
await model.delete();
|
|
118
|
+
|
|
119
|
+
// Update all previous period ids
|
|
120
|
+
await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
121
|
+
for (const period of updateWhere) {
|
|
122
|
+
await period.setPreviousPeriodId();
|
|
123
|
+
await period.save();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
// Clear platform cache
|
|
@@ -95,7 +95,7 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
95
95
|
: []
|
|
96
96
|
);
|
|
97
97
|
|
|
98
|
-
const defaultTemplateTypes = organization ? types.filter(type => EmailTemplateStruct.isSavedEmail(type)) : types;
|
|
98
|
+
const defaultTemplateTypes = organization ? types.filter(type => !EmailTemplateStruct.isSavedEmail(type)) : types;
|
|
99
99
|
const defaultTemplates = defaultTemplateTypes.length === 0
|
|
100
100
|
? []
|
|
101
101
|
: (await EmailTemplate.where({
|
|
@@ -106,6 +106,20 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
106
106
|
},
|
|
107
107
|
}));
|
|
108
108
|
|
|
109
|
+
if (organization && (request.query.webshopId || request.query.groupIds)) {
|
|
110
|
+
const orgDefaults = (await EmailTemplate.where({
|
|
111
|
+
organizationId: organization.id,
|
|
112
|
+
webshopId: null,
|
|
113
|
+
groupId: null,
|
|
114
|
+
type: {
|
|
115
|
+
sign: 'IN',
|
|
116
|
+
value: defaultTemplateTypes,
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
defaultTemplates.unshift(...orgDefaults);
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
return new Response(templates.concat(defaultTemplates).map(template => EmailTemplateStruct.create(template)));
|
|
110
124
|
}
|
|
111
125
|
}
|
|
@@ -125,6 +125,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
125
125
|
organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles);
|
|
126
126
|
organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey;
|
|
127
127
|
organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
|
|
128
|
+
organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
|
|
128
129
|
|
|
129
130
|
if (request.body.privateMeta.mollieProfile !== undefined) {
|
|
130
131
|
organization.privateMeta.mollieProfile = patchObject(organization.privateMeta.mollieProfile, request.body.privateMeta.mollieProfile);
|
|
@@ -6,6 +6,7 @@ import { QueueHandler } from '@stamhoofd/queues';
|
|
|
6
6
|
import { BalanceItemStatus, BalanceItemType, BalanceItemWithPayments, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { BalanceItemService } from '../../../../services/BalanceItemService';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -36,7 +37,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
36
37
|
throw Context.auth.error();
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
if (request.body.changes.length
|
|
40
|
+
if (request.body.changes.length === 0) {
|
|
40
41
|
return new Response([]);
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -208,8 +209,14 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
208
209
|
|
|
209
210
|
await BalanceItem.updateOutstanding(updateOutstandingBalance);
|
|
210
211
|
|
|
212
|
+
// Reallocate
|
|
213
|
+
await BalanceItemService.reallocate(updateOutstandingBalance, organization.id);
|
|
214
|
+
|
|
215
|
+
// Reload returnedModels
|
|
216
|
+
const returnedModelsReloaded = await BalanceItem.getByIDs(...returnedModels.map(m => m.id));
|
|
217
|
+
|
|
211
218
|
return new Response(
|
|
212
|
-
await BalanceItem.getStructureWithPayments(
|
|
219
|
+
await BalanceItem.getStructureWithPayments(returnedModelsReloaded),
|
|
213
220
|
);
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -200,6 +200,9 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
await BalanceItem.updateOutstanding(balanceItems);
|
|
203
|
+
|
|
204
|
+
// Reallocate
|
|
205
|
+
await BalanceItemService.reallocate(balanceItems, organization.id);
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
changedPayments.push(payment);
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -37,7 +37,7 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
37
37
|
throw Context.auth.error();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
|
|
40
|
+
const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type, true);
|
|
41
41
|
let paymentModels: Payment[] = [];
|
|
42
42
|
|
|
43
43
|
switch (request.params.type) {
|
|
@@ -100,6 +100,26 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
100
100
|
.fetch();
|
|
101
101
|
break;
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
case ReceivableBalanceType.registration: {
|
|
105
|
+
paymentModels = await Payment.select()
|
|
106
|
+
.where('organizationId', organization.id)
|
|
107
|
+
.join(
|
|
108
|
+
SQL.join(BalanceItemPayment.table)
|
|
109
|
+
.where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
|
|
110
|
+
)
|
|
111
|
+
.join(
|
|
112
|
+
SQL.join(BalanceItem.table)
|
|
113
|
+
.where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
|
|
114
|
+
)
|
|
115
|
+
.where(SQL.column(BalanceItem.table, 'registrationId'), request.params.id)
|
|
116
|
+
.andWhere(
|
|
117
|
+
SQL.whereNot('status', PaymentStatus.Failed),
|
|
118
|
+
)
|
|
119
|
+
.groupBy(SQL.column(Payment.table, 'id'))
|
|
120
|
+
.fetch();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
|
|
@@ -108,8 +128,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
108
128
|
const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
|
|
109
129
|
|
|
110
130
|
const created = new CachedBalance();
|
|
111
|
-
created.
|
|
131
|
+
created.amountOpen = 0;
|
|
112
132
|
created.amountPending = 0;
|
|
133
|
+
created.amountPaid = 0;
|
|
113
134
|
created.organizationId = organization.id;
|
|
114
135
|
created.objectId = request.params.id;
|
|
115
136
|
created.objectType = request.params.type;
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -48,20 +48,8 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
48
48
|
|
|
49
49
|
scopeFilter = {
|
|
50
50
|
organizationId: organization.id,
|
|
51
|
-
$or: {
|
|
52
|
-
amount: { $neq: 0 },
|
|
53
|
-
amountPending: { $neq: 0 },
|
|
54
|
-
nextDueAt: { $neq: null },
|
|
55
|
-
},
|
|
56
51
|
};
|
|
57
52
|
|
|
58
|
-
if (!Context.auth.hasSomePlatformAccess()) {
|
|
59
|
-
// Cannot see debt between organizations
|
|
60
|
-
scopeFilter.objectType = {
|
|
61
|
-
$neq: ReceivableBalanceType.organization,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
53
|
const query = CachedBalance
|
|
66
54
|
.select();
|
|
67
55
|
|
|
@@ -346,7 +346,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
346
346
|
model.settings.period = period.getBaseStructure();
|
|
347
347
|
|
|
348
348
|
if (model.type !== GroupType.EventRegistration) {
|
|
349
|
-
|
|
349
|
+
// Note: start date is curomizable, as long as it stays between period start and end
|
|
350
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
351
|
+
model.settings.startDate = period.startDate;
|
|
352
|
+
}
|
|
353
|
+
|
|
350
354
|
model.settings.endDate = period.endDate;
|
|
351
355
|
}
|
|
352
356
|
}
|
|
@@ -446,9 +450,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
446
450
|
model.status = struct.status;
|
|
447
451
|
model.type = struct.type;
|
|
448
452
|
model.settings.period = period.getBaseStructure();
|
|
449
|
-
model.settings.startDate = period.startDate;
|
|
450
453
|
model.settings.endDate = period.endDate;
|
|
451
454
|
|
|
455
|
+
// Note: start date is curomizable, as long as it stays between period start and end
|
|
456
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
457
|
+
model.settings.startDate = period.startDate;
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
453
461
|
// Create a temporary permission role for this user
|
|
454
462
|
const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
|
|
@@ -263,7 +263,7 @@ export class AdminPermissionChecker {
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
const cachedBalance = await CachedBalance.getForObjects([member.id]);
|
|
266
|
-
if (cachedBalance.length === 0 || (cachedBalance[0].
|
|
266
|
+
if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
|
|
267
267
|
return true;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -1063,6 +1063,7 @@ export class AdminPermissionChecker {
|
|
|
1063
1063
|
for (const registration of cloned.registrations) {
|
|
1064
1064
|
registration.price = 0;
|
|
1065
1065
|
registration.pricePaid = 0;
|
|
1066
|
+
registration.balances = [];
|
|
1066
1067
|
}
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
|
|
3
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
2
|
+
import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { Context } from './Context';
|
|
@@ -323,6 +323,9 @@ export class AuthenticatedStructures {
|
|
|
323
323
|
}
|
|
324
324
|
const organizations = new Map<string, Organization>();
|
|
325
325
|
|
|
326
|
+
const registrationIds = Formatter.uniqueArray(members.flatMap(m => m.registrations.map(r => r.id)));
|
|
327
|
+
const balances = await CachedBalance.getForObjects(registrationIds, Context.organization?.id ?? null);
|
|
328
|
+
|
|
326
329
|
if (includeUser) {
|
|
327
330
|
for (const organizationId of includeUser.permissions?.organizationPermissions.keys() ?? []) {
|
|
328
331
|
if (includeContextOrganization || organizationId !== Context.auth.organization?.id) {
|
|
@@ -365,7 +368,25 @@ export class AuthenticatedStructures {
|
|
|
365
368
|
}
|
|
366
369
|
}
|
|
367
370
|
member.registrations = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false));
|
|
368
|
-
const
|
|
371
|
+
const balancesPermission = await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read);
|
|
372
|
+
|
|
373
|
+
const blob = MemberWithRegistrationsBlob.create({
|
|
374
|
+
...member,
|
|
375
|
+
registrations: member.registrations.map((r) => {
|
|
376
|
+
const base = r.getStructure();
|
|
377
|
+
|
|
378
|
+
base.balances = balancesPermission
|
|
379
|
+
? (balances.filter(b => r.id === b.objectId).map((b) => {
|
|
380
|
+
return GenericBalance.create(b);
|
|
381
|
+
}))
|
|
382
|
+
: [];
|
|
383
|
+
|
|
384
|
+
return base;
|
|
385
|
+
}),
|
|
386
|
+
details: member.details,
|
|
387
|
+
users: member.users.map(u => u.getStructure()),
|
|
388
|
+
});
|
|
389
|
+
|
|
369
390
|
memberBlobs.push(
|
|
370
391
|
await Context.auth.filterMemberData(member, blob),
|
|
371
392
|
);
|
|
@@ -568,9 +589,15 @@ export class AuthenticatedStructures {
|
|
|
568
589
|
|
|
569
590
|
const organizationStructs = await this.organizations(organizations);
|
|
570
591
|
|
|
592
|
+
const registrationIds = Formatter.uniqueArray([
|
|
593
|
+
...balances.filter(b => b.objectType === ReceivableBalanceType.registration).map(b => b.objectId),
|
|
594
|
+
]);
|
|
595
|
+
const registrations = await Registration.getByIDs(...registrationIds);
|
|
596
|
+
|
|
571
597
|
const memberIds = Formatter.uniqueArray([
|
|
572
598
|
...balances.filter(b => b.objectType === ReceivableBalanceType.member).map(b => b.objectId),
|
|
573
599
|
...responsibilities.map(r => r.memberId),
|
|
600
|
+
...registrations.map(r => r.memberId),
|
|
574
601
|
]);
|
|
575
602
|
const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
|
|
576
603
|
|
|
@@ -655,6 +682,49 @@ export class AuthenticatedStructures {
|
|
|
655
682
|
});
|
|
656
683
|
}
|
|
657
684
|
}
|
|
685
|
+
else if (balance.objectType === ReceivableBalanceType.registration) {
|
|
686
|
+
const registration = registrations.find(r => r.id === balance.objectId) ?? null;
|
|
687
|
+
if (!registration) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
const member = members.find(m => m.id === registration.memberId) ?? null;
|
|
691
|
+
if (member) {
|
|
692
|
+
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
693
|
+
object = ReceivableBalanceObject.create({
|
|
694
|
+
id: balance.objectId,
|
|
695
|
+
name: member.details.name,
|
|
696
|
+
contacts: [
|
|
697
|
+
...(member.details.getMemberEmails().length
|
|
698
|
+
? [
|
|
699
|
+
ReceivableBalanceObjectContact.create({
|
|
700
|
+
firstName: member.details.firstName ?? '',
|
|
701
|
+
lastName: member.details.lastName ?? '',
|
|
702
|
+
emails: member.details.getMemberEmails(),
|
|
703
|
+
meta: {
|
|
704
|
+
type: 'member',
|
|
705
|
+
responsibilityIds: [],
|
|
706
|
+
url,
|
|
707
|
+
},
|
|
708
|
+
}),
|
|
709
|
+
]
|
|
710
|
+
: []),
|
|
711
|
+
|
|
712
|
+
...(member.details.parentsHaveAccess
|
|
713
|
+
? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
|
|
714
|
+
firstName: p.firstName ?? '',
|
|
715
|
+
lastName: p.lastName ?? '',
|
|
716
|
+
emails: [p.email!],
|
|
717
|
+
meta: {
|
|
718
|
+
type: 'parent',
|
|
719
|
+
responsibilityIds: [],
|
|
720
|
+
url,
|
|
721
|
+
},
|
|
722
|
+
}))
|
|
723
|
+
: []),
|
|
724
|
+
],
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
658
728
|
else if (balance.objectType === ReceivableBalanceType.user) {
|
|
659
729
|
const user = users.find(m => m.id === balance.objectId) ?? null;
|
|
660
730
|
if (user) {
|
|
@@ -12,13 +12,9 @@ export async function resumeEmails() {
|
|
|
12
12
|
const emails = Email.fromRows(result, Email.table);
|
|
13
13
|
|
|
14
14
|
for (const email of emails) {
|
|
15
|
-
if (!email.userId) {
|
|
16
|
-
console.warn('Cannot retry sending email because userId is not set - which is required for setting the scope', email.id);
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
15
|
console.log('Resuming email that has sending status on boot', email.id);
|
|
20
16
|
|
|
21
|
-
const user = await User.getByID(email.userId);
|
|
17
|
+
const user = email.userId ? (await User.getByID(email.userId)) : await User.getSystem();
|
|
22
18
|
if (!user) {
|
|
23
19
|
console.warn('Cannot retry sending email because user not found', email.id);
|
|
24
20
|
continue;
|