@stamhoofd/models 2.121.0 → 2.122.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/dist/factories/GroupFactory.d.ts.map +1 -1
  2. package/dist/factories/GroupFactory.js +1 -1
  3. package/dist/factories/GroupFactory.js.map +1 -1
  4. package/dist/factories/OrganizationFactory.d.ts +2 -1
  5. package/dist/factories/OrganizationFactory.d.ts.map +1 -1
  6. package/dist/factories/OrganizationFactory.js +9 -1
  7. package/dist/factories/OrganizationFactory.js.map +1 -1
  8. package/dist/factories/STPackageFactory.js.map +1 -1
  9. package/dist/factories/UserFactory.d.ts.map +1 -1
  10. package/dist/factories/UserFactory.js +2 -2
  11. package/dist/factories/UserFactory.js.map +1 -1
  12. package/dist/helpers/EmailBuilder.d.ts.map +1 -1
  13. package/dist/helpers/EmailBuilder.js +8 -8
  14. package/dist/helpers/EmailBuilder.js.map +1 -1
  15. package/dist/helpers/Handlebars.d.ts.map +1 -1
  16. package/dist/helpers/Handlebars.js +7 -1
  17. package/dist/helpers/Handlebars.js.map +1 -1
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/migrations/1605262045-import-postcodes.d.ts.map +1 -1
  23. package/dist/migrations/1605262045-import-postcodes.js +58 -24
  24. package/dist/migrations/1605262045-import-postcodes.js.map +1 -1
  25. package/dist/migrations/1605262046-import-postcodes-nl.d.ts.map +1 -1
  26. package/dist/migrations/1605262046-import-postcodes-nl.js +54 -17
  27. package/dist/migrations/1605262046-import-postcodes-nl.js.map +1 -1
  28. package/dist/migrations/1719567881-organization-periodId.sql +2 -0
  29. package/dist/migrations/1719567882-groups-periodId.sql +2 -0
  30. package/dist/migrations/1720080975-convert-charset.d.ts +4 -0
  31. package/dist/migrations/1720080975-convert-charset.d.ts.map +1 -0
  32. package/dist/migrations/1720080975-convert-charset.js +26 -0
  33. package/dist/migrations/1720080975-convert-charset.js.map +1 -0
  34. package/dist/migrations/1720080976-convert-charset-leads.d.ts.map +1 -1
  35. package/dist/migrations/1720080976-convert-charset-leads.js +11 -10
  36. package/dist/migrations/1720080976-convert-charset-leads.js.map +1 -1
  37. package/dist/migrations/1721400546-users-memberId.sql +2 -0
  38. package/dist/migrations/1722269236-group-waitinglist-id.sql +2 -1
  39. package/dist/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  40. package/dist/migrations/1722525787-depending-balance-item.sql +2 -0
  41. package/dist/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  42. package/dist/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  43. package/dist/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  44. package/dist/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  45. package/dist/migrations/1733504881-negative-invoice-id.sql +6 -0
  46. package/dist/migrations/1733994455-balance-item-status-open.d.ts +4 -0
  47. package/dist/migrations/1733994455-balance-item-status-open.d.ts.map +1 -0
  48. package/dist/migrations/1733994455-balance-item-status-open.js +28 -0
  49. package/dist/migrations/1733994455-balance-item-status-open.js.map +1 -0
  50. package/dist/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  51. package/dist/migrations/1769087809-payments-invoice-id.sql +2 -0
  52. package/dist/migrations/1772033555-balance-item-package-id.sql +2 -0
  53. package/dist/migrations/1778796615-payments-reversing-payment-id.sql +2 -0
  54. package/dist/migrations/1779443446-transfer-fees.sql +3 -0
  55. package/dist/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  56. package/dist/migrations/1779968328-payments-admin-user-id.sql +5 -0
  57. package/dist/migrations/1779970611-payments-refunded-amount.sql +2 -0
  58. package/dist/migrations/1779972640-balance-items-failed-at.sql +2 -0
  59. package/dist/migrations/1780328285-document-template-locked.sql +2 -0
  60. package/dist/migrations/1780328286-document-locked.sql +2 -0
  61. package/dist/migrations/1780412083-documents-set-locked.d.ts +4 -0
  62. package/dist/migrations/1780412083-documents-set-locked.d.ts.map +1 -0
  63. package/dist/migrations/1780412083-documents-set-locked.js +14 -0
  64. package/dist/migrations/1780412083-documents-set-locked.js.map +1 -0
  65. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts +4 -0
  66. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts.map +1 -0
  67. package/dist/migrations/1780928401-v1-groups-migration-data.js +44 -0
  68. package/dist/migrations/1780928401-v1-groups-migration-data.js.map +1 -0
  69. package/dist/models/BalanceItem.d.ts +7 -2
  70. package/dist/models/BalanceItem.d.ts.map +1 -1
  71. package/dist/models/BalanceItem.js +41 -38
  72. package/dist/models/BalanceItem.js.map +1 -1
  73. package/dist/models/CachedBalance.d.ts +6 -1
  74. package/dist/models/CachedBalance.d.ts.map +1 -1
  75. package/dist/models/CachedBalance.js +3 -2
  76. package/dist/models/CachedBalance.js.map +1 -1
  77. package/dist/models/Document.d.ts +4 -0
  78. package/dist/models/Document.d.ts.map +1 -1
  79. package/dist/models/Document.js +26 -3
  80. package/dist/models/Document.js.map +1 -1
  81. package/dist/models/DocumentTemplate.d.ts +4 -0
  82. package/dist/models/DocumentTemplate.d.ts.map +1 -1
  83. package/dist/models/DocumentTemplate.js +37 -1
  84. package/dist/models/DocumentTemplate.js.map +1 -1
  85. package/dist/models/Email.d.ts.map +1 -1
  86. package/dist/models/Email.js +1 -1
  87. package/dist/models/Email.js.map +1 -1
  88. package/dist/models/EmailVerificationCode.d.ts.map +1 -1
  89. package/dist/models/EmailVerificationCode.js +3 -13
  90. package/dist/models/EmailVerificationCode.js.map +1 -1
  91. package/dist/models/Event.d.ts +2 -1
  92. package/dist/models/Event.d.ts.map +1 -1
  93. package/dist/models/Event.js +3 -0
  94. package/dist/models/Event.js.map +1 -1
  95. package/dist/models/EventNotification.d.ts.map +1 -1
  96. package/dist/models/EventNotification.js +5 -5
  97. package/dist/models/EventNotification.js.map +1 -1
  98. package/dist/models/Invoice.d.ts +1 -0
  99. package/dist/models/Invoice.d.ts.map +1 -1
  100. package/dist/models/Invoice.js +8 -0
  101. package/dist/models/Invoice.js.map +1 -1
  102. package/dist/models/MemberPlatformMembership.d.ts.map +1 -1
  103. package/dist/models/MemberPlatformMembership.js +9 -0
  104. package/dist/models/MemberPlatformMembership.js.map +1 -1
  105. package/dist/models/Order.d.ts.map +1 -1
  106. package/dist/models/Order.js +1 -0
  107. package/dist/models/Order.js.map +1 -1
  108. package/dist/models/Organization.d.ts +23 -25
  109. package/dist/models/Organization.d.ts.map +1 -1
  110. package/dist/models/Organization.js +92 -64
  111. package/dist/models/Organization.js.map +1 -1
  112. package/dist/models/PasswordToken.d.ts +5 -1
  113. package/dist/models/PasswordToken.d.ts.map +1 -1
  114. package/dist/models/PasswordToken.js +18 -17
  115. package/dist/models/PasswordToken.js.map +1 -1
  116. package/dist/models/Payment.d.ts +21 -2
  117. package/dist/models/Payment.d.ts.map +1 -1
  118. package/dist/models/Payment.js +43 -2
  119. package/dist/models/Payment.js.map +1 -1
  120. package/dist/models/Registration.d.ts.map +1 -1
  121. package/dist/models/Registration.js +3 -3
  122. package/dist/models/Registration.js.map +1 -1
  123. package/dist/models/STCredit.d.ts +4 -0
  124. package/dist/models/STCredit.d.ts.map +1 -1
  125. package/dist/models/STCredit.js +28 -0
  126. package/dist/models/STCredit.js.map +1 -1
  127. package/dist/models/STInvoice.d.ts +7 -1
  128. package/dist/models/STInvoice.d.ts.map +1 -1
  129. package/dist/models/STInvoice.js +9 -0
  130. package/dist/models/STInvoice.js.map +1 -1
  131. package/dist/models/STPackage.d.ts +4 -0
  132. package/dist/models/STPackage.d.ts.map +1 -1
  133. package/dist/models/STPackage.js +12 -1
  134. package/dist/models/STPackage.js.map +1 -1
  135. package/dist/models/UsedRegisterCode.d.ts +9 -0
  136. package/dist/models/UsedRegisterCode.d.ts.map +1 -1
  137. package/dist/models/UsedRegisterCode.js +31 -0
  138. package/dist/models/UsedRegisterCode.js.map +1 -1
  139. package/dist/models/_relations.js +25 -0
  140. package/dist/models/_relations.js.map +1 -1
  141. package/dist/models/addresses/City.d.ts +4 -4
  142. package/dist/models/addresses/City.d.ts.map +1 -1
  143. package/dist/models/addresses/City.js +6 -6
  144. package/dist/models/addresses/City.js.map +1 -1
  145. package/dist/models/addresses/PostalCode.d.ts +2 -2
  146. package/dist/models/addresses/PostalCode.d.ts.map +1 -1
  147. package/dist/models/addresses/PostalCode.js +4 -3
  148. package/dist/models/addresses/PostalCode.js.map +1 -1
  149. package/dist/models/addresses/Street.d.ts +3 -3
  150. package/dist/models/addresses/Street.d.ts.map +1 -1
  151. package/dist/models/addresses/Street.js +4 -4
  152. package/dist/models/addresses/Street.js.map +1 -1
  153. package/dist/models/index.d.ts +1 -0
  154. package/dist/models/index.d.ts.map +1 -1
  155. package/dist/models/index.js +1 -0
  156. package/dist/models/index.js.map +1 -1
  157. package/dist/models/v1GroupMigrationData.d.ts +22 -0
  158. package/dist/models/v1GroupMigrationData.d.ts.map +1 -0
  159. package/dist/models/v1GroupMigrationData.js +48 -0
  160. package/dist/models/v1GroupMigrationData.js.map +1 -0
  161. package/package.json +32 -13
  162. package/src/factories/GroupFactory.ts +4 -6
  163. package/src/factories/OrganizationFactory.ts +12 -4
  164. package/src/factories/STPackageFactory.ts +2 -2
  165. package/src/factories/UserFactory.ts +4 -5
  166. package/src/helpers/EmailBuilder.ts +19 -28
  167. package/src/helpers/Handlebars.ts +6 -1
  168. package/src/index.ts +0 -1
  169. package/src/migrations/1605262045-import-postcodes.ts +62 -25
  170. package/src/migrations/1605262046-import-postcodes-nl.ts +58 -17
  171. package/src/migrations/1719567881-organization-periodId.sql +2 -0
  172. package/src/migrations/1719567882-groups-periodId.sql +2 -0
  173. package/src/migrations/1720080975-convert-charset.ts +34 -0
  174. package/src/migrations/1720080976-convert-charset-leads.ts +16 -13
  175. package/src/migrations/1721400546-users-memberId.sql +2 -0
  176. package/src/migrations/1722269236-group-waitinglist-id.sql +2 -1
  177. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  178. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  179. package/src/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  180. package/src/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  181. package/src/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  182. package/src/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  183. package/src/migrations/1733504881-negative-invoice-id.sql +6 -0
  184. package/src/migrations/1733994455-balance-item-status-open.ts +30 -0
  185. package/src/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  186. package/src/migrations/1769087809-payments-invoice-id.sql +2 -0
  187. package/src/migrations/1772033555-balance-item-package-id.sql +2 -0
  188. package/src/migrations/1778796615-payments-reversing-payment-id.sql +2 -0
  189. package/src/migrations/1779443446-transfer-fees.sql +3 -0
  190. package/src/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  191. package/src/migrations/1779968328-payments-admin-user-id.sql +5 -0
  192. package/src/migrations/1779970611-payments-refunded-amount.sql +2 -0
  193. package/src/migrations/1779972640-balance-items-failed-at.sql +2 -0
  194. package/src/migrations/1780328285-document-template-locked.sql +2 -0
  195. package/src/migrations/1780328286-document-locked.sql +2 -0
  196. package/src/migrations/1780412083-documents-set-locked.ts +18 -0
  197. package/src/migrations/1780928401-v1-groups-migration-data.ts +50 -0
  198. package/src/models/BalanceItem.ts +44 -43
  199. package/src/models/CachedBalance.test.ts +46 -46
  200. package/src/models/CachedBalance.ts +7 -7
  201. package/src/models/Document.ts +34 -13
  202. package/src/models/DocumentTemplate.ts +56 -17
  203. package/src/models/Email.test.ts +3 -3
  204. package/src/models/Email.ts +28 -49
  205. package/src/models/EmailVerificationCode.ts +8 -22
  206. package/src/models/Event.ts +6 -4
  207. package/src/models/EventNotification.ts +6 -6
  208. package/src/models/Invoice.ts +9 -0
  209. package/src/models/MemberPlatformMembership.test.ts +70 -0
  210. package/src/models/MemberPlatformMembership.ts +16 -12
  211. package/src/models/Order.ts +14 -26
  212. package/src/models/Organization.ts +122 -93
  213. package/src/models/PasswordToken.ts +21 -21
  214. package/src/models/Payment.ts +42 -4
  215. package/src/models/Registration.ts +3 -3
  216. package/src/models/STCredit.ts +32 -0
  217. package/src/models/STInvoice.ts +11 -5
  218. package/src/models/STPackage.ts +19 -14
  219. package/src/models/UsedRegisterCode.ts +34 -0
  220. package/src/models/_relations.ts +29 -0
  221. package/src/models/addresses/City.ts +8 -6
  222. package/src/models/addresses/PostalCode.test.ts +1 -0
  223. package/src/models/addresses/PostalCode.ts +5 -3
  224. package/src/models/addresses/Street.ts +6 -4
  225. package/src/models/index.ts +2 -0
  226. package/src/models/v1GroupMigrationData.ts +43 -0
  227. package/dist/helpers/MemberMerger.d.ts +0 -14
  228. package/dist/helpers/MemberMerger.d.ts.map +0 -1
  229. package/dist/helpers/MemberMerger.js +0 -364
  230. package/dist/helpers/MemberMerger.js.map +0 -1
  231. package/dist/migrations/1720080975-convert-charset.sql +0 -85
  232. package/dist/migrations/1723202126-member-number-index.sql +0 -2
  233. package/dist/models/OneTimeToken.d.ts +0 -38
  234. package/dist/models/OneTimeToken.d.ts.map +0 -1
  235. package/dist/models/OneTimeToken.js +0 -125
  236. package/dist/models/OneTimeToken.js.map +0 -1
  237. package/src/helpers/MemberMerger.test.ts +0 -782
  238. package/src/helpers/MemberMerger.ts +0 -577
  239. package/src/migrations/1720080975-convert-charset.sql +0 -85
  240. package/src/migrations/1723202126-member-number-index.sql +0 -2
  241. package/src/models/OneTimeToken.ts +0 -133
@@ -1,8 +1,8 @@
1
1
  import { column, ManyToManyRelation } from '@simonbackx/simple-database';
2
- import { MapDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { EnumDecoder } from '@simonbackx/simple-encoding';
3
3
  import { QueryableModel } from '@stamhoofd/sql';
4
- import type { RecordAnswer} from '@stamhoofd/structures';
5
- import { EventNotificationStatus, RecordAnswerDecoder } from '@stamhoofd/structures';
4
+ import type { RecordAnswer } from '@stamhoofd/structures';
5
+ import { EventNotificationStatus, RecordAnswerMapDecoder } from '@stamhoofd/structures';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { Event } from './Event.js';
8
8
 
@@ -26,7 +26,7 @@ export class EventNotification extends QueryableModel {
26
26
  @column({ type: 'datetime' })
27
27
  endDate: Date;
28
28
 
29
- @column({ type: 'string' })
29
+ @column({ type: 'string', decoder: new EnumDecoder(EventNotificationStatus) })
30
30
  status = EventNotificationStatus.Draft;
31
31
 
32
32
  /**
@@ -39,13 +39,13 @@ export class EventNotification extends QueryableModel {
39
39
  @column({ type: 'string' })
40
40
  organizationId: string;
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
  /**
46
46
  * Contains the answers of an event notification that were accepted
47
47
  */
48
- @column({ type: 'json', decoder: new MapDecoder(StringDecoder, RecordAnswerDecoder) })
48
+ @column({ type: 'json', decoder: RecordAnswerMapDecoder })
49
49
  acceptedRecordAnswers: Map<string, RecordAnswer> = new Map();
50
50
 
51
51
  @column({ type: 'string', nullable: true })
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
5
5
  import { ArrayDecoder } from '@simonbackx/simple-encoding';
6
6
  import { QueryableModel } from '@stamhoofd/sql';
7
7
  import { InvoicedBalanceItem } from './InvoicedBalanceItem.js';
8
+ import { Formatter } from '@stamhoofd/utility';
8
9
 
9
10
  export class Invoice extends QueryableModel {
10
11
  static table = 'invoices';
@@ -153,4 +154,12 @@ export class Invoice extends QueryableModel {
153
154
  const invoicedBalanceItems = await InvoicedBalanceItem.select().where('invoiceId', invoices.map(i => i.id)).fetch();
154
155
  return { invoicedBalanceItems };
155
156
  }
157
+
158
+ generateCustomerFilename(ext: 'pdf' | 'xml') {
159
+ if (!this.number || !this.invoicedAt) {
160
+ return this.id + '.' + ext;
161
+ }
162
+ const date = this.invoicedAt
163
+ return Formatter.dateIso(date) + ' - ' + (this.totalWithVAT < 0 ? $t('%1aE') : $t('%1Zw')) + ' ' + this.number + ' - ' + Formatter.fileSlug(this.seller.name) + '.' + ext;
164
+ }
156
165
  }
@@ -0,0 +1,70 @@
1
+ import { PlatformMembershipType, PlatformMembershipTypeBehaviour, PlatformMembershipTypeConfig } from '@stamhoofd/structures';
2
+
3
+ import { MemberFactory } from '../factories/MemberFactory.js';
4
+ import { OrganizationFactory } from '../factories/OrganizationFactory.js';
5
+ import { RegistrationPeriodFactory } from '../factories/RegistrationPeriodFactory.js';
6
+ import { MemberPlatformMembership } from './MemberPlatformMembership.js';
7
+ import { Platform } from './Platform.js';
8
+
9
+ describe('MemberPlatformMembership', () => {
10
+ beforeEach(() => {
11
+ vitest.useFakeTimers({ toFake: ['Date'] }).setSystemTime(new Date(2024, 4, 1, 0, 0, 0, 0));
12
+ });
13
+
14
+ afterEach(() => {
15
+ vitest.useRealTimers();
16
+ });
17
+
18
+ async function setupDaysMembership(maximumDays: number) {
19
+ const period = await new RegistrationPeriodFactory({
20
+ startDate: new Date(2024, 0, 1, 0, 0, 0, 0),
21
+ endDate: new Date(2024, 11, 31, 23, 59, 59, 999),
22
+ }).create();
23
+ const organization = await new OrganizationFactory({ period }).create();
24
+ const member = await new MemberFactory({ organization }).create();
25
+ const membershipType = PlatformMembershipType.create({
26
+ name: 'Days membership',
27
+ behaviour: PlatformMembershipTypeBehaviour.Days,
28
+ periods: new Map([
29
+ [period.id, PlatformMembershipTypeConfig.create({
30
+ startDate: period.startDate,
31
+ endDate: period.endDate,
32
+ maximumDays,
33
+ })],
34
+ ]),
35
+ });
36
+
37
+ const platform = await Platform.getForEditing();
38
+ platform.periodId = period.id;
39
+ platform.config.membershipTypes = [membershipType];
40
+ await platform.save();
41
+
42
+ const membership = new MemberPlatformMembership();
43
+ membership.memberId = member.id;
44
+ membership.membershipTypeId = membershipType.id;
45
+ membership.organizationId = organization.id;
46
+ membership.periodId = period.id;
47
+
48
+ return { member, membership };
49
+ }
50
+
51
+ test('allows the inclusive maximum number of days', async () => {
52
+ const { member, membership } = await setupDaysMembership(2);
53
+ membership.startDate = new Date(2024, 4, 1, 0, 0, 0, 0);
54
+ membership.endDate = new Date(2024, 4, 2, 0, 0, 0, 0);
55
+
56
+ await expect(membership.calculatePrice(member)).resolves.toBeUndefined();
57
+ expect(membership.maximumFreeAmount).toBe(2);
58
+ });
59
+
60
+ test('rejects days memberships that exceed maximum days', async () => {
61
+ const { member, membership } = await setupDaysMembership(2);
62
+ membership.startDate = new Date(2024, 4, 1, 0, 0, 0, 0);
63
+ membership.endDate = new Date(2024, 4, 3, 0, 0, 0, 0);
64
+
65
+ await expect(membership.calculatePrice(member)).rejects.toMatchObject({
66
+ code: 'invalid_field',
67
+ field: 'endDate',
68
+ });
69
+ });
70
+ });
@@ -207,8 +207,7 @@ export class MemberPlatformMembership extends QueryableModel {
207
207
  this.startDate = startBrussels.toJSDate();
208
208
  this.endDate = endBrussels.toJSDate();
209
209
  this.expireDate = null;
210
- }
211
- else {
210
+ } else {
212
211
  this.endDate = Formatter.luxon(periodConfig.endDate).set({ hour: 23, minute: 59, second: 59, millisecond: 0 }).toJSDate();
213
212
  this.expireDate = periodConfig.expireDate ? Formatter.luxon(periodConfig.expireDate).set({ hour: 23, minute: 59, second: 59, millisecond: 0 }).toJSDate() : null;
214
213
 
@@ -222,8 +221,7 @@ export class MemberPlatformMembership extends QueryableModel {
222
221
  startBrussels = startBrussels.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
223
222
  this.startDate = startBrussels.toJSDate();
224
223
  }
225
- }
226
- else {
224
+ } else {
227
225
  // Keep the set date, but make sure it is at 0:00 in CET
228
226
  let startBrussels = Formatter.luxon(this.startDate);
229
227
  startBrussels = startBrussels.set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
@@ -248,6 +246,16 @@ export class MemberPlatformMembership extends QueryableModel {
248
246
  this.endDate = Formatter.luxon(this.startDate).set({ hour: 23, minute: 59, second: 59, millisecond: 0 }).toJSDate();
249
247
  }
250
248
 
249
+ const maximumEndDate = periodConfig.getMaximumEndDate(this.startDate, membershipType.behaviour);
250
+ if (this.endDate > maximumEndDate) {
251
+ throw new SimpleError({
252
+ code: 'invalid_field',
253
+ field: 'endDate',
254
+ message: 'End date is after the maximum allowed end date',
255
+ human: $t('%15C', { date: Formatter.date(maximumEndDate) }),
256
+ });
257
+ }
258
+
251
259
  if (periodConfig.trialDays) {
252
260
  // Check whether you are elegible for a trial
253
261
  const latestTrialDate = await this.isElegibleForTrial(member);
@@ -267,12 +275,10 @@ export class MemberPlatformMembership extends QueryableModel {
267
275
  }
268
276
 
269
277
  this.trialUntil = trialUntil.toJSDate();
270
- }
271
- else {
278
+ } else {
272
279
  this.trialUntil = null;
273
280
  }
274
- }
275
- else {
281
+ } else {
276
282
  // No trial
277
283
  this.trialUntil = null;
278
284
  }
@@ -347,8 +353,7 @@ export class MemberPlatformMembership extends QueryableModel {
347
353
  if (alreadyUsed < periodConfig.amountFree) {
348
354
  freeDays = periodConfig.amountFree - alreadyUsed;
349
355
  console.log('Free membership created for ', this.id, periodConfig.amountFree, alreadyUsed);
350
- }
351
- else {
356
+ } else {
352
357
  console.log('No free membership created for', this.id, periodConfig.amountFree, alreadyUsed);
353
358
  }
354
359
  }
@@ -359,8 +364,7 @@ export class MemberPlatformMembership extends QueryableModel {
359
364
  this.priceWithoutDiscount = earliestPriceConfig.calculatePrice(tagIds, false, days);
360
365
  this.price = priceConfig.calculatePrice(tagIds, shouldApplyReducedPrice, Math.max(0, days - freeDays));
361
366
  this.freeAmount = Math.min(days, freeDays);
362
- }
363
- else {
367
+ } else {
364
368
  this.priceWithoutDiscount = earliestPriceConfig.getBasePrice(tagIds, false);
365
369
  this.price = priceConfig.getBasePrice(tagIds, shouldApplyReducedPrice);
366
370
  this.maximumFreeAmount = this.price > 0 ? 1 : 0;
@@ -286,8 +286,7 @@ export class Order extends QueryableModel {
286
286
  if (webshop) {
287
287
  await this.setRelation(Order.webshop, webshop).updateTickets();
288
288
  }
289
- }
290
- else {
289
+ } else {
291
290
  this.markUpdated();
292
291
  await this.save();
293
292
  }
@@ -427,8 +426,7 @@ export class Order extends QueryableModel {
427
426
  // Remove any reserved stock
428
427
  const updated = Order.updateTimeSlotStock(timeSlot, this.data, false);
429
428
  changed = changed || updated;
430
- }
431
- else {
429
+ } else {
432
430
  this.data.reservedOrder = false;
433
431
  this.data.reservedPersons = 0;
434
432
  changed = true;
@@ -442,8 +440,7 @@ export class Order extends QueryableModel {
442
440
  if (timeSlot) {
443
441
  const updated = Order.updateTimeSlotStock(timeSlot, this.data, add);
444
442
  changed = changed || updated;
445
- }
446
- else {
443
+ } else {
447
444
  console.error('Missing timeslot ' + s.id + ' in webshop ' + this.webshopId);
448
445
  }
449
446
  }
@@ -456,6 +453,7 @@ export class Order extends QueryableModel {
456
453
  if (previousData !== null) {
457
454
  // Already removed
458
455
  item.reservedSeats = [];
456
+ // eslint-disable-next-line no-useless-assignment
459
457
  changed = true;
460
458
  }
461
459
 
@@ -491,8 +489,7 @@ export class Order extends QueryableModel {
491
489
  code.reserved = false;
492
490
  discountCodeUsageMap.set(code.id, (discountCodeUsageMap.get(code.id) ?? 0) - 1);
493
491
  changed = true;
494
- }
495
- else if (!code.reserved && add) {
492
+ } else if (!code.reserved && add) {
496
493
  code.reserved = true;
497
494
  discountCodeUsageMap.set(code.id, (discountCodeUsageMap.get(code.id) ?? 0) + 1);
498
495
  changed = true;
@@ -599,8 +596,7 @@ export class Order extends QueryableModel {
599
596
  ticketMap.set(item.product.id, offset + item.amount);
600
597
  }
601
598
  }
602
- }
603
- else {
599
+ } else {
604
600
  // Create a shared ticket for the whole order
605
601
  const ticket = new Ticket();
606
602
  ticket.orderId = this.id;
@@ -752,8 +748,7 @@ export class Order extends QueryableModel {
752
748
  // Needs to happen before validation, because we can include the tickets in the validation that way
753
749
  if (this.validAt === null) {
754
750
  await this.setRelation(Order.webshop, webshop).markValid(payment, tickets);
755
- }
756
- else {
751
+ } else {
757
752
  this.markUpdated();
758
753
  await this.save();
759
754
 
@@ -764,8 +759,7 @@ export class Order extends QueryableModel {
764
759
  if (this.data.customer.email.length > 0) {
765
760
  if (didCreateTickets) {
766
761
  await this.setRelation(Order.webshop, webshop).sendTickets();
767
- }
768
- else {
762
+ } else {
769
763
  if (payment && payment.method === PaymentMethod.Transfer) {
770
764
  await this.setRelation(Order.webshop, webshop).sendPaidMail();
771
765
  }
@@ -976,40 +970,34 @@ export class Order extends QueryableModel {
976
970
  await this.sendEmailTemplate({
977
971
  type: EmailTemplateType.TicketsConfirmationPOS,
978
972
  });
979
- }
980
- else {
973
+ } else {
981
974
  await this.sendEmailTemplate({
982
975
  type: EmailTemplateType.TicketsConfirmation,
983
976
  });
984
977
  }
985
- }
986
- else {
978
+ } else {
987
979
  if (this.webshop.meta.ticketType === WebshopTicketType.None) {
988
980
  if (payment && payment.method === PaymentMethod.Transfer) {
989
981
  // Also send a copy
990
982
  await this.sendEmailTemplate({
991
983
  type: EmailTemplateType.OrderConfirmationTransfer,
992
984
  });
993
- }
994
- else if (payment && payment.method === PaymentMethod.PointOfSale) {
985
+ } else if (payment && payment.method === PaymentMethod.PointOfSale) {
995
986
  await this.sendEmailTemplate({
996
987
  type: EmailTemplateType.OrderConfirmationPOS,
997
988
  });
998
- }
999
- else {
989
+ } else {
1000
990
  // Also send a copy
1001
991
  await this.sendEmailTemplate({
1002
992
  type: EmailTemplateType.OrderConfirmationOnline,
1003
993
  });
1004
994
  }
1005
- }
1006
- else {
995
+ } else {
1007
996
  if (payment && payment.method === PaymentMethod.Transfer) {
1008
997
  await this.sendEmailTemplate({
1009
998
  type: EmailTemplateType.TicketsConfirmationTransfer,
1010
999
  });
1011
- }
1012
- else {
1000
+ } else {
1013
1001
  console.error('Unexpected missing tickets for order where tickets are expected');
1014
1002
  }
1015
1003
  }
@@ -6,19 +6,21 @@ import { I18n } from '@stamhoofd/backend-i18n/I18n';
6
6
  import type { EmailInterfaceRecipient } from '@stamhoofd/email';
7
7
  import { QueueHandler } from '@stamhoofd/queues';
8
8
  import { QueryableModel, SQL } from '@stamhoofd/sql';
9
- import type { OrganizationEmail, PrivatePaymentConfiguration } from '@stamhoofd/structures';
10
- import { Address, appToUri, Company, DNSRecordStatus, EmailTemplateType, GroupType, OrganizationMetaData, OrganizationPrivateMetaData, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, Recipient, Replacement, STPackageType, TransferSettings } from '@stamhoofd/structures';
11
- import { Formatter } from '@stamhoofd/utility';
9
+ import type { AppType, OrganizationEmail, PrivatePaymentConfiguration } from '@stamhoofd/structures';
10
+ import { AccessRight, Address, appToUri, Company, DNSRecordStatus, EmailTemplateType, getAppHost, GroupType, OrganizationMetaData, OrganizationPrivateMetaData, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, Recipient, Replacement, STPackageType, TransferSettings } from '@stamhoofd/structures';
11
+ import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
12
+ import { Country } from '@stamhoofd/types/Country';
13
+ import { Language } from '@stamhoofd/types/Language';
14
+ import { Formatter, Sorter } from '@stamhoofd/utility';
12
15
  import { v4 as uuidv4 } from 'uuid';
13
16
  import { validateDNSRecords } from '../helpers/DNSValidator.js';
14
17
  import { OrganizationServerMetaData } from '../structures/OrganizationServerMetaData.js';
15
18
  import { Group } from './Group.js';
16
19
  import { OrganizationRegistrationPeriod } from './OrganizationRegistrationPeriod.js';
17
- import { StripeAccount } from './StripeAccount.js';
18
- import { Country } from '@stamhoofd/types/Country';
19
- import { Language } from '@stamhoofd/types/Language';
20
20
  import { Registration } from './Registration.js';
21
- import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
21
+ import { StripeAccount } from './StripeAccount.js';
22
+ import { Token } from './Token.js';
23
+ import type { User } from './User.js';
22
24
 
23
25
  export class Organization extends QueryableModel {
24
26
  static table = 'organizations';
@@ -211,57 +213,12 @@ export class Organization extends QueryableModel {
211
213
  return organization;
212
214
  }
213
215
 
214
- /**
215
- * Potentially includes a path
216
- */
217
- getRegistrationHost(i18n?: { language: Language; locale: string }): string {
218
- if (this.registerDomain) {
219
- let d = this.registerDomain;
220
-
221
- if (i18n && i18n.language !== this.i18n.language) {
222
- d += '/' + i18n.language;
223
- }
224
-
225
- return d;
226
- }
227
- return this.getDefaultRegistrationHost(i18n);
228
- }
229
-
230
- getDefaultRegistrationHost(i18n?: { language: Language; locale: string }): string {
231
- if (!STAMHOOFD.domains.registration) {
232
- return STAMHOOFD.domains.dashboard + '/' + (i18n?.locale ?? this.i18n.locale) + '/' + appToUri('registration') + '/' + this.uri;
233
- }
234
- let defaultDomain = STAMHOOFD.domains.registration[this.address.country] ?? STAMHOOFD.domains.registration[''];
235
-
236
- if (i18n && i18n.language !== this.i18n.language) {
237
- defaultDomain += '/' + i18n.language;
238
- }
239
-
240
- return this.uri + '.' + defaultDomain;
241
- }
242
-
243
216
  getDashboardHost(i18n?: { language: Language; locale: string }): string {
244
- return STAMHOOFD.domains.dashboard + '/' + (i18n?.locale ?? this.i18n.locale) + '/' + appToUri('dashboard') + '/' + this.uri;
245
- }
246
-
247
- get registerUrl() {
248
- return 'https://' + this.getRegistrationHost();
249
- }
250
-
251
- /**
252
- * @deprecated
253
- * use getRegistrationHost
254
- */
255
- getHost(i18n?: I18n): string {
256
- return this.getRegistrationHost(i18n);
217
+ return getAppHost('dashboard', this, true, i18n);
257
218
  }
258
219
 
259
- /**
260
- * @deprecated
261
- * Use getDefaultRegistrationHost
262
- */
263
- getDefaultHost(i18n?: I18n): string {
264
- return this.getDefaultRegistrationHost(i18n);
220
+ get registerUrl(): string {
221
+ return 'https://' + getAppHost('registration', this, false);
265
222
  }
266
223
 
267
224
  get marketingDomain(): string {
@@ -303,8 +260,7 @@ export class Organization extends QueryableModel {
303
260
  await created.save();
304
261
  return created;
305
262
  });
306
- }
307
- else {
263
+ } else {
308
264
  oPeriod = oPeriods[0];
309
265
  }
310
266
 
@@ -367,8 +323,7 @@ export class Organization extends QueryableModel {
367
323
 
368
324
  console.log('Did set register domain for ' + this.id + ' to ' + organization.registerDomain);
369
325
  }
370
- }
371
- else {
326
+ } else {
372
327
  // Clear register domain
373
328
  if (organization.registerDomain) {
374
329
  // We need to clear it, to prevent sending e-mails with invalid links
@@ -406,8 +361,7 @@ export class Organization extends QueryableModel {
406
361
  type: EmailTemplateType.OrganizationStableDNS,
407
362
  bcc: true,
408
363
  });
409
- }
410
- else if (!wasActive && this.privateMeta.mailDomainActive && (!didSendDomainSetupMail || didSendWarning) && !organization.serverMeta.isDNSUnstable) {
364
+ } else if (!wasActive && this.privateMeta.mailDomainActive && (!didSendDomainSetupMail || didSendWarning) && !organization.serverMeta.isDNSUnstable) {
411
365
  organization.serverMeta.didSendDomainSetupMail = true;
412
366
  await organization.save();
413
367
 
@@ -415,15 +369,13 @@ export class Organization extends QueryableModel {
415
369
  await this.sendEmailTemplate({
416
370
  type: EmailTemplateType.OrganizationDNSSetupComplete,
417
371
  });
418
- }
419
- else {
372
+ } else {
420
373
  await this.sendEmailTemplate({
421
374
  type: EmailTemplateType.OrganizationValidDNS,
422
375
  });
423
376
  }
424
377
  }
425
- }
426
- else {
378
+ } else {
427
379
  // DNS settings gone broken
428
380
  if (organization.privateMeta.mailDomain) {
429
381
  organization.privateMeta.pendingMailDomain = organization.privateMeta.pendingMailDomain ?? organization.privateMeta.mailDomain;
@@ -448,8 +400,7 @@ export class Organization extends QueryableModel {
448
400
  type: EmailTemplateType.OrganizationUnstableDNS,
449
401
  bcc: true,
450
402
  });
451
- }
452
- else if (!organization.serverMeta.isDNSUnstable && organization.serverMeta.didSendDomainSetupMail && organization.serverMeta.DNSRecordWarningCount == 0) {
403
+ } else if (!organization.serverMeta.isDNSUnstable && organization.serverMeta.didSendDomainSetupMail && organization.serverMeta.DNSRecordWarningCount == 0) {
453
404
  organization.serverMeta.DNSRecordWarningCount += 1;
454
405
  await organization.save();
455
406
 
@@ -500,6 +451,10 @@ export class Organization extends QueryableModel {
500
451
  token: 'mailDomain',
501
452
  value: this.privateMeta.mailDomain ?? this.privateMeta.pendingMailDomain ?? '',
502
453
  }),
454
+ Replacement.create({
455
+ token: 'organizationName',
456
+ value: this.name,
457
+ }),
503
458
  ],
504
459
  unsubscribeType: 'marketing',
505
460
  fromStamhoofd: true,
@@ -547,8 +502,7 @@ export class Organization extends QueryableModel {
547
502
 
548
503
  await client.send(deleteCmd);
549
504
  console.log('Deleted AWS mail idenitiy @' + this.id + ' for ' + this.privateMeta.mailDomain);
550
- }
551
- catch (e) {
505
+ } catch (e) {
552
506
  console.error('Could not delete AWS email identitiy @' + this.id + ' for ' + this.privateMeta.mailDomain);
553
507
  console.error(e);
554
508
  }
@@ -615,8 +569,7 @@ export class Organization extends QueryableModel {
615
569
  // Recreate it immediately
616
570
  exists = false;
617
571
  }
618
- }
619
- catch (e) {
572
+ } catch (e) {
620
573
  console.error(e);
621
574
  }
622
575
 
@@ -860,6 +813,17 @@ export class Organization extends QueryableModel {
860
813
  return admins.filter(a => a.permissions && a.permissions.forOrganization(this)?.hasFullAccess());
861
814
  }
862
815
 
816
+ /**
817
+ * These email addresess are private
818
+ */
819
+ async getFinanceAdmins() {
820
+ const admins = await this.getAdmins();
821
+ const filtered = admins.filter(a => a.permissions && (a.permissions.forOrganization(this)?.hasFullAccess() || a.permissions.forOrganization(this)?.hasAccessRight(AccessRight.OrganizationFinanceDirector)));
822
+
823
+ // Only full access
824
+ return filtered;
825
+ }
826
+
863
827
  /**
864
828
  * These email addresess are private
865
829
  */
@@ -879,32 +843,90 @@ export class Organization extends QueryableModel {
879
843
  return filtered.flatMap(f => f.getEmailTo());
880
844
  }
881
845
 
846
+ adminsToRecipients(admins: User[]) {
847
+ return admins.flatMap((f) => {
848
+ return Recipient.create({
849
+ firstName: f.firstName,
850
+ lastName: f.lastName,
851
+ email: f.email,
852
+ replacements: [],
853
+ });
854
+ });
855
+ }
856
+
882
857
  /**
883
858
  * These email addresess are private
884
859
  */
885
860
  async getAdminRecipients(): Promise<Recipient[]> {
886
- let filtered = await this.getFullAdmins();
861
+ const filtered = await this.getFullAdmins();
862
+ return this.adminsToRecipients(filtered);
863
+ }
887
864
 
888
- if (STAMHOOFD.environment === 'production') {
889
- if (filtered.length > 1) {
890
- // remove stamhoofd email addresses
891
- filtered = filtered.filter(e => !e.email.endsWith('@stamhoofd.be') && !e.email.endsWith('@stamhoofd.nl'));
892
- }
865
+ /**
866
+ * These email addresess are private
867
+ */
868
+ async getFinanceAdminRecipients(): Promise<Recipient[]> {
869
+ const filtered = await this.getFullAdmins();
870
+ return this.adminsToRecipients(filtered);
871
+ }
872
+
873
+ /**
874
+ * These email addresess are private
875
+ */
876
+ async getInvoicingToEmails() {
877
+ // Circular reference fix
878
+ const filtered = await this.getFinanceAdmins();
879
+
880
+ if (filtered.length > 0) {
881
+ return filtered.flatMap(f => f.getEmailTo()).map((recipient) => {
882
+ if (!recipient.name) {
883
+ return recipient.email;
884
+ }
885
+ const cleanedName = Formatter.emailSenderName(recipient.name);
886
+ if (cleanedName.length < 2) {
887
+ return recipient.email;
888
+ }
889
+ return '"' + cleanedName + '" <' + recipient.email + '>';
890
+ }).join(', ');
893
891
  }
894
892
 
895
- return filtered.flatMap((f) => {
896
- return Recipient.create({
897
- firstName: f.firstName,
898
- lastName: f.lastName,
899
- email: f.email,
900
- replacements: [
901
- Replacement.create({
902
- token: 'organizationName',
903
- value: this.name,
904
- }),
905
- ],
906
- });
893
+ return undefined;
894
+ }
895
+
896
+ /**
897
+ * Returns one email for invoices. since in ubl we can only add one address.
898
+ * We choose the oldest user that was active in the last 3 months (otherwise the oldest user if noone was active)
899
+ */
900
+ async getInvoicingToEmail(): Promise<string | undefined> {
901
+ // Circular reference fix
902
+ const admins = await this.getAdmins();
903
+
904
+ const tokens = await Token.select().where('userId', admins.map(a => a.id)).fetch();
905
+
906
+ // Sort by admins that were active in the last 3 months, then creation date
907
+ const cutoffDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * 31 * 3);
908
+ admins.sort((a, b) => {
909
+ const aTokens = tokens.filter(t => t.userId === a.id);
910
+ const bTokens = tokens.filter(t => t.userId === b.id);
911
+ const aActive = !!aTokens.find(t => t.updatedAt > cutoffDate);
912
+ const bActive = !!bTokens.find(t => t.updatedAt > cutoffDate);
913
+ return Sorter.stack(
914
+ Sorter.byBooleanValue(aActive, bActive),
915
+ Sorter.byDateValue(b.createdAt, a.createdAt),
916
+ );
907
917
  });
918
+
919
+ const filtered = admins.filter(a => a.verified && a.permissions && !a.email.endsWith('@stamhoofd.be') && (a.permissions.forOrganization(this)?.hasFullAccess() || a.permissions.forOrganization(this)?.hasAccessRight(AccessRight.OrganizationFinanceDirector)));
920
+
921
+ if (filtered.length > 0) {
922
+ return filtered.map(f => f.email)[0];
923
+ }
924
+ const filtered2 = admins.filter(a => a.verified && a.permissions && (a.permissions.forOrganization(this)?.hasFullAccess() || a.permissions.forOrganization(this)?.hasAccessRight(AccessRight.OrganizationFinanceDirector)));
925
+
926
+ if (filtered2.length > 0) {
927
+ return filtered2.map(f => f.email)[0];
928
+ }
929
+ return undefined;
908
930
  }
909
931
 
910
932
  /**
@@ -926,10 +948,10 @@ export class Organization extends QueryableModel {
926
948
  if (mandate) {
927
949
  return {
928
950
  provider: mandate.provider,
929
- stripeAccount: null
930
- }
951
+ stripeAccount: null,
952
+ };
931
953
  }
932
-
954
+
933
955
  let stripeAccount = (config.stripeAccountId ? (await StripeAccount.getByID(config.stripeAccountId)) : null) ?? null;
934
956
  if (stripeAccount && stripeAccount.organizationId !== this.id) {
935
957
  console.warn('Stripe account ' + stripeAccount.id + ' is not linked to organization ' + this.id);
@@ -1016,7 +1038,7 @@ export class Organization extends QueryableModel {
1016
1038
  * Assures at least one company at all times
1017
1039
  */
1018
1040
  get defaultCompanies() {
1019
- return this.meta.companies.length
1041
+ const b = this.meta.companies.length
1020
1042
  ? this.meta.companies
1021
1043
  : [
1022
1044
  Company.create({
@@ -1024,5 +1046,12 @@ export class Organization extends QueryableModel {
1024
1046
  address: this.address,
1025
1047
  }),
1026
1048
  ];
1049
+
1050
+ // Missing address -> use organization address
1051
+ if (!b[0].address) {
1052
+ b[0].address = this.address;
1053
+ }
1054
+
1055
+ return b;
1027
1056
  }
1028
1057
  }