@stamhoofd/models 2.55.2 → 2.57.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 (46) hide show
  1. package/dist/src/helpers/Handlebars.d.ts +1 -1
  2. package/dist/src/helpers/Handlebars.d.ts.map +1 -1
  3. package/dist/src/helpers/Handlebars.js +14 -1
  4. package/dist/src/helpers/Handlebars.js.map +1 -1
  5. package/dist/src/migrations/1732285080-audit-logs.sql +18 -0
  6. package/dist/src/models/AuditLog.d.ts +24 -0
  7. package/dist/src/models/AuditLog.d.ts.map +1 -0
  8. package/dist/src/models/AuditLog.js +72 -0
  9. package/dist/src/models/AuditLog.js.map +1 -0
  10. package/dist/src/models/BalanceItem.d.ts +1 -6
  11. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  12. package/dist/src/models/BalanceItem.js +0 -91
  13. package/dist/src/models/BalanceItem.js.map +1 -1
  14. package/dist/src/models/BalanceItemPayment.d.ts +1 -10
  15. package/dist/src/models/BalanceItemPayment.d.ts.map +1 -1
  16. package/dist/src/models/BalanceItemPayment.js +0 -31
  17. package/dist/src/models/BalanceItemPayment.js.map +1 -1
  18. package/dist/src/models/Document.d.ts +1 -7
  19. package/dist/src/models/Document.d.ts.map +1 -1
  20. package/dist/src/models/Document.js +19 -8
  21. package/dist/src/models/Document.js.map +1 -1
  22. package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
  23. package/dist/src/models/DocumentTemplate.js +87 -17
  24. package/dist/src/models/DocumentTemplate.js.map +1 -1
  25. package/dist/src/models/Member.js +1 -1
  26. package/dist/src/models/Member.js.map +1 -1
  27. package/dist/src/models/Registration.d.ts +0 -4
  28. package/dist/src/models/Registration.d.ts.map +1 -1
  29. package/dist/src/models/Registration.js +0 -37
  30. package/dist/src/models/Registration.js.map +1 -1
  31. package/dist/src/models/index.d.ts +1 -0
  32. package/dist/src/models/index.d.ts.map +1 -1
  33. package/dist/src/models/index.js +1 -0
  34. package/dist/src/models/index.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/src/helpers/Handlebars.ts +16 -1
  38. package/src/migrations/1732285080-audit-logs.sql +18 -0
  39. package/src/models/AuditLog.ts +58 -0
  40. package/src/models/BalanceItem.ts +0 -105
  41. package/src/models/BalanceItemPayment.ts +1 -40
  42. package/src/models/Document.ts +23 -10
  43. package/src/models/DocumentTemplate.ts +96 -19
  44. package/src/models/Member.ts +1 -1
  45. package/src/models/Registration.ts +0 -46
  46. package/src/models/index.ts +1 -0
@@ -3,6 +3,7 @@ import { Image } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import Handlebars from 'handlebars';
5
5
  import { Interval } from 'luxon';
6
+ import bwipjs from '@bwip-js/node';
6
7
 
7
8
  Handlebars.registerHelper('eq', (a, b) => a == b);
8
9
  Handlebars.registerHelper('neq', (a, b) => a !== b);
@@ -192,8 +193,22 @@ Handlebars.registerHelper('src-height', (a, options) => {
192
193
  }
193
194
  });
194
195
 
196
+ Handlebars.registerHelper('datamatrix', (a) => {
197
+ if (typeof a !== 'string') {
198
+ return '';
199
+ }
200
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
201
+ const svgCode = bwipjs.toSVG({
202
+ bcid: 'datamatrix',
203
+ text: a,
204
+ } as any);
205
+
206
+ // Base64 encode the string
207
+ return 'data:image/svg+xml;base64,' + Buffer.from(svgCode).toString('base64');
208
+ });
209
+
195
210
  // Rander handlebars template
196
- export function render(htmlTemplate: string, context: any): string | null {
211
+ export async function render(htmlTemplate: string, context: any): Promise<string | null> {
197
212
  try {
198
213
  const template = Handlebars.compile(htmlTemplate);
199
214
  const renderedHtml = template(context);
@@ -0,0 +1,18 @@
1
+ CREATE TABLE `audit_logs` (
2
+ `id` varchar(36) NOT NULL DEFAULT '',
3
+ `type` varchar(36) NOT NULL,
4
+ `organizationId` varchar(36) DEFAULT NULL,
5
+ `userId` varchar(36) DEFAULT NULL,
6
+ `objectId` varchar(36) DEFAULT NULL,
7
+ `description` text,
8
+ `replacements` json NOT NULL,
9
+ `patchList` json NOT NULL,
10
+ `createdAt` datetime NOT NULL,
11
+ PRIMARY KEY (`id`),
12
+ KEY `createdAt` (`createdAt` DESC) USING BTREE,
13
+ KEY `objectId` (`objectId`,`createdAt` DESC) USING BTREE,
14
+ KEY `organizationId` (`organizationId`),
15
+ KEY `userId` (`userId`),
16
+ CONSTRAINT `audit_logs_ibfk_1` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
17
+ CONSTRAINT `audit_logs_ibfk_2` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
18
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,58 @@
1
+ import { column, Model } from '@simonbackx/simple-database';
2
+ import { ArrayDecoder, Decoder, MapDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import { AuditLogPatchItem, AuditLogReplacement, AuditLogType } from '@stamhoofd/structures';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ export class AuditLog extends Model {
7
+ static table = 'audit_logs';
8
+
9
+ // Columns
10
+ @column({
11
+ primary: true, type: 'string', beforeSave(value) {
12
+ return value ?? uuidv4();
13
+ },
14
+ })
15
+ id!: string;
16
+
17
+ @column({ type: 'string' })
18
+ type: AuditLogType = AuditLogType.Unknown;
19
+
20
+ /**
21
+ * Set to make the log visible for this specific organization - otherwise it is private for the platform
22
+ */
23
+ @column({ type: 'string', nullable: true })
24
+ organizationId: string | null;
25
+
26
+ /**
27
+ * User who performed the action
28
+ */
29
+ @column({ type: 'string', nullable: true })
30
+ userId: string | null = null;
31
+
32
+ /**
33
+ * Main involved object ID - e.g. the member id
34
+ */
35
+ @column({ type: 'string', nullable: true })
36
+ objectId: string | null = null;
37
+
38
+ @column({ type: 'string' })
39
+ description: string = '';
40
+
41
+ @column({ type: 'json', decoder: new MapDecoder(StringDecoder, AuditLogReplacement as Decoder<AuditLogReplacement>) })
42
+ replacements: Map<string, AuditLogReplacement> = new Map();
43
+
44
+ @column({ type: 'json', decoder: new ArrayDecoder(AuditLogPatchItem as Decoder<AuditLogPatchItem>) })
45
+ patchList: AuditLogPatchItem[] = [];
46
+
47
+ @column({
48
+ type: 'datetime', beforeSave(old?: any) {
49
+ if (old !== undefined) {
50
+ return old;
51
+ }
52
+ const date = new Date();
53
+ date.setMilliseconds(0);
54
+ return date;
55
+ },
56
+ })
57
+ createdAt: Date;
58
+ }
@@ -123,111 +123,6 @@ export class BalanceItem extends Model {
123
123
  return this.price - this.pricePaid - this.pricePending;
124
124
  }
125
125
 
126
- async markUpdated(payment: Payment, organization: Organization) {
127
- // For orders: mark order as changed (so they are refetched in front ends)
128
- if (this.orderId) {
129
- const { Order } = await import('./Order');
130
- const order = await Order.getByID(this.orderId);
131
- if (order) {
132
- await order.paymentChanged(payment, organization);
133
- }
134
- }
135
- }
136
-
137
- async markPaid(payment: Payment | null, organization: Organization) {
138
- if (this.status === BalanceItemStatus.Hidden) {
139
- await BalanceItem.reactivateItems([this]);
140
- }
141
-
142
- // status and pricePaid changes are handled inside balanceitempayment
143
- if (this.dependingBalanceItemId) {
144
- const depending = await BalanceItem.getByID(this.dependingBalanceItemId);
145
- if (depending && depending.status === BalanceItemStatus.Hidden) {
146
- await BalanceItem.reactivateItems([depending]);
147
- }
148
- }
149
-
150
- // If registration
151
- if (this.registrationId) {
152
- const { Registration } = await import('./Registration');
153
- const registration = await Registration.getByID(this.registrationId);
154
-
155
- if (registration) {
156
- // 1. Mark registration as being valid
157
- if (registration.registeredAt === null || registration.deactivatedAt) {
158
- await registration.markValid();
159
-
160
- const { Group } = await import('./Group');
161
-
162
- // Update group occupancy
163
- // TODO: maybe we should schedule this, to prevent doing many updates at once
164
- const group = await Group.getByID(registration.groupId);
165
- if (group) {
166
- await group.updateOccupancy();
167
- await group.save();
168
- }
169
- }
170
- }
171
- }
172
-
173
- // If order
174
- if (this.orderId) {
175
- const { Order } = await import('./Order');
176
- const order = await Order.getByID(this.orderId);
177
- if (order) {
178
- await order.markPaid(payment, organization);
179
-
180
- // Save number in balacance description
181
- if (order.number !== null) {
182
- const webshop = await Webshop.getByID(order.webshopId);
183
-
184
- if (webshop) {
185
- this.description = order.generateBalanceDescription(webshop);
186
- await this.save();
187
- }
188
- }
189
- }
190
- }
191
- }
192
-
193
- async undoPaid(payment: Payment | null, organization: Organization) {
194
- // If order
195
- if (this.orderId) {
196
- const { Order } = await import('./Order');
197
- const order = await Order.getByID(this.orderId);
198
- if (order) {
199
- await order.undoPaid(payment, organization);
200
- }
201
- }
202
- }
203
-
204
- async markFailed(payment: Payment, organization: Organization) {
205
- // If order
206
- if (this.orderId) {
207
- const { Order } = await import('./Order');
208
- const order = await Order.getByID(this.orderId);
209
- if (order) {
210
- await order.onPaymentFailed(payment, organization);
211
-
212
- if (order.status === OrderStatus.Deleted) {
213
- this.status = BalanceItemStatus.Hidden;
214
- await this.save();
215
- }
216
- }
217
- }
218
- }
219
-
220
- async undoFailed(payment: Payment, organization: Organization) {
221
- // If order
222
- if (this.orderId) {
223
- const { Order } = await import('./Order');
224
- const order = await Order.getByID(this.orderId);
225
- if (order) {
226
- await order.undoPaymentFailed(payment, organization);
227
- }
228
- }
229
- }
230
-
231
126
  updateStatus() {
232
127
  this.status = (this.pricePaid !== 0 || this.price === 0) && this.pricePaid >= this.price ? BalanceItemStatus.Paid : (this.pricePaid !== 0 ? BalanceItemStatus.Pending : (this.status === BalanceItemStatus.Hidden ? BalanceItemStatus.Hidden : BalanceItemStatus.Pending));
233
128
  }
@@ -1,8 +1,7 @@
1
1
  import { column, ManyToOneRelation, Model } from '@simonbackx/simple-database';
2
- import { BalanceItemStatus } from '@stamhoofd/structures';
3
2
  import { v4 as uuidv4 } from 'uuid';
4
3
 
5
- import { BalanceItem, Organization, Payment } from './';
4
+ import { BalanceItem, Payment } from './';
6
5
 
7
6
  /**
8
7
  * Keeps track of all the created payments of a balance item, which contains the (tries) to pay a balance item.
@@ -57,42 +56,4 @@ export class BalanceItemPayment extends Model {
57
56
 
58
57
  static balanceItem = new ManyToOneRelation(BalanceItem, 'balanceItem');
59
58
  static payment = new ManyToOneRelation(Payment, 'payment');
60
-
61
- async markPaid(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
62
- // Update cached amountPaid of the balance item (this will get overwritten later, but we need it to calculate the status)
63
- this.balanceItem.pricePaid += this.price;
64
-
65
- // Update status
66
- const old = this.balanceItem.status;
67
- this.balanceItem.updateStatus();
68
- await this.balanceItem.save();
69
-
70
- // Do logic of balance item
71
- if (this.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid) {
72
- // Only call markPaid once (if it wasn't (partially) paid before)
73
- await this.balanceItem.markPaid(this.payment, organization);
74
- }
75
- else {
76
- await this.balanceItem.markUpdated(this.payment, organization);
77
- }
78
- }
79
-
80
- /**
81
- * Call this once a earlier succeeded payment is no longer succeeded
82
- */
83
- async undoPaid(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
84
- await this.balanceItem.undoPaid(this.payment, organization);
85
- }
86
-
87
- async markFailed(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
88
- // Do logic of balance item
89
- await this.balanceItem.markFailed(this.payment, organization);
90
- }
91
-
92
- async undoFailed(this: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
93
- // Reactivate deleted items
94
- await this.balanceItem.undoFailed(this.payment, organization);
95
- }
96
59
  }
97
-
98
- type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<Key, Model> : never;
@@ -1,5 +1,5 @@
1
1
  import { column, Model } from '@simonbackx/simple-database';
2
- import { Document as DocumentStruct, DocumentData, DocumentStatus } from '@stamhoofd/structures';
2
+ import { Document as DocumentStruct, DocumentData, DocumentStatus, Platform, Version } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
@@ -67,13 +67,26 @@ export class Document extends Model {
67
67
 
68
68
  buildContext(organization: Organization) {
69
69
  // Convert the field answers in a simplified javascript object
70
- const data = {
71
- 'id': this.id,
72
- 'name': this.data.name,
73
- 'number': this.number,
74
- 'created_at': this.createdAt,
75
- 'organization.logo': organization.meta.squareLogo,
70
+ const data: Record<string, any> = {
71
+ id: this.id,
72
+ name: this.data.name,
73
+ number: this.number,
74
+ created_at: this.createdAt,
76
75
  };
76
+ const platformLogo = Platform.shared.config.logoDocuments ?? Platform.shared.config.horizontalLogo ?? Platform.shared.config.squareLogo;
77
+ const organizationLogo = organization.meta.horizontalLogo ?? organization.meta.squareLogo;
78
+
79
+ if (organizationLogo) {
80
+ data['organization'] = {
81
+ logo: organizationLogo.encode({ version: Version }) ?? null,
82
+ };
83
+ }
84
+
85
+ if (platformLogo) {
86
+ data['platform'] = {
87
+ logo: platformLogo.encode({ version: Version }) ?? null,
88
+ };
89
+ }
77
90
 
78
91
  for (const field of this.data.fieldAnswers.values()) {
79
92
  const keys = field.settings.id.split('.');
@@ -188,14 +201,14 @@ export class Document extends Model {
188
201
  return null;
189
202
  }
190
203
 
191
- return this.getRenderedHtmlForTemplate(organization, template.html);
204
+ return await this.getRenderedHtmlForTemplate(organization, template.html);
192
205
  }
193
206
 
194
207
  // Rander handlebars template
195
- private getRenderedHtmlForTemplate(organization: Organization, htmlTemplate: string): string | null {
208
+ private async getRenderedHtmlForTemplate(organization: Organization, htmlTemplate: string): Promise<string | null> {
196
209
  try {
197
210
  const context = this.buildContext(organization);
198
- const renderedHtml = render(htmlTemplate, context);
211
+ const renderedHtml = await render(htmlTemplate, context);
199
212
  return renderedHtml;
200
213
  }
201
214
  catch (e) {
@@ -1,7 +1,7 @@
1
1
  import { column, Model } from '@simonbackx/simple-database';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import { DocumentData, DocumentPrivateSettings, DocumentSettings, DocumentStatus, DocumentTemplatePrivate, GroupType, RecordAddressAnswer, RecordAnswer, RecordAnswerDecoder, RecordDateAnswer, RecordPriceAnswer, RecordSettings, RecordTextAnswer, RecordType } from '@stamhoofd/structures';
4
+ import { BalanceItemStatus, DocumentData, DocumentPrivateSettings, DocumentSettings, DocumentStatus, DocumentTemplatePrivate, GroupType, NationalRegisterNumberOptOut, Parent, RecordAddressAnswer, RecordAnswer, RecordAnswerDecoder, RecordDateAnswer, RecordPriceAnswer, RecordSettings, RecordTextAnswer, RecordType } from '@stamhoofd/structures';
5
5
  import { Sorter } from '@stamhoofd/utility';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
 
@@ -11,6 +11,7 @@ import { Document } from './Document';
11
11
  import { Group } from './Group';
12
12
  import { Member, RegistrationWithMember } from './Member';
13
13
  import { Organization } from './Organization';
14
+ import { User } from './User';
14
15
 
15
16
  export class DocumentTemplate extends Model {
16
17
  static table = 'document_templates';
@@ -72,7 +73,7 @@ export class DocumentTemplate extends Model {
72
73
  let missingData = false;
73
74
 
74
75
  const group = await Group.getByID(registration.groupId);
75
- const { payments } = await BalanceItem.getForRegistration(registration.id);
76
+ const { items: balanceItems, payments } = await BalanceItem.getForRegistration(registration.id);
76
77
 
77
78
  const paidAtDates = payments.flatMap(p => p.paidAt ? [p.paidAt?.getTime()] : []);
78
79
 
@@ -81,13 +82,11 @@ export class DocumentTemplate extends Model {
81
82
 
82
83
  // Some fields are supported by default in linked fields
83
84
  const defaultData: Record<string, RecordAnswer> = {
84
- // "registration.startDate": registration.group.settings.startDate,
85
- // "registration.endDate": registration.group.settings.endDate,
86
85
  'group.name': RecordTextAnswer.create({
87
86
  settings: RecordSettings.create({
88
87
  id: 'group.name',
89
88
  type: RecordType.Text,
90
- }), // settings will be overwritten
89
+ }),
91
90
  value: group?.settings?.name ?? '',
92
91
  }),
93
92
  'group.type': RecordTextAnswer.create({
@@ -142,13 +141,17 @@ export class DocumentTemplate extends Model {
142
141
  settings: RecordSettings.create({}), // settings will be overwritten
143
142
  value: registration.member.details.lastName,
144
143
  }),
144
+ 'member.nationalRegisterNumber': RecordTextAnswer.create({
145
+ settings: RecordSettings.create({}), // settings will be overwritten
146
+ value: registration.member.details.nationalRegisterNumber === NationalRegisterNumberOptOut ? null : registration.member.details.nationalRegisterNumber,
147
+ }),
145
148
  'member.address': RecordAddressAnswer.create({
146
149
  settings: RecordSettings.create({}), // settings will be overwritten
147
- address: registration.member.details.address ?? null,
150
+ address: registration.member.details.address ?? registration.member.details.getAllAddresses()[0] ?? null,
148
151
  }),
149
152
  'member.email': RecordTextAnswer.create({
150
153
  settings: RecordSettings.create({}), // settings will be overwritten
151
- value: registration.member.details.email ?? null,
154
+ value: registration.member.details.getMemberEmails()[0] ?? registration.member.details.getParentEmails()[0],
152
155
  }),
153
156
  'member.birthDay': RecordDateAnswer.create({
154
157
  settings: RecordSettings.create({}), // settings will be overwritten
@@ -162,6 +165,10 @@ export class DocumentTemplate extends Model {
162
165
  settings: RecordSettings.create({}), // settings will be overwritten
163
166
  value: registration.member.details.parents[0]?.lastName,
164
167
  }),
168
+ 'parents[0].nationalRegisterNumber': RecordTextAnswer.create({
169
+ settings: RecordSettings.create({}), // settings will be overwritten
170
+ value: registration.member.details.parents[0]?.nationalRegisterNumber === NationalRegisterNumberOptOut ? null : registration.member.details.parents[0]?.nationalRegisterNumber,
171
+ }),
165
172
  'parents[0].address': RecordAddressAnswer.create({
166
173
  settings: RecordSettings.create({}), // settings will be overwritten
167
174
  address: registration.member.details.parents[0]?.address ?? null,
@@ -178,6 +185,10 @@ export class DocumentTemplate extends Model {
178
185
  settings: RecordSettings.create({}), // settings will be overwritten
179
186
  value: registration.member.details.parents[1]?.lastName,
180
187
  }),
188
+ 'parents[1].nationalRegisterNumber': RecordTextAnswer.create({
189
+ settings: RecordSettings.create({}), // settings will be overwritten
190
+ value: registration.member.details.parents[1]?.nationalRegisterNumber === NationalRegisterNumberOptOut ? null : registration.member.details.parents[1]?.nationalRegisterNumber,
191
+ }),
181
192
  'parents[1].address': RecordAddressAnswer.create({
182
193
  settings: RecordSettings.create({}), // settings will be overwritten
183
194
  address: registration.member.details.parents[1]?.address ?? null,
@@ -188,13 +199,83 @@ export class DocumentTemplate extends Model {
188
199
  }),
189
200
  };
190
201
 
202
+ const allRecords = this.privateSettings.templateDefinition.documentFieldCategories.flatMap(c => c.getAllRecords());
203
+ const hasDebtor = allRecords.find(s => s.id.startsWith('debtor.'));
204
+
205
+ if (hasDebtor) {
206
+ const parentsWithNRN = registration.member.details.parents.filter(p => p.nationalRegisterNumber !== NationalRegisterNumberOptOut && p.nationalRegisterNumber);
207
+ let debtor: Parent | undefined = parentsWithNRN[0] ?? registration.member.details.parents[0];
208
+ if (parentsWithNRN.length > 1) {
209
+ for (const balanceItem of balanceItems) {
210
+ if (balanceItem && balanceItem.userId && balanceItem.status === BalanceItemStatus.Paid) {
211
+ const user = await User.getByID(balanceItem.userId);
212
+ if (user) {
213
+ const parent = parentsWithNRN.find(p => p.hasEmail(user.email));
214
+
215
+ if (parent) {
216
+ debtor = parent;
217
+ break;
218
+ }
219
+
220
+ if (!debtor.nationalRegisterNumber) {
221
+ const parent = registration.member.details.parents.find(p => p.hasEmail(user.email));
222
+ if (parent) {
223
+ debtor = parent;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ Object.assign(defaultData, {
232
+ 'debtor.firstName': RecordTextAnswer.create({
233
+ settings: RecordSettings.create({}), // settings will be overwritten
234
+ value: debtor?.firstName ?? '',
235
+ }),
236
+ 'debtor.lastName': RecordTextAnswer.create({
237
+ settings: RecordSettings.create({}), // settings will be overwritten
238
+ value: debtor?.lastName ?? '',
239
+ }),
240
+ 'debtor.nationalRegisterNumber': RecordTextAnswer.create({
241
+ settings: RecordSettings.create({}), // settings will be overwritten
242
+ value: debtor?.nationalRegisterNumber === NationalRegisterNumberOptOut ? '' : debtor?.nationalRegisterNumber,
243
+ }),
244
+ 'debtor.address': RecordAddressAnswer.create({
245
+ settings: RecordSettings.create({}), // settings will be overwritten
246
+ address: debtor?.address ?? null,
247
+ }),
248
+ 'debtor.email': RecordTextAnswer.create({
249
+ settings: RecordSettings.create({}), // settings will be overwritten
250
+ value: debtor?.email ?? null,
251
+ }),
252
+ });
253
+ }
254
+
191
255
  // Add data that is different for each member
192
- for (const field of this.privateSettings.templateDefinition.documentFieldCategories.flatMap(c => c.getAllRecords())) {
256
+ for (const field of allRecords) {
193
257
  // Where do we need to find the answer to this linked field?
194
258
  // - Could either return an id of a recordSetting connected to member
195
259
  // - or an idea of defaultData that is supported by default
196
260
  // The result is always a recordAnswer whose type should match the type of the linkedField
197
- const linkedToMemberAnswerSettingsIds = this.settings.linkedFields.get(field.id);
261
+ let linkedToMemberAnswerSettingsIds = this.settings.linkedFields.get(field.id) ?? [field.id];
262
+
263
+ if (linkedToMemberAnswerSettingsIds.length === 0) {
264
+ linkedToMemberAnswerSettingsIds = [field.id];
265
+ }
266
+ console.log('Checking', field.id);
267
+
268
+ // Check if this field has been manually disabled by a global checkbox
269
+ const enableField = this.settings.fieldAnswers.get('enable[' + field.id + ']');
270
+ if (enableField && enableField.objectValue === false) {
271
+ field.required = false;
272
+
273
+ const clone = RecordAnswerDecoder.getClassForType(field.type).create({
274
+ settings: field,
275
+ });
276
+ fieldAnswers.set(field.id, clone);
277
+ continue;
278
+ }
198
279
 
199
280
  let found = false;
200
281
 
@@ -375,17 +456,13 @@ export class DocumentTemplate extends Model {
375
456
  }
376
457
 
377
458
  if (this.settings.minPrice !== null) {
378
- const fieldId = 'registration.price';
379
- let price: null | number = null;
380
-
381
- const answer = fieldAnswers.get(fieldId);
382
- if (answer && answer instanceof RecordPriceAnswer) {
383
- if (answer.value !== null) {
384
- price = answer.value;
385
- }
459
+ if ((registration.price ?? 0) < this.settings.minPrice) {
460
+ return false;
386
461
  }
462
+ }
387
463
 
388
- if ((price ?? 0) < this.settings.minPrice) {
464
+ if (this.settings.minPricePaid !== null) {
465
+ if ((registration.pricePaid ?? 0) < this.settings.minPricePaid && (registration.price ?? 0) > 0) {
389
466
  return false;
390
467
  }
391
468
  }
@@ -527,7 +604,7 @@ export class DocumentTemplate extends Model {
527
604
 
528
605
  try {
529
606
  const context = await this.buildContext(organization);
530
- const renderedHtml = render(this.privateSettings.templateDefinition.xmlExport, context);
607
+ const renderedHtml = await render(this.privateSettings.templateDefinition.xmlExport, context);
531
608
  return renderedHtml;
532
609
  }
533
610
  catch (e) {
@@ -188,7 +188,7 @@ export class Member extends Model {
188
188
  static async getRegistrationWithMembersForGroup(groupId: string): Promise<RegistrationWithMember[]> {
189
189
  let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
190
190
 
191
- query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`;
191
+ query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND \`${Registration.table}\`.\`registeredAt\` is not null AND \`${Registration.table}\`.\`deactivatedAt\` is null\n`;
192
192
 
193
193
  // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
194
194
  query += `where \`${Registration.table}\`.\`groupId\` = ?`;
@@ -185,52 +185,6 @@ export class Registration extends Model {
185
185
  }
186
186
  }
187
187
 
188
- async deactivate() {
189
- if (this.deactivatedAt !== null) {
190
- return;
191
- }
192
-
193
- // Clear the registration
194
- this.deactivatedAt = new Date();
195
- await this.save();
196
- this.scheduleStockUpdate();
197
-
198
- const { Member } = await import('./Member');
199
- await Member.updateMembershipsForId(this.memberId);
200
- }
201
-
202
- async markValid(this: Registration, options?: { skipEmail?: boolean }) {
203
- if (this.registeredAt !== null && this.deactivatedAt === null) {
204
- await this.save();
205
- return false;
206
- }
207
-
208
- this.reservedUntil = null;
209
- this.registeredAt = this.registeredAt ?? new Date();
210
- this.deactivatedAt = null;
211
- this.canRegister = false;
212
- await this.save();
213
- this.scheduleStockUpdate();
214
-
215
- const { Member } = await import('./Member');
216
- await Member.updateMembershipsForId(this.memberId);
217
-
218
- if (options?.skipEmail !== true) {
219
- await this.sendEmailTemplate({
220
- type: EmailTemplateType.RegistrationConfirmation,
221
- });
222
- }
223
-
224
- const member = await Member.getByID(this.memberId);
225
- if (member) {
226
- const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
227
- registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
228
- await Document.updateForRegistration(this.setRelation(registrationMemberRelation, member));
229
- }
230
-
231
- return true;
232
- }
233
-
234
188
  async getRecipients(organization: Organization, group: import('./').Group) {
235
189
  const { Member } = await import('./Member');
236
190
 
@@ -52,3 +52,4 @@ export * from './Email';
52
52
  export * from './EmailRecipient';
53
53
  export * from './Event';
54
54
  export * from './CachedBalance';
55
+ export * from './AuditLog';