@stamhoofd/backend 2.63.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.
Files changed (27) hide show
  1. package/index.ts +8 -6
  2. package/package.json +10 -10
  3. package/src/crons/index.ts +1 -0
  4. package/src/crons/update-cached-balances.ts +39 -0
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
  6. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
  7. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +48 -3
  9. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
  10. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -2
  11. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -0
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +22 -1
  13. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +6 -8
  14. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
  15. package/src/helpers/AdminPermissionChecker.ts +2 -1
  16. package/src/helpers/AuthenticatedStructures.ts +73 -3
  17. package/src/helpers/MembershipCharger.ts +4 -0
  18. package/src/helpers/OrganizationCharger.ts +4 -0
  19. package/src/seeds/1734596144-fill-previous-period-id.ts +55 -0
  20. package/src/services/BalanceItemService.ts +22 -3
  21. package/src/services/PaymentReallocationService.test.ts +746 -0
  22. package/src/services/PaymentReallocationService.ts +339 -0
  23. package/src/services/PaymentService.ts +13 -0
  24. package/src/services/PlatformMembershipService.ts +167 -137
  25. package/src/sql-filters/receivable-balances.ts +1 -1
  26. package/src/sql-sorters/receivable-balances.ts +3 -3
  27. /package/src/seeds/{1733996431-update-cached-outstanding-balance-from-items.ts → 1734700082-update-cached-outstanding-balance-from-items.ts} +0 -0
@@ -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 blob = member.getStructureWithRegistrations();
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) {
@@ -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,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
+ });
@@ -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
  };