@stamhoofd/backend 2.62.0 → 2.64.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 +11 -11
- package/src/audit-logs/PaymentLogger.ts +1 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/update-cached-balances.ts +39 -0
- package/src/email-recipient-loaders/receivable-balances.ts +5 -0
- 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 +85 -25
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +89 -30
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +62 -17
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +52 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +8 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +19 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -5
- package/src/helpers/AdminPermissionChecker.ts +3 -2
- package/src/helpers/AuthenticatedStructures.ts +127 -9
- package/src/helpers/MembershipCharger.ts +4 -0
- package/src/helpers/OrganizationCharger.ts +4 -0
- package/src/seeds/1733994455-balance-item-status-open.ts +30 -0
- package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
- package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +40 -0
- package/src/services/BalanceItemPaymentService.ts +8 -4
- 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/receivable-balances.ts +2 -1
- package/src/sql-sorters/receivable-balances.ts +3 -3
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import backendEnv from '@stamhoofd/backend-env';
|
|
2
2
|
backendEnv.load();
|
|
3
3
|
|
|
4
|
-
import { Column, Database, Migration
|
|
4
|
+
import { Column, Database, Migration } from '@simonbackx/simple-database';
|
|
5
5
|
import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
|
|
6
6
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
7
7
|
import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
|
|
@@ -10,14 +10,14 @@ import { loadLogger } from '@stamhoofd/logging';
|
|
|
10
10
|
import { Version } from '@stamhoofd/structures';
|
|
11
11
|
import { sleep } from '@stamhoofd/utility';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
|
|
14
|
+
import { Platform } from '@stamhoofd/models';
|
|
14
15
|
import { resumeEmails } from './src/helpers/EmailResumer';
|
|
16
|
+
import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
|
|
15
17
|
import { ContextMiddleware } from './src/middleware/ContextMiddleware';
|
|
16
|
-
import { Platform } from '@stamhoofd/models';
|
|
17
18
|
import { AuditLogService } from './src/services/AuditLogService';
|
|
18
|
-
import { PlatformMembershipService } from './src/services/PlatformMembershipService';
|
|
19
19
|
import { DocumentService } from './src/services/DocumentService';
|
|
20
|
-
import {
|
|
20
|
+
import { PlatformMembershipService } from './src/services/PlatformMembershipService';
|
|
21
21
|
|
|
22
22
|
process.on('unhandledRejection', (error: Error) => {
|
|
23
23
|
console.error('unhandledRejection');
|
|
@@ -39,7 +39,9 @@ if (new Date().getTimezoneOffset() !== 0) {
|
|
|
39
39
|
const seeds = async () => {
|
|
40
40
|
try {
|
|
41
41
|
// Internal
|
|
42
|
-
await
|
|
42
|
+
await AuditLogService.disable(async () => {
|
|
43
|
+
await Migration.runAll(__dirname + '/src/seeds');
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
catch (e) {
|
|
45
47
|
console.error('Failed to run seeds:');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.64.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"@bwip-js/node": "^4.5.1",
|
|
35
35
|
"@mollie/api-client": "3.7.0",
|
|
36
36
|
"@simonbackx/simple-database": "1.27.0",
|
|
37
|
-
"@simonbackx/simple-encoding": "2.
|
|
37
|
+
"@simonbackx/simple-encoding": "2.19.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.64.0",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.64.0",
|
|
42
|
+
"@stamhoofd/email": "2.64.0",
|
|
43
|
+
"@stamhoofd/models": "2.64.0",
|
|
44
|
+
"@stamhoofd/queues": "2.64.0",
|
|
45
|
+
"@stamhoofd/sql": "2.64.0",
|
|
46
|
+
"@stamhoofd/structures": "2.64.0",
|
|
47
|
+
"@stamhoofd/utility": "2.64.0",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "80eebbb0b4fb84ac979041c3f8c7762863622367"
|
|
68
68
|
}
|
|
@@ -26,7 +26,7 @@ export const PaymentLogger = new ModelLogger(Payment, {
|
|
|
26
26
|
},
|
|
27
27
|
|
|
28
28
|
createReplacements(model, options) {
|
|
29
|
-
let name = `${PaymentMethodHelper.getPaymentName(model.method)}`;
|
|
29
|
+
let name = `${PaymentMethodHelper.getPaymentName(model.method, model.type)}`;
|
|
30
30
|
|
|
31
31
|
if (model.customer?.dynamicName) {
|
|
32
32
|
name += ` van ${model.customer.dynamicName}`;
|
package/src/crons/index.ts
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CachedBalance } from '@stamhoofd/models';
|
|
2
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
3
|
+
|
|
4
|
+
registerCron('updateCachedBalances', updateCachedBalances);
|
|
5
|
+
|
|
6
|
+
async function updateCachedBalances() {
|
|
7
|
+
// Check if between 3 - 6 AM
|
|
8
|
+
if ((new Date().getHours() > 6 || new Date().getHours() < 3) && STAMHOOFD.environment !== 'development') {
|
|
9
|
+
console.log('Not between 3 and 6 AM, skipping.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const balances = await CachedBalance.select().where(
|
|
14
|
+
CachedBalance.whereNeedsUpdate(),
|
|
15
|
+
).limit(100).fetch();
|
|
16
|
+
|
|
17
|
+
// Group by object type and by organization id
|
|
18
|
+
const grouped = new Map<string, CachedBalance[]>();
|
|
19
|
+
|
|
20
|
+
for (const balance of balances) {
|
|
21
|
+
const key = balance.objectType + '-' + balance.organizationId;
|
|
22
|
+
const arr = grouped.get(key);
|
|
23
|
+
|
|
24
|
+
if (!arr) {
|
|
25
|
+
grouped.set(key, [balance]);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
arr.push(balance);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [_, balances] of grouped) {
|
|
33
|
+
const balance = balances[0];
|
|
34
|
+
|
|
35
|
+
const ids = balances.map(b => b.objectId);
|
|
36
|
+
console.log('Updating', ids.length, balance.objectType, 'for', balance.organizationId);
|
|
37
|
+
await CachedBalance.updateForObjects(balance.organizationId, ids, balance.objectType);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -18,6 +18,11 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
18
18
|
email,
|
|
19
19
|
replacements: [
|
|
20
20
|
Replacement.create({
|
|
21
|
+
token: 'objectName',
|
|
22
|
+
value: balance.object.name,
|
|
23
|
+
}),
|
|
24
|
+
Replacement.create({
|
|
25
|
+
// Deprecated: for backwards compatibility
|
|
21
26
|
token: 'organizationName',
|
|
22
27
|
value: balance.object.name,
|
|
23
28
|
}),
|
|
@@ -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
|
|
|
@@ -5,7 +5,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Email } from '@stamhoofd/email';
|
|
7
7
|
import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
8
|
-
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType,
|
|
8
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, BalanceItem as BalanceItemStruct, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version, PaymentType } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
10
10
|
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -104,7 +104,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
104
104
|
|
|
105
105
|
// Validate balance items (can only happen serverside)
|
|
106
106
|
const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id);
|
|
107
|
-
let memberBalanceItemsStructs:
|
|
107
|
+
let memberBalanceItemsStructs: BalanceItemStruct[] = [];
|
|
108
108
|
let balanceItemsModels: BalanceItem[] = [];
|
|
109
109
|
if (balanceItemIds.length > 0) {
|
|
110
110
|
balanceItemsModels = await BalanceItem.where({ id: { sign: 'IN', value: balanceItemIds }, organizationId: organization.id });
|
|
@@ -114,7 +114,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
114
114
|
message: 'Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw.',
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
memberBalanceItemsStructs =
|
|
117
|
+
memberBalanceItemsStructs = balanceItemsModels.map(i => i.getStructure());
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const memberIds = Formatter.uniqueArray(
|
|
@@ -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,8 +460,12 @@ 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
|
-
balanceItem2.status = BalanceItemStatus.Hidden;
|
|
468
|
+
balanceItem2.status = BalanceItemStatus.Hidden;
|
|
433
469
|
await balanceItem2.save();
|
|
434
470
|
|
|
435
471
|
// do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
|
|
@@ -440,12 +476,16 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
440
476
|
balanceItem.userId = user.id;
|
|
441
477
|
}
|
|
442
478
|
|
|
443
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
479
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
444
480
|
balanceItem.pricePaid = 0;
|
|
445
481
|
|
|
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
|
}
|
|
@@ -554,7 +594,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
554
594
|
if (oldestMember) {
|
|
555
595
|
balanceItem.memberId = oldestMember.id;
|
|
556
596
|
}
|
|
557
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
597
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
558
598
|
await balanceItem.save();
|
|
559
599
|
createdBalanceItems.push(balanceItem);
|
|
560
600
|
}
|
|
@@ -579,7 +619,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
579
619
|
}
|
|
580
620
|
}
|
|
581
621
|
|
|
582
|
-
balanceItem.status = BalanceItemStatus.Hidden;
|
|
622
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
583
623
|
await balanceItem.save();
|
|
584
624
|
|
|
585
625
|
createdBalanceItems.push(balanceItem);
|
|
@@ -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)) {
|
|
@@ -674,6 +719,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
674
719
|
// Calculate total price to pay
|
|
675
720
|
let totalPrice = 0;
|
|
676
721
|
const payMembers: MemberWithRegistrations[] = [];
|
|
722
|
+
let hasNegative = false;
|
|
677
723
|
|
|
678
724
|
for (const [balanceItem, price] of balanceItems) {
|
|
679
725
|
if (organization.id !== balanceItem.organizationId) {
|
|
@@ -694,6 +740,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
694
740
|
});
|
|
695
741
|
}
|
|
696
742
|
|
|
743
|
+
if (price < 0) {
|
|
744
|
+
hasNegative = true;
|
|
745
|
+
}
|
|
746
|
+
|
|
697
747
|
totalPrice += price;
|
|
698
748
|
|
|
699
749
|
if (price > 0 && balanceItem.memberId) {
|
|
@@ -709,27 +759,33 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
709
759
|
}
|
|
710
760
|
|
|
711
761
|
if (totalPrice < 0) {
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// })
|
|
762
|
+
// todo: try to make it non-negative by reducing some balance items
|
|
763
|
+
throw new SimpleError({
|
|
764
|
+
code: 'empty_data',
|
|
765
|
+
message: 'Oeps! De totaalprijs is negatief.',
|
|
766
|
+
});
|
|
718
767
|
}
|
|
768
|
+
const payment = new Payment();
|
|
769
|
+
payment.method = checkout.paymentMethod ?? PaymentMethod.Unknown;
|
|
719
770
|
|
|
720
771
|
if (totalPrice === 0) {
|
|
721
|
-
|
|
722
|
-
|
|
772
|
+
if (balanceItems.size === 0) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Create an egalizing payment
|
|
776
|
+
payment.method = PaymentMethod.Unknown;
|
|
723
777
|
|
|
724
|
-
|
|
778
|
+
if (hasNegative) {
|
|
779
|
+
payment.type = PaymentType.Reallocation;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
else if (payment.method === PaymentMethod.Unknown) {
|
|
725
783
|
throw new SimpleError({
|
|
726
784
|
code: 'invalid_data',
|
|
727
785
|
message: 'Oeps, je hebt geen betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw.',
|
|
728
786
|
});
|
|
729
787
|
}
|
|
730
788
|
|
|
731
|
-
const payment = new Payment();
|
|
732
|
-
|
|
733
789
|
// Who will receive this money?
|
|
734
790
|
payment.organizationId = organization.id;
|
|
735
791
|
|
|
@@ -793,11 +849,16 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
793
849
|
}
|
|
794
850
|
}
|
|
795
851
|
|
|
796
|
-
payment.method = checkout.paymentMethod;
|
|
797
852
|
payment.status = PaymentStatus.Created;
|
|
853
|
+
payment.paidAt = null;
|
|
798
854
|
payment.price = totalPrice;
|
|
799
855
|
|
|
800
|
-
if (
|
|
856
|
+
if (totalPrice === 0) {
|
|
857
|
+
payment.status = PaymentStatus.Succeeded;
|
|
858
|
+
payment.paidAt = new Date();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
801
862
|
// remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
|
|
802
863
|
payment.transferSettings = organization.mappedTransferSettings;
|
|
803
864
|
|
|
@@ -821,7 +882,6 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
821
882
|
},
|
|
822
883
|
);
|
|
823
884
|
}
|
|
824
|
-
payment.paidAt = null;
|
|
825
885
|
|
|
826
886
|
// Determine the payment provider
|
|
827
887
|
// Throws if invalid
|
|
@@ -861,7 +921,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
861
921
|
console.error(e);
|
|
862
922
|
}
|
|
863
923
|
}
|
|
864
|
-
else if (payment.method !== PaymentMethod.PointOfSale) {
|
|
924
|
+
else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
|
|
865
925
|
if (!checkout.redirectUrl || !checkout.cancelUrl) {
|
|
866
926
|
throw new Error('Should have been caught earlier');
|
|
867
927
|
}
|
|
@@ -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
|