@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
|
@@ -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';
|
|
@@ -40,8 +40,6 @@ export class AuthenticatedStructures {
|
|
|
40
40
|
|
|
41
41
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions;
|
|
42
42
|
|
|
43
|
-
console.log('includeSettlements', includeSettlements);
|
|
44
|
-
|
|
45
43
|
const { payingOrganizations } = await Payment.loadPayingOrganizations(payments);
|
|
46
44
|
|
|
47
45
|
return Payment.getGeneralStructureFromRelations({
|
|
@@ -325,6 +323,9 @@ export class AuthenticatedStructures {
|
|
|
325
323
|
}
|
|
326
324
|
const organizations = new Map<string, Organization>();
|
|
327
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
|
+
|
|
328
329
|
if (includeUser) {
|
|
329
330
|
for (const organizationId of includeUser.permissions?.organizationPermissions.keys() ?? []) {
|
|
330
331
|
if (includeContextOrganization || organizationId !== Context.auth.organization?.id) {
|
|
@@ -367,7 +368,25 @@ export class AuthenticatedStructures {
|
|
|
367
368
|
}
|
|
368
369
|
}
|
|
369
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));
|
|
370
|
-
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
|
+
|
|
371
390
|
memberBlobs.push(
|
|
372
391
|
await Context.auth.filterMemberData(member, blob),
|
|
373
392
|
);
|
|
@@ -570,12 +589,23 @@ export class AuthenticatedStructures {
|
|
|
570
589
|
|
|
571
590
|
const organizationStructs = await this.organizations(organizations);
|
|
572
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
|
+
|
|
573
597
|
const memberIds = Formatter.uniqueArray([
|
|
574
598
|
...balances.filter(b => b.objectType === ReceivableBalanceType.member).map(b => b.objectId),
|
|
575
599
|
...responsibilities.map(r => r.memberId),
|
|
600
|
+
...registrations.map(r => r.memberId),
|
|
576
601
|
]);
|
|
577
602
|
const members = memberIds.length > 0 ? await Member.getByIDs(...memberIds) : [];
|
|
578
603
|
|
|
604
|
+
const userIds = Formatter.uniqueArray([
|
|
605
|
+
...balances.filter(b => b.objectType === ReceivableBalanceType.user).map(b => b.objectId),
|
|
606
|
+
]);
|
|
607
|
+
const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
|
|
608
|
+
|
|
579
609
|
const result: ReceivableBalanceStruct[] = [];
|
|
580
610
|
for (const balance of balances) {
|
|
581
611
|
let object = ReceivableBalanceObject.create({
|
|
@@ -584,7 +614,7 @@ export class AuthenticatedStructures {
|
|
|
584
614
|
});
|
|
585
615
|
|
|
586
616
|
if (balance.objectType === ReceivableBalanceType.organization) {
|
|
587
|
-
const organization = organizationStructs.find(o => o.id
|
|
617
|
+
const organization = organizationStructs.find(o => o.id === balance.objectId) ?? null;
|
|
588
618
|
if (organization) {
|
|
589
619
|
const theseResponsibilities = responsibilities.filter(r => r.organizationId === organization.id);
|
|
590
620
|
const thisMembers = members.flatMap((m) => {
|
|
@@ -605,6 +635,7 @@ export class AuthenticatedStructures {
|
|
|
605
635
|
lastName: member.lastName ?? '',
|
|
606
636
|
emails: member.details.getMemberEmails(),
|
|
607
637
|
meta: {
|
|
638
|
+
type: 'organization',
|
|
608
639
|
responsibilityIds: responsibilities.map(r => r.responsibilityId),
|
|
609
640
|
url: organization.dashboardUrl + '/boekhouding/openstaand/' + (Context.organization?.uri ?? ''),
|
|
610
641
|
},
|
|
@@ -615,14 +646,101 @@ export class AuthenticatedStructures {
|
|
|
615
646
|
else if (balance.objectType === ReceivableBalanceType.member) {
|
|
616
647
|
const member = members.find(m => m.id === balance.objectId) ?? null;
|
|
617
648
|
if (member) {
|
|
649
|
+
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
618
650
|
object = ReceivableBalanceObject.create({
|
|
619
651
|
id: balance.objectId,
|
|
620
652
|
name: member.details.name,
|
|
653
|
+
contacts: [
|
|
654
|
+
...(member.details.getMemberEmails().length
|
|
655
|
+
? [
|
|
656
|
+
ReceivableBalanceObjectContact.create({
|
|
657
|
+
firstName: member.details.firstName ?? '',
|
|
658
|
+
lastName: member.details.lastName ?? '',
|
|
659
|
+
emails: member.details.getMemberEmails(),
|
|
660
|
+
meta: {
|
|
661
|
+
type: 'member',
|
|
662
|
+
responsibilityIds: [],
|
|
663
|
+
url,
|
|
664
|
+
},
|
|
665
|
+
}),
|
|
666
|
+
]
|
|
667
|
+
: []),
|
|
668
|
+
|
|
669
|
+
...(member.details.parentsHaveAccess
|
|
670
|
+
? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
|
|
671
|
+
firstName: p.firstName ?? '',
|
|
672
|
+
lastName: p.lastName ?? '',
|
|
673
|
+
emails: [p.email!],
|
|
674
|
+
meta: {
|
|
675
|
+
type: 'parent',
|
|
676
|
+
responsibilityIds: [],
|
|
677
|
+
url,
|
|
678
|
+
},
|
|
679
|
+
}))
|
|
680
|
+
: []),
|
|
681
|
+
],
|
|
682
|
+
});
|
|
683
|
+
}
|
|
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
|
+
}
|
|
728
|
+
else if (balance.objectType === ReceivableBalanceType.user) {
|
|
729
|
+
const user = users.find(m => m.id === balance.objectId) ?? null;
|
|
730
|
+
if (user) {
|
|
731
|
+
const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
|
|
732
|
+
object = ReceivableBalanceObject.create({
|
|
733
|
+
id: balance.objectId,
|
|
734
|
+
name: user.name || user.email,
|
|
621
735
|
contacts: [
|
|
622
736
|
ReceivableBalanceObjectContact.create({
|
|
623
|
-
firstName:
|
|
624
|
-
lastName:
|
|
625
|
-
emails:
|
|
737
|
+
firstName: user.firstName ?? '',
|
|
738
|
+
lastName: user.lastName ?? '',
|
|
739
|
+
emails: [user.email],
|
|
740
|
+
meta: {
|
|
741
|
+
responsibilityIds: [],
|
|
742
|
+
url,
|
|
743
|
+
},
|
|
626
744
|
}),
|
|
627
745
|
],
|
|
628
746
|
});
|
|
@@ -3,6 +3,7 @@ import { BalanceItem, Member, MemberPlatformMembership, Platform } from '@stamho
|
|
|
3
3
|
import { SQL, SQLOrderBy, SQLWhereSign } from '@stamhoofd/sql';
|
|
4
4
|
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemType } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
+
import { BalanceItemService } from '../services/BalanceItemService';
|
|
6
7
|
|
|
7
8
|
export const MembershipCharger = {
|
|
8
9
|
async charge() {
|
|
@@ -122,6 +123,9 @@ export const MembershipCharger = {
|
|
|
122
123
|
|
|
123
124
|
await BalanceItem.updateOutstanding(createdBalanceItems);
|
|
124
125
|
|
|
126
|
+
// Reallocate
|
|
127
|
+
await BalanceItemService.reallocate(createdBalanceItems, chargeVia);
|
|
128
|
+
|
|
125
129
|
if (memberships.length < chunkSize) {
|
|
126
130
|
break;
|
|
127
131
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
2
|
import { BalanceItem, Platform } from '@stamhoofd/models';
|
|
3
3
|
import { BalanceItemType, Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { BalanceItemService } from '../services/BalanceItemService';
|
|
4
5
|
|
|
5
6
|
export class OrganizationCharger {
|
|
6
7
|
static async chargeFromPlatform(args: { organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
|
|
@@ -30,6 +31,9 @@ export class OrganizationCharger {
|
|
|
30
31
|
|
|
31
32
|
await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
|
|
32
33
|
await BalanceItem.updateOutstanding(balanceItems);
|
|
34
|
+
|
|
35
|
+
// Reallocate
|
|
36
|
+
await BalanceItemService.reallocate(balanceItems, chargingOrganizationId);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct }): BalanceItem {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Database, Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { BalanceItemStatus } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment === 'test') {
|
|
6
|
+
console.log('skipped in tests');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const query = `
|
|
11
|
+
UPDATE
|
|
12
|
+
balance_items
|
|
13
|
+
SET status = ?
|
|
14
|
+
WHERE status IN (?)`;
|
|
15
|
+
await Database.update(query, [
|
|
16
|
+
BalanceItemStatus.Due,
|
|
17
|
+
['Paid', 'Pending'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const q2 = `
|
|
21
|
+
UPDATE
|
|
22
|
+
balance_items
|
|
23
|
+
SET status = ?,
|
|
24
|
+
amount = coalesce(nullif(ROUND(coalesce(pricePaid / nullif(unitPrice, 0), 0)), 0), 1)
|
|
25
|
+
WHERE amount = 0 AND status = ?`;
|
|
26
|
+
await Database.update(q2, [
|
|
27
|
+
BalanceItemStatus.Canceled,
|
|
28
|
+
BalanceItemStatus.Due,
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment == 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.stdout.write('\n');
|
|
12
|
+
let c = 0;
|
|
13
|
+
let id: string = '';
|
|
14
|
+
|
|
15
|
+
await logger.setContext({ tags: ['seed'] }, async () => {
|
|
16
|
+
while (true) {
|
|
17
|
+
const items = await RegistrationPeriod.where({
|
|
18
|
+
id: {
|
|
19
|
+
value: id,
|
|
20
|
+
sign: '>',
|
|
21
|
+
},
|
|
22
|
+
}, { limit: 1000, sort: ['id'] });
|
|
23
|
+
|
|
24
|
+
if (items.length === 0) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
process.stdout.write('.');
|
|
29
|
+
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
await item.setPreviousPeriodId();
|
|
32
|
+
if (await item.save()) {
|
|
33
|
+
c += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (items.length < 1000) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
id = items[items.length - 1].id;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log('Updated ' + c + ' registration periods');
|
|
45
|
+
|
|
46
|
+
// Now update platform
|
|
47
|
+
const platform = await Platform.getShared();
|
|
48
|
+
await platform.setPreviousPeriodId();
|
|
49
|
+
await platform.save();
|
|
50
|
+
|
|
51
|
+
console.log('Updated platform');
|
|
52
|
+
|
|
53
|
+
// Do something here
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment == 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.stdout.write('\n');
|
|
12
|
+
let c = 0;
|
|
13
|
+
let id: string = '';
|
|
14
|
+
|
|
15
|
+
await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
|
|
16
|
+
while (true) {
|
|
17
|
+
const items = await BalanceItem.where({
|
|
18
|
+
id: {
|
|
19
|
+
value: id,
|
|
20
|
+
sign: '>',
|
|
21
|
+
},
|
|
22
|
+
}, { limit: 1000, sort: ['id'] });
|
|
23
|
+
|
|
24
|
+
await BalanceItem.updateOutstanding(items);
|
|
25
|
+
|
|
26
|
+
c += items.length;
|
|
27
|
+
process.stdout.write('.');
|
|
28
|
+
|
|
29
|
+
if (items.length < 1000) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
id = items[items.length - 1].id;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log('Updated outstanding balance for ' + c + ' items');
|
|
37
|
+
|
|
38
|
+
// Do something here
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
});
|
|
@@ -7,16 +7,20 @@ type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<
|
|
|
7
7
|
|
|
8
8
|
export const BalanceItemPaymentService = {
|
|
9
9
|
async markPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
10
|
+
const wasPaid = balanceItemPayment.balanceItem.priceOpen === 0;
|
|
11
|
+
|
|
10
12
|
// Update cached amountPaid of the balance item (balanceItemPayment will get overwritten later, but we need it to calculate the status)
|
|
11
13
|
balanceItemPayment.balanceItem.pricePaid += balanceItemPayment.price;
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Hidden && balanceItemPayment.balanceItem.pricePaid !== 0) {
|
|
16
|
+
balanceItemPayment.balanceItem.status = BalanceItemStatus.Due;
|
|
17
|
+
}
|
|
18
|
+
|
|
16
19
|
await balanceItemPayment.balanceItem.save();
|
|
20
|
+
const isPaid = balanceItemPayment.balanceItem.priceOpen === 0;
|
|
17
21
|
|
|
18
22
|
// Do logic of balance item
|
|
19
|
-
if (
|
|
23
|
+
if (isPaid && !wasPaid && balanceItemPayment.price >= 0 && balanceItemPayment.balanceItem.status === BalanceItemStatus.Due) {
|
|
20
24
|
// Only call markPaid once (if it wasn't (partially) paid before)
|
|
21
25
|
await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
22
26
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BalanceItem, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
|
|
2
|
-
import { AuditLogSource, BalanceItemStatus, OrderStatus } from '@stamhoofd/structures';
|
|
3
|
-
import { RegistrationService } from './RegistrationService';
|
|
2
|
+
import { AuditLogSource, BalanceItemStatus, OrderStatus, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
3
|
import { AuditLogService } from './AuditLogService';
|
|
4
|
+
import { RegistrationService } from './RegistrationService';
|
|
5
|
+
import { PaymentReallocationService } from './PaymentReallocationService';
|
|
6
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
7
|
|
|
6
8
|
export const BalanceItemService = {
|
|
7
9
|
async markPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
|
|
@@ -45,6 +47,24 @@ export const BalanceItemService = {
|
|
|
45
47
|
}
|
|
46
48
|
},
|
|
47
49
|
|
|
50
|
+
async reallocate(balanceItems: BalanceItem[], organizationId: string) {
|
|
51
|
+
const memberIds = Formatter.uniqueArray(balanceItems.map(b => b.memberId).filter(b => b !== null));
|
|
52
|
+
const payingOrganizationIds = Formatter.uniqueArray(balanceItems.map(b => b.payingOrganizationId).filter(b => b !== null));
|
|
53
|
+
const userIds = Formatter.uniqueArray(balanceItems.map(b => b.userId).filter(b => b !== null));
|
|
54
|
+
|
|
55
|
+
for (const memberId of memberIds) {
|
|
56
|
+
await PaymentReallocationService.reallocate(organizationId, memberId, ReceivableBalanceType.member);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const payingOrganizationId of payingOrganizationIds) {
|
|
60
|
+
await PaymentReallocationService.reallocate(organizationId, payingOrganizationId, ReceivableBalanceType.organization);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const userId of userIds) {
|
|
64
|
+
await PaymentReallocationService.reallocate(organizationId, userId, ReceivableBalanceType.user);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
48
68
|
async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
49
69
|
// For orders: mark order as changed (so they are refetched in front ends)
|
|
50
70
|
if (balanceItem.orderId) {
|
|
@@ -93,5 +113,4 @@ export const BalanceItemService = {
|
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
},
|
|
96
|
-
|
|
97
116
|
};
|