@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.
Files changed (49) hide show
  1. package/index.ts +8 -6
  2. package/package.json +10 -10
  3. package/src/audit-logs/EmailLogger.ts +7 -1
  4. package/src/audit-logs/ModelLogger.ts +17 -2
  5. package/src/crons/balance-emails.ts +232 -0
  6. package/src/crons/index.ts +2 -0
  7. package/src/crons/update-cached-balances.ts +39 -0
  8. package/src/email-recipient-loaders/members.ts +14 -4
  9. package/src/email-recipient-loaders/receivable-balances.ts +29 -15
  10. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +47 -12
  11. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -18
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +41 -16
  13. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -0
  14. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +6 -3
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +48 -3
  16. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +15 -1
  17. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +15 -1
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
  19. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -2
  20. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -0
  21. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +23 -2
  22. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +0 -12
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +10 -2
  24. package/src/helpers/AdminPermissionChecker.ts +2 -1
  25. package/src/helpers/AuthenticatedStructures.ts +73 -3
  26. package/src/helpers/EmailResumer.ts +1 -5
  27. package/src/helpers/MemberUserSyncer.ts +22 -1
  28. package/src/helpers/MembershipCharger.ts +5 -0
  29. package/src/helpers/OrganizationCharger.ts +4 -0
  30. package/src/helpers/TagHelper.ts +7 -14
  31. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +4 -14
  32. package/src/seeds/1729253172-update-orders.ts +7 -18
  33. package/src/seeds/{1733996431-update-cached-outstanding-balance-from-items.ts → 1734596144-fill-previous-period-id.ts} +21 -6
  34. package/src/seeds/{1726494420-update-cached-outstanding-balance-from-items.ts → 1735577912-update-cached-outstanding-balance-from-items.ts} +1 -14
  35. package/src/services/BalanceItemService.ts +22 -3
  36. package/src/services/PaymentReallocationService.test.ts +746 -0
  37. package/src/services/PaymentReallocationService.ts +339 -0
  38. package/src/services/PaymentService.ts +13 -0
  39. package/src/services/PlatformMembershipService.ts +167 -137
  40. package/src/sql-filters/members.ts +1 -0
  41. package/src/sql-filters/receivable-balances.ts +16 -2
  42. package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +19 -0
  43. package/src/sql-sorters/receivable-balances.ts +3 -3
  44. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +0 -253
  45. package/src/helpers/ModelHelper.ts +0 -32
  46. package/src/seeds/1726055544-balance-item-paid.ts +0 -11
  47. package/src/seeds/1726055545-balance-item-pending.ts +0 -11
  48. package/src/seeds/1726494419-update-cached-outstanding-balance.ts +0 -53
  49. package/src/seeds/1728928973-balance-item-pending.ts +0 -11
@@ -1,9 +1,10 @@
1
- import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
1
+ import { CachedBalance, Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
3
  import { AuditLogSource, MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
4
4
  import crypto from 'crypto';
5
5
  import basex from 'base-x';
6
6
  import { AuditLogService } from '../services/AuditLogService';
7
+ import { Formatter } from '@stamhoofd/utility';
7
8
 
8
9
  const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
9
10
  const customBase = basex(ALPHABET);
@@ -185,6 +186,9 @@ export class MemberUserSyncerStatic {
185
186
  console.log('Removing access for ' + user.id + ' to member ' + member.id);
186
187
  await Member.users.reverse('members').unlink(user, member);
187
188
 
189
+ // Update balance of this user, as it could have changed
190
+ await this.updateUserBalance(user.id, member.id);
191
+
188
192
  if (user.memberId === member.id) {
189
193
  user.memberId = null;
190
194
  }
@@ -305,6 +309,23 @@ export class MemberUserSyncerStatic {
305
309
  if (!member.users.find(u => u.id === user.id)) {
306
310
  await Member.users.reverse('members').link(user, [member]);
307
311
  member.users.push(user);
312
+
313
+ // Update balance of this user, as it could have changed
314
+ await this.updateUserBalance(user.id, member.id);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Update the balance after making a change in linked member/users
320
+ */
321
+ async updateUserBalance(userId: string, memberId: string) {
322
+ // Update balance of this user, as it could have changed
323
+ const memberBalances = await CachedBalance.getForObjects([memberId]);
324
+ if (memberBalances.length > 0) {
325
+ const organizationIds = Formatter.uniqueArray(memberBalances.map(b => b.organizationId));
326
+ for (const organizationId of organizationIds) {
327
+ await CachedBalance.updateForUsers(organizationId, [userId]);
328
+ }
308
329
  }
309
330
  }
310
331
  }
@@ -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() {
@@ -35,6 +36,7 @@ export const MembershipCharger = {
35
36
  .where('balanceItemId', null)
36
37
  .where('deletedAt', null)
37
38
  .whereNot('organizationId', chargeVia)
39
+ .where(SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date()))
38
40
  .limit(chunkSize)
39
41
  .orderBy(
40
42
  new SQLOrderBy({
@@ -122,6 +124,9 @@ export const MembershipCharger = {
122
124
 
123
125
  await BalanceItem.updateOutstanding(createdBalanceItems);
124
126
 
127
+ // Reallocate
128
+ await BalanceItemService.reallocate(createdBalanceItems, chargeVia);
129
+
125
130
  if (memberships.length < chunkSize) {
126
131
  break;
127
132
  }
@@ -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 {
@@ -1,7 +1,6 @@
1
1
  import { Organization, Platform } from '@stamhoofd/models';
2
2
  import { QueueHandler } from '@stamhoofd/queues';
3
3
  import { AuditLogSource, OrganizationTag, TagHelper as SharedTagHelper } from '@stamhoofd/structures';
4
- import { ModelHelper } from './ModelHelper';
5
4
  import { AuditLogService } from '../services/AuditLogService';
6
5
 
7
6
  export class TagHelper extends SharedTagHelper {
@@ -14,17 +13,15 @@ export class TagHelper extends SharedTagHelper {
14
13
  let platform = await Platform.getShared();
15
14
 
16
15
  const tagCounts = new Map<string, number>();
17
- await this.loopOrganizations(async (organizations) => {
18
- for (const organization of organizations) {
19
- organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
20
16
 
21
- for (const tag of organization.meta.tags) {
22
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
23
- }
24
- }
17
+ for await (const organization of Organization.select().all()) {
18
+ organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
25
19
 
26
- await Promise.all(organizations.map(organization => organization.save()));
27
- });
20
+ for (const tag of organization.meta.tags) {
21
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
22
+ }
23
+ await organization.save();
24
+ }
28
25
 
29
26
  // Reload platform to avoid race conditions
30
27
  platform = await Platform.getShared();
@@ -39,10 +36,6 @@ export class TagHelper extends SharedTagHelper {
39
36
  });
40
37
  }
41
38
 
42
- private static async loopOrganizations(onBatchReceived: (batch: Organization[]) => Promise<void>) {
43
- await ModelHelper.loop(Organization, 'id', onBatchReceived, { limit: 10 });
44
- }
45
-
46
39
  /**
47
40
  * Removes child tag ids that do not exist and sorts the tags.
48
41
  * @param platformTags
@@ -2,6 +2,9 @@ import { Migration } from '@simonbackx/simple-database';
2
2
  import { logger } from '@simonbackx/simple-logging';
3
3
  import { BalanceItem } from '@stamhoofd/models';
4
4
 
5
+ /**
6
+ * This migration is required to keep '1733994455-balance-item-status-open' working
7
+ */
5
8
  export default new Migration(async () => {
6
9
  if (STAMHOOFD.environment == 'test') {
7
10
  console.log('skipped in tests');
@@ -10,26 +13,13 @@ export default new Migration(async () => {
10
13
 
11
14
  process.stdout.write('\n');
12
15
  let c = 0;
13
- let id: string = '';
14
16
 
15
17
  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
-
18
+ for await (const items of BalanceItem.select().limit(1000).allBatched()) {
24
19
  await BalanceItem.updateOutstanding(items);
25
20
 
26
21
  c += items.length;
27
22
  process.stdout.write('.');
28
-
29
- if (items.length < 1000) {
30
- break;
31
- }
32
- id = items[items.length - 1].id;
33
23
  }
34
24
  });
35
25
 
@@ -1,7 +1,5 @@
1
1
  import { Migration } from '@simonbackx/simple-database';
2
2
  import { Order } from '@stamhoofd/models';
3
- import { sleep } from '@stamhoofd/utility';
4
- import { ModelHelper } from '../helpers/ModelHelper';
5
3
 
6
4
  export default new Migration(async () => {
7
5
  if (STAMHOOFD.environment === 'test') {
@@ -11,22 +9,13 @@ export default new Migration(async () => {
11
9
 
12
10
  console.log('Start saving orders.');
13
11
 
14
- const limit = 100;
15
- let count = limit;
12
+ const batchSize = 100;
13
+ let count = 0;
16
14
 
17
- await ModelHelper.loop(Order, 'id', async (batch: Order[]) => {
18
- console.log('Saving orders...', `(${count})`);
19
-
20
- // save all orders to update the new columns
21
- for (const order of batch) {
22
- await order.save();
23
- }
24
-
25
- count += limit;
26
- },
27
- { limit });
28
-
29
- await sleep(1000);
15
+ for await (const order of Order.select().limit(batchSize).all()) {
16
+ await order.save();
17
+ count += 1;
18
+ }
30
19
 
31
- console.log('Finished saving orders.');
20
+ console.log('Finished saving ' + count + ' orders.');
32
21
  });
@@ -1,6 +1,6 @@
1
1
  import { Migration } from '@simonbackx/simple-database';
2
2
  import { logger } from '@simonbackx/simple-logging';
3
- import { BalanceItem } from '@stamhoofd/models';
3
+ import { Platform, RegistrationPeriod } from '@stamhoofd/models';
4
4
 
5
5
  export default new Migration(async () => {
6
6
  if (STAMHOOFD.environment == 'test') {
@@ -12,20 +12,28 @@ export default new Migration(async () => {
12
12
  let c = 0;
13
13
  let id: string = '';
14
14
 
15
- await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
15
+ await logger.setContext({ tags: ['seed'] }, async () => {
16
16
  while (true) {
17
- const items = await BalanceItem.where({
17
+ const items = await RegistrationPeriod.where({
18
18
  id: {
19
19
  value: id,
20
20
  sign: '>',
21
21
  },
22
22
  }, { limit: 1000, sort: ['id'] });
23
23
 
24
- await BalanceItem.updateOutstanding(items);
24
+ if (items.length === 0) {
25
+ break;
26
+ }
25
27
 
26
- c += items.length;
27
28
  process.stdout.write('.');
28
29
 
30
+ for (const item of items) {
31
+ await item.setPreviousPeriodId();
32
+ if (await item.save()) {
33
+ c += 1;
34
+ }
35
+ }
36
+
29
37
  if (items.length < 1000) {
30
38
  break;
31
39
  }
@@ -33,7 +41,14 @@ export default new Migration(async () => {
33
41
  }
34
42
  });
35
43
 
36
- console.log('Updated outstanding balance for ' + c + ' items');
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');
37
52
 
38
53
  // Do something here
39
54
  return Promise.resolve();
@@ -10,26 +10,13 @@ export default new Migration(async () => {
10
10
 
11
11
  process.stdout.write('\n');
12
12
  let c = 0;
13
- let id: string = '';
14
13
 
15
14
  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
-
15
+ for await (const items of BalanceItem.select().limit(1000).allBatched()) {
24
16
  await BalanceItem.updateOutstanding(items);
25
17
 
26
18
  c += items.length;
27
19
  process.stdout.write('.');
28
-
29
- if (items.length < 1000) {
30
- break;
31
- }
32
- id = items[items.length - 1].id;
33
20
  }
34
21
  });
35
22
 
@@ -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
  };