@stamhoofd/models 2.63.0 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/dist/src/helpers/EmailBuilder.d.ts +6 -1
  2. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  3. package/dist/src/helpers/EmailBuilder.js +60 -20
  4. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  5. package/dist/src/helpers/MemberMerger.d.ts.map +1 -1
  6. package/dist/src/helpers/MemberMerger.js +1 -2
  7. package/dist/src/helpers/MemberMerger.js.map +1 -1
  8. package/dist/src/migrations/1605262045-import-postcodes.d.ts +3 -3
  9. package/dist/src/migrations/1605262045-import-postcodes.js +10 -13
  10. package/dist/src/migrations/1734429094-registration-trial-until.sql +3 -0
  11. package/dist/src/migrations/1734429095-membership-trial-until.sql +2 -0
  12. package/dist/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
  13. package/dist/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
  14. package/dist/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
  15. package/dist/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
  16. package/dist/src/migrations/1735573520-emails-email-type.sql +2 -0
  17. package/dist/src/migrations/1735573521-email-recipients-email-type.sql +4 -0
  18. package/dist/src/migrations/1735573522-emails-indexes.sql +3 -0
  19. package/dist/src/migrations/1735982691-cached-balance-email-reminder-counts.sql +4 -0
  20. package/dist/src/migrations/1735994471-default-email-templates.sql +5 -0
  21. package/dist/src/models/AuditLog.d.ts +2 -7
  22. package/dist/src/models/AuditLog.d.ts.map +1 -1
  23. package/dist/src/models/AuditLog.js +1 -15
  24. package/dist/src/models/AuditLog.js.map +1 -1
  25. package/dist/src/models/BalanceItem.d.ts +4 -13
  26. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  27. package/dist/src/models/BalanceItem.js +21 -33
  28. package/dist/src/models/BalanceItem.js.map +1 -1
  29. package/dist/src/models/BalanceItemPayment.d.ts +3 -2
  30. package/dist/src/models/BalanceItemPayment.d.ts.map +1 -1
  31. package/dist/src/models/BalanceItemPayment.js +2 -1
  32. package/dist/src/models/BalanceItemPayment.js.map +1 -1
  33. package/dist/src/models/BuckarooPayment.d.ts +2 -2
  34. package/dist/src/models/BuckarooPayment.d.ts.map +1 -1
  35. package/dist/src/models/BuckarooPayment.js +2 -1
  36. package/dist/src/models/BuckarooPayment.js.map +1 -1
  37. package/dist/src/models/CachedBalance.d.ts +12 -10
  38. package/dist/src/models/CachedBalance.d.ts.map +1 -1
  39. package/dist/src/models/CachedBalance.js +122 -39
  40. package/dist/src/models/CachedBalance.js.map +1 -1
  41. package/dist/src/models/Document.d.ts +3 -3
  42. package/dist/src/models/Document.d.ts.map +1 -1
  43. package/dist/src/models/Document.js +2 -1
  44. package/dist/src/models/Document.js.map +1 -1
  45. package/dist/src/models/DocumentTemplate.d.ts +2 -2
  46. package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
  47. package/dist/src/models/DocumentTemplate.js +38 -10
  48. package/dist/src/models/DocumentTemplate.js.map +1 -1
  49. package/dist/src/models/Email.d.ts +10 -4
  50. package/dist/src/models/Email.d.ts.map +1 -1
  51. package/dist/src/models/Email.js +68 -25
  52. package/dist/src/models/Email.js.map +1 -1
  53. package/dist/src/models/EmailRecipient.d.ts +14 -8
  54. package/dist/src/models/EmailRecipient.d.ts.map +1 -1
  55. package/dist/src/models/EmailRecipient.js +19 -14
  56. package/dist/src/models/EmailRecipient.js.map +1 -1
  57. package/dist/src/models/EmailTemplate.d.ts +2 -7
  58. package/dist/src/models/EmailTemplate.d.ts.map +1 -1
  59. package/dist/src/models/EmailTemplate.js +1 -15
  60. package/dist/src/models/EmailTemplate.js.map +1 -1
  61. package/dist/src/models/EmailVerificationCode.d.ts +2 -2
  62. package/dist/src/models/EmailVerificationCode.d.ts.map +1 -1
  63. package/dist/src/models/EmailVerificationCode.js +2 -1
  64. package/dist/src/models/EmailVerificationCode.js.map +1 -1
  65. package/dist/src/models/Event.d.ts +2 -2
  66. package/dist/src/models/Event.d.ts.map +1 -1
  67. package/dist/src/models/Event.js +2 -1
  68. package/dist/src/models/Event.js.map +1 -1
  69. package/dist/src/models/Group.d.ts +2 -7
  70. package/dist/src/models/Group.d.ts.map +1 -1
  71. package/dist/src/models/Group.js +1 -17
  72. package/dist/src/models/Group.js.map +1 -1
  73. package/dist/src/models/Image.d.ts +2 -2
  74. package/dist/src/models/Image.d.ts.map +1 -1
  75. package/dist/src/models/Image.js +2 -1
  76. package/dist/src/models/Image.js.map +1 -1
  77. package/dist/src/models/Member.d.ts +6 -9
  78. package/dist/src/models/Member.d.ts.map +1 -1
  79. package/dist/src/models/Member.js +3 -42
  80. package/dist/src/models/Member.js.map +1 -1
  81. package/dist/src/models/MemberPlatformMembership.d.ts +12 -8
  82. package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
  83. package/dist/src/models/MemberPlatformMembership.js +80 -20
  84. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  85. package/dist/src/models/MemberResponsibilityRecord.d.ts +2 -7
  86. package/dist/src/models/MemberResponsibilityRecord.d.ts.map +1 -1
  87. package/dist/src/models/MemberResponsibilityRecord.js +1 -15
  88. package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
  89. package/dist/src/models/MemberUser.d.ts +8 -0
  90. package/dist/src/models/MemberUser.d.ts.map +1 -0
  91. package/dist/src/models/MemberUser.js +26 -0
  92. package/dist/src/models/MemberUser.js.map +1 -0
  93. package/dist/src/models/MergedMember.d.ts +3 -3
  94. package/dist/src/models/MergedMember.d.ts.map +1 -1
  95. package/dist/src/models/MergedMember.js +3 -2
  96. package/dist/src/models/MergedMember.js.map +1 -1
  97. package/dist/src/models/MolliePayment.d.ts +2 -2
  98. package/dist/src/models/MolliePayment.d.ts.map +1 -1
  99. package/dist/src/models/MolliePayment.js +2 -1
  100. package/dist/src/models/MolliePayment.js.map +1 -1
  101. package/dist/src/models/MollieToken.d.ts +2 -2
  102. package/dist/src/models/MollieToken.d.ts.map +1 -1
  103. package/dist/src/models/MollieToken.js +2 -1
  104. package/dist/src/models/MollieToken.js.map +1 -1
  105. package/dist/src/models/OneTimeToken.d.ts +2 -2
  106. package/dist/src/models/OneTimeToken.d.ts.map +1 -1
  107. package/dist/src/models/OneTimeToken.js +2 -1
  108. package/dist/src/models/OneTimeToken.js.map +1 -1
  109. package/dist/src/models/Order.d.ts +3 -2
  110. package/dist/src/models/Order.d.ts.map +1 -1
  111. package/dist/src/models/Order.js +2 -1
  112. package/dist/src/models/Order.js.map +1 -1
  113. package/dist/src/models/Organization.d.ts +2 -2
  114. package/dist/src/models/Organization.d.ts.map +1 -1
  115. package/dist/src/models/Organization.js +2 -1
  116. package/dist/src/models/Organization.js.map +1 -1
  117. package/dist/src/models/OrganizationRegistrationPeriod.d.ts +2 -2
  118. package/dist/src/models/OrganizationRegistrationPeriod.d.ts.map +1 -1
  119. package/dist/src/models/OrganizationRegistrationPeriod.js +2 -1
  120. package/dist/src/models/OrganizationRegistrationPeriod.js.map +1 -1
  121. package/dist/src/models/PasswordToken.d.ts +3 -2
  122. package/dist/src/models/PasswordToken.d.ts.map +1 -1
  123. package/dist/src/models/PasswordToken.js +2 -1
  124. package/dist/src/models/PasswordToken.js.map +1 -1
  125. package/dist/src/models/PayconiqPayment.d.ts +2 -2
  126. package/dist/src/models/PayconiqPayment.d.ts.map +1 -1
  127. package/dist/src/models/PayconiqPayment.js +2 -1
  128. package/dist/src/models/PayconiqPayment.js.map +1 -1
  129. package/dist/src/models/Payment.d.ts +2 -7
  130. package/dist/src/models/Payment.d.ts.map +1 -1
  131. package/dist/src/models/Payment.js +1 -15
  132. package/dist/src/models/Payment.js.map +1 -1
  133. package/dist/src/models/Platform.d.ts +5 -3
  134. package/dist/src/models/Platform.d.ts.map +1 -1
  135. package/dist/src/models/Platform.js +11 -2
  136. package/dist/src/models/Platform.js.map +1 -1
  137. package/dist/src/models/RegisterCode.d.ts +2 -2
  138. package/dist/src/models/RegisterCode.d.ts.map +1 -1
  139. package/dist/src/models/RegisterCode.js +2 -1
  140. package/dist/src/models/RegisterCode.js.map +1 -1
  141. package/dist/src/models/Registration.d.ts +20 -7
  142. package/dist/src/models/Registration.d.ts.map +1 -1
  143. package/dist/src/models/Registration.js +25 -61
  144. package/dist/src/models/Registration.js.map +1 -1
  145. package/dist/src/models/RegistrationPeriod.d.ts +4 -2
  146. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  147. package/dist/src/models/RegistrationPeriod.js +23 -1
  148. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  149. package/dist/src/models/STCredit.d.ts +2 -2
  150. package/dist/src/models/STCredit.d.ts.map +1 -1
  151. package/dist/src/models/STCredit.js +2 -1
  152. package/dist/src/models/STCredit.js.map +1 -1
  153. package/dist/src/models/STInvoice.d.ts +3 -2
  154. package/dist/src/models/STInvoice.d.ts.map +1 -1
  155. package/dist/src/models/STInvoice.js +2 -1
  156. package/dist/src/models/STInvoice.js.map +1 -1
  157. package/dist/src/models/STPackage.d.ts +2 -2
  158. package/dist/src/models/STPackage.d.ts.map +1 -1
  159. package/dist/src/models/STPackage.js +2 -1
  160. package/dist/src/models/STPackage.js.map +1 -1
  161. package/dist/src/models/STPendingInvoice.d.ts +3 -2
  162. package/dist/src/models/STPendingInvoice.d.ts.map +1 -1
  163. package/dist/src/models/STPendingInvoice.js +2 -1
  164. package/dist/src/models/STPendingInvoice.js.map +1 -1
  165. package/dist/src/models/StripeAccount.d.ts +2 -2
  166. package/dist/src/models/StripeAccount.d.ts.map +1 -1
  167. package/dist/src/models/StripeAccount.js +2 -1
  168. package/dist/src/models/StripeAccount.js.map +1 -1
  169. package/dist/src/models/StripeCheckoutSession.d.ts +2 -2
  170. package/dist/src/models/StripeCheckoutSession.d.ts.map +1 -1
  171. package/dist/src/models/StripeCheckoutSession.js +2 -1
  172. package/dist/src/models/StripeCheckoutSession.js.map +1 -1
  173. package/dist/src/models/StripePaymentIntent.d.ts +2 -2
  174. package/dist/src/models/StripePaymentIntent.d.ts.map +1 -1
  175. package/dist/src/models/StripePaymentIntent.js +2 -1
  176. package/dist/src/models/StripePaymentIntent.js.map +1 -1
  177. package/dist/src/models/Ticket.d.ts +3 -2
  178. package/dist/src/models/Ticket.d.ts.map +1 -1
  179. package/dist/src/models/Ticket.js +2 -1
  180. package/dist/src/models/Ticket.js.map +1 -1
  181. package/dist/src/models/Token.d.ts +3 -2
  182. package/dist/src/models/Token.d.ts.map +1 -1
  183. package/dist/src/models/Token.js +2 -1
  184. package/dist/src/models/Token.js.map +1 -1
  185. package/dist/src/models/UsedRegisterCode.d.ts +2 -2
  186. package/dist/src/models/UsedRegisterCode.d.ts.map +1 -1
  187. package/dist/src/models/UsedRegisterCode.js +2 -1
  188. package/dist/src/models/UsedRegisterCode.js.map +1 -1
  189. package/dist/src/models/User.d.ts +7 -2
  190. package/dist/src/models/User.d.ts.map +1 -1
  191. package/dist/src/models/User.js +27 -4
  192. package/dist/src/models/User.js.map +1 -1
  193. package/dist/src/models/UserPermissions.d.ts +3 -2
  194. package/dist/src/models/UserPermissions.d.ts.map +1 -1
  195. package/dist/src/models/UserPermissions.js +2 -1
  196. package/dist/src/models/UserPermissions.js.map +1 -1
  197. package/dist/src/models/Webshop.d.ts +3 -2
  198. package/dist/src/models/Webshop.d.ts.map +1 -1
  199. package/dist/src/models/Webshop.js +2 -1
  200. package/dist/src/models/Webshop.js.map +1 -1
  201. package/dist/src/models/WebshopDiscountCode.d.ts +2 -2
  202. package/dist/src/models/WebshopDiscountCode.d.ts.map +1 -1
  203. package/dist/src/models/WebshopDiscountCode.js +2 -1
  204. package/dist/src/models/WebshopDiscountCode.js.map +1 -1
  205. package/dist/src/models/addresses/City.d.ts +3 -2
  206. package/dist/src/models/addresses/City.d.ts.map +1 -1
  207. package/dist/src/models/addresses/City.js +2 -1
  208. package/dist/src/models/addresses/City.js.map +1 -1
  209. package/dist/src/models/addresses/PostalCode.d.ts +3 -2
  210. package/dist/src/models/addresses/PostalCode.d.ts.map +1 -1
  211. package/dist/src/models/addresses/PostalCode.js +2 -1
  212. package/dist/src/models/addresses/PostalCode.js.map +1 -1
  213. package/dist/src/models/addresses/Province.d.ts +2 -2
  214. package/dist/src/models/addresses/Province.d.ts.map +1 -1
  215. package/dist/src/models/addresses/Province.js +2 -1
  216. package/dist/src/models/addresses/Province.js.map +1 -1
  217. package/dist/src/models/addresses/Street.d.ts +3 -2
  218. package/dist/src/models/addresses/Street.d.ts.map +1 -1
  219. package/dist/src/models/addresses/Street.js +2 -1
  220. package/dist/src/models/addresses/Street.js.map +1 -1
  221. package/dist/src/models/index.d.ts +1 -0
  222. package/dist/src/models/index.d.ts.map +1 -1
  223. package/dist/src/models/index.js +1 -0
  224. package/dist/src/models/index.js.map +1 -1
  225. package/dist/tsconfig.tsbuildinfo +1 -1
  226. package/package.json +2 -2
  227. package/src/helpers/EmailBuilder.ts +82 -27
  228. package/src/helpers/MemberMerger.ts +2 -3
  229. package/src/migrations/1605262045-import-postcodes.ts +6 -9
  230. package/src/migrations/1734429094-registration-trial-until.sql +3 -0
  231. package/src/migrations/1734429095-membership-trial-until.sql +2 -0
  232. package/src/migrations/1734535120-registration-period-previous-period-id.sql +3 -0
  233. package/src/migrations/1734535121-platform-previous-period-id.sql +3 -0
  234. package/src/migrations/1734626607-cached-balance-amount-open.sql +2 -0
  235. package/src/migrations/1734698906-cached-balance-amount-paid.sql +2 -0
  236. package/src/migrations/1735573520-emails-email-type.sql +2 -0
  237. package/src/migrations/1735573521-email-recipients-email-type.sql +4 -0
  238. package/src/migrations/1735573522-emails-indexes.sql +3 -0
  239. package/src/migrations/1735982691-cached-balance-email-reminder-counts.sql +4 -0
  240. package/src/migrations/1735994471-default-email-templates.sql +5 -0
  241. package/src/models/AuditLog.ts +3 -21
  242. package/src/models/BalanceItem.ts +30 -46
  243. package/src/models/BalanceItemPayment.ts +3 -2
  244. package/src/models/BuckarooPayment.ts +3 -2
  245. package/src/models/CachedBalance.ts +166 -46
  246. package/src/models/Document.ts +4 -3
  247. package/src/models/DocumentTemplate.ts +43 -12
  248. package/src/models/Email.ts +80 -32
  249. package/src/models/EmailRecipient.ts +20 -20
  250. package/src/models/EmailTemplate.ts +3 -21
  251. package/src/models/EmailVerificationCode.ts +3 -2
  252. package/src/models/Event.ts +3 -2
  253. package/src/models/Group.ts +4 -23
  254. package/src/models/Image.ts +3 -2
  255. package/src/models/Member.ts +6 -52
  256. package/src/models/MemberPlatformMembership.ts +95 -26
  257. package/src/models/MemberResponsibilityRecord.ts +3 -21
  258. package/src/models/MemberUser.ts +18 -0
  259. package/src/models/MergedMember.ts +4 -3
  260. package/src/models/MolliePayment.ts +3 -2
  261. package/src/models/MollieToken.ts +3 -2
  262. package/src/models/OneTimeToken.ts +3 -2
  263. package/src/models/Order.ts +3 -2
  264. package/src/models/Organization.ts +3 -2
  265. package/src/models/OrganizationRegistrationPeriod.ts +3 -2
  266. package/src/models/PasswordToken.ts +3 -2
  267. package/src/models/PayconiqPayment.ts +3 -2
  268. package/src/models/Payment.ts +3 -21
  269. package/src/models/Platform.ts +13 -4
  270. package/src/models/RegisterCode.ts +3 -2
  271. package/src/models/Registration.ts +24 -68
  272. package/src/models/RegistrationPeriod.ts +30 -3
  273. package/src/models/STCredit.ts +3 -2
  274. package/src/models/STInvoice.ts +3 -2
  275. package/src/models/STPackage.ts +3 -2
  276. package/src/models/STPendingInvoice.ts +3 -2
  277. package/src/models/StripeAccount.ts +3 -2
  278. package/src/models/StripeCheckoutSession.ts +3 -2
  279. package/src/models/StripePaymentIntent.ts +3 -2
  280. package/src/models/Ticket.ts +3 -2
  281. package/src/models/Token.ts +3 -2
  282. package/src/models/UsedRegisterCode.ts +3 -2
  283. package/src/models/User.ts +31 -3
  284. package/src/models/UserPermissions.ts +3 -2
  285. package/src/models/Webshop.ts +3 -2
  286. package/src/models/WebshopDiscountCode.ts +3 -2
  287. package/src/models/addresses/City.ts +3 -2
  288. package/src/models/addresses/PostalCode.ts +3 -2
  289. package/src/models/addresses/Province.ts +3 -2
  290. package/src/models/addresses/Street.ts +3 -2
  291. package/src/models/index.ts +1 -0
@@ -1,5 +1,5 @@
1
- import { column, Model } from '@simonbackx/simple-database';
2
- import { EditorSmartButton, EditorSmartVariable, EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse, Recipient, SortItemDirection, StamhoofdFilter } from '@stamhoofd/structures';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse, Recipient, Replacement, SortItemDirection, StamhoofdFilter } from '@stamhoofd/structures';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
 
5
5
  import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
@@ -7,13 +7,14 @@ import { SimpleError } from '@simonbackx/simple-errors';
7
7
  import { I18n } from '@stamhoofd/backend-i18n';
8
8
  import { Email as EmailClass } from '@stamhoofd/email';
9
9
  import { QueueHandler } from '@stamhoofd/queues';
10
- import { SQL, SQLWhereSign } from '@stamhoofd/sql';
10
+ import { QueryableModel, SQL, SQLWhereSign } from '@stamhoofd/sql';
11
11
  import { Formatter } from '@stamhoofd/utility';
12
- import { getEmailBuilder } from '../helpers/EmailBuilder';
12
+ import { fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
13
13
  import { EmailRecipient } from './EmailRecipient';
14
14
  import { Organization } from './Organization';
15
+ import { EmailTemplate } from './EmailTemplate';
15
16
 
16
- export class Email extends Model {
17
+ export class Email extends QueryableModel {
17
18
  static table = 'emails';
18
19
 
19
20
  @column({
@@ -32,6 +33,13 @@ export class Email extends Model {
32
33
  @column({ type: 'json', decoder: EmailRecipientFilter })
33
34
  recipientFilter: EmailRecipientFilter = EmailRecipientFilter.create({});
34
35
 
36
+ /**
37
+ * Helper to prevent sending too many emails to the same person.
38
+ * Allows for filtering on objects that didn't receive a specific email yet
39
+ */
40
+ @column({ type: 'string', nullable: true })
41
+ emailType: string | null = null;
42
+
35
43
  @column({ type: 'string', nullable: true })
36
44
  subject: string | null;
37
45
 
@@ -204,7 +212,29 @@ export class Email extends Model {
204
212
  return '"' + cleanedName + '" <' + address + '>';
205
213
  }
206
214
 
207
- async send() {
215
+ async setFromTemplate(type: EmailTemplateType) {
216
+ // Most specific template: for specific group
217
+ let templates = (await EmailTemplate.where({ type, organizationId: this.organizationId, groupId: null, webshopId: null }));
218
+
219
+ // Then default
220
+ if (templates.length === 0 && this.organizationId) {
221
+ templates = (await EmailTemplate.where({ type, organizationId: null, groupId: null, webshopId: null }));
222
+ }
223
+
224
+ if (templates.length === 0) {
225
+ // No default
226
+ return false;
227
+ }
228
+ const defaultTemplate = templates[0];
229
+ this.html = defaultTemplate.html;
230
+ this.text = defaultTemplate.text;
231
+ this.subject = defaultTemplate.subject;
232
+ this.json = defaultTemplate.json;
233
+
234
+ return true;
235
+ }
236
+
237
+ async send(): Promise<Email | null> {
208
238
  this.throwIfNotReadyToSend();
209
239
  await this.save();
210
240
 
@@ -218,10 +248,27 @@ export class Email extends Model {
218
248
  human: 'De e-mail die je probeert te versturen bestaat niet meer',
219
249
  });
220
250
  }
221
- if (upToDate.status === EmailStatus.Sent) {
251
+ if (upToDate.status === EmailStatus.Sent || upToDate.status === EmailStatus.Failed) {
222
252
  // Already done
223
253
  // In other cases -> queue has stopped and we can retry
224
- return;
254
+ return upToDate;
255
+ }
256
+
257
+ if (upToDate.status === EmailStatus.Sending) {
258
+ // This is a retry.
259
+ if (upToDate.emailType) {
260
+ // Not eligible for retry
261
+ upToDate.status = EmailStatus.Failed;
262
+ await upToDate.save();
263
+ return upToDate;
264
+ }
265
+ if (upToDate.createdAt < new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 2)) {
266
+ // Too long
267
+ console.error('Email has been sending for too long. Marking as failed...', upToDate.id);
268
+ upToDate.status = EmailStatus.Failed;
269
+ await upToDate.save();
270
+ return upToDate;
271
+ }
225
272
  }
226
273
  const organization = upToDate.organizationId ? await Organization.getByID(upToDate.organizationId) : null;
227
274
  upToDate.throwIfNotReadyToSend();
@@ -345,9 +392,7 @@ export class Email extends Model {
345
392
  });
346
393
  sendingPromises.push(promise);
347
394
 
348
- const virtualRecipient = Recipient.create({
349
- ...recipient,
350
- });
395
+ const virtualRecipient = recipient.getRecipient();
351
396
 
352
397
  const callback = async (error: Error | null) => {
353
398
  if (error === null) {
@@ -378,7 +423,7 @@ export class Email extends Model {
378
423
  replyTo,
379
424
  subject: upToDate.subject!,
380
425
  html: upToDate.html!,
381
- type: 'broadcast',
426
+ type: upToDate.emailType ? 'transactional' : 'broadcast',
382
427
  attachments,
383
428
  callback(error: Error | null) {
384
429
  callback(error).catch(console.error);
@@ -391,11 +436,18 @@ export class Email extends Model {
391
436
  await Promise.all(sendingPromises);
392
437
  }
393
438
 
394
- console.log('Finished sending email', upToDate.id);
439
+ if (upToDate.recipientCount === 0 && upToDate.userId === null) {
440
+ // We only delete automated emails (email type) if they have no recipients
441
+ console.log('No recipients found for email ', upToDate.id, ' deleting...');
442
+ await upToDate.delete();
443
+ return null;
444
+ }
395
445
 
446
+ console.log('Finished sending email', upToDate.id);
396
447
  // Mark email as sent
397
448
  upToDate.status = EmailStatus.Sent;
398
449
  await upToDate.save();
450
+ return upToDate;
399
451
  });
400
452
  }
401
453
 
@@ -498,18 +550,19 @@ export class Email extends Model {
498
550
  let request: LimitedFilteredRequest | null = new LimitedFilteredRequest({
499
551
  filter: subfilter.filter,
500
552
  sort: [{ key: 'id', order: SortItemDirection.ASC }],
501
- limit: 1000,
553
+ limit: 100,
502
554
  search: subfilter.search,
503
555
  });
504
556
 
505
557
  while (request) {
506
- console.log('Loading email page', subfilter.type, request);
507
558
  const response = await loader.fetch(request, subfilter.subfilter);
508
559
 
509
560
  count += response.results.length;
510
561
 
511
562
  for (const item of response.results) {
512
563
  const recipient = new EmailRecipient();
564
+ recipient.emailType = upToDate.emailType;
565
+ recipient.objectId = item.objectId;
513
566
  recipient.emailId = upToDate.id;
514
567
  recipient.email = item.email;
515
568
  recipient.firstName = item.firstName;
@@ -523,10 +576,6 @@ export class Email extends Model {
523
576
  }
524
577
  }
525
578
 
526
- // todo: loop all members that match the filter in batches of 1000
527
- // create a new row for every member + calculate the replacement values
528
- // todo: do intermediate checks on whether the email was deleted, and stop processing if needed
529
-
530
579
  upToDate.recipientsStatus = EmailRecipientsStatus.Created;
531
580
  upToDate.recipientCount = count;
532
581
  await upToDate.save();
@@ -578,7 +627,6 @@ export class Email extends Model {
578
627
  });
579
628
 
580
629
  while (request) {
581
- console.log('Loading email page', subfilter.type, request);
582
630
  const response = await loader.fetch(request, subfilter.subfilter);
583
631
 
584
632
  // Note: it is possible that a result in the database doesn't return a recipient (in memory filtering)
@@ -613,32 +661,32 @@ export class Email extends Model {
613
661
  }
614
662
 
615
663
  async getPreviewStructure() {
616
- const recipient = await SQL.select()
617
- .from(SQL.table(EmailRecipient.table))
618
- .where(SQL.column('emailId'), this.id)
664
+ const emailRecipient = await EmailRecipient.select()
665
+ .where('emailId', this.id)
619
666
  .first(false);
620
667
 
621
668
  let recipientRow: EmailRecipientStruct | undefined;
622
669
 
623
- if (recipient) {
624
- const emailRecipient = EmailRecipient.fromRow(recipient[EmailRecipient.table]);
625
- if (emailRecipient) {
626
- recipientRow = emailRecipient.getStructure();
627
- }
670
+ if (emailRecipient) {
671
+ recipientRow = emailRecipient.getStructure();
628
672
  }
629
673
 
630
674
  if (!recipientRow) {
631
675
  recipientRow = getExampleRecipient();
632
676
  }
633
677
 
634
- const smartVariables = recipientRow ? EditorSmartVariable.forRecipient(recipientRow) : [];
635
- const smartButtons = recipientRow ? EditorSmartButton.forRecipient(recipientRow) : [];
678
+ const virtualRecipient = recipientRow.getRecipient();
679
+
680
+ await fillRecipientReplacements(virtualRecipient, {
681
+ organization: this.organizationId ? (await Organization.getByID(this.organizationId))! : null,
682
+ fromAddress: this.fromAddress,
683
+ });
684
+
685
+ recipientRow.replacements = virtualRecipient.replacements;
636
686
 
637
687
  return EmailPreview.create({
638
688
  ...this,
639
689
  exampleRecipient: recipientRow,
640
- smartVariables,
641
- smartButtons,
642
690
  });
643
691
  }
644
692
  }
@@ -1,11 +1,11 @@
1
- import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
2
- import { EmailRecipient as EmailRecipientStruct, Replacement } from '@stamhoofd/structures';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { EmailRecipient as EmailRecipientStruct, Recipient, Replacement } from '@stamhoofd/structures';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
 
5
5
  import { ArrayDecoder } from '@simonbackx/simple-encoding';
6
- import { SQL, SQLSelect } from '@stamhoofd/sql';
6
+ import { QueryableModel } from '@stamhoofd/sql';
7
7
 
8
- export class EmailRecipient extends Model {
8
+ export class EmailRecipient extends QueryableModel {
9
9
  static table = 'email_recipients';
10
10
 
11
11
  @column({
@@ -18,6 +18,20 @@ export class EmailRecipient extends Model {
18
18
  @column({ type: 'string' })
19
19
  emailId: string;
20
20
 
21
+ /**
22
+ * Helper to prevent sending too many emails to the same person.
23
+ * Allows for filtering on objects that didn't receive a specific email yet
24
+ */
25
+ @column({ type: 'string', nullable: true })
26
+ objectId: string | null = null;
27
+
28
+ /**
29
+ * Helper to prevent sending too many emails to the same person.
30
+ * Allows for filtering on objects that didn't receive a specific email yet
31
+ */
32
+ @column({ type: 'string', nullable: true })
33
+ emailType: string | null = null;
34
+
21
35
  @column({ type: 'string', nullable: true })
22
36
  firstName: string | null = null;
23
37
 
@@ -80,21 +94,7 @@ export class EmailRecipient extends Model {
80
94
  return EmailRecipientStruct.create(this);
81
95
  }
82
96
 
83
- /**
84
- * Experimental: needs to move to library
85
- */
86
- static select() {
87
- const transformer = (row: SQLResultNamespacedRow): EmailRecipient => {
88
- const d = (this as typeof EmailRecipient & typeof Model).fromRow(row[this.table] as any) as EmailRecipient | undefined;
89
-
90
- if (!d) {
91
- throw new Error('EmailTemplate not found');
92
- }
93
-
94
- return d;
95
- };
96
-
97
- const select = new SQLSelect(transformer, SQL.wildcard());
98
- return select.from(SQL.table(this.table));
97
+ getRecipient() {
98
+ return this.getStructure().getRecipient();
99
99
  }
100
100
  }
@@ -1,13 +1,13 @@
1
- import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
2
  import { AnyDecoder } from '@simonbackx/simple-encoding';
3
- import { SQL, SQLSelect } from '@stamhoofd/sql';
3
+ import { QueryableModel } from '@stamhoofd/sql';
4
4
  import { EmailTemplate as EmailTemplateStruct, EmailTemplateType } from '@stamhoofd/structures';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
 
7
7
  /**
8
8
  * Holds the challenges for a given email. User should not exist, since that would allow user enumeration attacks
9
9
  */
10
- export class EmailTemplate extends Model {
10
+ export class EmailTemplate extends QueryableModel {
11
11
  static table = 'email_templates';
12
12
 
13
13
  @column({
@@ -65,22 +65,4 @@ export class EmailTemplate extends Model {
65
65
  getStructure() {
66
66
  return EmailTemplateStruct.create(this);
67
67
  }
68
-
69
- /**
70
- * Experimental: needs to move to library
71
- */
72
- static select() {
73
- const transformer = (row: SQLResultNamespacedRow): EmailTemplate => {
74
- const d = (this as typeof EmailTemplate & typeof Model).fromRow(row[this.table] as any) as EmailTemplate | undefined;
75
-
76
- if (!d) {
77
- throw new Error('EmailTemplate not found');
78
- }
79
-
80
- return d;
81
- };
82
-
83
- const select = new SQLSelect(transformer, SQL.wildcard());
84
- return select.from(SQL.table(this.table));
85
- }
86
68
  }
@@ -1,6 +1,7 @@
1
- import { column, Model } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
2
  import { SimpleError } from '@simonbackx/simple-errors';
3
3
  import { I18n } from '@stamhoofd/backend-i18n';
4
+ import { QueryableModel } from '@stamhoofd/sql';
4
5
  import { EmailTemplateType, Recipient, Replacement } from '@stamhoofd/structures';
5
6
  import basex from 'base-x';
6
7
  import crypto from 'crypto';
@@ -38,7 +39,7 @@ async function randomInt(max: number): Promise<number> {
38
39
  /**
39
40
  * Holds the verificationCodes for a given email (not a user, since a user can switch email addresses and might avoid verification that way)
40
41
  */
41
- export class EmailVerificationCode extends Model {
42
+ export class EmailVerificationCode extends QueryableModel {
42
43
  static table = 'email_verification_codes';
43
44
 
44
45
  @column({
@@ -1,9 +1,10 @@
1
- import { column, Model } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { QueryableModel } from '@stamhoofd/sql';
2
3
  import { EventMeta, Event as EventStruct, GroupType } from '@stamhoofd/structures';
3
4
  import { v4 as uuidv4 } from 'uuid';
4
5
  import { Group } from './Group';
5
6
 
6
- export class Event extends Model {
7
+ export class Event extends QueryableModel {
7
8
  static table = 'events';
8
9
 
9
10
  @column({ primary: true, type: 'string', beforeSave(value) {
@@ -1,10 +1,10 @@
1
- import { column, Database, ManyToOneRelation, Model, OneToManyRelation, SQLResultNamespacedRow } from '@simonbackx/simple-database';
1
+ import { column, Database, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
2
2
  import { GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType, StockReservation } from '@stamhoofd/structures';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
 
5
5
  import { ArrayDecoder } from '@simonbackx/simple-encoding';
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { SQL, SQLSelect } from '@stamhoofd/sql';
7
+ import { QueryableModel } from '@stamhoofd/sql';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
9
  import { Member, MemberWithRegistrations, OrganizationRegistrationPeriod, Payment, Registration, User } from './';
10
10
 
@@ -21,7 +21,7 @@ if (Registration === undefined) {
21
21
  throw new Error('Import Registration is undefined');
22
22
  }
23
23
 
24
- export class Group extends Model {
24
+ export class Group extends QueryableModel {
25
25
  static table = 'groups';
26
26
 
27
27
  @column({
@@ -100,10 +100,9 @@ export class Group extends Model {
100
100
  static async getAll(organizationId: string, periodId: string | null, active = true) {
101
101
  const w: any = periodId ? { periodId } : {};
102
102
  if (active) {
103
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
104
103
  return await Group.where({ organizationId, deletedAt: null, ...w });
105
104
  }
106
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
105
+
107
106
  return await Group.where({ organizationId, ...w });
108
107
  }
109
108
 
@@ -299,24 +298,6 @@ export class Group extends Model {
299
298
  static async freeStockReservations(groupId: string, reservations: StockReservation[]) {
300
299
  return await this.applyStockReservations(groupId, reservations, true);
301
300
  }
302
-
303
- /**
304
- * Experimental: needs to move to library
305
- */
306
- static select() {
307
- const transformer = (row: SQLResultNamespacedRow): Group => {
308
- const d = (this as typeof Group & typeof Model).fromRow(row[this.table] as any) as Group | undefined;
309
-
310
- if (!d) {
311
- throw new Error('EmailTemplate not found');
312
- }
313
-
314
- return d;
315
- };
316
-
317
- const select = new SQLSelect(transformer, SQL.wildcard());
318
- return select.from(SQL.table(this.table));
319
- }
320
301
  }
321
302
 
322
303
  Registration.group = new ManyToOneRelation(Group, 'group');
@@ -1,12 +1,13 @@
1
- import { column, Model } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
2
  import { ArrayDecoder } from '@simonbackx/simple-encoding';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { QueryableModel } from '@stamhoofd/sql';
4
5
  import { File, Resolution, ResolutionRequest } from '@stamhoofd/structures';
5
6
  import AWS from 'aws-sdk';
6
7
  import sharp from 'sharp';
7
8
  import { v4 as uuidv4 } from 'uuid';
8
9
 
9
- export class Image extends Model {
10
+ export class Image extends QueryableModel {
10
11
  static table = 'images';
11
12
 
12
13
  @column({ primary: true, type: 'string', beforeSave(value) {
@@ -1,6 +1,6 @@
1
- import { column, Database, ManyToManyRelation, ManyToOneRelation, Model, OneToManyRelation } from '@simonbackx/simple-database';
2
- import { SQL } from '@stamhoofd/sql';
3
- import { MemberDetails, MemberWithRegistrationsBlob, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
1
+ import { column, Database, ManyToManyRelation, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
2
+ import { QueryableModel, SQL } from '@stamhoofd/sql';
3
+ import { MemberDetails, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
 
@@ -13,7 +13,7 @@ export type MemberWithRegistrations = Member & {
13
13
  // Defined here to prevent cycles
14
14
  export type RegistrationWithMember = Registration & { member: Member };
15
15
 
16
- export class Member extends Model {
16
+ export class Member extends QueryableModel {
17
17
  static table = 'members';
18
18
 
19
19
  // #region Columns
@@ -63,7 +63,8 @@ export class Member extends Model {
63
63
  details: MemberDetails;
64
64
 
65
65
  /**
66
- * Not yet paid balance
66
+ * @deprecated
67
+ * Unreliable since a member can have outstanding balance to multiple organizations now
67
68
  */
68
69
  @column({ type: 'integer' })
69
70
  outstandingBalance = 0;
@@ -103,44 +104,6 @@ export class Member extends Model {
103
104
  return (await this.getBlobByIds(id))[0] ?? null;
104
105
  }
105
106
 
106
- /**
107
- * Update the outstanding balance of multiple members in one go (or all members)
108
- */
109
- static async updateOutstandingBalance(memberIds: string[] | 'all') {
110
- if (memberIds !== 'all' && memberIds.length == 0) {
111
- return;
112
- }
113
-
114
- const params: any[] = [];
115
- let firstWhere = '';
116
- let secondWhere = '';
117
-
118
- if (memberIds !== 'all') {
119
- firstWhere = ` AND memberId IN (?)`;
120
- params.push(memberIds);
121
-
122
- secondWhere = `WHERE members.id IN (?)`;
123
- params.push(memberIds);
124
- }
125
-
126
- const query = `UPDATE
127
- members
128
- LEFT JOIN (
129
- SELECT
130
- memberId,
131
- sum(unitPrice * amount) - sum(pricePaid) AS outstandingBalance
132
- FROM
133
- balance_items
134
- WHERE status != 'Hidden'${firstWhere}
135
- GROUP BY
136
- memberId
137
- ) i ON i.memberId = members.id
138
- SET members.outstandingBalance = COALESCE(i.outstandingBalance, 0)
139
- ${secondWhere}`;
140
-
141
- await Database.update(query, params);
142
- }
143
-
144
107
  /**
145
108
  * Fetch all registrations with members with their corresponding (valid) registrations
146
109
  */
@@ -377,15 +340,6 @@ export class Member extends Model {
377
340
  return this.getBlobByIds(...(await this.getMemberIdsWithRegistrationForUser(user)));
378
341
  }
379
342
 
380
- getStructureWithRegistrations(this: MemberWithRegistrations, forOrganization: null | boolean = null) {
381
- return MemberWithRegistrationsBlob.create({
382
- ...this,
383
- registrations: this.registrations.map(r => r.getStructure()),
384
- details: this.details,
385
- users: this.users.map(u => u.getStructure()),
386
- });
387
- }
388
-
389
343
  static getRegistrationWithMemberStructure(registration: RegistrationWithMember & { group: import('./Group').Group }): RegistrationWithMemberStruct {
390
344
  return RegistrationWithMemberStruct.create({
391
345
  ...registration.getStructure(),