@stamhoofd/models 2.121.0 → 2.122.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 (241) hide show
  1. package/dist/factories/GroupFactory.d.ts.map +1 -1
  2. package/dist/factories/GroupFactory.js +1 -1
  3. package/dist/factories/GroupFactory.js.map +1 -1
  4. package/dist/factories/OrganizationFactory.d.ts +2 -1
  5. package/dist/factories/OrganizationFactory.d.ts.map +1 -1
  6. package/dist/factories/OrganizationFactory.js +9 -1
  7. package/dist/factories/OrganizationFactory.js.map +1 -1
  8. package/dist/factories/STPackageFactory.js.map +1 -1
  9. package/dist/factories/UserFactory.d.ts.map +1 -1
  10. package/dist/factories/UserFactory.js +2 -2
  11. package/dist/factories/UserFactory.js.map +1 -1
  12. package/dist/helpers/EmailBuilder.d.ts.map +1 -1
  13. package/dist/helpers/EmailBuilder.js +8 -8
  14. package/dist/helpers/EmailBuilder.js.map +1 -1
  15. package/dist/helpers/Handlebars.d.ts.map +1 -1
  16. package/dist/helpers/Handlebars.js +7 -1
  17. package/dist/helpers/Handlebars.js.map +1 -1
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/migrations/1605262045-import-postcodes.d.ts.map +1 -1
  23. package/dist/migrations/1605262045-import-postcodes.js +58 -24
  24. package/dist/migrations/1605262045-import-postcodes.js.map +1 -1
  25. package/dist/migrations/1605262046-import-postcodes-nl.d.ts.map +1 -1
  26. package/dist/migrations/1605262046-import-postcodes-nl.js +54 -17
  27. package/dist/migrations/1605262046-import-postcodes-nl.js.map +1 -1
  28. package/dist/migrations/1719567881-organization-periodId.sql +2 -0
  29. package/dist/migrations/1719567882-groups-periodId.sql +2 -0
  30. package/dist/migrations/1720080975-convert-charset.d.ts +4 -0
  31. package/dist/migrations/1720080975-convert-charset.d.ts.map +1 -0
  32. package/dist/migrations/1720080975-convert-charset.js +26 -0
  33. package/dist/migrations/1720080975-convert-charset.js.map +1 -0
  34. package/dist/migrations/1720080976-convert-charset-leads.d.ts.map +1 -1
  35. package/dist/migrations/1720080976-convert-charset-leads.js +11 -10
  36. package/dist/migrations/1720080976-convert-charset-leads.js.map +1 -1
  37. package/dist/migrations/1721400546-users-memberId.sql +2 -0
  38. package/dist/migrations/1722269236-group-waitinglist-id.sql +2 -1
  39. package/dist/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  40. package/dist/migrations/1722525787-depending-balance-item.sql +2 -0
  41. package/dist/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  42. package/dist/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  43. package/dist/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  44. package/dist/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  45. package/dist/migrations/1733504881-negative-invoice-id.sql +6 -0
  46. package/dist/migrations/1733994455-balance-item-status-open.d.ts +4 -0
  47. package/dist/migrations/1733994455-balance-item-status-open.d.ts.map +1 -0
  48. package/dist/migrations/1733994455-balance-item-status-open.js +28 -0
  49. package/dist/migrations/1733994455-balance-item-status-open.js.map +1 -0
  50. package/dist/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  51. package/dist/migrations/1769087809-payments-invoice-id.sql +2 -0
  52. package/dist/migrations/1772033555-balance-item-package-id.sql +2 -0
  53. package/dist/migrations/1778796615-payments-reversing-payment-id.sql +2 -0
  54. package/dist/migrations/1779443446-transfer-fees.sql +3 -0
  55. package/dist/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  56. package/dist/migrations/1779968328-payments-admin-user-id.sql +5 -0
  57. package/dist/migrations/1779970611-payments-refunded-amount.sql +2 -0
  58. package/dist/migrations/1779972640-balance-items-failed-at.sql +2 -0
  59. package/dist/migrations/1780328285-document-template-locked.sql +2 -0
  60. package/dist/migrations/1780328286-document-locked.sql +2 -0
  61. package/dist/migrations/1780412083-documents-set-locked.d.ts +4 -0
  62. package/dist/migrations/1780412083-documents-set-locked.d.ts.map +1 -0
  63. package/dist/migrations/1780412083-documents-set-locked.js +14 -0
  64. package/dist/migrations/1780412083-documents-set-locked.js.map +1 -0
  65. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts +4 -0
  66. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts.map +1 -0
  67. package/dist/migrations/1780928401-v1-groups-migration-data.js +44 -0
  68. package/dist/migrations/1780928401-v1-groups-migration-data.js.map +1 -0
  69. package/dist/models/BalanceItem.d.ts +7 -2
  70. package/dist/models/BalanceItem.d.ts.map +1 -1
  71. package/dist/models/BalanceItem.js +41 -38
  72. package/dist/models/BalanceItem.js.map +1 -1
  73. package/dist/models/CachedBalance.d.ts +6 -1
  74. package/dist/models/CachedBalance.d.ts.map +1 -1
  75. package/dist/models/CachedBalance.js +3 -2
  76. package/dist/models/CachedBalance.js.map +1 -1
  77. package/dist/models/Document.d.ts +4 -0
  78. package/dist/models/Document.d.ts.map +1 -1
  79. package/dist/models/Document.js +26 -3
  80. package/dist/models/Document.js.map +1 -1
  81. package/dist/models/DocumentTemplate.d.ts +4 -0
  82. package/dist/models/DocumentTemplate.d.ts.map +1 -1
  83. package/dist/models/DocumentTemplate.js +37 -1
  84. package/dist/models/DocumentTemplate.js.map +1 -1
  85. package/dist/models/Email.d.ts.map +1 -1
  86. package/dist/models/Email.js +1 -1
  87. package/dist/models/Email.js.map +1 -1
  88. package/dist/models/EmailVerificationCode.d.ts.map +1 -1
  89. package/dist/models/EmailVerificationCode.js +3 -13
  90. package/dist/models/EmailVerificationCode.js.map +1 -1
  91. package/dist/models/Event.d.ts +2 -1
  92. package/dist/models/Event.d.ts.map +1 -1
  93. package/dist/models/Event.js +3 -0
  94. package/dist/models/Event.js.map +1 -1
  95. package/dist/models/EventNotification.d.ts.map +1 -1
  96. package/dist/models/EventNotification.js +5 -5
  97. package/dist/models/EventNotification.js.map +1 -1
  98. package/dist/models/Invoice.d.ts +1 -0
  99. package/dist/models/Invoice.d.ts.map +1 -1
  100. package/dist/models/Invoice.js +8 -0
  101. package/dist/models/Invoice.js.map +1 -1
  102. package/dist/models/MemberPlatformMembership.d.ts.map +1 -1
  103. package/dist/models/MemberPlatformMembership.js +9 -0
  104. package/dist/models/MemberPlatformMembership.js.map +1 -1
  105. package/dist/models/Order.d.ts.map +1 -1
  106. package/dist/models/Order.js +1 -0
  107. package/dist/models/Order.js.map +1 -1
  108. package/dist/models/Organization.d.ts +23 -25
  109. package/dist/models/Organization.d.ts.map +1 -1
  110. package/dist/models/Organization.js +92 -64
  111. package/dist/models/Organization.js.map +1 -1
  112. package/dist/models/PasswordToken.d.ts +5 -1
  113. package/dist/models/PasswordToken.d.ts.map +1 -1
  114. package/dist/models/PasswordToken.js +18 -17
  115. package/dist/models/PasswordToken.js.map +1 -1
  116. package/dist/models/Payment.d.ts +21 -2
  117. package/dist/models/Payment.d.ts.map +1 -1
  118. package/dist/models/Payment.js +43 -2
  119. package/dist/models/Payment.js.map +1 -1
  120. package/dist/models/Registration.d.ts.map +1 -1
  121. package/dist/models/Registration.js +3 -3
  122. package/dist/models/Registration.js.map +1 -1
  123. package/dist/models/STCredit.d.ts +4 -0
  124. package/dist/models/STCredit.d.ts.map +1 -1
  125. package/dist/models/STCredit.js +28 -0
  126. package/dist/models/STCredit.js.map +1 -1
  127. package/dist/models/STInvoice.d.ts +7 -1
  128. package/dist/models/STInvoice.d.ts.map +1 -1
  129. package/dist/models/STInvoice.js +9 -0
  130. package/dist/models/STInvoice.js.map +1 -1
  131. package/dist/models/STPackage.d.ts +4 -0
  132. package/dist/models/STPackage.d.ts.map +1 -1
  133. package/dist/models/STPackage.js +12 -1
  134. package/dist/models/STPackage.js.map +1 -1
  135. package/dist/models/UsedRegisterCode.d.ts +9 -0
  136. package/dist/models/UsedRegisterCode.d.ts.map +1 -1
  137. package/dist/models/UsedRegisterCode.js +31 -0
  138. package/dist/models/UsedRegisterCode.js.map +1 -1
  139. package/dist/models/_relations.js +25 -0
  140. package/dist/models/_relations.js.map +1 -1
  141. package/dist/models/addresses/City.d.ts +4 -4
  142. package/dist/models/addresses/City.d.ts.map +1 -1
  143. package/dist/models/addresses/City.js +6 -6
  144. package/dist/models/addresses/City.js.map +1 -1
  145. package/dist/models/addresses/PostalCode.d.ts +2 -2
  146. package/dist/models/addresses/PostalCode.d.ts.map +1 -1
  147. package/dist/models/addresses/PostalCode.js +4 -3
  148. package/dist/models/addresses/PostalCode.js.map +1 -1
  149. package/dist/models/addresses/Street.d.ts +3 -3
  150. package/dist/models/addresses/Street.d.ts.map +1 -1
  151. package/dist/models/addresses/Street.js +4 -4
  152. package/dist/models/addresses/Street.js.map +1 -1
  153. package/dist/models/index.d.ts +1 -0
  154. package/dist/models/index.d.ts.map +1 -1
  155. package/dist/models/index.js +1 -0
  156. package/dist/models/index.js.map +1 -1
  157. package/dist/models/v1GroupMigrationData.d.ts +22 -0
  158. package/dist/models/v1GroupMigrationData.d.ts.map +1 -0
  159. package/dist/models/v1GroupMigrationData.js +48 -0
  160. package/dist/models/v1GroupMigrationData.js.map +1 -0
  161. package/package.json +32 -13
  162. package/src/factories/GroupFactory.ts +4 -6
  163. package/src/factories/OrganizationFactory.ts +12 -4
  164. package/src/factories/STPackageFactory.ts +2 -2
  165. package/src/factories/UserFactory.ts +4 -5
  166. package/src/helpers/EmailBuilder.ts +19 -28
  167. package/src/helpers/Handlebars.ts +6 -1
  168. package/src/index.ts +0 -1
  169. package/src/migrations/1605262045-import-postcodes.ts +62 -25
  170. package/src/migrations/1605262046-import-postcodes-nl.ts +58 -17
  171. package/src/migrations/1719567881-organization-periodId.sql +2 -0
  172. package/src/migrations/1719567882-groups-periodId.sql +2 -0
  173. package/src/migrations/1720080975-convert-charset.ts +34 -0
  174. package/src/migrations/1720080976-convert-charset-leads.ts +16 -13
  175. package/src/migrations/1721400546-users-memberId.sql +2 -0
  176. package/src/migrations/1722269236-group-waitinglist-id.sql +2 -1
  177. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  178. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  179. package/src/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  180. package/src/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  181. package/src/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  182. package/src/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  183. package/src/migrations/1733504881-negative-invoice-id.sql +6 -0
  184. package/src/migrations/1733994455-balance-item-status-open.ts +30 -0
  185. package/src/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  186. package/src/migrations/1769087809-payments-invoice-id.sql +2 -0
  187. package/src/migrations/1772033555-balance-item-package-id.sql +2 -0
  188. package/src/migrations/1778796615-payments-reversing-payment-id.sql +2 -0
  189. package/src/migrations/1779443446-transfer-fees.sql +3 -0
  190. package/src/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  191. package/src/migrations/1779968328-payments-admin-user-id.sql +5 -0
  192. package/src/migrations/1779970611-payments-refunded-amount.sql +2 -0
  193. package/src/migrations/1779972640-balance-items-failed-at.sql +2 -0
  194. package/src/migrations/1780328285-document-template-locked.sql +2 -0
  195. package/src/migrations/1780328286-document-locked.sql +2 -0
  196. package/src/migrations/1780412083-documents-set-locked.ts +18 -0
  197. package/src/migrations/1780928401-v1-groups-migration-data.ts +50 -0
  198. package/src/models/BalanceItem.ts +44 -43
  199. package/src/models/CachedBalance.test.ts +46 -46
  200. package/src/models/CachedBalance.ts +7 -7
  201. package/src/models/Document.ts +34 -13
  202. package/src/models/DocumentTemplate.ts +56 -17
  203. package/src/models/Email.test.ts +3 -3
  204. package/src/models/Email.ts +28 -49
  205. package/src/models/EmailVerificationCode.ts +8 -22
  206. package/src/models/Event.ts +6 -4
  207. package/src/models/EventNotification.ts +6 -6
  208. package/src/models/Invoice.ts +9 -0
  209. package/src/models/MemberPlatformMembership.test.ts +70 -0
  210. package/src/models/MemberPlatformMembership.ts +16 -12
  211. package/src/models/Order.ts +14 -26
  212. package/src/models/Organization.ts +122 -93
  213. package/src/models/PasswordToken.ts +21 -21
  214. package/src/models/Payment.ts +42 -4
  215. package/src/models/Registration.ts +3 -3
  216. package/src/models/STCredit.ts +32 -0
  217. package/src/models/STInvoice.ts +11 -5
  218. package/src/models/STPackage.ts +19 -14
  219. package/src/models/UsedRegisterCode.ts +34 -0
  220. package/src/models/_relations.ts +29 -0
  221. package/src/models/addresses/City.ts +8 -6
  222. package/src/models/addresses/PostalCode.test.ts +1 -0
  223. package/src/models/addresses/PostalCode.ts +5 -3
  224. package/src/models/addresses/Street.ts +6 -4
  225. package/src/models/index.ts +2 -0
  226. package/src/models/v1GroupMigrationData.ts +43 -0
  227. package/dist/helpers/MemberMerger.d.ts +0 -14
  228. package/dist/helpers/MemberMerger.d.ts.map +0 -1
  229. package/dist/helpers/MemberMerger.js +0 -364
  230. package/dist/helpers/MemberMerger.js.map +0 -1
  231. package/dist/migrations/1720080975-convert-charset.sql +0 -85
  232. package/dist/migrations/1723202126-member-number-index.sql +0 -2
  233. package/dist/models/OneTimeToken.d.ts +0 -38
  234. package/dist/models/OneTimeToken.d.ts.map +0 -1
  235. package/dist/models/OneTimeToken.js +0 -125
  236. package/dist/models/OneTimeToken.js.map +0 -1
  237. package/src/helpers/MemberMerger.test.ts +0 -782
  238. package/src/helpers/MemberMerger.ts +0 -577
  239. package/src/migrations/1720080975-convert-charset.sql +0 -85
  240. package/src/migrations/1723202126-member-number-index.sql +0 -2
  241. package/src/models/OneTimeToken.ts +0 -133
@@ -3,6 +3,7 @@ import { DocumentData, DocumentStatus, Document as DocumentStruct, Platform, Ver
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
6
+ import { SimpleError } from '@simonbackx/simple-errors';
6
7
  import { QueryableModel } from '@stamhoofd/sql';
7
8
  import { render } from '../helpers/Handlebars.js';
8
9
  import type { Member, MemberWithRegistrations, RegistrationWithMember } from './Member.js';
@@ -38,6 +39,9 @@ export class Document extends QueryableModel {
38
39
  @column({ type: 'string' })
39
40
  status = DocumentStatus.Draft;
40
41
 
42
+ @column({ type: 'boolean' })
43
+ isLocked = false;
44
+
41
45
  /**
42
46
  * Assigned when exporting the document
43
47
  */
@@ -63,6 +67,18 @@ export class Document extends QueryableModel {
63
67
  })
64
68
  updatedAt: Date;
65
69
 
70
+ override save(options?: Parameters<QueryableModel['save']>[0] & { forceSave?: boolean }): Promise<boolean> {
71
+ if (!options?.forceSave && this.isLocked) {
72
+ throw new SimpleError({
73
+ code: 'locked',
74
+ message: 'Document is locked',
75
+ human: $t(`%1Uc`),
76
+ });
77
+ }
78
+
79
+ return super.save(options);
80
+ }
81
+
66
82
  getStructure() {
67
83
  return DocumentStruct.create(this);
68
84
  }
@@ -84,6 +100,7 @@ export class Document extends QueryableModel {
84
100
  };
85
101
  const platformLogo = Platform.shared.config.logoDocuments ?? Platform.shared.config.horizontalLogo ?? Platform.shared.config.squareLogo;
86
102
  const organizationLogo = organization.meta.horizontalLogo ?? organization.meta.squareLogo;
103
+ const logo = organizationLogo || platformLogo;
87
104
 
88
105
  if (organizationLogo) {
89
106
  data['organization'] = {
@@ -98,6 +115,10 @@ export class Document extends QueryableModel {
98
115
  };
99
116
  }
100
117
 
118
+ if (logo) {
119
+ data['logo'] = logo.encode({ version: Version }) ?? null;
120
+ }
121
+
101
122
  for (const field of this.data.fieldAnswers.values()) {
102
123
  const keys = field.settings.id.split('.');
103
124
  let current = data;
@@ -121,6 +142,11 @@ export class Document extends QueryableModel {
121
142
  }
122
143
 
123
144
  async updateData(): Promise<void> {
145
+ if (this.isLocked) {
146
+ console.log('Document is locked, skipping update');
147
+ return;
148
+ }
149
+
124
150
  const DocumentTemplate = (await import('./DocumentTemplate.js')).DocumentTemplate;
125
151
  const template = await DocumentTemplate.getByID(this.templateId);
126
152
  if (!template) {
@@ -165,8 +191,7 @@ export class Document extends QueryableModel {
165
191
  await this.updateForRegistrations(loadedMember.registrations.filter(r => r.registeredAt && r.deactivatedAt === null && r.organizationId === organizationId).map(r => r.id), organizationId);
166
192
  }
167
193
  }
168
- }
169
- catch (e) {
194
+ } catch (e) {
170
195
  console.error(e);
171
196
  }
172
197
  }
@@ -176,13 +201,12 @@ export class Document extends QueryableModel {
176
201
  console.log('Updating documents for registration', registration.id);
177
202
 
178
203
  const DocumentTemplate = (await import('./DocumentTemplate.js')).DocumentTemplate;
179
- const templates = await DocumentTemplate.where({ updatesEnabled: 1, organizationId: registration.organizationId });
204
+ const templates = await DocumentTemplate.where({ updatesEnabled: 1, isLocked: 0, organizationId: registration.organizationId });
180
205
 
181
206
  for (const template of templates) {
182
207
  await template.updateForRegistration(registration);
183
208
  }
184
- }
185
- catch (e) {
209
+ } catch (e) {
186
210
  console.error(e);
187
211
  }
188
212
  }
@@ -208,7 +232,7 @@ export class Document extends QueryableModel {
208
232
  console.log('Updating documents for updateForRegistrations', registrationIds, organizationId);
209
233
 
210
234
  const DocumentTemplate = (await import('./DocumentTemplate.js')).DocumentTemplate;
211
- const templates = await DocumentTemplate.where({ updatesEnabled: 1, organizationId });
235
+ const templates = await DocumentTemplate.where({ updatesEnabled: 1, isLocked: 0, organizationId });
212
236
 
213
237
  if (templates.length) {
214
238
  const Member = (await import('./Member.js')).Member;
@@ -219,8 +243,7 @@ export class Document extends QueryableModel {
219
243
  await template.updateForRegistrations(loadedRegistrations);
220
244
  }
221
245
  }
222
- }
223
- catch (e) {
246
+ } catch (e) {
224
247
  console.error(e);
225
248
  }
226
249
  }
@@ -230,7 +253,7 @@ export class Document extends QueryableModel {
230
253
  console.log('Updating documents for group', groupId);
231
254
 
232
255
  const DocumentTemplate = (await import('./DocumentTemplate.js')).DocumentTemplate;
233
- const templates = await DocumentTemplate.where({ updatesEnabled: 1, organizationId });
256
+ const templates = await DocumentTemplate.where({ updatesEnabled: 1, isLocked: 0, organizationId });
234
257
 
235
258
  if (templates.length) {
236
259
  const Member = (await import('./Member.js')).Member;
@@ -240,8 +263,7 @@ export class Document extends QueryableModel {
240
263
  await template.updateForRegistrations(registrations);
241
264
  }
242
265
  }
243
- }
244
- catch (e) {
266
+ } catch (e) {
245
267
  console.error(e);
246
268
  }
247
269
  }
@@ -263,8 +285,7 @@ export class Document extends QueryableModel {
263
285
  const context = this.buildContext(organization);
264
286
  const renderedHtml = await render(htmlTemplate, context);
265
287
  return renderedHtml;
266
- }
267
- catch (e) {
288
+ } catch (e) {
268
289
  console.error('Failed to render document html', e);
269
290
  return null;
270
291
  }
@@ -1,7 +1,7 @@
1
1
  import { column } from '@simonbackx/simple-database';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import type { Parent, RecordAnswer} from '@stamhoofd/structures';
4
+ import type { Parent, RecordAnswer } from '@stamhoofd/structures';
5
5
  import { BalanceItemStatus, DocumentData, DocumentPrivateSettings, DocumentSettings, DocumentStatus, DocumentTemplatePrivate, GroupType, NationalRegisterNumberOptOut, RecordAddressAnswer, RecordAnswerDecoder, RecordDateAnswer, RecordPriceAnswer, RecordSettings, RecordTextAnswer, RecordType } from '@stamhoofd/structures';
6
6
  import { Formatter, Sorter } from '@stamhoofd/utility';
7
7
  import { v4 as uuidv4 } from 'uuid';
@@ -37,6 +37,9 @@ export class DocumentTemplate extends QueryableModel {
37
37
  @column({ type: 'string' })
38
38
  status = DocumentStatus.Draft;
39
39
 
40
+ @column({ type: 'boolean' })
41
+ isLocked = false;
42
+
40
43
  @column({ type: 'boolean' })
41
44
  updatesEnabled = true;
42
45
 
@@ -87,6 +90,18 @@ export class DocumentTemplate extends QueryableModel {
87
90
  })
88
91
  updatedAt: Date;
89
92
 
93
+ override save(options?: Parameters<QueryableModel['save']>[0] & { forceSave?: boolean }): Promise<boolean> {
94
+ if (!options?.forceSave && this.isLocked) {
95
+ throw new SimpleError({
96
+ code: 'locked',
97
+ message: 'Document template is locked',
98
+ human: $t(`%1Uc`),
99
+ });
100
+ }
101
+
102
+ return super.save(options);
103
+ }
104
+
90
105
  getPrivateStructure() {
91
106
  return DocumentTemplatePrivate.create(this);
92
107
  }
@@ -363,8 +378,7 @@ export class DocumentTemplate extends QueryableModel {
363
378
  found = true;
364
379
  fieldAnswers.set(field.id, clone);
365
380
  break;
366
- }
367
- else {
381
+ } else {
368
382
  console.warn('Found type mismatch for default data: ' + linkedToMemberAnswerSettingsId + ' - ' + field.id);
369
383
  }
370
384
  }
@@ -412,8 +426,7 @@ export class DocumentTemplate extends QueryableModel {
412
426
  for (const answer of fieldAnswers.values()) {
413
427
  try {
414
428
  answer.validate();
415
- }
416
- catch (e) {
429
+ } catch (e) {
417
430
  missingData = true;
418
431
 
419
432
  console.log('Missing data because of validation error', e, answer, answer.settings);
@@ -453,6 +466,14 @@ export class DocumentTemplate extends QueryableModel {
453
466
  }
454
467
 
455
468
  async updateForRegistration(registration: RegistrationWithMember, existingDocuments?: Document[]): Promise<Document[]> {
469
+ if (this.isLocked) {
470
+ throw new SimpleError({
471
+ code: 'locked',
472
+ message: 'Document template is locked',
473
+ human: $t(`%1Uc`),
474
+ });
475
+ }
476
+
456
477
  existingDocuments = existingDocuments !== undefined ? existingDocuments : await Document.where({ templateId: this.id, registrationId: registration.id }, { limit: 5 });
457
478
 
458
479
  if (!this.checkRegistrationIncluded(registration)) {
@@ -549,8 +570,7 @@ export class DocumentTemplate extends QueryableModel {
549
570
  if (age > this.settings.maxAge) {
550
571
  return false;
551
572
  }
552
- }
553
- else {
573
+ } else {
554
574
  console.warn('Missing registration.startDate in fieldAnswers when checking maxAge');
555
575
  }
556
576
  }
@@ -581,11 +601,20 @@ export class DocumentTemplate extends QueryableModel {
581
601
  async buildAll({ generateNumbers = false } = {}) {
582
602
  QueueHandler.abort('documents-build-all/' + this.id);
583
603
  return await QueueHandler.schedule('documents-build-all/' + this.id, async ({ abort }) => {
604
+ if (this.isLocked) {
605
+ return await Document.where({ templateId: this.id });
606
+ }
607
+
584
608
  if (!this.updatesEnabled) {
585
609
  // Check status
586
610
  const documents = await Document.where({ templateId: this.id });
587
611
  for (const document of documents) {
588
612
  abort.throwIfAborted();
613
+
614
+ if (document.isLocked) {
615
+ continue;
616
+ }
617
+
589
618
  await this.updateAnswers(document); // Only update global data
590
619
  if (document.status === DocumentStatus.Draft || document.status === DocumentStatus.Published) {
591
620
  document.status = this.status;
@@ -597,6 +626,11 @@ export class DocumentTemplate extends QueryableModel {
597
626
  if (generateNumbers) {
598
627
  for (const document of documents) {
599
628
  abort.throwIfAborted();
629
+
630
+ if (document.isLocked) {
631
+ continue;
632
+ }
633
+
600
634
  if (document.number === null && document.status === DocumentStatus.Published) {
601
635
  document.number = this.nextNumberForDocuments(documents);
602
636
  await document.save();
@@ -627,11 +661,15 @@ export class DocumentTemplate extends QueryableModel {
627
661
  const documents = await Document.where({ templateId: this.id });
628
662
  for (const document of documents) {
629
663
  abort.throwIfAborted();
664
+
665
+ if (document.isLocked) {
666
+ continue;
667
+ }
668
+
630
669
  if (!documentSet.has(document.id)) {
631
670
  if (document.number === null) {
632
671
  await document.delete();
633
- }
634
- else {
672
+ } else {
635
673
  document.status = DocumentStatus.Deleted;
636
674
  await document.save();
637
675
  }
@@ -644,6 +682,11 @@ export class DocumentTemplate extends QueryableModel {
644
682
  if (generateNumbers) {
645
683
  for (const document of allDocuments) {
646
684
  abort.throwIfAborted();
685
+
686
+ if (document.isLocked) {
687
+ continue;
688
+ }
689
+
647
690
  if (document.number === null && document.status === DocumentStatus.Published) {
648
691
  document.number = this.nextNumberForDocuments(allDocuments);
649
692
  await document.save();
@@ -733,8 +776,7 @@ export class DocumentTemplate extends QueryableModel {
733
776
  const context = await this.buildContext(organization);
734
777
  const renderedHtml = await render(this.privateSettings.templateDefinition.xmlExport, context);
735
778
  return renderedHtml;
736
- }
737
- catch (e) {
779
+ } catch (e) {
738
780
  if (isSimpleError(e) || isSimpleErrors(e)) {
739
781
  throw e;
740
782
  }
@@ -753,8 +795,7 @@ export class DocumentTemplate extends QueryableModel {
753
795
  answer.settings = field;
754
796
  try {
755
797
  answer.validate();
756
- }
757
- catch (e) {
798
+ } catch (e) {
758
799
  // Invalid
759
800
  return false;
760
801
  }
@@ -774,8 +815,7 @@ export class DocumentTemplate extends QueryableModel {
774
815
  if (!existing.isReviewedAfter(addAnswer)) {
775
816
  newAnswers.set(addAnswer.settings.id, addAnswer);
776
817
  }
777
- }
778
- else {
818
+ } else {
779
819
  newAnswers.set(addAnswer.settings.id, addAnswer);
780
820
  }
781
821
  }
@@ -786,8 +826,7 @@ export class DocumentTemplate extends QueryableModel {
786
826
  if (document.status !== DocumentStatus.Deleted) {
787
827
  if (!complete) {
788
828
  document.status = DocumentStatus.MissingData;
789
- }
790
- else {
829
+ } else {
791
830
  if (document.status === DocumentStatus.MissingData) {
792
831
  document.status = this.status;
793
832
  }
@@ -896,7 +896,7 @@ describe('Model.Email', () => {
896
896
  organizationId: organization.id,
897
897
  type: BalanceItemType.Other,
898
898
  amount: 2,
899
- unitPrice: 12_99,
899
+ unitPrice: 12_99_00,
900
900
  description: 'Test balance item',
901
901
  }).create();
902
902
  await CachedBalance.updateForUsers(organization.id, [existingUser.id]);
@@ -940,12 +940,12 @@ describe('Model.Email', () => {
940
940
  // Check if the table is correct
941
941
  expect(EmailMocker.getSucceededEmail(0).html).toMatch('<table');
942
942
  expect(EmailMocker.getSucceededEmail(0).html).toMatch('2 x '); // amount
943
- expect(EmailMocker.getSucceededEmail(0).html).toMatch(Formatter.price(12_99)); // unit price
943
+ expect(EmailMocker.getSucceededEmail(0).html).toMatch(Formatter.price(12_99_00)); // unit price
944
944
  expect(EmailMocker.getSucceededEmail(0).html).toMatch('<td>' + expectedAmount); // total price in table
945
945
  expect(EmailMocker.getSucceededEmail(0).html).toMatch('Test balance item'); // description
946
946
 
947
947
  expect(EmailMocker.getSucceededEmail(0).text).toMatch('2 x '); // amount
948
- expect(EmailMocker.getSucceededEmail(0).text).toMatch(Formatter.price(12_99)); // unit price
948
+ expect(EmailMocker.getSucceededEmail(0).text).toMatch(Formatter.price(12_99_00)); // unit price
949
949
  expect(EmailMocker.getSucceededEmail(0).text).toMatch(expectedAmount); // total price in table
950
950
  expect(EmailMocker.getSucceededEmail(0).text?.toLowerCase()).toMatch('test balance item'); // description
951
951
  }, 15_000);
@@ -23,11 +23,9 @@ type Attachment = { filename: string; path?: string; href?: string; content?: st
23
23
  function errorToSimpleErrors(e: unknown) {
24
24
  if (isSimpleErrors(e)) {
25
25
  return e;
26
- }
27
- else if (isSimpleError(e)) {
26
+ } else if (isSimpleError(e)) {
28
27
  return new SimpleErrors(e);
29
- }
30
- else {
28
+ } else {
31
29
  return new SimpleErrors(
32
30
  new SimpleError({
33
31
  code: 'unknown_error',
@@ -471,22 +469,22 @@ export class Email extends QueryableModel {
471
469
  case 'hard-bounce': {
472
470
  base.set('hardBouncesCount',
473
471
  SQL.calculation(SQL.column('hardBouncesCount'))
474
- .add(readDynamicSQLExpression(1))
475
- );
472
+ .add(readDynamicSQLExpression(1)),
473
+ );
476
474
  break;
477
475
  }
478
476
  case 'soft-bounce': {
479
477
  base.set('softBouncesCount',
480
478
  SQL.calculation(SQL.column('softBouncesCount'))
481
- .add(readDynamicSQLExpression(1))
482
- );
479
+ .add(readDynamicSQLExpression(1)),
480
+ );
483
481
  break;
484
482
  }
485
483
  case 'complaint': {
486
484
  base.set('spamComplaintsCount',
487
485
  SQL.calculation(SQL.column('spamComplaintsCount'))
488
- .add(readDynamicSQLExpression(1))
489
- );
486
+ .add(readDynamicSQLExpression(1)),
487
+ );
490
488
  break;
491
489
  }
492
490
  }
@@ -604,8 +602,7 @@ export class Email extends QueryableModel {
604
602
  });
605
603
  if (waitForSending) {
606
604
  return await this.resumeSending();
607
- }
608
- else {
605
+ } else {
609
606
  this.resumeSending().catch(console.error);
610
607
  }
611
608
  return this;
@@ -642,8 +639,7 @@ export class Email extends QueryableModel {
642
639
  contentType: attachment.contentType ?? undefined,
643
640
  encoding: 'base64',
644
641
  });
645
- }
646
- else {
642
+ } else {
647
643
  // Note: because we send lots of emails, we better download the file here so we can reuse it in every email instead of downloading it every time
648
644
  const withSigned = await attachment.file!.withSignedUrl();
649
645
  if (!withSigned || !withSigned.signedUrl) {
@@ -655,12 +651,11 @@ export class Email extends QueryableModel {
655
651
  }
656
652
 
657
653
  const filePath = withSigned.signedUrl;
658
- let fileBuffer: Buffer | null = null;
654
+ let fileBuffer: Buffer | null;
659
655
  try {
660
656
  const response = await fetch(filePath);
661
657
  fileBuffer = Buffer.from(await response.arrayBuffer());
662
- }
663
- catch (e) {
658
+ } catch (e) {
664
659
  throw new SimpleError({
665
660
  code: 'attachment_not_found',
666
661
  message: 'Attachment not found',
@@ -707,8 +702,7 @@ export class Email extends QueryableModel {
707
702
  // Update repacements that have been generated
708
703
  recipient.replacements = virtualRecipient.replacements;
709
704
  await recipient.save();
710
- }
711
- else {
705
+ } else {
712
706
  recipient.failCount += 1;
713
707
  recipient.failErrorMessage = error.message;
714
708
  if (recipient.failError) {
@@ -719,8 +713,7 @@ export class Email extends QueryableModel {
719
713
  recipient.lastFailedAt = new Date();
720
714
  await recipient.save();
721
715
  }
722
- }
723
- catch (e) {
716
+ } catch (e) {
724
717
  console.error(e);
725
718
  }
726
719
  promiseResolve();
@@ -761,8 +754,7 @@ export class Email extends QueryableModel {
761
754
  console.log('Email already sent, skipping...', upToDate.id);
762
755
  return upToDate;
763
756
  }
764
- }
765
- else {
757
+ } else {
766
758
  if (singleRecipientId) {
767
759
  // Not possible
768
760
  throw new SimpleError({
@@ -786,8 +778,7 @@ export class Email extends QueryableModel {
786
778
  await upToDate.save();
787
779
  return upToDate;
788
780
  }
789
- }
790
- else if (upToDate.status !== EmailStatus.Queued) {
781
+ } else if (upToDate.status !== EmailStatus.Queued) {
791
782
  console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
792
783
  return upToDate;
793
784
  }
@@ -888,8 +879,7 @@ export class Email extends QueryableModel {
888
879
 
889
880
  try {
890
881
  await upToDate.save();
891
- }
892
- finally {
882
+ } finally {
893
883
  isSavingStatus = false;
894
884
  }
895
885
  }
@@ -938,8 +928,7 @@ export class Email extends QueryableModel {
938
928
  // Failed or soft-failed
939
929
  if (recipient.failError && isSoftEmailRecipientError(recipient.failError)) {
940
930
  softFailedCount += 1;
941
- }
942
- else {
931
+ } else {
943
932
  failedCount += 1;
944
933
  }
945
934
  skipped++;
@@ -957,13 +946,11 @@ export class Email extends QueryableModel {
957
946
  if (recipient.sentAt) {
958
947
  succeededCount += 1;
959
948
  await saveStatus();
960
- }
961
- else {
949
+ } else {
962
950
  // Failed or soft-failed
963
951
  if (recipient.failError && isSoftEmailRecipientError(recipient.failError)) {
964
952
  softFailedCount += 1;
965
- }
966
- else {
953
+ } else {
967
954
  failedCount += 1;
968
955
  }
969
956
  await saveStatus();
@@ -973,14 +960,12 @@ export class Email extends QueryableModel {
973
960
 
974
961
  if (sendingPromises.length > 0 || skipped > 0) {
975
962
  await Promise.all(sendingPromises);
976
- }
977
- else {
963
+ } else {
978
964
  break;
979
965
  }
980
966
  }
981
967
  }
982
- }
983
- catch (e) {
968
+ } catch (e) {
984
969
  if (!upToDate) {
985
970
  throw e;
986
971
  }
@@ -1023,8 +1008,7 @@ export class Email extends QueryableModel {
1023
1008
  human: $t(`%1EG`),
1024
1009
  }),
1025
1010
  );
1026
- }
1027
- else {
1011
+ } else {
1028
1012
  upToDate.status = EmailStatus.Sent;
1029
1013
  upToDate.emailErrors = null;
1030
1014
  }
@@ -1115,8 +1099,7 @@ export class Email extends QueryableModel {
1115
1099
  await upToDate.save();
1116
1100
 
1117
1101
  console.log('Updated recipient count for email', id, 'to', count);
1118
- }
1119
- catch (e) {
1102
+ } catch (e) {
1120
1103
  if (isAbortedError(e)) {
1121
1104
  return;
1122
1105
  }
@@ -1263,8 +1246,7 @@ export class Email extends QueryableModel {
1263
1246
  recipient.replacements = merged;
1264
1247
 
1265
1248
  break;
1266
- }
1267
- else {
1249
+ } else {
1268
1250
  console.log('Could not merge duplicate email recipient', item.email, other.id, 'keeping both', other.replacements, item.replacements);
1269
1251
  }
1270
1252
  }
@@ -1283,8 +1265,7 @@ export class Email extends QueryableModel {
1283
1265
 
1284
1266
  if (!recipient.email || duplicateOfRecipientId) {
1285
1267
  countWithoutEmail += 1;
1286
- }
1287
- else {
1268
+ } else {
1288
1269
  count += 1;
1289
1270
  }
1290
1271
  }
@@ -1299,8 +1280,7 @@ export class Email extends QueryableModel {
1299
1280
  upToDate.recipientsErrors = null;
1300
1281
  upToDate.membersCount = membersSet.size;
1301
1282
  await upToDate.save();
1302
- }
1303
- catch (e: unknown) {
1283
+ } catch (e: unknown) {
1304
1284
  console.error('Failed to build recipients for email', id);
1305
1285
  console.error(e);
1306
1286
  upToDate.recipientsStatus = EmailRecipientsStatus.NotCreated;
@@ -1381,8 +1361,7 @@ export class Email extends QueryableModel {
1381
1361
  }
1382
1362
 
1383
1363
  console.warn('No example recipient found for email', id);
1384
- }
1385
- catch (e) {
1364
+ } catch (e) {
1386
1365
  console.error('Failed to build example recipient for email', id);
1387
1366
  console.error(e);
1388
1367
  }
@@ -1,15 +1,15 @@
1
1
  import { column } from '@simonbackx/simple-database';
2
2
  import { SimpleError } from '@simonbackx/simple-errors';
3
- import type {I18n} from '@stamhoofd/backend-i18n';
3
+ import type { I18n } from '@stamhoofd/backend-i18n';
4
4
  import { QueryableModel } from '@stamhoofd/sql';
5
- import { EmailTemplateType, Recipient, Replacement } from '@stamhoofd/structures';
5
+ import { appToUri, EmailTemplateType, getAppHost, Recipient, Replacement } from '@stamhoofd/structures';
6
6
  import basex from 'base-x';
7
7
  import crypto from 'crypto';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
9
  import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
10
10
  import { Platform } from './Platform.js';
11
- import type {User} from './User.js';
12
- import type {Organization} from './Organization.js';
11
+ import type { User } from './User.js';
12
+ import type { Organization } from './Organization.js';
13
13
 
14
14
  const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
15
15
  const bs58 = basex(ALPHABET);
@@ -121,20 +121,8 @@ export class EmailVerificationCode extends QueryableModel {
121
121
  }
122
122
 
123
123
  getEmailVerificationUrl(user: User, organization: Organization | null, i18n: I18n) {
124
- let host: string;
125
- if (user.permissions || !organization || STAMHOOFD.userMode === 'platform') {
126
- host = 'https://' + (STAMHOOFD.domains.dashboard ?? 'stamhoofd.app') + '/' + i18n.locale;
127
- }
128
- else {
129
- // Add language if different than default
130
- host = 'https://' + organization.getHost();
131
-
132
- if (i18n.language !== organization.i18n.language) {
133
- host += '/' + i18n.language;
134
- }
135
- }
136
-
137
- return host + '/verify-email' + (user.organizationPermissions && this.organizationId ? '/' + encodeURIComponent(this.organizationId) : '') + '?code=' + encodeURIComponent(this.code) + '&token=' + encodeURIComponent(this.token);
124
+ const host = getAppHost('verify-email', organization, !!user.permissions, i18n);
125
+ return 'https://' + host + '?code=' + encodeURIComponent(this.code) + '&token=' + encodeURIComponent(this.token) + '&email=' + encodeURIComponent(this.email);
138
126
  }
139
127
 
140
128
  /**
@@ -283,8 +271,7 @@ export class EmailVerificationCode extends QueryableModel {
283
271
  },
284
272
  type: 'transactional',
285
273
  });
286
- }
287
- else {
274
+ } else {
288
275
  await sendEmailTemplate(organization, {
289
276
  recipients: [
290
277
  Recipient.create({
@@ -359,8 +346,7 @@ export class EmailVerificationCode extends QueryableModel {
359
346
 
360
347
  // Expire in 3 hours
361
348
  verificationCode.expiresAt = new Date(new Date().getTime() + 1000 * 60 * 60 * 3);
362
- }
363
- else {
349
+ } else {
364
350
  verificationCode = verificationCodes[0];
365
351
 
366
352
  if (verificationCode.email !== email || verificationCode.expiresAt < new Date(new Date().getTime() - 15 * 60 * 1000) || verificationCode.tries >= EmailVerificationCode.MAX_TRIES) {
@@ -68,6 +68,10 @@ export class Event extends QueryableModel {
68
68
  });
69
69
  }
70
70
 
71
+ get slug() {
72
+ return this.getStructure().slug;
73
+ }
74
+
71
75
  /**
72
76
  * @deprecated
73
77
  */
@@ -101,8 +105,7 @@ export class Event extends QueryableModel {
101
105
  group.settings.requireOrganizationIds = [this.organizationId];
102
106
  group.settings.requireOrganizationTags = [];
103
107
  group.settings.requirePlatformMembershipOn = null;
104
- }
105
- else {
108
+ } else {
106
109
  group.settings.requireOrganizationTags = this.meta.organizationTagIds ?? [];
107
110
 
108
111
  // Everyone can register
@@ -118,8 +121,7 @@ export class Event extends QueryableModel {
118
121
  if (waitingList) {
119
122
  if (group.settings.allowRegistrationsByOrganization) {
120
123
  waitingList.settings.allowRegistrationsByOrganization = true;
121
- }
122
- else {
124
+ } else {
123
125
  waitingList.settings.allowRegistrationsByOrganization = false;
124
126
  }
125
127
  await this.syncGroupRequirements(waitingList);