@stamhoofd/backend 2.108.0 → 2.109.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 (38) hide show
  1. package/index.ts +2 -2
  2. package/package.json +20 -20
  3. package/src/crons/disable-auto-update-documents.test.ts +164 -0
  4. package/src/crons/disable-auto-update-documents.ts +82 -0
  5. package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +8 -7
  7. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
  8. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
  9. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
  10. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
  11. package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
  12. package/src/excel-loaders/members.ts +61 -7
  13. package/src/excel-loaders/registrations.ts +123 -2
  14. package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
  15. package/src/helpers/SQLTranslatedString.ts +14 -0
  16. package/src/helpers/TagHelper.test.ts +9 -9
  17. package/src/helpers/outstandingBalanceJoin.ts +49 -0
  18. package/src/seeds/1765896674-document-update-year.test.ts +179 -0
  19. package/src/seeds/1765896674-document-update-year.ts +75 -0
  20. package/src/seeds/1766150402-document-published-at.test.ts +46 -0
  21. package/src/seeds/1766150402-document-published-at.ts +20 -0
  22. package/src/services/PaymentService.ts +14 -32
  23. package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
  24. package/src/sql-filters/document-templates.ts +45 -0
  25. package/src/sql-filters/documents.ts +1 -1
  26. package/src/sql-filters/events.ts +6 -6
  27. package/src/sql-filters/groups.ts +7 -6
  28. package/src/sql-filters/members.ts +31 -26
  29. package/src/sql-filters/orders.ts +16 -16
  30. package/src/sql-filters/organizations.ts +11 -11
  31. package/src/sql-filters/payments.ts +10 -10
  32. package/src/sql-filters/registrations.ts +14 -6
  33. package/src/sql-sorters/document-templates.ts +79 -0
  34. package/src/sql-sorters/documents.ts +1 -1
  35. package/src/sql-sorters/members.ts +22 -0
  36. package/src/sql-sorters/orders.ts +5 -5
  37. package/src/sql-sorters/organizations.ts +3 -3
  38. package/src/sql-sorters/registrations.ts +186 -15
@@ -71,7 +71,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
71
71
  },
72
72
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
73
73
  return new SQLOrderBy({
74
- column: SQL.jsonValue(SQL.column('data'), '$.value.paymentMethod'),
74
+ column: SQL.jsonExtract(SQL.column('data'), '$.value.paymentMethod'),
75
75
  direction,
76
76
  });
77
77
  },
@@ -82,7 +82,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
82
82
  },
83
83
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
84
84
  return new SQLOrderBy({
85
- column: SQL.jsonValue(SQL.column('data'), '$.value.checkoutMethod.type'),
85
+ column: SQL.jsonExtract(SQL.column('data'), '$.value.checkoutMethod.type'),
86
86
  direction,
87
87
  });
88
88
  },
@@ -93,7 +93,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
93
93
  },
94
94
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
95
95
  return new SQLOrderBy({
96
- column: SQL.jsonValue(SQL.column('data'), '$.value.timeSlot.date'),
96
+ column: SQL.jsonExtract(SQL.column('data'), '$.value.timeSlot.date'),
97
97
  direction,
98
98
  });
99
99
  },
@@ -104,7 +104,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
104
104
  },
105
105
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
106
106
  return new SQLOrderBy({
107
- column: SQL.jsonValue(SQL.column('data'), '$.value.timeSlot.endTime'),
107
+ column: SQL.jsonExtract(SQL.column('data'), '$.value.timeSlot.endTime'),
108
108
  direction,
109
109
  });
110
110
  },
@@ -126,7 +126,7 @@ export const orderSorters: SQLSortDefinitions<Order> = {
126
126
  },
127
127
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
128
128
  return new SQLOrderBy({
129
- column: SQL.jsonValue(SQL.column('data'), '$.value.totalPrice'),
129
+ column: SQL.jsonExtract(SQL.column('data'), '$.value.totalPrice'),
130
130
  direction,
131
131
  });
132
132
  },
@@ -60,7 +60,7 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
60
60
  },
61
61
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
62
62
  return new SQLOrderBy({
63
- column: SQL.jsonValue(SQL.column('meta'), '$.value.type'),
63
+ column: SQL.jsonExtract(SQL.column('meta'), '$.value.type'),
64
64
  direction,
65
65
  });
66
66
  },
@@ -71,7 +71,7 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
71
71
  },
72
72
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
73
73
  return new SQLOrderBy({
74
- column: SQL.jsonValue(SQL.column('address'), '$.value.city'),
74
+ column: SQL.jsonExtract(SQL.column('address'), '$.value.city'),
75
75
  direction,
76
76
  });
77
77
  },
@@ -82,7 +82,7 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
82
82
  },
83
83
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
84
84
  return new SQLOrderBy({
85
- column: SQL.jsonValue(SQL.column('address'), '$.value.country'),
85
+ column: SQL.jsonExtract(SQL.column('address'), '$.value.country'),
86
86
  direction,
87
87
  });
88
88
  },
@@ -1,10 +1,31 @@
1
- import { Member } from '@stamhoofd/models';
2
- import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
- import { RegistrationWithMemberBlob } from '@stamhoofd/structures';
1
+ import { Group, Member, Organization } from '@stamhoofd/models';
2
+ import { SQL, SQLIfNull, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+ import { MemberWithRegistrationsBlob, Organization as OrganizationStruct, RegistrationWithMemberBlob } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
- import { memberJoin } from '../sql-filters/registrations';
5
+ import { memberCachedBalanceForOrganizationJoin, registrationCachedBalanceJoin } from '../helpers/outstandingBalanceJoin.js';
6
+ import { SQLTranslatedString } from '../helpers/SQLTranslatedString.js';
7
+ import { groupJoin, memberJoin, organizationJoin } from '../sql-filters/registrations.js';
6
8
 
7
- export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob> = {
9
+ export class RegistrationSortData {
10
+ readonly registration: RegistrationWithMemberBlob;
11
+ private organizations: OrganizationStruct[];
12
+
13
+ constructor({ registration, organizations }: { registration: RegistrationWithMemberBlob; organizations: OrganizationStruct[] }) {
14
+ this.registration = registration;
15
+ this.organizations = organizations;
16
+ }
17
+
18
+ get organization() {
19
+ const organization = this.organizations.find(o => o.id === this.registration.organizationId);
20
+ if (!organization) {
21
+ throw new Error('Organization not found for registration');
22
+ }
23
+
24
+ return organization;
25
+ }
26
+ }
27
+
28
+ export const registrationSorters: SQLSortDefinitions<RegistrationSortData> = {
8
29
  // WARNING! TEST NEW SORTERS THOROUGHLY!
9
30
  // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
10
31
  // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
@@ -14,8 +35,8 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
14
35
  // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
15
36
 
16
37
  'id': {
17
- getValue(a) {
18
- return a.id;
38
+ getValue({ registration }) {
39
+ return registration.id;
19
40
  },
20
41
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
21
42
  return new SQLOrderBy({
@@ -25,8 +46,8 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
25
46
  },
26
47
  },
27
48
  'registeredAt': {
28
- getValue(a) {
29
- return a.registeredAt;
49
+ getValue({ registration }) {
50
+ return registration.registeredAt;
30
51
  },
31
52
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
32
53
  return new SQLOrderBy({
@@ -35,9 +56,95 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
35
56
  });
36
57
  },
37
58
  },
59
+ 'groupPrice': {
60
+ getValue: ({ registration }) => registration.groupPrice.name.toString(),
61
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
62
+ return new SQLOrderBy({
63
+ column: new SQLTranslatedString(SQL.column('groupPrice'), '$.value.name'),
64
+ direction,
65
+ });
66
+ },
67
+ },
68
+ 'trialUntil': {
69
+ getValue({ registration }) {
70
+ return registration.trialUntil;
71
+ },
72
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
73
+ return new SQLOrderBy({
74
+ column: SQL.column('trialUntil'),
75
+ direction,
76
+ });
77
+ },
78
+ },
79
+ 'startDate': {
80
+ getValue({ registration }) {
81
+ return registration.startDate;
82
+ },
83
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
84
+ return new SQLOrderBy({
85
+ column: SQL.column('startDate'),
86
+ direction,
87
+ });
88
+ },
89
+ },
90
+ 'endDate': {
91
+ getValue({ registration }) {
92
+ return registration.endDate;
93
+ },
94
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
95
+ return new SQLOrderBy({
96
+ column: SQL.column('endDate'),
97
+ direction,
98
+ });
99
+ },
100
+ },
101
+ 'memberCachedBalance.amountOpen': {
102
+ getValue({ registration }) {
103
+ return registration.member.balances.reduce((sum, r) => sum + (r.amountOpen), 0);
104
+ },
105
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
106
+ return new SQLOrderBy({
107
+ column: new SQLIfNull(SQL.column('memberCachedBalance', 'amountOpen'), 0),
108
+ direction,
109
+ });
110
+ },
111
+ join: memberCachedBalanceForOrganizationJoin,
112
+ select: [SQL.column('memberCachedBalance', 'amountOpen')],
113
+ },
114
+ 'registrationCachedBalance.toPay': {
115
+ getValue({ registration }) {
116
+ return registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPending), 0);
117
+ },
118
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
119
+ return new SQLOrderBy({
120
+ column: new SQLIfNull(SQL.column('registrationCachedBalance', 'toPay'), 0),
121
+ direction,
122
+ });
123
+ },
124
+ join: registrationCachedBalanceJoin,
125
+ select: [SQL.column('registrationCachedBalance', 'toPay')],
126
+ },
127
+ 'registrationCachedBalance.price': {
128
+ getValue({ registration }) {
129
+ return registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPaid + r.amountPending), 0);
130
+ },
131
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
132
+ return new SQLOrderBy({
133
+ column: new SQLIfNull(SQL.column('registrationCachedBalance', 'price'), 0),
134
+ direction,
135
+ });
136
+ },
137
+ join: registrationCachedBalanceJoin,
138
+ select: [SQL.column('registrationCachedBalance', 'price')],
139
+ },
140
+ 'member.memberNumber': createMemberColumnSorter({
141
+ columnName: 'memberNumber',
142
+ getValue: member => member.details.memberNumber ?? '',
143
+
144
+ }),
38
145
  'member.firstName': {
39
- getValue(a) {
40
- return a.member.firstName;
146
+ getValue({ registration }) {
147
+ return registration.member.firstName;
41
148
  },
42
149
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
43
150
  return new SQLOrderBy({
@@ -49,8 +156,8 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
49
156
  select: [SQL.column(Member.table, 'firstName')],
50
157
  },
51
158
  'member.lastName': {
52
- getValue(a) {
53
- return a.member.lastName;
159
+ getValue({ registration }) {
160
+ return registration.member.lastName;
54
161
  },
55
162
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
56
163
  return new SQLOrderBy({
@@ -62,8 +169,8 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
62
169
  select: [SQL.column(Member.table, 'lastName')],
63
170
  },
64
171
  'member.birthDay': {
65
- getValue(a) {
66
- return a.member.details.birthDay === null ? null : Formatter.dateIso(a.member.details.birthDay);
172
+ getValue({ registration }) {
173
+ return registration.member.details.birthDay === null ? null : Formatter.dateIso(registration.member.details.birthDay);
67
174
  },
68
175
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
69
176
  return new SQLOrderBy({
@@ -74,4 +181,68 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
74
181
  join: memberJoin,
75
182
  select: [SQL.column(Member.table, 'birthDay')],
76
183
  },
184
+ 'member.createdAt': {
185
+ getValue({ registration }) {
186
+ return registration.member.createdAt;
187
+ },
188
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
189
+ return new SQLOrderBy({
190
+ column: SQL.column(Member.table, 'createdAt'),
191
+ direction,
192
+ });
193
+ },
194
+ join: memberJoin,
195
+ select: [SQL.column(Member.table, 'createdAt')],
196
+ },
197
+ 'organization.name': {
198
+ getValue: ({ organization }) => organization.name,
199
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
200
+ return new SQLOrderBy({
201
+ column: SQL.column(Organization.table, 'name'),
202
+ direction,
203
+ });
204
+ },
205
+ join: organizationJoin,
206
+ select: [SQL.column(Organization.table, 'name')],
207
+ },
208
+ 'organization.uri': {
209
+ getValue: ({ organization }) => organization.uri,
210
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
211
+ return new SQLOrderBy({
212
+ column: SQL.column(Organization.table, 'uri'),
213
+ direction,
214
+ });
215
+ },
216
+ join: organizationJoin,
217
+ select: [SQL.column(Organization.table, 'uri')],
218
+ },
219
+ 'group.name': {
220
+ getValue: ({ registration }) => registration.group.settings.name.toString(),
221
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
222
+ return new SQLOrderBy({
223
+ column: new SQLTranslatedString(SQL.column(Group.table, 'settings'), '$.value.name'),
224
+ direction,
225
+ });
226
+ },
227
+ join: groupJoin,
228
+ },
77
229
  };
230
+
231
+ /**
232
+ * Helper function for simple sort on member column
233
+ * @param param0
234
+ * @returns
235
+ */
236
+ function createMemberColumnSorter<T>({ columnName, getValue }: { columnName: string; getValue: (member: MemberWithRegistrationsBlob) => T }) {
237
+ return {
238
+ getValue: ({ registration }: RegistrationSortData) => getValue(registration.member),
239
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
240
+ return new SQLOrderBy({
241
+ column: SQL.column(Member.table, columnName),
242
+ direction,
243
+ });
244
+ },
245
+ join: memberJoin,
246
+ select: [SQL.column(Member.table, columnName)],
247
+ };
248
+ }