@stamhoofd/backend 2.83.5 → 2.84.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 (100) hide show
  1. package/index.ts +19 -4
  2. package/package.json +18 -14
  3. package/src/crons/amazon-ses.ts +26 -5
  4. package/src/crons/balance-emails.ts +18 -17
  5. package/src/email-recipient-loaders/registrations.ts +87 -0
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
  7. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
  8. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
  10. package/src/endpoints/global/files/UploadFile.ts +11 -16
  11. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
  12. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
  13. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
  17. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
  18. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
  19. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
  20. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
  21. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
  25. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
  27. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
  28. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
  29. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
  30. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
  31. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
  32. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
  33. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
  34. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
  35. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
  36. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
  37. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
  38. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
  39. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
  40. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
  41. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
  42. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
  43. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
  44. package/src/excel-loaders/members.ts +233 -232
  45. package/src/excel-loaders/payments.ts +1 -1
  46. package/src/excel-loaders/receivable-balances.ts +1 -1
  47. package/src/excel-loaders/registrations.ts +153 -0
  48. package/src/helpers/AdminPermissionChecker.ts +65 -37
  49. package/src/helpers/AuthenticatedStructures.ts +43 -3
  50. package/src/helpers/Context.ts +29 -1
  51. package/src/helpers/GlobalHelper.ts +3 -1
  52. package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
  53. package/src/helpers/GroupedThrottledQueue.ts +108 -0
  54. package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
  55. package/src/helpers/MemberCharger.ts +0 -5
  56. package/src/helpers/MembershipCharger.ts +3 -9
  57. package/src/helpers/OrganizationCharger.ts +0 -5
  58. package/src/helpers/ThrottledQueue.test.ts +194 -0
  59. package/src/helpers/ThrottledQueue.ts +145 -0
  60. package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
  61. package/src/middleware/ContextMiddleware.ts +1 -1
  62. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
  63. package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
  64. package/src/services/BalanceItemPaymentService.ts +1 -33
  65. package/src/services/BalanceItemService.ts +167 -48
  66. package/src/services/FileSignService.ts +18 -13
  67. package/src/services/MemberRecordStore.ts +28 -19
  68. package/src/services/PaymentReallocationService.test.ts +25 -14
  69. package/src/services/PaymentReallocationService.ts +29 -10
  70. package/src/services/PaymentService.ts +4 -16
  71. package/src/services/PlatformMembershipService.ts +8 -4
  72. package/src/services/RegistrationService.ts +66 -2
  73. package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
  74. package/src/sql-filters/groups.ts +67 -0
  75. package/src/sql-filters/members.ts +33 -58
  76. package/src/sql-filters/organization-registration-periods.ts +8 -0
  77. package/src/sql-filters/registration-periods.ts +8 -0
  78. package/src/sql-filters/registrations.ts +11 -22
  79. package/src/sql-sorters/groups.ts +24 -0
  80. package/src/sql-sorters/organization-registration-periods.ts +24 -0
  81. package/src/sql-sorters/registration-periods.ts +47 -0
  82. package/src/sql-sorters/registrations.ts +77 -0
  83. package/tests/actions/patchOrganizationMember.ts +27 -0
  84. package/tests/actions/patchPaymentStatus.ts +45 -0
  85. package/tests/actions/patchUserMember.ts +27 -0
  86. package/tests/assertions/assertBalances.ts +49 -0
  87. package/tests/e2e/api-rate-limits.test.ts +5 -5
  88. package/tests/e2e/bundle-discounts.test.ts +4060 -0
  89. package/tests/e2e/charge-members.test.ts +27 -24
  90. package/tests/e2e/documents.test.ts +398 -0
  91. package/tests/e2e/register.test.ts +292 -312
  92. package/tests/helpers/PayconiqMocker.ts +55 -0
  93. package/tests/init/index.ts +5 -0
  94. package/tests/init/initAdmin.ts +14 -0
  95. package/tests/init/initBundleDiscount.ts +47 -0
  96. package/tests/init/initPayconiq.ts +9 -0
  97. package/tests/init/initPlatformAdmin.ts +13 -0
  98. package/tests/init/initStripe.ts +21 -0
  99. package/tests/jest.setup.ts +29 -0
  100. package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
@@ -31,10 +31,7 @@ export const PaymentService = {
31
31
  await BalanceItemPaymentService.markPaid(balanceItemPayment, organization);
32
32
  }
33
33
 
34
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
35
-
36
- // Reallocate
37
- await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
34
+ await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
38
35
  });
39
36
  return;
40
37
  }
@@ -58,10 +55,7 @@ export const PaymentService = {
58
55
  await BalanceItemPaymentService.undoPaid(balanceItemPayment, organization);
59
56
  }
60
57
 
61
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
62
-
63
- // Reallocate
64
- await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
58
+ await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
65
59
  });
66
60
  }
67
61
 
@@ -76,10 +70,7 @@ export const PaymentService = {
76
70
  await BalanceItemPaymentService.markFailed(balanceItemPayment, organization);
77
71
  }
78
72
 
79
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
80
-
81
- // Reallocate
82
- await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
73
+ await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
83
74
  });
84
75
  }
85
76
 
@@ -94,10 +85,7 @@ export const PaymentService = {
94
85
  await BalanceItemPaymentService.undoFailed(balanceItemPayment, organization);
95
86
  }
96
87
 
97
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
98
-
99
- // Reallocate
100
- await BalanceItemService.reallocate(balanceItemPayments.map(p => p.balanceItem), organization.id);
88
+ await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
101
89
  });
102
90
  }
103
91
  });
@@ -138,9 +138,6 @@ export class PlatformMembershipService {
138
138
 
139
139
  static async updateMembershipsForId(id: string, silent = false) {
140
140
  if (STAMHOOFD.userMode === 'organization') {
141
- if (!silent) {
142
- console.warn('Skipping automatic membership for: ' + id, ' - organization mode');
143
- }
144
141
  return;
145
142
  }
146
143
 
@@ -157,10 +154,17 @@ export class PlatformMembershipService {
157
154
  }
158
155
  return;
159
156
  }
157
+ if (me.organizationId) {
158
+ if (!silent) {
159
+ console.warn('Cannot update members for a member with organization set', me.id);
160
+ }
161
+ return;
162
+ }
163
+
160
164
  const platform = await Platform.getSharedStruct();
161
165
  const periods = await RegistrationPeriod.select()
162
166
  .where('locked', false)
163
- .where('organizationId', me.organizationId)
167
+ .where('organizationId', null)
164
168
  .where('endDate', SQLWhereSign.GreaterEqual, new Date()) // Avoid updating the price of past periods that were not yet locked
165
169
  .fetch();
166
170
 
@@ -1,11 +1,12 @@
1
1
  import { ManyToOneRelation } from '@simonbackx/simple-database';
2
- import { Document, Group, Member, Registration } from '@stamhoofd/models';
3
- import { AuditLogSource, EmailTemplateType, StockReservation } from '@stamhoofd/structures';
2
+ import { BalanceItem, Document, Group, Member, Registration } from '@stamhoofd/models';
3
+ import { AppliedRegistrationDiscount, AuditLogSource, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, EmailTemplateType, StockReservation, TranslatedString, Version } from '@stamhoofd/structures';
4
4
  import { AuditLogService } from './AuditLogService';
5
5
  import { GroupService } from './GroupService';
6
6
  import { PlatformMembershipService } from './PlatformMembershipService';
7
7
  import { QueueHandler } from '@stamhoofd/queues';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
+ import { encodeObject, patchContainsChanges } from '@simonbackx/simple-encoding';
9
10
 
10
11
  export const RegistrationService = {
11
12
  async markValid(registrationId: string) {
@@ -44,6 +45,69 @@ export const RegistrationService = {
44
45
  return true;
45
46
  },
46
47
 
48
+ async updateDiscounts(registrationId: string) {
49
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
50
+ await QueueHandler.schedule('registration-discounts-update-' + registrationId, async function (this: undefined) {
51
+ const registration = await Registration.getByID(registrationId);
52
+ if (!registration) {
53
+ throw new Error('Registration not found');
54
+ }
55
+
56
+ // Fetch all discounts that have been granted to this registration
57
+ const discountBalanceItems = await BalanceItem.select()
58
+ .where('registrationId', registrationId)
59
+ .where('organizationId', registration.organizationId)
60
+ .where('type', BalanceItemType.RegistrationBundleDiscount)
61
+ .where('status', BalanceItemStatus.Due)
62
+ .fetch();
63
+
64
+ let before: string | undefined;
65
+ if (STAMHOOFD.environment !== 'production') {
66
+ // This is an expensive operation, so keep track of whether it was actually necessary so we can detect performance issues
67
+ before = JSON.stringify(encodeObject(registration.discounts, { version: Version }));
68
+ }
69
+
70
+ // Reset registration discounts
71
+ registration.discounts = new Map();
72
+
73
+ for (const balanceItem of discountBalanceItems) {
74
+ const discount = balanceItem.relations.get(BalanceItemRelationType.Discount);
75
+ if (!discount) {
76
+ continue;
77
+ }
78
+
79
+ let existing = registration.discounts.get(discount.id);
80
+
81
+ if (!existing) {
82
+ existing = AppliedRegistrationDiscount.create({
83
+ name: discount.name,
84
+ amount: 0,
85
+ });
86
+ registration.discounts.set(discount.id, existing);
87
+ }
88
+ existing.amount += -balanceItem.price; // price is negative means it has been discounted, and we store a positive amount with the discount
89
+
90
+ if (existing.amount === 0) {
91
+ // Delete the discount
92
+ registration.discounts.delete(discount.id);
93
+ }
94
+ }
95
+
96
+ console.log('Saving updated discounts for', registrationId, registration.discounts);
97
+
98
+ if (STAMHOOFD.environment !== 'production') {
99
+ const after = JSON.stringify(encodeObject(registration.discounts, { version: Version }));
100
+ if (before === after) {
101
+ console.warn('Unnecessary update of registration discounts', registrationId, before);
102
+ }
103
+ }
104
+
105
+ await registration.save();
106
+ return true;
107
+ });
108
+ });
109
+ },
110
+
47
111
  async deactivate(registration: Registration, _group?: Group, _member?: Member) {
48
112
  if (registration.deactivatedAt !== null) {
49
113
  return;
@@ -0,0 +1,43 @@
1
+ import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLOneToOneRelationFilterCompiler, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
2
+ import { organizationFilterCompilers } from './organizations';
3
+
4
+ export const baseRegistrationFilterCompilers: SQLFilterDefinitions = {
5
+ ...baseSQLFilterCompilers,
6
+ id: createSQLColumnFilterCompiler('id'),
7
+ /**
8
+ * @deprecated
9
+ */
10
+ price: createSQLColumnFilterCompiler('price', { nullable: true }),
11
+ /**
12
+ * @deprecated
13
+ */
14
+ pricePaid: createSQLColumnFilterCompiler('pricePaid'),
15
+ canRegister: createSQLColumnFilterCompiler('canRegister'),
16
+ organizationId: createSQLColumnFilterCompiler('organizationId'),
17
+ groupId: createSQLColumnFilterCompiler('groupId'),
18
+ registeredAt: createSQLColumnFilterCompiler('registeredAt', { nullable: true }),
19
+ periodId: createSQLColumnFilterCompiler(SQL.column('registrations', 'periodId')),
20
+ deactivatedAt: createSQLColumnFilterCompiler(SQL.column('registrations', 'deactivatedAt'), { nullable: true }),
21
+ group: createSQLFilterNamespace({
22
+ ...baseSQLFilterCompilers,
23
+ id: createSQLColumnFilterCompiler('groupId'),
24
+ name: createSQLExpressionFilterCompiler(
25
+ SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name'),
26
+ { isJSONValue: true, type: SQLValueType.JSONString },
27
+ ),
28
+ status: createSQLExpressionFilterCompiler(
29
+ SQL.column('groups', 'status'),
30
+ { isJSONValue: true, type: SQLValueType.JSONString },
31
+ ),
32
+ defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId'), { nullable: true }),
33
+ }),
34
+ organization: createSQLOneToOneRelationFilterCompiler(
35
+ SQL.select()
36
+ .from(SQL.table('organizations'))
37
+ .where(
38
+ SQL.column('organizations', 'id'),
39
+ SQL.column('registrations', 'organizationId'),
40
+ ),
41
+ organizationFilterCompilers,
42
+ ),
43
+ };
@@ -0,0 +1,67 @@
1
+ import { SQL, createColumnFilter, SQLModernFilterDefinitions, SQLValueType, baseModernSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, SQLModernValueType, createWildcardColumnFilter, SQLJsonExtract } from '@stamhoofd/sql';
2
+
3
+ export const groupFilterCompilers: SQLModernFilterDefinitions = {
4
+ ...baseModernSQLFilterCompilers,
5
+ id: createColumnFilter({
6
+ expression: SQL.column('id'),
7
+ type: SQLModernValueType.String,
8
+ nullable: false,
9
+ }),
10
+ organizationId: createColumnFilter({
11
+ expression: SQL.column('organizationId'),
12
+ type: SQLModernValueType.String,
13
+ nullable: false,
14
+ }),
15
+ periodId: createColumnFilter({
16
+ expression: SQL.column('periodId'),
17
+ type: SQLModernValueType.String,
18
+ nullable: false,
19
+ }),
20
+ name: createColumnFilter({
21
+ expression: SQL.jsonValue(SQL.column('settings'), '$.value.name'),
22
+ type: SQLModernValueType.JSONString,
23
+ nullable: false,
24
+ }),
25
+ status: createColumnFilter({
26
+ expression: SQL.column('status'),
27
+ type: SQLModernValueType.String,
28
+ nullable: false,
29
+ }),
30
+ defaultAgeGroupId: createColumnFilter({
31
+ expression: SQL.column('defaultAgeGroupId'),
32
+ type: SQLModernValueType.String,
33
+ nullable: true,
34
+ }),
35
+ bundleDiscounts: createWildcardColumnFilter(
36
+ (key: string) => ({
37
+ expression: SQL.jsonValue(SQL.column('settings'), `$.value.prices[*].bundleDiscounts.${SQLJsonExtract.escapePathComponent(key)}`, true),
38
+ type: SQLModernValueType.JSONArray,
39
+ nullable: true,
40
+ }),
41
+ (key: string) => ({
42
+ ...baseModernSQLFilterCompilers,
43
+ name: createColumnFilter({
44
+ expression: SQL.jsonValue(SQL.column('settings'), `$.value.prices[*].bundleDiscounts.${SQLJsonExtract.escapePathComponent(key)}.name`, true),
45
+ type: SQLModernValueType.JSONArray,
46
+ nullable: true,
47
+ }),
48
+ }),
49
+ ),
50
+
51
+ /* name: createSQLExpressionFilterCompiler(
52
+ SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name'),
53
+ { isJSONValue: true, type: SQLValueType.JSONString },
54
+ ),
55
+ status: createSQLExpressionFilterCompiler(
56
+ SQL.column('groups', 'status'),
57
+ { isJSONValue: true, type: SQLValueType.JSONString },
58
+ ),
59
+ defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId'), { nullable: true }),
60
+
61
+ bundleDiscountIds: createSQLExpressionFilterCompiler(
62
+ SQL.jsonKeys(
63
+ SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.prices[0].bundleDiscounts'),
64
+ ),
65
+ { isJSONValue: true, isJSONObject: true },
66
+ ), */
67
+ };
@@ -1,37 +1,39 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { Member } from '@stamhoofd/models';
2
3
  import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLScalar, SQLValueType, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler } from '@stamhoofd/sql';
3
4
  import { AccessRight } from '@stamhoofd/structures';
4
5
  import { Formatter } from '@stamhoofd/utility';
5
6
  import { Context } from '../helpers/Context';
7
+ import { baseRegistrationFilterCompilers } from './base-registration-filter-compilers';
6
8
  import { organizationFilterCompilers } from './organizations';
7
- import { registrationFilterCompilers } from './registrations';
9
+
10
+ const membersTable = SQL.table(Member.table);
8
11
 
9
12
  /**
10
13
  * Defines how to filter members in the database from StamhoofdFilter objects
11
14
  */
12
15
  export const memberFilterCompilers: SQLFilterDefinitions = {
13
16
  ...baseSQLFilterCompilers,
14
- 'id': createSQLColumnFilterCompiler('id'),
15
- 'memberNumber': createSQLColumnFilterCompiler('memberNumber'),
16
- 'firstName': createSQLColumnFilterCompiler('firstName'),
17
- 'lastName': createSQLColumnFilterCompiler('lastName'),
17
+ 'id': createSQLColumnFilterCompiler(SQL.column(membersTable, 'id')),
18
+ 'memberNumber': createSQLColumnFilterCompiler(SQL.column(membersTable, 'memberNumber')),
19
+ 'firstName': createSQLColumnFilterCompiler(SQL.column(membersTable, 'firstName')),
20
+ 'lastName': createSQLColumnFilterCompiler(SQL.column(membersTable, 'lastName')),
18
21
  'name': createSQLExpressionFilterCompiler(
19
22
  new SQLConcat(
20
- SQL.column('firstName'),
23
+ SQL.column(membersTable, 'firstName'),
21
24
  new SQLScalar(' '),
22
- SQL.column('lastName'),
25
+ SQL.column(membersTable, 'lastName'),
23
26
  ),
24
27
  ),
25
28
  'age': createSQLExpressionFilterCompiler(
26
- new SQLAge(SQL.column('birthDay')),
29
+ new SQLAge(SQL.column(membersTable, 'birthDay')),
27
30
  { nullable: true },
28
31
  ),
29
32
  'gender': createSQLExpressionFilterCompiler(
30
- SQL.jsonValue(SQL.column('details'), '$.value.gender'),
33
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.gender'),
31
34
  { isJSONValue: true, type: SQLValueType.JSONString },
32
35
  ),
33
-
34
- 'birthDay': createSQLColumnFilterCompiler('birthDay', {
36
+ 'birthDay': createSQLColumnFilterCompiler(SQL.column(membersTable, 'birthDay'), {
35
37
  normalizeValue: (d) => {
36
38
  if (typeof d === 'number') {
37
39
  const date = new Date(d);
@@ -40,13 +42,11 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
40
42
  return d;
41
43
  },
42
44
  }),
43
-
44
45
  'organizationName': createSQLExpressionFilterCompiler(
45
46
  SQL.column('organizations', 'name'),
46
47
  ),
47
-
48
48
  'details.requiresFinancialSupport': createSQLExpressionFilterCompiler(
49
- SQL.jsonValue(SQL.column('details'), '$.value.requiresFinancialSupport.value'),
49
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.requiresFinancialSupport.value'),
50
50
  { isJSONValue: true, type: SQLValueType.JSONBoolean, checkPermission: async () => {
51
51
  const organization = Context.organization;
52
52
  if (!organization) {
@@ -65,97 +65,86 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
65
65
  }
66
66
  } },
67
67
  ),
68
-
69
68
  'email': createSQLExpressionFilterCompiler(
70
- SQL.jsonValue(SQL.column('details'), '$.value.email'),
69
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.email'),
71
70
  { isJSONValue: true, type: SQLValueType.JSONString },
72
71
  ),
73
-
74
72
  'parentEmail': createSQLExpressionFilterCompiler(
75
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].email'),
73
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].email'),
76
74
  { isJSONValue: true, isJSONObject: true, type: SQLValueType.JSONString },
77
75
  ),
78
-
79
76
  'details.parents[0]': createSQLFilterNamespace({
80
77
  name: createSQLExpressionFilterCompiler(
81
78
  new SQLConcat(
82
- SQL.jsonUnquotedValue(SQL.column('details'), '$.value.parents[0].firstName'),
79
+ SQL.jsonUnquotedValue(SQL.column(membersTable, 'details'), '$.value.parents[0].firstName'),
83
80
  new SQLScalar(' '),
84
- SQL.jsonUnquotedValue(SQL.column('details'), '$.value.parents[0].lastName'),
81
+ SQL.jsonUnquotedValue(SQL.column(membersTable, 'details'), '$.value.parents[0].lastName'),
85
82
  ),
86
83
  { isJSONValue: true, isJSONObject: false, type: SQLValueType.JSONString },
87
84
  ),
88
85
  }),
89
-
90
86
  'details.parents[1]': createSQLFilterNamespace({
91
87
  name: createSQLExpressionFilterCompiler(
92
88
  new SQLConcat(
93
- SQL.jsonUnquotedValue(SQL.column('details'), '$.value.parents[1].firstName'),
89
+ SQL.jsonUnquotedValue(SQL.column(membersTable, 'details'), '$.value.parents[1].firstName'),
94
90
  new SQLScalar(' '),
95
- SQL.jsonUnquotedValue(SQL.column('details'), '$.value.parents[1].lastName'),
91
+ SQL.jsonUnquotedValue(SQL.column(membersTable, 'details'), '$.value.parents[1].lastName'),
96
92
  ),
97
93
  { isJSONValue: true, isJSONObject: false, type: SQLValueType.JSONString },
98
94
  ),
99
95
  }),
100
-
101
96
  'unverifiedEmail': createSQLExpressionFilterCompiler(
102
- SQL.jsonValue(SQL.column('details'), '$.value.unverifiedEmails'),
97
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.unverifiedEmails'),
103
98
  { isJSONValue: true, isJSONObject: true, type: SQLValueType.JSONString },
104
99
  ),
105
-
106
100
  'phone': createSQLExpressionFilterCompiler(
107
- SQL.jsonValue(SQL.column('details'), '$.value.phone'),
101
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.phone'),
108
102
  { isJSONValue: true },
109
103
  ),
110
-
111
104
  'details.address': createSQLFilterNamespace({
112
105
  city: createSQLExpressionFilterCompiler(
113
- SQL.jsonValue(SQL.column('details'), '$.value.address.city'),
106
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.address.city'),
114
107
  { isJSONValue: true, type: SQLValueType.JSONString },
115
108
  ),
116
109
  postalCode: createSQLExpressionFilterCompiler(
117
- SQL.jsonValue(SQL.column('details'), '$.value.address.postalCode'),
110
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.address.postalCode'),
118
111
  { isJSONValue: true, type: SQLValueType.JSONString },
119
112
  ),
120
113
  street: createSQLExpressionFilterCompiler(
121
- SQL.jsonValue(SQL.column('details'), '$.value.address.street'),
114
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.address.street'),
122
115
  { isJSONValue: true, type: SQLValueType.JSONString },
123
116
  ),
124
117
  number: createSQLExpressionFilterCompiler(
125
- SQL.jsonValue(SQL.column('details'), '$.value.address.number'),
118
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.address.number'),
126
119
  { isJSONValue: true, type: SQLValueType.JSONString },
127
120
  ),
128
121
  }),
129
-
130
122
  'details.parents[*].address': createSQLFilterNamespace({
131
123
  city: createSQLExpressionFilterCompiler(
132
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.city'),
124
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].address.city'),
133
125
  { isJSONValue: true, isJSONObject: true },
134
126
  ),
135
127
  postalCode: createSQLExpressionFilterCompiler(
136
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.postalCode'),
128
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].address.postalCode'),
137
129
  { isJSONValue: true, isJSONObject: true },
138
130
  ),
139
131
  street: createSQLExpressionFilterCompiler(
140
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.street'),
132
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].address.street'),
141
133
  { isJSONValue: true, isJSONObject: true },
142
134
  ),
143
135
  number: createSQLExpressionFilterCompiler(
144
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.number'),
136
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].address.number'),
145
137
  { isJSONValue: true, isJSONObject: true },
146
138
  ),
147
139
  }),
148
-
149
140
  'parentPhone': createSQLExpressionFilterCompiler(
150
- SQL.jsonValue(SQL.column('details'), '$.value.parents[*].phone'),
141
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.parents[*].phone'),
151
142
  { isJSONValue: true, isJSONObject: true },
152
143
  ),
153
-
154
144
  'unverifiedPhone': createSQLExpressionFilterCompiler(
155
- SQL.jsonValue(SQL.column('details'), '$.value.unverifiedPhones'),
145
+ SQL.jsonValue(SQL.column(membersTable, 'details'), '$.value.unverifiedPhones'),
156
146
  { isJSONValue: true, isJSONObject: true },
157
147
  ),
158
-
159
148
  'registrations': createSQLRelationFilterCompiler(
160
149
  SQL.select()
161
150
  .from(
@@ -168,14 +157,6 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
168
157
  SQL.column('registrations', 'groupId'),
169
158
  ),
170
159
  )
171
- .join(
172
- SQL.join(
173
- SQL.table('organizations'),
174
- ).where(
175
- SQL.column('organizations', 'id'),
176
- SQL.column('registrations', 'organizationId'),
177
- ),
178
- )
179
160
  .where(
180
161
  SQL.column('memberId'),
181
162
  SQL.column('members', 'id'),
@@ -189,12 +170,8 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
189
170
  SQL.column('groups', 'deletedAt'),
190
171
  null,
191
172
  ),
192
- {
193
- ...registrationFilterCompilers,
194
- organization: createSQLFilterNamespace(organizationFilterCompilers),
195
- },
173
+ baseRegistrationFilterCompilers,
196
174
  ),
197
-
198
175
  'responsibilities': createSQLRelationFilterCompiler(
199
176
  SQL.select()
200
177
  .from(
@@ -236,7 +213,6 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
236
213
  organization: createSQLFilterNamespace(organizationFilterCompilers),
237
214
  },
238
215
  ),
239
-
240
216
  'platformMemberships': createSQLRelationFilterCompiler(
241
217
  SQL.select()
242
218
  .from(
@@ -267,7 +243,6 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
267
243
  generated: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'generated')),
268
244
  },
269
245
  ),
270
-
271
246
  'organizations': createSQLRelationFilterCompiler(
272
247
  SQL.select()
273
248
  .from(
@@ -0,0 +1,8 @@
1
+ import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler } from '@stamhoofd/sql';
2
+
3
+ export const organizationRegistrationPeriodFilterCompilers: SQLFilterDefinitions = {
4
+ ...baseSQLFilterCompilers,
5
+ id: createSQLColumnFilterCompiler('id'),
6
+ organizationId: createSQLColumnFilterCompiler('organizationId'),
7
+ periodId: createSQLColumnFilterCompiler('periodId'),
8
+ };
@@ -0,0 +1,8 @@
1
+ import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler } from '@stamhoofd/sql';
2
+
3
+ export const registrationPeriodFilterCompilers: SQLFilterDefinitions = {
4
+ ...baseSQLFilterCompilers,
5
+ id: createSQLColumnFilterCompiler('id'),
6
+ startDate: createSQLColumnFilterCompiler('startDate'),
7
+ endDate: createSQLColumnFilterCompiler('endDate'),
8
+ };
@@ -1,26 +1,15 @@
1
- import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQL, createSQLFilterNamespace, createSQLExpressionFilterCompiler, SQLValueType } from '@stamhoofd/sql';
1
+ import { Member, Registration } from '@stamhoofd/models';
2
+ import { baseSQLFilterCompilers, createSQLJoinedRelationFilterCompiler, SQL, SQLFilterDefinitions } from '@stamhoofd/sql';
3
+ import { baseRegistrationFilterCompilers } from './base-registration-filter-compilers';
4
+ import { memberFilterCompilers } from './members';
5
+
6
+ export const memberJoin = SQL.join(Member.table).where(SQL.column(Member.table, 'id'), SQL.column(Registration.table, 'memberId'));
2
7
 
3
8
  export const registrationFilterCompilers: SQLFilterDefinitions = {
4
9
  ...baseSQLFilterCompilers,
5
- price: createSQLColumnFilterCompiler('price', { nullable: true }),
6
- pricePaid: createSQLColumnFilterCompiler('pricePaid'),
7
- canRegister: createSQLColumnFilterCompiler('canRegister'),
8
- organizationId: createSQLColumnFilterCompiler('organizationId'),
9
- groupId: createSQLColumnFilterCompiler('groupId'),
10
- registeredAt: createSQLColumnFilterCompiler('registeredAt', { nullable: true }),
11
- periodId: createSQLColumnFilterCompiler(SQL.column('registrations', 'periodId')),
12
-
13
- group: createSQLFilterNamespace({
14
- ...baseSQLFilterCompilers,
15
- id: createSQLColumnFilterCompiler('groupId'),
16
- name: createSQLExpressionFilterCompiler(
17
- SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name'),
18
- { isJSONValue: true, type: SQLValueType.JSONString },
19
- ),
20
- status: createSQLExpressionFilterCompiler(
21
- SQL.column('groups', 'status'),
22
- { isJSONValue: true, type: SQLValueType.JSONString },
23
- ),
24
- defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId'), { nullable: true }),
25
- }),
10
+ ...baseRegistrationFilterCompilers,
11
+ member: createSQLJoinedRelationFilterCompiler(
12
+ memberJoin,
13
+ memberFilterCompilers,
14
+ ),
26
15
  };
@@ -0,0 +1,24 @@
1
+ import { Group } from '@stamhoofd/models';
2
+ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+
4
+ export const groupSorters: SQLSortDefinitions<Group> = {
5
+ // WARNING! TEST NEW SORTERS THOROUGHLY!
6
+ // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
7
+ // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
8
+ // You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
9
+ // Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
10
+ // And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
11
+ // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
12
+
13
+ id: {
14
+ getValue(a) {
15
+ return a.id;
16
+ },
17
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
18
+ return new SQLOrderBy({
19
+ column: SQL.column('id'),
20
+ direction,
21
+ });
22
+ },
23
+ },
24
+ };
@@ -0,0 +1,24 @@
1
+ import { OrganizationRegistrationPeriod } from '@stamhoofd/models';
2
+ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+
4
+ export const organizationRegistrationPeriodSorters: SQLSortDefinitions<OrganizationRegistrationPeriod> = {
5
+ // WARNING! TEST NEW SORTERS THOROUGHLY!
6
+ // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
7
+ // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
8
+ // You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
9
+ // Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
10
+ // And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
11
+ // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
12
+
13
+ id: {
14
+ getValue(a) {
15
+ return a.id;
16
+ },
17
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
18
+ return new SQLOrderBy({
19
+ column: SQL.column('id'),
20
+ direction,
21
+ });
22
+ },
23
+ },
24
+ };
@@ -0,0 +1,47 @@
1
+ import { RegistrationPeriod } from '@stamhoofd/models';
2
+ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+
5
+ export const registrationPeriodSorters: SQLSortDefinitions<RegistrationPeriod> = {
6
+ // WARNING! TEST NEW SORTERS THOROUGHLY!
7
+ // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
8
+ // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
9
+ // You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
10
+ // Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
11
+ // And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
12
+ // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
13
+
14
+ id: {
15
+ getValue(a) {
16
+ return a.id;
17
+ },
18
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
19
+ return new SQLOrderBy({
20
+ column: SQL.column('id'),
21
+ direction,
22
+ });
23
+ },
24
+ },
25
+ startDate: {
26
+ getValue(a) {
27
+ return Formatter.dateTimeIso(a.startDate, 'UTC');
28
+ },
29
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
30
+ return new SQLOrderBy({
31
+ column: SQL.column('startDate'),
32
+ direction,
33
+ });
34
+ },
35
+ },
36
+ endDate: {
37
+ getValue(a) {
38
+ return Formatter.dateTimeIso(a.endDate, 'UTC');
39
+ },
40
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
41
+ return new SQLOrderBy({
42
+ column: SQL.column('endDate'),
43
+ direction,
44
+ });
45
+ },
46
+ },
47
+ };