@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,13 +1,14 @@
1
- import { column, Model, SQLResultNamespacedRow } from '@simonbackx/simple-database';
2
- import { SQL, SQLAlias, SQLCalculation, SQLGreatest, SQLMin, SQLMinusSign, SQLMultiplicationSign, SQLSelect, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
3
- import { BalanceItem as BalanceItemStruct, BalanceItemStatus, ReceivableBalanceType } from '@stamhoofd/structures';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { QueryableModel, SQL, SQLAlias, SQLMin, SQLSelectAs, SQLSum, SQLWhere, SQLWhereSign } from '@stamhoofd/sql';
3
+ import { BalanceItemStatus, BalanceItem as BalanceItemStruct, ReceivableBalanceType } from '@stamhoofd/structures';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import { BalanceItem } from './BalanceItem';
6
+ import { MemberUser } from './MemberUser';
6
7
 
7
8
  /**
8
9
  * Keeps track of how much a member/user owes or needs to be reimbursed.
9
10
  */
10
- export class CachedBalance extends Model {
11
+ export class CachedBalance extends QueryableModel {
11
12
  static table = 'cached_outstanding_balances';
12
13
 
13
14
  @column({
@@ -33,7 +34,10 @@ export class CachedBalance extends Model {
33
34
  objectType: ReceivableBalanceType;
34
35
 
35
36
  @column({ type: 'integer' })
36
- amount = 0;
37
+ amountPaid = 0;
38
+
39
+ @column({ type: 'integer' })
40
+ amountOpen = 0;
37
41
 
38
42
  /**
39
43
  * The sum of unconfirmed payments
@@ -47,6 +51,15 @@ export class CachedBalance extends Model {
47
51
  @column({ type: 'datetime', nullable: true })
48
52
  nextDueAt: Date | null = null;
49
53
 
54
+ @column({ type: 'datetime', nullable: true })
55
+ lastReminderEmail: Date | null = null;
56
+
57
+ @column({ type: 'integer' })
58
+ lastReminderAmountOpen = 0;
59
+
60
+ @column({ type: 'integer' })
61
+ reminderEmailCount = 0;
62
+
50
63
  @column({
51
64
  type: 'datetime', beforeSave(old?: any) {
52
65
  if (old !== undefined) {
@@ -70,6 +83,10 @@ export class CachedBalance extends Model {
70
83
  updatedAt: Date;
71
84
 
72
85
  static async getForObjects(objectIds: string[], limitOrganizationId?: string | null): Promise<CachedBalance[]> {
86
+ if (objectIds.length === 0) {
87
+ return [];
88
+ }
89
+
73
90
  const query = this.select()
74
91
  .where('objectId', objectIds);
75
92
 
@@ -81,6 +98,10 @@ export class CachedBalance extends Model {
81
98
  }
82
99
 
83
100
  static async updateForObjects(organizationId: string, objectIds: string[], objectType: ReceivableBalanceType) {
101
+ if (objectIds.length === 0) {
102
+ return;
103
+ }
104
+
84
105
  switch (objectType) {
85
106
  case ReceivableBalanceType.organization:
86
107
  await this.updateForOrganizations(organizationId, objectIds);
@@ -91,17 +112,26 @@ export class CachedBalance extends Model {
91
112
  case ReceivableBalanceType.user:
92
113
  await this.updateForUsers(organizationId, objectIds);
93
114
  break;
115
+ case ReceivableBalanceType.registration:
116
+ await this.updateForRegistrations(organizationId, objectIds);
117
+ break;
94
118
  }
95
119
  }
96
120
 
97
- static async balanceForObjects(organizationId: string, objectIds: string[], objectType: ReceivableBalanceType) {
121
+ static async balanceForObjects(organizationId: string, objectIds: string[], objectType: ReceivableBalanceType, includeUserMembers = false) {
122
+ if (objectIds.length === 0) {
123
+ return [];
124
+ }
125
+
98
126
  switch (objectType) {
99
127
  case ReceivableBalanceType.organization:
100
128
  return await this.balanceForOrganizations(organizationId, objectIds);
101
129
  case ReceivableBalanceType.member:
102
130
  return await this.balanceForMembers(organizationId, objectIds);
103
131
  case ReceivableBalanceType.user:
104
- return await this.balanceForUsers(organizationId, objectIds);
132
+ return await this.balanceForUsers(organizationId, objectIds, includeUserMembers);
133
+ case ReceivableBalanceType.registration:
134
+ return await this.balanceForRegistrations(organizationId, objectIds);
105
135
  }
106
136
  }
107
137
 
@@ -122,15 +152,25 @@ export class CachedBalance extends Model {
122
152
  return await query.fetch();
123
153
  }
124
154
 
155
+ static whereNeedsUpdate() {
156
+ return SQL.where('nextDueAt', SQLWhereSign.LessEqual, BalanceItemStruct.getDueOffset());
157
+ }
158
+
125
159
  private static async fetchForObjects(organizationId: string, objectIds: string[], columnName: string, customWhere?: SQLWhere) {
126
160
  const dueOffset = BalanceItemStruct.getDueOffset();
127
161
  const query = SQL.select(
128
162
  SQL.column(columnName),
163
+ new SQLSelectAs(
164
+ new SQLSum(
165
+ SQL.column('pricePaid'),
166
+ ),
167
+ new SQLAlias('data__amountPaid'),
168
+ ),
129
169
  new SQLSelectAs(
130
170
  new SQLSum(
131
171
  SQL.column('priceOpen'),
132
172
  ),
133
- new SQLAlias('data__amount'),
173
+ new SQLAlias('data__amountOpen'),
134
174
  ),
135
175
  new SQLSelectAs(
136
176
  new SQLSum(
@@ -162,11 +202,17 @@ export class CachedBalance extends Model {
162
202
  new SQLAlias('data__dueAt'),
163
203
  ),
164
204
  // If the current amount_due is negative, we can ignore that negative part if there is a future due item
205
+ new SQLSelectAs(
206
+ new SQLSum(
207
+ SQL.column('pricePaid'),
208
+ ),
209
+ new SQLAlias('data__amountPaid'),
210
+ ),
165
211
  new SQLSelectAs(
166
212
  new SQLSum(
167
213
  SQL.column('priceOpen'),
168
214
  ),
169
- new SQLAlias('data__amount'),
215
+ new SQLAlias('data__amountOpen'),
170
216
  ),
171
217
  new SQLSelectAs(
172
218
  new SQLSum(
@@ -185,7 +231,7 @@ export class CachedBalance extends Model {
185
231
 
186
232
  const dueResult = await dueQuery.fetch();
187
233
 
188
- const results: [string, { amount: number; amountPending: number; nextDueAt: Date | null }][] = [];
234
+ const results: [string, { amountPaid: number; amountOpen: number; amountPending: number; nextDueAt: Date | null }][] = [];
189
235
  for (const row of result) {
190
236
  if (!row['data']) {
191
237
  throw new Error('Invalid data namespace');
@@ -196,22 +242,27 @@ export class CachedBalance extends Model {
196
242
  }
197
243
 
198
244
  const objectId = row[BalanceItem.table][columnName];
199
- const amount = row['data']['amount'];
245
+ const amountOpen = row['data']['amountOpen'];
200
246
  const amountPending = row['data']['amountPending'];
247
+ const amountPaid = row['data']['amountPaid'];
201
248
 
202
249
  if (typeof objectId !== 'string') {
203
250
  throw new Error('Invalid objectId');
204
251
  }
205
252
 
206
- if (typeof amount !== 'number') {
207
- throw new Error('Invalid amount');
253
+ if (typeof amountOpen !== 'number') {
254
+ throw new Error('Invalid amountOpen');
208
255
  }
209
256
 
210
257
  if (typeof amountPending !== 'number') {
211
258
  throw new Error('Invalid amountPending');
212
259
  }
213
260
 
214
- results.push([objectId, { amount, amountPending, nextDueAt: null }]);
261
+ if (typeof amountPaid !== 'number') {
262
+ throw new Error('Invalid amountPaid');
263
+ }
264
+
265
+ results.push([objectId, { amountPaid, amountOpen, amountPending, nextDueAt: null }]);
215
266
  }
216
267
 
217
268
  for (const row of dueResult) {
@@ -225,8 +276,9 @@ export class CachedBalance extends Model {
225
276
 
226
277
  const objectId = row[BalanceItem.table][columnName];
227
278
  const dueAt = row['data']['dueAt'];
228
- const amount = row['data']['amount'];
279
+ const amountOpen = row['data']['amountOpen'];
229
280
  const amountPending = row['data']['amountPending'];
281
+ const amountPaid = row['data']['amountPaid'];
230
282
 
231
283
  if (typeof objectId !== 'string') {
232
284
  throw new Error('Invalid objectId');
@@ -236,77 +288,103 @@ export class CachedBalance extends Model {
236
288
  throw new Error('Invalid dueAt');
237
289
  }
238
290
 
239
- if (typeof amount !== 'number') {
240
- throw new Error('Invalid amount');
291
+ if (typeof amountOpen !== 'number') {
292
+ throw new Error('Invalid amountOpen');
241
293
  }
242
294
 
243
295
  if (typeof amountPending !== 'number') {
244
296
  throw new Error('Invalid amountPending');
245
297
  }
246
298
 
299
+ if (typeof amountPaid !== 'number') {
300
+ throw new Error('Invalid amountPaid');
301
+ }
302
+
247
303
  const result = results.find(r => r[0] === objectId);
248
304
  if (result) {
249
305
  result[1].nextDueAt = dueAt;
250
306
 
251
- if (result[1].amount < 0) {
252
- if (amount > 0) {
307
+ if (result[1].amountOpen < 0) {
308
+ if (amountOpen > 0) {
253
309
  // Let the future due amount fill in the gap until maximum 0
254
- result[1].amount = Math.min(0, result[1].amount + amount);
310
+ result[1].amountOpen = Math.min(0, result[1].amountOpen + amountOpen);
255
311
  }
256
312
  }
257
313
 
258
314
  result[1].amountPending += amountPending;
315
+ result[1].amountPaid += amountPaid;
259
316
  }
260
317
  else {
261
- results.push([objectId, { amount: 0, amountPending: amountPending, nextDueAt: dueAt }]);
318
+ results.push([objectId, { amountPaid, amountOpen: 0, amountPending, nextDueAt: dueAt }]);
262
319
  }
263
320
  }
264
321
 
265
322
  // Add missing object ids (with 0 amount, otherwise we don't reset the amounts back to zero when all the balance items are hidden)
266
323
  for (const objectId of objectIds) {
267
324
  if (!results.find(([id]) => id === objectId)) {
268
- results.push([objectId, { amount: 0, amountPending: 0, nextDueAt: null }]);
325
+ results.push([objectId, { amountPaid: 0, amountOpen: 0, amountPending: 0, nextDueAt: null }]);
269
326
  }
270
327
  }
271
328
 
272
329
  return results;
273
330
  }
274
331
 
275
- private static async setForResults(organizationId: string, result: [string, { amount: number; amountPending: number; nextDueAt: null | Date }][], objectType: ReceivableBalanceType) {
332
+ private static async setForResults(organizationId: string, result: [string, { amountPaid: number; amountOpen: number; amountPending: number; nextDueAt: null | Date }][], objectType: ReceivableBalanceType) {
276
333
  if (result.length === 0) {
277
334
  return;
278
335
  }
279
- const query = SQL.insert(this.table)
336
+ const query = this.insert()
280
337
  .columns(
281
338
  'id',
282
339
  'organizationId',
283
340
  'objectId',
284
341
  'objectType',
285
- 'amount',
342
+ 'amountPaid',
343
+ 'amountOpen',
286
344
  'amountPending',
287
345
  'nextDueAt',
288
346
  'createdAt',
289
347
  'updatedAt',
348
+ 'reminderEmailCount',
290
349
  )
291
- .values(...result.map(([objectId, { amount, amountPending, nextDueAt }]) => {
350
+ .values(...result.map(([objectId, { amountPaid, amountOpen, amountPending, nextDueAt }]) => {
292
351
  return [
293
352
  uuidv4(),
294
353
  organizationId,
295
354
  objectId,
296
355
  objectType,
297
- amount,
356
+ amountPaid,
357
+ amountOpen,
298
358
  amountPending,
299
359
  nextDueAt,
300
360
  new Date(),
301
361
  new Date(),
362
+ 0,
302
363
  ];
303
364
  }))
304
365
  .as('v')
305
366
  .onDuplicateKeyUpdate(
306
- SQL.assignment('amount', SQL.column('v', 'amount')),
367
+ SQL.assignment('amountPaid', SQL.column('v', 'amountPaid')),
368
+ SQL.assignment('amountOpen', SQL.column('v', 'amountOpen')),
307
369
  SQL.assignment('amountPending', SQL.column('v', 'amountPending')),
308
370
  SQL.assignment('nextDueAt', SQL.column('v', 'nextDueAt')),
309
371
  SQL.assignment('updatedAt', SQL.column('v', 'updatedAt')),
372
+
373
+ // Reset email count if amountOpen is zero
374
+ SQL.assignment(
375
+ 'reminderEmailCount',
376
+ SQL.if(SQL.column('v', 'amountOpen'), 0)
377
+ .then(0)
378
+ .else(SQL.column('reminderEmailCount')),
379
+ ),
380
+
381
+ // Reset lastReminderEmail if amountOpen is zero
382
+ SQL.assignment(
383
+ 'lastReminderEmail',
384
+ SQL.if(SQL.column('v', 'amountOpen'), 0)
385
+ .then(null)
386
+ .else(SQL.column('lastReminderEmail')),
387
+ ),
310
388
  );
311
389
 
312
390
  await query.insert();
@@ -328,11 +406,55 @@ export class CachedBalance extends Model {
328
406
  await this.setForResults(organizationId, results, ReceivableBalanceType.member);
329
407
  }
330
408
 
409
+ static async updateForRegistrations(organizationId: string, registrationIds: string[]) {
410
+ if (registrationIds.length === 0) {
411
+ return;
412
+ }
413
+ const results = await this.fetchForObjects(organizationId, registrationIds, 'registrationId');
414
+ await this.setForResults(organizationId, results, ReceivableBalanceType.registration);
415
+ }
416
+
331
417
  static async updateForUsers(organizationId: string, userIds: string[]) {
332
418
  if (userIds.length === 0) {
333
419
  return;
334
420
  }
335
- const results = await this.fetchForObjects(organizationId, userIds, 'userId', SQL.where('memberId', null));
421
+ const results = await this.fetchForObjects(
422
+ organizationId,
423
+ userIds,
424
+ 'userId',
425
+ SQL.where('memberId', null),
426
+ );
427
+
428
+ // We also need to include the balance of the related members for each user
429
+
430
+ // Fetch members of these users
431
+ const memberUsers = await MemberUser.select().where('usersId', userIds).fetch();
432
+
433
+ const memberCachedBalances = await this.getForObjects(memberUsers.map(mu => mu.membersId), organizationId);
434
+
435
+ for (const memberCachedBalance of memberCachedBalances) {
436
+ const userIds = memberUsers.filter(mu => mu.membersId === memberCachedBalance.objectId).map(mu => mu.usersId);
437
+
438
+ for (const userId of userIds) {
439
+ // if already in results: append
440
+ const result = results.find(([objectId]) => objectId === userId);
441
+
442
+ if (result) {
443
+ result[1].amountPaid += memberCachedBalance.amountPaid;
444
+ result[1].amountOpen += memberCachedBalance.amountOpen;
445
+ result[1].amountPending += memberCachedBalance.amountPending;
446
+ if (memberCachedBalance.nextDueAt && (!result[1].nextDueAt || memberCachedBalance.nextDueAt > result[1].nextDueAt)) {
447
+ result[1].nextDueAt = memberCachedBalance.nextDueAt;
448
+ }
449
+ }
450
+ else {
451
+ // Not possible
452
+ throw new Error('User not found in results');
453
+ }
454
+ }
455
+ }
456
+
457
+ // Fetch cached balance of these members and merge the results
336
458
  await this.setForResults(organizationId, results, ReceivableBalanceType.user);
337
459
  }
338
460
 
@@ -350,28 +472,26 @@ export class CachedBalance extends Model {
350
472
  return await this.fetchBalanceItems(organizationId, memberIds, 'memberId');
351
473
  }
352
474
 
353
- static async balanceForUsers(organizationId: string, userIds: string[]) {
475
+ static async balanceForUsers(organizationId: string, userIds: string[], includeUserMembers = false) {
354
476
  if (userIds.length === 0) {
355
477
  return [];
356
478
  }
357
- return await this.fetchBalanceItems(organizationId, userIds, 'userId', SQL.where('memberId', null));
358
- }
479
+ const base = await this.fetchBalanceItems(organizationId, userIds, 'userId', SQL.where('memberId', null));
359
480
 
360
- /**
361
- * Experimental: needs to move to library
362
- */
363
- static select() {
364
- const transformer = (row: SQLResultNamespacedRow): CachedBalance => {
365
- const d = (this as typeof CachedBalance & typeof Model).fromRow(row[this.table] as any) as CachedBalance | undefined;
366
-
367
- if (!d) {
368
- throw new Error('EmailTemplate not found');
369
- }
481
+ if (!includeUserMembers) {
482
+ return base;
483
+ }
370
484
 
371
- return d;
372
- };
485
+ const memberUsers = await MemberUser.select().where('usersId', userIds).fetch();
486
+ const memberIds = memberUsers.map(mu => mu.membersId);
487
+ const memberCachedBalances = await this.balanceForMembers(organizationId, memberIds);
488
+ return base.concat(memberCachedBalances);
489
+ }
373
490
 
374
- const select = new SQLSelect(transformer, SQL.wildcard());
375
- return select.from(SQL.table(this.table));
491
+ static async balanceForRegistrations(organizationId: string, registrationIds: string[]) {
492
+ if (registrationIds.length === 0) {
493
+ return [];
494
+ }
495
+ return await this.fetchBalanceItems(organizationId, registrationIds, 'registrationId');
376
496
  }
377
497
  }
@@ -1,14 +1,15 @@
1
- import { column, Model } from '@simonbackx/simple-database';
2
- import { Document as DocumentStruct, DocumentData, DocumentStatus, Platform, Version } from '@stamhoofd/structures';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { DocumentData, DocumentStatus, Document as DocumentStruct, Platform, Version } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
6
+ import { QueryableModel } from '@stamhoofd/sql';
6
7
  import { render } from '../helpers/Handlebars';
7
8
  import { RegistrationWithMember } from './Member';
8
9
  import { Organization } from './Organization';
9
10
  import { Registration } from './Registration';
10
11
 
11
- export class Document extends Model {
12
+ export class Document extends QueryableModel {
12
13
  static table = 'documents';
13
14
 
14
15
  @column({ primary: true, type: 'string', beforeSave(value) {
@@ -1,20 +1,21 @@
1
- import { column, Model } from '@simonbackx/simple-database';
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
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
 
8
+ import { QueryableModel } from '@stamhoofd/sql';
8
9
  import { render } from '../helpers/Handlebars';
9
10
  import { BalanceItem } from './BalanceItem';
10
11
  import { Document } from './Document';
11
12
  import { Group } from './Group';
12
13
  import { Member, RegistrationWithMember } from './Member';
13
14
  import { Organization } from './Organization';
14
- import { User } from './User';
15
15
  import { Registration } from './Registration';
16
+ import { User } from './User';
16
17
 
17
- export class DocumentTemplate extends Model {
18
+ export class DocumentTemplate extends QueryableModel {
18
19
  static table = 'document_templates';
19
20
 
20
21
  @column({ primary: true, type: 'string', beforeSave(value) {
@@ -74,9 +75,11 @@ export class DocumentTemplate extends Model {
74
75
  let missingData = false;
75
76
 
76
77
  const group = await Group.getByID(registration.groupId);
77
- const { items: balanceItems, payments } = await BalanceItem.getForRegistration(registration.id);
78
+ const { items: balanceItems, payments } = await BalanceItem.getForRegistration(registration.id, this.organizationId);
78
79
 
79
80
  const paidAtDates = payments.flatMap(p => p.paidAt ? [p.paidAt?.getTime()] : []);
81
+ const price = balanceItems.reduce((sum, item) => sum + item.price, 0);
82
+ const pricePaid = balanceItems.reduce((sum, item) => sum + item.pricePaid, 0);
80
83
 
81
84
  // We take the minimum date here, because there is a highter change of later paymetns to be for other things than the registration itself
82
85
  const paidAt = paidAtDates.length ? new Date(Math.min(...paidAtDates)) : null;
@@ -102,7 +105,7 @@ export class DocumentTemplate extends Model {
102
105
  id: 'registration.startDate',
103
106
  type: RecordType.Date,
104
107
  }), // settings will be overwritten
105
- dateValue: group?.settings?.startDate,
108
+ dateValue: registration.startDate ?? group?.settings?.startDate,
106
109
  }),
107
110
  'registration.endDate': RecordDateAnswer.create({
108
111
  settings: RecordSettings.create({
@@ -114,16 +117,36 @@ export class DocumentTemplate extends Model {
114
117
  'registration.price':
115
118
  RecordPriceAnswer.create({
116
119
  settings: RecordSettings.create({
120
+ id: 'registration.price',
121
+ type: RecordType.Price,
122
+ }), // settings will be overwritten
123
+ value: price,
124
+ }),
125
+ // This one is duplicated in case it got disabled (we need to use it to check if document is included)
126
+ 'registration.priceOriginal':
127
+ RecordPriceAnswer.create({
128
+ settings: RecordSettings.create({
129
+ id: 'registration.priceOriginal',
130
+ type: RecordType.Price,
131
+ }), // settings will be overwritten
132
+ value: price,
133
+ }),
134
+ // This one is duplicated in case it got disabled (we need to use it to check if document is included)
135
+ 'registration.pricePaidOriginal':
136
+ RecordPriceAnswer.create({
137
+ settings: RecordSettings.create({
138
+ id: 'registration.pricePaidOriginal',
117
139
  type: RecordType.Price,
118
140
  }), // settings will be overwritten
119
- value: registration.price,
141
+ value: pricePaid,
120
142
  }),
121
143
  'registration.pricePaid':
122
144
  RecordPriceAnswer.create({
123
145
  settings: RecordSettings.create({
146
+ id: 'registration.pricePaid',
124
147
  type: RecordType.Price,
125
148
  }), // settings will be overwritten
126
- value: registration.pricePaid,
149
+ value: pricePaid,
127
150
  }),
128
151
  'registration.paidAt':
129
152
  RecordDateAnswer.create({
@@ -483,14 +506,22 @@ export class DocumentTemplate extends Model {
483
506
  }
484
507
  }
485
508
 
486
- if (this.settings.minPrice !== null) {
487
- if ((registration.price ?? 0) < this.settings.minPrice) {
488
- return false;
509
+ if (this.settings.minPrice !== null && this.settings.minPrice > 0) {
510
+ const priceAnswer = fieldAnswers.get('registration.priceOriginal');
511
+ if (priceAnswer && priceAnswer instanceof RecordPriceAnswer) {
512
+ if ((priceAnswer.value ?? 0) < this.settings.minPrice) {
513
+ return false;
514
+ }
489
515
  }
490
516
  }
491
517
 
492
- if (this.settings.minPricePaid !== null) {
493
- if ((registration.pricePaid ?? 0) < this.settings.minPricePaid && (registration.price ?? 0) > 0) {
518
+ if (this.settings.minPricePaid !== null && this.settings.minPricePaid > 0) {
519
+ const pricePaidAnswer = fieldAnswers.get('registration.pricePaidOriginal');
520
+ const priceAnswer = fieldAnswers.get('registration.priceOriginal');
521
+ const price = (priceAnswer instanceof RecordPriceAnswer ? priceAnswer.value : 0) ?? 0;
522
+ const pricePaid = (pricePaidAnswer instanceof RecordPriceAnswer ? pricePaidAnswer.value : 0) ?? 0;
523
+
524
+ if (pricePaid < this.settings.minPricePaid && price > 0) {
494
525
  return false;
495
526
  }
496
527
  }