@stamhoofd/models 2.120.6 → 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 (296) 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/RegistrationInvitationFactory.d.ts +15 -0
  9. package/dist/factories/RegistrationInvitationFactory.d.ts.map +1 -0
  10. package/dist/factories/RegistrationInvitationFactory.js +18 -0
  11. package/dist/factories/RegistrationInvitationFactory.js.map +1 -0
  12. package/dist/factories/STPackageFactory.js.map +1 -1
  13. package/dist/factories/UserFactory.d.ts.map +1 -1
  14. package/dist/factories/UserFactory.js +2 -2
  15. package/dist/factories/UserFactory.js.map +1 -1
  16. package/dist/factories/index.d.ts +1 -0
  17. package/dist/factories/index.d.ts.map +1 -1
  18. package/dist/factories/index.js +1 -0
  19. package/dist/factories/index.js.map +1 -1
  20. package/dist/helpers/EmailBuilder.d.ts.map +1 -1
  21. package/dist/helpers/EmailBuilder.js +8 -8
  22. package/dist/helpers/EmailBuilder.js.map +1 -1
  23. package/dist/helpers/Handlebars.d.ts.map +1 -1
  24. package/dist/helpers/Handlebars.js +10 -1
  25. package/dist/helpers/Handlebars.js.map +1 -1
  26. package/dist/helpers/InvoiceCounter.d.ts +24 -0
  27. package/dist/helpers/InvoiceCounter.d.ts.map +1 -0
  28. package/dist/helpers/InvoiceCounter.js +133 -0
  29. package/dist/helpers/InvoiceCounter.js.map +1 -0
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +0 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/migrations/1605262045-import-postcodes.d.ts.map +1 -1
  35. package/dist/migrations/1605262045-import-postcodes.js +58 -24
  36. package/dist/migrations/1605262045-import-postcodes.js.map +1 -1
  37. package/dist/migrations/1605262046-import-postcodes-nl.d.ts.map +1 -1
  38. package/dist/migrations/1605262046-import-postcodes-nl.js +54 -17
  39. package/dist/migrations/1605262046-import-postcodes-nl.js.map +1 -1
  40. package/dist/migrations/1719567881-organization-periodId.sql +2 -0
  41. package/dist/migrations/1719567882-groups-periodId.sql +2 -0
  42. package/dist/migrations/1720080975-convert-charset.d.ts +4 -0
  43. package/dist/migrations/1720080975-convert-charset.d.ts.map +1 -0
  44. package/dist/migrations/1720080975-convert-charset.js +26 -0
  45. package/dist/migrations/1720080975-convert-charset.js.map +1 -0
  46. package/dist/migrations/1720080976-convert-charset-leads.d.ts.map +1 -1
  47. package/dist/migrations/1720080976-convert-charset-leads.js +11 -10
  48. package/dist/migrations/1720080976-convert-charset-leads.js.map +1 -1
  49. package/dist/migrations/1721400546-users-memberId.sql +2 -0
  50. package/dist/migrations/1722269236-group-waitinglist-id.sql +2 -1
  51. package/dist/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  52. package/dist/migrations/1722525787-depending-balance-item.sql +2 -0
  53. package/dist/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  54. package/dist/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  55. package/dist/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  56. package/dist/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  57. package/dist/migrations/1733504881-negative-invoice-id.sql +6 -0
  58. package/dist/migrations/1733994455-balance-item-status-open.d.ts +4 -0
  59. package/dist/migrations/1733994455-balance-item-status-open.d.ts.map +1 -0
  60. package/dist/migrations/1733994455-balance-item-status-open.js +28 -0
  61. package/dist/migrations/1733994455-balance-item-status-open.js.map +1 -0
  62. package/dist/migrations/1763216320-bigint-balance-item-payments.sql +2 -0
  63. package/dist/migrations/1763216320-bigint-balance-items.sql +5 -0
  64. package/dist/migrations/1763216320-bigint-orders.sql +2 -0
  65. package/dist/migrations/1763216320-bigint-payments.sql +2 -0
  66. package/dist/migrations/1763216332-bigint-balance-item-price-total.sql +2 -0
  67. package/dist/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  68. package/dist/migrations/1769087809-payments-invoice-id.sql +2 -0
  69. package/dist/migrations/1772033555-balance-item-package-id.sql +2 -0
  70. package/dist/migrations/1776873089-create-registration-invitations-table.sql +13 -0
  71. package/dist/migrations/1778657958-payments-create-mandate.sql +2 -0
  72. package/dist/migrations/1778657959-payments-mandate-id.sql +2 -0
  73. package/dist/migrations/1778796615-payments-reversing-payment-id.sql +5 -0
  74. package/dist/migrations/1778950642-price-invoiced.sql +2 -0
  75. package/dist/migrations/1779443446-transfer-fees.sql +3 -0
  76. package/dist/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  77. package/dist/migrations/1779968328-payments-admin-user-id.sql +5 -0
  78. package/dist/migrations/1779970611-payments-refunded-amount.sql +2 -0
  79. package/dist/migrations/1779972640-balance-items-failed-at.sql +2 -0
  80. package/dist/migrations/1780328285-document-template-locked.sql +2 -0
  81. package/dist/migrations/1780328286-document-locked.sql +2 -0
  82. package/dist/migrations/1780412083-documents-set-locked.d.ts +4 -0
  83. package/dist/migrations/1780412083-documents-set-locked.d.ts.map +1 -0
  84. package/dist/migrations/1780412083-documents-set-locked.js +14 -0
  85. package/dist/migrations/1780412083-documents-set-locked.js.map +1 -0
  86. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts +4 -0
  87. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts.map +1 -0
  88. package/dist/migrations/1780928401-v1-groups-migration-data.js +44 -0
  89. package/dist/migrations/1780928401-v1-groups-migration-data.js.map +1 -0
  90. package/dist/models/BalanceItem.d.ts +14 -1
  91. package/dist/models/BalanceItem.d.ts.map +1 -1
  92. package/dist/models/BalanceItem.js +91 -41
  93. package/dist/models/BalanceItem.js.map +1 -1
  94. package/dist/models/CachedBalance.d.ts +6 -1
  95. package/dist/models/CachedBalance.d.ts.map +1 -1
  96. package/dist/models/CachedBalance.js +3 -2
  97. package/dist/models/CachedBalance.js.map +1 -1
  98. package/dist/models/Document.d.ts +4 -0
  99. package/dist/models/Document.d.ts.map +1 -1
  100. package/dist/models/Document.js +26 -3
  101. package/dist/models/Document.js.map +1 -1
  102. package/dist/models/DocumentTemplate.d.ts +4 -0
  103. package/dist/models/DocumentTemplate.d.ts.map +1 -1
  104. package/dist/models/DocumentTemplate.js +37 -1
  105. package/dist/models/DocumentTemplate.js.map +1 -1
  106. package/dist/models/Email.d.ts.map +1 -1
  107. package/dist/models/Email.js +1 -1
  108. package/dist/models/Email.js.map +1 -1
  109. package/dist/models/EmailVerificationCode.d.ts.map +1 -1
  110. package/dist/models/EmailVerificationCode.js +3 -13
  111. package/dist/models/EmailVerificationCode.js.map +1 -1
  112. package/dist/models/Event.d.ts +2 -1
  113. package/dist/models/Event.d.ts.map +1 -1
  114. package/dist/models/Event.js +3 -0
  115. package/dist/models/Event.js.map +1 -1
  116. package/dist/models/EventNotification.d.ts.map +1 -1
  117. package/dist/models/EventNotification.js +5 -5
  118. package/dist/models/EventNotification.js.map +1 -1
  119. package/dist/models/Group.d.ts +4 -0
  120. package/dist/models/Group.d.ts.map +1 -1
  121. package/dist/models/Group.js +17 -0
  122. package/dist/models/Group.js.map +1 -1
  123. package/dist/models/Invoice.d.ts +1 -0
  124. package/dist/models/Invoice.d.ts.map +1 -1
  125. package/dist/models/Invoice.js +8 -8
  126. package/dist/models/Invoice.js.map +1 -1
  127. package/dist/models/MemberPlatformMembership.d.ts.map +1 -1
  128. package/dist/models/MemberPlatformMembership.js +9 -0
  129. package/dist/models/MemberPlatformMembership.js.map +1 -1
  130. package/dist/models/MollieToken.d.ts +4 -8
  131. package/dist/models/MollieToken.d.ts.map +1 -1
  132. package/dist/models/MollieToken.js +37 -90
  133. package/dist/models/MollieToken.js.map +1 -1
  134. package/dist/models/Order.d.ts.map +1 -1
  135. package/dist/models/Order.js +1 -0
  136. package/dist/models/Order.js.map +1 -1
  137. package/dist/models/Organization.d.ts +30 -23
  138. package/dist/models/Organization.d.ts.map +1 -1
  139. package/dist/models/Organization.js +113 -61
  140. package/dist/models/Organization.js.map +1 -1
  141. package/dist/models/PasswordToken.d.ts +5 -1
  142. package/dist/models/PasswordToken.d.ts.map +1 -1
  143. package/dist/models/PasswordToken.js +18 -17
  144. package/dist/models/PasswordToken.js.map +1 -1
  145. package/dist/models/Payment.d.ts +35 -3
  146. package/dist/models/Payment.d.ts.map +1 -1
  147. package/dist/models/Payment.js +66 -3
  148. package/dist/models/Payment.js.map +1 -1
  149. package/dist/models/Registration.d.ts +1 -0
  150. package/dist/models/Registration.d.ts.map +1 -1
  151. package/dist/models/Registration.js +4 -3
  152. package/dist/models/Registration.js.map +1 -1
  153. package/dist/models/RegistrationInvitation.d.ts +14 -0
  154. package/dist/models/RegistrationInvitation.d.ts.map +1 -0
  155. package/dist/models/RegistrationInvitation.js +45 -0
  156. package/dist/models/RegistrationInvitation.js.map +1 -0
  157. package/dist/models/STCredit.d.ts +4 -0
  158. package/dist/models/STCredit.d.ts.map +1 -1
  159. package/dist/models/STCredit.js +28 -0
  160. package/dist/models/STCredit.js.map +1 -1
  161. package/dist/models/STInvoice.d.ts +7 -1
  162. package/dist/models/STInvoice.d.ts.map +1 -1
  163. package/dist/models/STInvoice.js +9 -0
  164. package/dist/models/STInvoice.js.map +1 -1
  165. package/dist/models/STPackage.d.ts +4 -0
  166. package/dist/models/STPackage.d.ts.map +1 -1
  167. package/dist/models/STPackage.js +12 -1
  168. package/dist/models/STPackage.js.map +1 -1
  169. package/dist/models/UsedRegisterCode.d.ts +9 -0
  170. package/dist/models/UsedRegisterCode.d.ts.map +1 -1
  171. package/dist/models/UsedRegisterCode.js +31 -0
  172. package/dist/models/UsedRegisterCode.js.map +1 -1
  173. package/dist/models/User.d.ts +1 -1
  174. package/dist/models/User.d.ts.map +1 -1
  175. package/dist/models/User.js +1 -1
  176. package/dist/models/User.js.map +1 -1
  177. package/dist/models/_relations.js +25 -0
  178. package/dist/models/_relations.js.map +1 -1
  179. package/dist/models/addresses/City.d.ts +4 -4
  180. package/dist/models/addresses/City.d.ts.map +1 -1
  181. package/dist/models/addresses/City.js +6 -6
  182. package/dist/models/addresses/City.js.map +1 -1
  183. package/dist/models/addresses/PostalCode.d.ts +2 -2
  184. package/dist/models/addresses/PostalCode.d.ts.map +1 -1
  185. package/dist/models/addresses/PostalCode.js +4 -3
  186. package/dist/models/addresses/PostalCode.js.map +1 -1
  187. package/dist/models/addresses/Street.d.ts +3 -3
  188. package/dist/models/addresses/Street.d.ts.map +1 -1
  189. package/dist/models/addresses/Street.js +4 -4
  190. package/dist/models/addresses/Street.js.map +1 -1
  191. package/dist/models/index.d.ts +2 -0
  192. package/dist/models/index.d.ts.map +1 -1
  193. package/dist/models/index.js +2 -0
  194. package/dist/models/index.js.map +1 -1
  195. package/dist/models/v1GroupMigrationData.d.ts +22 -0
  196. package/dist/models/v1GroupMigrationData.d.ts.map +1 -0
  197. package/dist/models/v1GroupMigrationData.js +48 -0
  198. package/dist/models/v1GroupMigrationData.js.map +1 -0
  199. package/package.json +41 -13
  200. package/src/factories/GroupFactory.ts +4 -6
  201. package/src/factories/OrganizationFactory.ts +12 -4
  202. package/src/factories/RegistrationInvitationFactory.ts +24 -0
  203. package/src/factories/STPackageFactory.ts +2 -2
  204. package/src/factories/UserFactory.ts +4 -5
  205. package/src/factories/index.ts +1 -0
  206. package/src/helpers/EmailBuilder.ts +19 -28
  207. package/src/helpers/Handlebars.ts +10 -1
  208. package/src/helpers/InvoiceCounter.test.ts +220 -0
  209. package/src/helpers/InvoiceCounter.ts +162 -0
  210. package/src/index.ts +0 -1
  211. package/src/migrations/1605262045-import-postcodes.ts +62 -25
  212. package/src/migrations/1605262046-import-postcodes-nl.ts +58 -17
  213. package/src/migrations/1719567881-organization-periodId.sql +2 -0
  214. package/src/migrations/1719567882-groups-periodId.sql +2 -0
  215. package/src/migrations/1720080975-convert-charset.ts +34 -0
  216. package/src/migrations/1720080976-convert-charset-leads.ts +16 -13
  217. package/src/migrations/1721400546-users-memberId.sql +2 -0
  218. package/src/migrations/1722269236-group-waitinglist-id.sql +2 -1
  219. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  220. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  221. package/src/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  222. package/src/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  223. package/src/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  224. package/src/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  225. package/src/migrations/1733504881-negative-invoice-id.sql +6 -0
  226. package/src/migrations/1733994455-balance-item-status-open.ts +30 -0
  227. package/src/migrations/1763216320-bigint-balance-item-payments.sql +2 -0
  228. package/src/migrations/1763216320-bigint-balance-items.sql +5 -0
  229. package/src/migrations/1763216320-bigint-orders.sql +2 -0
  230. package/src/migrations/1763216320-bigint-payments.sql +2 -0
  231. package/src/migrations/1763216332-bigint-balance-item-price-total.sql +2 -0
  232. package/src/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  233. package/src/migrations/1769087809-payments-invoice-id.sql +2 -0
  234. package/src/migrations/1772033555-balance-item-package-id.sql +2 -0
  235. package/src/migrations/1776873089-create-registration-invitations-table.sql +13 -0
  236. package/src/migrations/1778657958-payments-create-mandate.sql +2 -0
  237. package/src/migrations/1778657959-payments-mandate-id.sql +2 -0
  238. package/src/migrations/1778796615-payments-reversing-payment-id.sql +5 -0
  239. package/src/migrations/1778950642-price-invoiced.sql +2 -0
  240. package/src/migrations/1779443446-transfer-fees.sql +3 -0
  241. package/src/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  242. package/src/migrations/1779968328-payments-admin-user-id.sql +5 -0
  243. package/src/migrations/1779970611-payments-refunded-amount.sql +2 -0
  244. package/src/migrations/1779972640-balance-items-failed-at.sql +2 -0
  245. package/src/migrations/1780328285-document-template-locked.sql +2 -0
  246. package/src/migrations/1780328286-document-locked.sql +2 -0
  247. package/src/migrations/1780412083-documents-set-locked.ts +18 -0
  248. package/src/migrations/1780928401-v1-groups-migration-data.ts +50 -0
  249. package/src/models/BalanceItem.ts +98 -43
  250. package/src/models/CachedBalance.test.ts +46 -46
  251. package/src/models/CachedBalance.ts +7 -7
  252. package/src/models/Document.ts +34 -13
  253. package/src/models/DocumentTemplate.ts +56 -17
  254. package/src/models/Email.test.ts +3 -3
  255. package/src/models/Email.ts +28 -49
  256. package/src/models/EmailVerificationCode.ts +8 -22
  257. package/src/models/Event.ts +6 -4
  258. package/src/models/EventNotification.ts +6 -6
  259. package/src/models/Group.ts +24 -3
  260. package/src/models/Invoice.ts +10 -9
  261. package/src/models/MemberPlatformMembership.test.ts +70 -0
  262. package/src/models/MemberPlatformMembership.ts +16 -12
  263. package/src/models/MollieToken.ts +42 -102
  264. package/src/models/Order.ts +14 -26
  265. package/src/models/Organization.ts +143 -86
  266. package/src/models/PasswordToken.ts +21 -21
  267. package/src/models/Payment.ts +61 -4
  268. package/src/models/Registration.ts +6 -5
  269. package/src/models/RegistrationInvitation.ts +40 -0
  270. package/src/models/STCredit.ts +32 -0
  271. package/src/models/STInvoice.ts +11 -5
  272. package/src/models/STPackage.ts +19 -14
  273. package/src/models/UsedRegisterCode.ts +34 -0
  274. package/src/models/User.ts +6 -7
  275. package/src/models/_relations.ts +29 -0
  276. package/src/models/addresses/City.ts +8 -6
  277. package/src/models/addresses/PostalCode.test.ts +1 -0
  278. package/src/models/addresses/PostalCode.ts +5 -3
  279. package/src/models/addresses/Street.ts +6 -4
  280. package/src/models/index.ts +3 -0
  281. package/src/models/v1GroupMigrationData.ts +43 -0
  282. package/dist/helpers/MemberMerger.d.ts +0 -14
  283. package/dist/helpers/MemberMerger.d.ts.map +0 -1
  284. package/dist/helpers/MemberMerger.js +0 -364
  285. package/dist/helpers/MemberMerger.js.map +0 -1
  286. package/dist/migrations/1720080975-convert-charset.sql +0 -85
  287. package/dist/migrations/1723202126-member-number-index.sql +0 -2
  288. package/dist/models/OneTimeToken.d.ts +0 -38
  289. package/dist/models/OneTimeToken.d.ts.map +0 -1
  290. package/dist/models/OneTimeToken.js +0 -125
  291. package/dist/models/OneTimeToken.js.map +0 -1
  292. package/src/helpers/MemberMerger.test.ts +0 -782
  293. package/src/helpers/MemberMerger.ts +0 -577
  294. package/src/migrations/1720080975-convert-charset.sql +0 -85
  295. package/src/migrations/1723202126-member-number-index.sql +0 -2
  296. package/src/models/OneTimeToken.ts +0 -133
@@ -1,13 +1,15 @@
1
1
  import type { ManyToOneRelation } from '@simonbackx/simple-database';
2
2
  import { column, Database } from '@simonbackx/simple-database';
3
- import type {I18n} from '@stamhoofd/backend-i18n';
3
+ import type { I18n } from '@stamhoofd/backend-i18n';
4
4
  import { QueryableModel } from '@stamhoofd/sql';
5
5
  import basex from 'base-x';
6
6
  import crypto from 'crypto';
7
7
 
8
- import type {Organization} from './Organization.js';
8
+ import type { Organization } from './Organization.js';
9
9
  import { User } from './User.js';
10
10
  import { SimpleError } from '@simonbackx/simple-errors';
11
+ import { getAppHost } from '@stamhoofd/structures';
12
+ import { Platform } from './Platform.js';
11
13
  const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
12
14
  const bs58 = basex(ALPHABET);
13
15
 
@@ -110,8 +112,7 @@ export class PasswordToken extends QueryableModel {
110
112
 
111
113
  if (validUntil) {
112
114
  token.validUntil = new Date(validUntil);
113
- }
114
- else {
115
+ } else {
115
116
  token.validUntil = new Date();
116
117
  token.validUntil.setTime(token.validUntil.getTime() + 3 * 3600 * 1000);
117
118
  }
@@ -122,29 +123,28 @@ export class PasswordToken extends QueryableModel {
122
123
  }
123
124
 
124
125
  static async getPasswordRecoveryUrl(user: User, organization: Organization | null, i18n: I18n, validUntil?: Date) {
125
- if (user.organizationId !== null && ((user.organizationId ?? null) !== (organization?.id ?? null))) {
126
- throw new Error('Unexpected mismatch in organization id for PasswordToken');
127
- }
128
126
  // Send an e-mail to say you already have an account + follow password forgot flow
129
127
  const token = await PasswordToken.createToken(user, validUntil);
128
+ return token.getPasswordRecoveryUrl(organization, i18n, user);
129
+ }
130
130
 
131
- let host: string;
132
- if (user.permissions || !organization || STAMHOOFD.userMode === 'platform') {
133
- host = 'https://' + (STAMHOOFD.domains.dashboard) + '/' + i18n.locale;
134
- if (user.organizationId && organization) {
135
- host += '/auto/' + encodeURIComponent(organization.uri);
136
- }
137
- return host + '/reset-password?token=' + encodeURIComponent(token.token);
131
+ /**
132
+ * Build the password recovery url for this (already created) token.
133
+ * Pass the user to avoid an extra query when it is already loaded.
134
+ */
135
+ async getPasswordRecoveryUrl(organization: Organization | null, i18n: I18n, user?: User) {
136
+ const tokenUser = user ?? await User.getByID(this.userId);
137
+ if (!tokenUser) {
138
+ throw new Error('PasswordToken without a valid user');
138
139
  }
139
140
 
140
- host = 'https://' + organization.getHost(i18n);
141
- return host + '/reset-password?token=' + encodeURIComponent(token.token);
142
- }
141
+ if (tokenUser.organizationId !== null && ((tokenUser.organizationId ?? null) !== (organization?.id ?? null))) {
142
+ throw new Error('Unexpected mismatch in organization id for PasswordToken');
143
+ }
143
144
 
144
- static async getMagicSignInUrl(user: User, organization: Organization) {
145
- // For now we don't add a token yet for security. We might add some sort of email validation thing later on
146
- const host = 'https://' + organization.getHost();
147
- return Promise.resolve(host + '/login' + '?email=' + encodeURIComponent(user.email) + '&hasAccount=' + (user.hasAccount() ? 1 : 0));
145
+ const hasOrganizationPermissions = organization ? tokenUser.permissions?.forOrganization(organization, await Platform.getSharedStruct())?.isEmpty === false : false;
146
+ const host = 'https://' + getAppHost(hasOrganizationPermissions ? 'dashboard' : 'registration', organization, hasOrganizationPermissions, i18n);
147
+ return host + '/reset-password?token=' + encodeURIComponent(this.token);
148
148
  }
149
149
 
150
150
  static async clearFor(userId: string) {
@@ -1,10 +1,10 @@
1
1
  import { column } from '@simonbackx/simple-database';
2
- import type { PaymentMethod, PaymentStatus} from '@stamhoofd/structures';
3
- import { BalanceItemDetailed, BalanceItemPaymentDetailed, BaseOrganization, PaymentCustomer, PaymentGeneral, PaymentProvider, PaymentType, Settlement, TransferSettings } from '@stamhoofd/structures';
2
+ import { BalanceItemDetailed, BalanceItemPaymentDetailed, BaseOrganization, PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus, PaymentType, Settlement, TransferSettings } from '@stamhoofd/structures';
4
3
  import { Formatter } from '@stamhoofd/utility';
5
4
  import { v4 as uuidv4 } from 'uuid';
6
5
 
7
- import { QueryableModel } from '@stamhoofd/sql';
6
+ import { QueryableModel, SQL } from '@stamhoofd/sql';
7
+ import { CreateMandateSettings } from '@stamhoofd/structures/checkout/CreateMandateSettings.js';
8
8
  import type { BalanceItem } from './BalanceItem.js';
9
9
  import type { BalanceItemPayment } from './BalanceItemPayment.js';
10
10
  import { Organization } from './Organization.js';
@@ -52,6 +52,19 @@ export class Payment extends QueryableModel {
52
52
  @column({ type: 'string', nullable: true })
53
53
  payingUserId: string | null = null;
54
54
 
55
+ /**
56
+ * External ID of the mandate used for this payment
57
+ */
58
+ @column({ type: 'string', nullable: true })
59
+ mandateId: string | null = null;
60
+
61
+ /**
62
+ * Link to related payment that was reversed.
63
+ * Used for refunds and chargebacks
64
+ */
65
+ @column({ type: 'string', nullable: true })
66
+ reversingPaymentId: string | null = null;
67
+
55
68
  /**
56
69
  * @deprecated
57
70
  */
@@ -75,19 +88,30 @@ export class Payment extends QueryableModel {
75
88
  @column({ type: 'string', nullable: true })
76
89
  stripeAccountId: string | null = null;
77
90
 
91
+ @column({ type: 'json', decoder: CreateMandateSettings, nullable: true })
92
+ createMandate: CreateMandateSettings | null = null;
93
+
78
94
  /**
79
95
  * Total price
80
96
  */
81
97
  @column({ type: 'integer' })
82
98
  price: number;
83
99
 
100
+ /**
101
+ * Total price refunded or charged back
102
+ */
103
+ @column({ type: 'integer' })
104
+ refundedAmount = 0;
105
+
84
106
  /**
85
107
  * The difference between the sum of the balance item payments price and the price of the payment, caused by rounding to 1 cent.
86
108
  * This cannot be >= 100 (= 0,01 euro) or <= -100 (=-0,01 euro)
87
109
  *
110
+ * For understanding the sign of the value, regard it as an extra balance item to the payment.
111
+ *
88
112
  * Just like all prices, this price is stored per ten thousand (1 = 0,0001 ). Storing smaller units is not possible because even in balance items, the price to pay cannot be smaller than 0,0001 euro
89
113
  *
90
- * E.g. total price to pay is 0,242 because of VAT, then we round this to 0,24. The roundingAmount will be -0,002 in this case.
114
+ * E.g. total price to pay is 0,242 because of VAT, then we round this to 0,24. The roundingAmount will be -0,002 in this case, because all the items (0,242) - 0,002 = 0,24.
91
115
  */
92
116
  @column({ type: 'integer' })
93
117
  roundingAmount = 0;
@@ -122,6 +146,20 @@ export class Payment extends QueryableModel {
122
146
  @column({ type: 'integer' })
123
147
  serviceFeeManualCharged = 0;
124
148
 
149
+ /**
150
+ * Transfer fee - if we need to charge this for a certain provider
151
+ *
152
+ * This EXCLUDES VAT
153
+ */
154
+ @column({ type: 'integer' })
155
+ transferFeeManual = 0;
156
+
157
+ /**
158
+ * Part of the transferFeeManual, that has been invoiced (added to outstanding balance)
159
+ */
160
+ @column({ type: 'integer' })
161
+ transferFeeManualCharged = 0;
162
+
125
163
  /**
126
164
  * Included in the total price
127
165
  */
@@ -137,6 +175,12 @@ export class Payment extends QueryableModel {
137
175
  @column({ type: 'string', nullable: true })
138
176
  invoiceId: string | null = null;
139
177
 
178
+ /**
179
+ * In case the payment was initiated by an admin or not.
180
+ */
181
+ @column({ type: 'string', nullable: true })
182
+ adminUserId: string | null = null;
183
+
140
184
  @column({
141
185
  type: 'datetime', beforeSave(old?: any) {
142
186
  if (old !== undefined) {
@@ -177,11 +221,24 @@ export class Payment extends QueryableModel {
177
221
  @column({ type: 'string', nullable: true })
178
222
  ibanName: string | null = null;
179
223
 
224
+ get canChangeStatus() {
225
+ return this.price !== 0 && (this.method === PaymentMethod.Transfer || this.method === PaymentMethod.PointOfSale || this.method === PaymentMethod.Unknown);
226
+ }
227
+
180
228
  generateDescription(organization: Organization, reference: string, replacements: { [key: string]: string } = {}) {
181
229
  const settings = this.transferSettings ?? organization.meta.transferSettings;
182
230
  this.transferDescription = settings.generateDescription(reference, organization.address.country, replacements);
183
231
  }
184
232
 
233
+ async updateRefundedAmount() {
234
+ this.refundedAmount = await Payment.select()
235
+ .where('organizationId', this.organizationId)
236
+ .where('reversingPaymentId', this.id)
237
+ .where('status', PaymentStatus.Succeeded)
238
+ .sum(SQL.column('price'));
239
+ await this.save();
240
+ }
241
+
185
242
  static roundPrice(price: number) {
186
243
  return Math.round(price / 100) * 100;
187
244
  }
@@ -1,12 +1,12 @@
1
1
  import type { ManyToOneRelation } from '@simonbackx/simple-database';
2
2
  import { column } from '@simonbackx/simple-database';
3
- import type { RecordAnswer} from '@stamhoofd/structures';
4
- import { AppliedRegistrationDiscount, GroupPrice, RecordAnswerDecoder, RegisterItemOption, Registration as RegistrationStructure, StockReservation } from '@stamhoofd/structures';
3
+ import type { RecordAnswer } from '@stamhoofd/structures';
4
+ import { AppliedRegistrationDiscount, GroupPrice, RecordAnswerMapDecoder, RegisterItemOption, Registration as RegistrationStructure, StockReservation } from '@stamhoofd/structures';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
 
7
7
  import { ArrayDecoder, MapDecoder, StringDecoder } from '@simonbackx/simple-encoding';
8
8
  import { QueryableModel } from '@stamhoofd/sql';
9
- import type {Group} from './Group.js';
9
+ import type { Group } from './Group.js';
10
10
 
11
11
  export class Registration extends QueryableModel {
12
12
  static table = 'registrations';
@@ -39,7 +39,7 @@ export class Registration extends QueryableModel {
39
39
  @column({ type: 'json', decoder: new ArrayDecoder(RegisterItemOption) })
40
40
  options: RegisterItemOption[] = [];
41
41
 
42
- @column({ type: 'json', decoder: new MapDecoder(StringDecoder, RecordAnswerDecoder) })
42
+ @column({ type: 'json', decoder: RecordAnswerMapDecoder })
43
43
  recordAnswers: Map<string, RecordAnswer> = new Map();
44
44
 
45
45
  /**
@@ -117,6 +117,7 @@ export class Registration extends QueryableModel {
117
117
  waitingList = false;
118
118
 
119
119
  /**
120
+ * @deprecated - use RegistrationInvitation instead
120
121
  * When a registration is on the waiting list or is invite only, set this to true to allow the user to
121
122
  * register normally.
122
123
  */
@@ -165,6 +166,6 @@ export class Registration extends QueryableModel {
165
166
  }
166
167
 
167
168
  shouldIncludeStock() {
168
- return (this.registeredAt !== null && this.deactivatedAt === null) || this.canRegister || (this.reservedUntil && this.reservedUntil > new Date());
169
+ return (this.registeredAt !== null && this.deactivatedAt === null) || (this.reservedUntil && this.reservedUntil > new Date());
169
170
  }
170
171
  }
@@ -0,0 +1,40 @@
1
+ import { column } from '@simonbackx/simple-database';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import { QueryableModel } from '@stamhoofd/sql';
5
+
6
+ /**
7
+ * Invitation to register for a group. If an invitation exists the member can always register even if he does not meet the requirements of the group.
8
+ * Used for allowing members who are on a waiting list to register for a group.
9
+ */
10
+ export class RegistrationInvitation extends QueryableModel {
11
+ static table = 'registration_invitations';
12
+
13
+ @column({
14
+ primary: true, type: 'string', beforeSave(value) {
15
+ return value ?? uuidv4();
16
+ },
17
+ })
18
+ id: string;
19
+
20
+ @column({ type: 'string' })
21
+ groupId: string;
22
+
23
+ @column({ type: 'string' })
24
+ memberId: string;
25
+
26
+ @column({ type: 'string' })
27
+ organizationId: string;
28
+
29
+ @column({
30
+ type: 'datetime', beforeSave(old?: any) {
31
+ if (old !== undefined) {
32
+ return old;
33
+ }
34
+ const date = new Date();
35
+ date.setMilliseconds(0);
36
+ return date;
37
+ },
38
+ })
39
+ createdAt: Date;
40
+ }
@@ -49,4 +49,36 @@ export class STCredit extends QueryableModel {
49
49
  skipUpdate: true,
50
50
  })
51
51
  updatedAt: Date;
52
+
53
+ static async getForOrganization(organizationId: string) {
54
+ return await STCredit.where({ organizationId }, {
55
+ sort: [{
56
+ column: 'createdAt',
57
+ direction: 'DESC',
58
+ }],
59
+ });
60
+ }
61
+
62
+ static async getBalance(organizationId: string) {
63
+ const now = new Date();
64
+ const credits = await this.getForOrganization(organizationId);
65
+ credits.reverse();
66
+ let balance = 0;
67
+
68
+ for (const credit of credits) {
69
+ if (credit.expireAt !== null && credit.expireAt <= now) {
70
+ continue;
71
+ }
72
+ // TODO: we can expire credits here
73
+ balance += credit.change;
74
+ if (balance < 0) {
75
+ // This is needed to make deleting credit and expiring credit work.
76
+ // At no point in time, the credits can get negative.
77
+ // E.g. Getting credits, using them, and later expiring 'getting the credits' won't have impact on future credits
78
+ balance = 0;
79
+ }
80
+ }
81
+
82
+ return { balance: balance };
83
+ }
52
84
  }
@@ -20,12 +20,14 @@ export class STInvoice extends QueryableModel {
20
20
  id!: string;
21
21
 
22
22
  /**
23
+ * @deprecated
23
24
  * Organization that made the invoice. Can be null if the organization was deleted and for the migration from V1 -> V2
24
25
  */
25
26
  @column({ type: 'string', nullable: true })
26
- organizationId: string | null;
27
+ protected organizationId: string | null;
27
28
 
28
29
  /**
30
+ *
29
31
  * Organization that is associated with this invoice (can be null if deleted or unknown)
30
32
  */
31
33
  @column({ type: 'string', nullable: true })
@@ -56,6 +58,12 @@ export class STInvoice extends QueryableModel {
56
58
  @column({ type: 'datetime', nullable: true })
57
59
  paidAt: Date | null = null;
58
60
 
61
+ /**
62
+ * If a invoice was refunded because of a cancellation, we store the negative invoice id here
63
+ */
64
+ @column({ type: 'string', nullable: true })
65
+ negativeInvoiceId: string | null = null;
66
+
59
67
  @column({
60
68
  type: 'datetime', beforeSave(old?: any) {
61
69
  if (old !== undefined) {
@@ -117,14 +125,12 @@ export class STInvoice extends QueryableModel {
117
125
  cardLabel: ('cardLabel' in details ? details.cardLabel : null),
118
126
  }),
119
127
  }));
120
- }
121
- catch (e) {
128
+ } catch (e) {
122
129
  console.error(e);
123
130
  }
124
131
  }
125
132
  }
126
- }
127
- catch (e) {
133
+ } catch (e) {
128
134
  console.error(e);
129
135
  }
130
136
  return mandates;
@@ -65,6 +65,19 @@ export class STPackage extends QueryableModel {
65
65
  @column({ type: 'datetime', nullable: true })
66
66
  lastEmailAt: Date | null = null;
67
67
 
68
+ /**
69
+ * Helper to handle edge cases where validUntil is null but removeAt is set
70
+ */
71
+ get endDate() {
72
+ if (!this.removeAt) {
73
+ return this.validUntil;
74
+ }
75
+ if (!this.validUntil) {
76
+ return this.removeAt;
77
+ }
78
+ return new Date(Math.min(this.validUntil.getTime(), this.removeAt.getTime()));
79
+ }
80
+
68
81
  async activate() {
69
82
  if (this.validAt !== null) {
70
83
  return;
@@ -150,8 +163,7 @@ export class STPackage extends QueryableModel {
150
163
  pack.meta.pricingType = STPricingType.Fixed;
151
164
  pack.validUntil = null;
152
165
  pack.removeAt = null;
153
- }
154
- else if (pack.meta.type === STPackageType.Members) {
166
+ } else if (pack.meta.type === STPackageType.Members) {
155
167
  pack.meta.serviceFeeFixed = 0;
156
168
  pack.meta.serviceFeePercentage = 0;
157
169
  pack.meta.serviceFeeMinimum = 0;
@@ -165,8 +177,6 @@ export class STPackage extends QueryableModel {
165
177
  }
166
178
 
167
179
  createStatus(): STPackageStatus {
168
- // TODO: if payment failed: temporary set valid until to 2 weeks after last/first failed payment
169
-
170
180
  return STPackageStatus.create({
171
181
  startDate: this.meta.startDate,
172
182
  validUntil: this.validUntil,
@@ -205,20 +215,16 @@ export class STPackage extends QueryableModel {
205
215
  if (this.meta.type === STPackageType.Members) {
206
216
  type = EmailTemplateType.MembersExpirationReminder;
207
217
  allowDays = 32;
208
- }
209
- else if (this.meta.type === STPackageType.Webshops) {
218
+ } else if (this.meta.type === STPackageType.Webshops) {
210
219
  type = EmailTemplateType.WebshopsExpirationReminder;
211
220
  allowDays = 32;
212
- }
213
- else if (this.meta.type === STPackageType.SingleWebshop) {
221
+ } else if (this.meta.type === STPackageType.SingleWebshop) {
214
222
  type = EmailTemplateType.SingleWebshopExpirationReminder;
215
223
  allowDays = 7;
216
- }
217
- else if (this.meta.type === STPackageType.TrialMembers) {
224
+ } else if (this.meta.type === STPackageType.TrialMembers) {
218
225
  type = EmailTemplateType.TrialMembersExpirationReminder;
219
226
  allowDays = 3;
220
- }
221
- else if (this.meta.type === STPackageType.TrialWebshops) {
227
+ } else if (this.meta.type === STPackageType.TrialWebshops) {
222
228
  type = EmailTemplateType.TrialWebshopsExpirationReminder;
223
229
  allowDays = 3;
224
230
  }
@@ -237,8 +243,7 @@ export class STPackage extends QueryableModel {
237
243
  });
238
244
  }
239
245
  this.lastEmailAt = new Date();
240
- }
241
- else {
246
+ } else {
242
247
  console.log('Skip sending expiration email for ' + this.id + ' (no type)');
243
248
  }
244
249
 
@@ -25,6 +25,13 @@ export class UsedRegisterCode extends QueryableModel {
25
25
  * Set if this has been rewarded
26
26
  */
27
27
  @column({ type: 'string', nullable: true })
28
+ balanceItemId: string | null = null;
29
+
30
+ /**
31
+ * @deprecated Migrated to balanceItemId
32
+ * Set if this has been rewarded
33
+ */
34
+ @column({ type: 'string', nullable: true })
28
35
  creditId: string | null = null;
29
36
 
30
37
  @column({
@@ -48,4 +55,31 @@ export class UsedRegisterCode extends QueryableModel {
48
55
  skipUpdate: true,
49
56
  })
50
57
  updatedAt: Date;
58
+
59
+ static async getFor(organizationId: string): Promise<UsedRegisterCode | undefined> {
60
+ const code = await this.where({ organizationId }, { limit: 1 });
61
+ return code[0] ?? undefined;
62
+ }
63
+
64
+ static async getAll(code: string) {
65
+ const used = await UsedRegisterCode.where({
66
+ code,
67
+ });
68
+ return used;
69
+ }
70
+
71
+ static async getUsed(code: string) {
72
+ const used = await UsedRegisterCode.select()
73
+ .where('code', code)
74
+ .andWhere('balanceItemId', '!=', null)
75
+ .fetch();
76
+ return used;
77
+ }
78
+
79
+ static async getUsedCount(code: string) {
80
+ return await UsedRegisterCode.select()
81
+ .where('code', code)
82
+ .andWhere('balanceItemId', '!=', null)
83
+ .count();
84
+ }
51
85
  }
@@ -141,20 +141,19 @@ export class User extends QueryableModel {
141
141
  return global;
142
142
  }
143
143
 
144
- static async getAdmins(organizationId: string, options?: { verified?: boolean }) {
144
+ static async getAdmins(organizationId: string | string[], options?: { verified?: boolean }) {
145
145
  if (STAMHOOFD.userMode === 'platform') {
146
146
  // Custom implementation
147
147
  const q = User.select()
148
148
  .where('organizationId', null)
149
149
  .where('permissions', '!=', null)
150
150
  .where(
151
- SQL.jsonExtract(
152
- SQL.jsonExtract(SQL.column('permissions'), '$.value.organizationPermissions'),
153
- '$."' + organizationId + '"',
154
- true,
151
+ SQL.jsonOverlaps(
152
+ SQL.jsonKeys(SQL.jsonExtract(SQL.column('permissions'), '$.value.organizationPermissions')),
153
+ SQL.cast(SQL.scalar(JSON.stringify(Array.isArray(organizationId) ? organizationId : [organizationId])), 'JSON'),
155
154
  ),
156
- '!=',
157
- null,
155
+ '=',
156
+ 1,
158
157
  )
159
158
  ;
160
159
 
@@ -22,6 +22,10 @@ import { Group } from './Group.js';
22
22
  import { Registration } from './Registration.js';
23
23
  import { Token } from './Token.js';
24
24
  import { PasswordToken } from './PasswordToken.js';
25
+ import { City } from './addresses/City.js';
26
+ import { Province } from './addresses/Province.js';
27
+ import { PostalCode } from './addresses/PostalCode.js';
28
+ import { Street } from './addresses/Street.js';
25
29
 
26
30
  if (User === undefined) {
27
31
  throw new Error('Unexpected missing User');
@@ -131,3 +135,28 @@ if (!PasswordToken.relations) {
131
135
  PasswordToken.user = new ManyToOneRelation(User, 'user');
132
136
  PasswordToken.user.foreignKey = 'userId';
133
137
  PasswordToken.relations.push(PasswordToken.user);
138
+
139
+ if (!City.relations) {
140
+ City.relations = [];
141
+ }
142
+ City.parentCity = new ManyToOneRelation(City, 'parentCity');
143
+ City.parentCity.foreignKey = 'parentCityId';
144
+ City.relations.push(City.parentCity);
145
+
146
+ City.province = new ManyToOneRelation(Province, 'province');
147
+ City.province.foreignKey = 'provinceId';
148
+ City.relations.push(City.province);
149
+
150
+ if (!PostalCode.relations) {
151
+ PostalCode.relations = [];
152
+ }
153
+ PostalCode.city = new ManyToOneRelation(City, 'city');
154
+ PostalCode.city.foreignKey = 'cityId';
155
+ PostalCode.relations.push(PostalCode.city);
156
+
157
+ if (!Street.relations) {
158
+ Street.relations = [];
159
+ }
160
+ Street.city = new ManyToOneRelation(City, 'city');
161
+ Street.city.foreignKey = 'cityId';
162
+ Street.relations.push(Street.city);
@@ -1,9 +1,10 @@
1
- import { column, ManyToOneRelation } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import type { ManyToOneRelation } from '@simonbackx/simple-database';
2
3
  import type { Country } from '@stamhoofd/types/Country';
3
4
  import { v4 as uuidv4 } from 'uuid';
4
5
 
5
6
  import { QueryableModel } from '@stamhoofd/sql';
6
- import { Province } from './Province.js';
7
+ import type { Province } from './Province.js';
7
8
 
8
9
  export class City extends QueryableModel {
9
10
  static table = 'cities';
@@ -18,15 +19,16 @@ export class City extends QueryableModel {
18
19
  @column({ type: 'string' })
19
20
  name: string;
20
21
 
21
- @column({ type: 'string', foreignKey: City.province })
22
+ @column({ type: 'string' })
22
23
  provinceId: string;
23
24
 
24
- @column({ type: 'string', foreignKey: City.parentCity, nullable: true })
25
+ @column({ type: 'string', nullable: true })
25
26
  parentCityId: string | null = null;
26
27
 
27
28
  @column({ type: 'string' })
28
29
  country: Country;
29
30
 
30
- static parentCity = new ManyToOneRelation(City, 'parentCity');
31
- static province = new ManyToOneRelation(Province, 'province');
31
+ // Relations are wired up in _relations.ts (after the classes are defined) to avoid circular references.
32
+ static parentCity: ManyToOneRelation<'parentCity', City>;
33
+ static province: ManyToOneRelation<'province', Province>;
32
34
  }
@@ -3,6 +3,7 @@ import { Country } from '@stamhoofd/types/Country';
3
3
  import { City } from './City.js';
4
4
  import { PostalCode } from './PostalCode.js';
5
5
  import { Province } from './Province.js';
6
+ import '../_relations.js';
6
7
 
7
8
  describe('Model.PostalCode', () => {
8
9
  let oostVlaanderen!: Province;
@@ -1,4 +1,5 @@
1
- import { column, Database, ManyToOneRelation } from '@simonbackx/simple-database';
1
+ import { column, Database } from '@simonbackx/simple-database';
2
+ import type { ManyToOneRelation } from '@simonbackx/simple-database';
2
3
  import { SimpleError } from '@simonbackx/simple-errors';
3
4
  import { QueryableModel } from '@stamhoofd/sql';
4
5
  import { Country } from '@stamhoofd/types/Country';
@@ -20,13 +21,14 @@ export class PostalCode extends QueryableModel {
20
21
  @column({ type: 'string' })
21
22
  postalCode: string;
22
23
 
23
- @column({ type: 'string', foreignKey: PostalCode.city })
24
+ @column({ type: 'string' })
24
25
  cityId: string;
25
26
 
26
27
  @column({ type: 'string' })
27
28
  country: Country;
28
29
 
29
- static city = new ManyToOneRelation(City, 'city');
30
+ // Relation is wired up in _relations.ts (after the classes are defined) to avoid circular references.
31
+ static city: ManyToOneRelation<'city', City>;
30
32
 
31
33
  /**
32
34
  * Search for a given city via it's postal code and country, collecting information about the city (id), parentCity (id) and province (id)
@@ -1,8 +1,9 @@
1
- import { column, ManyToOneRelation } from '@simonbackx/simple-database';
1
+ import { column } from '@simonbackx/simple-database';
2
+ import type { ManyToOneRelation } from '@simonbackx/simple-database';
2
3
  import { QueryableModel } from '@stamhoofd/sql';
3
4
  import { v4 as uuidv4 } from 'uuid';
4
5
 
5
- import { City } from './City.js';
6
+ import type { City } from './City.js';
6
7
 
7
8
  export class Street extends QueryableModel {
8
9
  static table = 'streets';
@@ -17,8 +18,9 @@ export class Street extends QueryableModel {
17
18
  @column({ type: 'string' })
18
19
  name: string;
19
20
 
20
- @column({ type: 'string', foreignKey: Street.city })
21
+ @column({ type: 'string' })
21
22
  cityId: string;
22
23
 
23
- static city = new ManyToOneRelation(City, 'city');
24
+ // Relation is wired up in _relations.ts (after the classes are defined) to avoid circular references.
25
+ static city: ManyToOneRelation<'city', City>;
24
26
  }
@@ -61,5 +61,8 @@ export * from './UitpasClientCredential.js';
61
61
 
62
62
  export * from './Invoice.js';
63
63
  export * from './InvoicedBalanceItem.js';
64
+ export * from './RegistrationInvitation.js';
65
+
66
+ export * from './v1GroupMigrationData.js';
64
67
 
65
68
  import './_relations.js';