@stamhoofd/models 2.78.4 → 2.79.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 (77) hide show
  1. package/dist/src/factories/GroupFactory.d.ts +4 -0
  2. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  3. package/dist/src/factories/GroupFactory.js +11 -1
  4. package/dist/src/factories/GroupFactory.js.map +1 -1
  5. package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts +14 -0
  6. package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts.map +1 -0
  7. package/dist/src/factories/MemberResponsibilityRecordFactory.js +34 -0
  8. package/dist/src/factories/MemberResponsibilityRecordFactory.js.map +1 -0
  9. package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts +13 -0
  10. package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts.map +1 -0
  11. package/dist/src/factories/OrganizationRegistrationPeriodFactory.js +20 -0
  12. package/dist/src/factories/OrganizationRegistrationPeriodFactory.js.map +1 -0
  13. package/dist/src/factories/OrganizationTagFactory.js +1 -1
  14. package/dist/src/factories/OrganizationTagFactory.js.map +1 -1
  15. package/dist/src/factories/PlatformResponsibilityFactory.d.ts +11 -0
  16. package/dist/src/factories/PlatformResponsibilityFactory.d.ts.map +1 -0
  17. package/dist/src/factories/PlatformResponsibilityFactory.js +25 -0
  18. package/dist/src/factories/PlatformResponsibilityFactory.js.map +1 -0
  19. package/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
  20. package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
  21. package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
  22. package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
  23. package/dist/src/factories/index.d.ts +3 -0
  24. package/dist/src/factories/index.d.ts.map +1 -1
  25. package/dist/src/factories/index.js +3 -0
  26. package/dist/src/factories/index.js.map +1 -1
  27. package/dist/src/helpers/EmailBuilder.d.ts +8 -10
  28. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  29. package/dist/src/helpers/EmailBuilder.js +44 -42
  30. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  31. package/dist/src/models/Email.d.ts +9 -2
  32. package/dist/src/models/Email.d.ts.map +1 -1
  33. package/dist/src/models/Email.js +18 -16
  34. package/dist/src/models/Email.js.map +1 -1
  35. package/dist/src/models/Email.test.js +183 -2
  36. package/dist/src/models/Email.test.js.map +1 -1
  37. package/dist/src/models/Member.d.ts +3 -1
  38. package/dist/src/models/Member.d.ts.map +1 -1
  39. package/dist/src/models/Member.js.map +1 -1
  40. package/dist/src/models/Order.d.ts +0 -2
  41. package/dist/src/models/Order.d.ts.map +1 -1
  42. package/dist/src/models/Order.js +0 -48
  43. package/dist/src/models/Order.js.map +1 -1
  44. package/dist/src/models/Organization.d.ts +1 -16
  45. package/dist/src/models/Organization.d.ts.map +1 -1
  46. package/dist/src/models/Organization.js +5 -86
  47. package/dist/src/models/Organization.js.map +1 -1
  48. package/dist/src/models/Platform.d.ts +11 -3
  49. package/dist/src/models/Platform.d.ts.map +1 -1
  50. package/dist/src/models/Platform.js +76 -24
  51. package/dist/src/models/Platform.js.map +1 -1
  52. package/dist/src/models/Platform.test.d.ts +2 -0
  53. package/dist/src/models/Platform.test.d.ts.map +1 -0
  54. package/dist/src/models/Platform.test.js +90 -0
  55. package/dist/src/models/Platform.test.js.map +1 -0
  56. package/dist/src/models/index.d.ts +1 -1
  57. package/dist/src/models/index.d.ts.map +1 -1
  58. package/dist/src/models/index.js +2 -3
  59. package/dist/src/models/index.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +4 -4
  62. package/src/factories/GroupFactory.ts +16 -3
  63. package/src/factories/MemberResponsibilityRecordFactory.ts +35 -0
  64. package/src/factories/OrganizationRegistrationPeriodFactory.ts +22 -0
  65. package/src/factories/OrganizationTagFactory.ts +1 -1
  66. package/src/factories/PlatformResponsibilityFactory.ts +25 -0
  67. package/src/factories/RegistrationPeriodFactory.ts +4 -0
  68. package/src/factories/index.ts +3 -0
  69. package/src/helpers/EmailBuilder.ts +54 -50
  70. package/src/models/Email.test.ts +217 -5
  71. package/src/models/Email.ts +22 -20
  72. package/src/models/Member.ts +3 -1
  73. package/src/models/Order.ts +0 -55
  74. package/src/models/Organization.ts +6 -101
  75. package/src/models/Platform.test.ts +107 -0
  76. package/src/models/Platform.ts +86 -25
  77. package/src/models/index.ts +1 -1
@@ -253,29 +253,6 @@ export class Order extends QueryableModel {
253
253
  if (webshop) {
254
254
  await this.setRelation(Order.webshop, webshop).updateTickets();
255
255
  }
256
-
257
- // Send an email if the payment failed after 15 minutes being pending
258
- // const difference = new Date().getTime() - this.createdAt.getTime()
259
- // if (difference > 1000 * 60 * 30 && difference < 1000 * 60 * 60 * 24) {
260
- // if (payment && payment.method !== PaymentMethod.Transfer && payment.method !== PaymentMethod.PointOfSale) {
261
- // console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Sending email.')
262
- // const webshop = await Webshop.getByID(this.webshopId)
263
- // if (!webshop) {
264
- // console.error("Missing organization or webshop for order "+this.id)
265
- // return
266
- // }
267
- // const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true)
268
- // await this.setRelation(Order.webshop, webshop.setRelation(Order.organization, organization)).sendEmailTemplate({
269
- // type: EmailTemplateType.OrderOnlinePaymentFailed,
270
- // from,
271
- // replyTo
272
- // })
273
- // } else {
274
- // console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Payment method not matching.')
275
- // }
276
- // } else {
277
- // console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Not sending email.')
278
- // }
279
256
  }
280
257
  else {
281
258
  this.markUpdated();
@@ -763,28 +740,18 @@ export class Order extends QueryableModel {
763
740
  }
764
741
 
765
742
  async sendPaidMail(this: Order & { webshop: Webshop & { organization: Organization } }) {
766
- const organization = this.webshop.organization;
767
- const { from, replyTo } = organization.getEmail(this.webshop.privateMeta.defaultEmailId, true);
768
-
769
743
  // For a tickets webshop, where the order was marked as paid / non-paid, we should still send the tickets email
770
744
  // - because the normal email is not editable
771
745
  const hasTickets = this.webshop.meta.hasTickets;
772
746
 
773
747
  await this.sendEmailTemplate({
774
748
  type: hasTickets ? EmailTemplateType.TicketsReceivedTransfer : EmailTemplateType.OrderReceivedTransfer,
775
- from,
776
- replyTo,
777
749
  });
778
750
  }
779
751
 
780
752
  async sendTickets(this: Order & { webshop: Webshop & { organization: Organization } }) {
781
- const organization = this.webshop.organization;
782
- const { from, replyTo } = organization.getEmail(this.webshop.privateMeta.defaultEmailId, true);
783
-
784
753
  await this.sendEmailTemplate({
785
754
  type: EmailTemplateType.TicketsReceivedTransfer,
786
- from,
787
- replyTo,
788
755
  });
789
756
  }
790
757
 
@@ -894,8 +861,6 @@ export class Order extends QueryableModel {
894
861
 
895
862
  async sendEmailTemplate(this: Order & { webshop: Webshop & { organization: Organization } }, data: {
896
863
  type: EmailTemplateType;
897
- from: string;
898
- replyTo?: string;
899
864
  to?: Recipient;
900
865
  }) {
901
866
  // Never send an email for archived webshops
@@ -970,25 +935,16 @@ export class Order extends QueryableModel {
970
935
  await this.save();
971
936
 
972
937
  if (this.data.customer.email.length > 0) {
973
- const webshop = this.webshop;
974
- const organization = webshop.organization;
975
-
976
- const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true);
977
-
978
938
  if (tickets.length > 0) {
979
939
  // Also send a copy
980
940
  if (payment && payment.method === PaymentMethod.PointOfSale) {
981
941
  await this.sendEmailTemplate({
982
942
  type: EmailTemplateType.TicketsConfirmationPOS,
983
- from,
984
- replyTo,
985
943
  });
986
944
  }
987
945
  else {
988
946
  await this.sendEmailTemplate({
989
947
  type: EmailTemplateType.TicketsConfirmation,
990
- from,
991
- replyTo,
992
948
  });
993
949
  }
994
950
  }
@@ -998,31 +954,23 @@ export class Order extends QueryableModel {
998
954
  // Also send a copy
999
955
  await this.sendEmailTemplate({
1000
956
  type: EmailTemplateType.OrderConfirmationTransfer,
1001
- from,
1002
- replyTo,
1003
957
  });
1004
958
  }
1005
959
  else if (payment && payment.method === PaymentMethod.PointOfSale) {
1006
960
  await this.sendEmailTemplate({
1007
961
  type: EmailTemplateType.OrderConfirmationPOS,
1008
- from,
1009
- replyTo,
1010
962
  });
1011
963
  }
1012
964
  else {
1013
965
  // Also send a copy
1014
966
  await this.sendEmailTemplate({
1015
967
  type: EmailTemplateType.OrderConfirmationOnline,
1016
- from,
1017
- replyTo,
1018
968
  });
1019
969
  }
1020
970
  }
1021
971
  else {
1022
972
  await this.sendEmailTemplate({
1023
973
  type: EmailTemplateType.TicketsConfirmationTransfer,
1024
- from,
1025
- replyTo,
1026
974
  });
1027
975
  }
1028
976
  }
@@ -1031,7 +979,6 @@ export class Order extends QueryableModel {
1031
979
  if (this.webshop.privateMeta.notificationEmails) {
1032
980
  const webshop = this.webshop;
1033
981
  const organization = webshop.organization;
1034
- const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true);
1035
982
  const i18n = organization.i18n;
1036
983
 
1037
984
  const webshopDashboardUrl = 'https://' + (STAMHOOFD.domains.dashboard ?? 'stamhoofd.app') + '/' + i18n.locale + '/webshops/' + Formatter.slug(webshop.meta.name) + '/orders';
@@ -1040,8 +987,6 @@ export class Order extends QueryableModel {
1040
987
  for (const email of this.webshop.privateMeta.notificationEmails) {
1041
988
  await this.sendEmailTemplate({
1042
989
  type: EmailTemplateType.OrderNotification,
1043
- from,
1044
- replyTo,
1045
990
  to: Recipient.create({
1046
991
  email,
1047
992
  replacements: [
@@ -1,5 +1,4 @@
1
1
  import { column, Database } from '@simonbackx/simple-database';
2
- import { DecodedRequest } from '@simonbackx/simple-endpoints';
3
2
  import { SimpleError } from '@simonbackx/simple-errors';
4
3
  import { I18n } from '@stamhoofd/backend-i18n';
5
4
  import { Email, EmailInterfaceRecipient } from '@stamhoofd/email';
@@ -12,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid';
12
11
 
13
12
  import { QueueHandler } from '@stamhoofd/queues';
14
13
  import { validateDNSRecords } from '../helpers/DNSValidator';
15
- import { getEmailBuilderForTemplate } from '../helpers/EmailBuilder';
14
+ import { sendEmailTemplate } from '../helpers/EmailBuilder';
16
15
  import { OrganizationServerMetaData } from '../structures/OrganizationServerMetaData';
17
16
  import { OrganizationRegistrationPeriod, StripeAccount } from './';
18
17
 
@@ -420,7 +419,6 @@ export class Organization extends QueryableModel {
420
419
  async sendEmailTemplate(data: {
421
420
  type: EmailTemplateType;
422
421
  personal?: boolean;
423
- replyTo?: string;
424
422
  bcc?: boolean;
425
423
  }) {
426
424
  const recipients = await this.getAdminRecipients();
@@ -443,15 +441,13 @@ export class Organization extends QueryableModel {
443
441
  ];
444
442
 
445
443
  // Create e-mail builder
446
- const builder = await getEmailBuilderForTemplate(this, {
444
+ await sendEmailTemplate(null, {
447
445
  replaceAll,
448
446
  recipients,
449
447
  template: {
450
448
  type: data.type,
451
449
  },
452
- from: Email.getInternalEmailFor(this.i18n),
453
- singleBcc: data.bcc ? 'simon@stamhoofd.be' : undefined,
454
- replyTo: data.replyTo,
450
+ singleBcc: data.bcc ? { email: 'simon@stamhoofd.be' } : undefined,
455
451
  type: 'transactional',
456
452
  defaultReplacements: [
457
453
  Replacement.create({
@@ -462,10 +458,6 @@ export class Organization extends QueryableModel {
462
458
  unsubscribeType: 'marketing',
463
459
  fromStamhoofd: true,
464
460
  });
465
-
466
- if (builder) {
467
- Email.schedule(builder);
468
- }
469
461
  }
470
462
 
471
463
  async deleteAWSMailIdenitity(mailDomain: string) {
@@ -855,99 +847,12 @@ export class Organization extends QueryableModel {
855
847
  /**
856
848
  * Return default e-mail address if no email addresses are set.
857
849
  */
858
- getDefaultFrom(i18n: I18n, withName = true, type: 'transactional' | 'broadcast' = 'broadcast') {
850
+ getDefaultFrom(i18n: I18n, type: 'transactional' | 'broadcast' = 'broadcast'): EmailInterfaceRecipient {
859
851
  const domain = type === 'transactional' ? i18n.localizedDomains.defaultTransactionalEmail() : i18n.localizedDomains.defaultBroadcastEmail();
860
852
 
861
- if (!withName) {
862
- return ('noreply-' + this.uri + '@' + domain);
863
- }
864
- return '"' + this.name.replaceAll('"', '\\"') + '" <' + ('noreply-' + this.uri + '@' + domain) + '>';
865
- }
866
-
867
- /**
868
- * @deprecated Switch to EmailBuilder.sendEmailTemplate
869
- */
870
- getEmail(id: string | null, strongDefault = false): { from: string; replyTo: string | undefined } {
871
- if (id === null) {
872
- return this.getDefaultEmail(strongDefault);
873
- }
874
-
875
- // Send confirmation e-mail
876
- let from = this.getDefaultFrom(this.i18n, false, strongDefault ? 'transactional' : 'broadcast');
877
- const sender: OrganizationEmail | undefined = this.privateMeta.emails.find(e => e.id === id);
878
- let replyTo: string | undefined = undefined;
879
-
880
- if (sender) {
881
- replyTo = sender.email;
882
-
883
- // Can we send from this e-mail or reply-to?
884
- if (replyTo && this.privateMeta.mailDomain && this.privateMeta.mailDomainActive && sender.email.endsWith('@' + this.privateMeta.mailDomain)) {
885
- from = sender.email;
886
- replyTo = undefined;
887
- }
888
-
889
- // Include name in form field
890
- if (sender.name) {
891
- from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + from + '>';
892
- }
893
- else {
894
- from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
895
- }
896
-
897
- if (replyTo) {
898
- if (sender.name) {
899
- replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
900
- }
901
- else {
902
- replyTo = '"' + this.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
903
- }
904
- }
905
- return { from, replyTo };
906
- }
907
- return this.getDefaultEmail(strongDefault);
908
- }
909
-
910
- /**
911
- * @deprecated Switch to EmailBuilder.sendEmailTemplate
912
- */
913
- getDefaultEmail(strongDefault = false): { from: string; replyTo: string | undefined } {
914
- // Send confirmation e-mail
915
- let from = strongDefault ? this.getDefaultFrom(this.i18n, false) : this.uri + '@stamhoofd.email';
916
- const sender: OrganizationEmail | undefined = this.privateMeta.emails.find(e => e.default) ?? this.privateMeta.emails[0];
917
- let replyTo: string | undefined = undefined;
918
-
919
- if (sender) {
920
- replyTo = sender.email;
921
-
922
- // Can we send from this e-mail or reply-to?
923
- if (replyTo && this.privateMeta.mailDomain && this.privateMeta.mailDomainActive && sender.email.endsWith('@' + this.privateMeta.mailDomain)) {
924
- from = sender.email;
925
- replyTo = undefined;
926
- }
927
-
928
- // Include name in form field
929
- if (sender.name) {
930
- from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + from + '>';
931
- }
932
- else {
933
- from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
934
- }
935
-
936
- if (replyTo) {
937
- if (sender.name) {
938
- replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
939
- }
940
- else {
941
- replyTo = '"' + this.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
942
- }
943
- }
944
- }
945
- else {
946
- from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
947
- }
948
-
949
853
  return {
950
- from, replyTo,
854
+ name: this.name,
855
+ email: ('noreply-' + this.uri + '@' + domain),
951
856
  };
952
857
  }
953
858
 
@@ -0,0 +1,107 @@
1
+ import { PlatformConfig, PlatformMembershipType } from '@stamhoofd/structures';
2
+ import { Platform } from './Platform';
3
+ import { Database } from '@simonbackx/simple-database';
4
+
5
+ describe('Model.Platform', () => {
6
+ describe('Shared caches', () => {
7
+ beforeEach(async () => {
8
+ const platform = await Platform.getByID('1');
9
+ platform!.config = PlatformConfig.create({});
10
+ await platform!.save();
11
+ });
12
+
13
+ test('Editable model changes do not propagate', async () => {
14
+ const editable = await Platform.getForEditing();
15
+ editable.config.membershipTypes = [
16
+ PlatformMembershipType.create({ id: '1', name: 'Test' }),
17
+ ];
18
+
19
+ const shared = (await Platform.getShared()) as any;
20
+ expect(shared.config.membershipTypes).toHaveLength(0);
21
+ });
22
+
23
+ test('Shared model is immutable', async () => {
24
+ const editable = await Platform.getForEditing();
25
+ editable.config.membershipTypes = [
26
+ PlatformMembershipType.create({ id: '1', name: 'Test' }),
27
+ ];
28
+ await editable.save();
29
+
30
+ const shared = (await Platform.getShared()) as any;
31
+ expect(() => {
32
+ shared.privateConfig.roles = [];
33
+ }).toThrow();
34
+
35
+ expect(() => {
36
+ shared.membershipOrganizationId = '2';
37
+ }).toThrow();
38
+
39
+ expect(shared.config.membershipTypes).toHaveLength(1);
40
+
41
+ expect(() => {
42
+ shared.concat.membershipTypes[0].name = 'Test2';
43
+ }).toThrow();
44
+ });
45
+
46
+ test('Saving changes propagates to all shared states', async () => {
47
+ const structBefore = await Platform.getSharedStruct();
48
+ const privateStructBefore = await Platform.getSharedPrivateStruct();
49
+ const sharedModelBefore = await Platform.getShared();
50
+
51
+ const editable = await Platform.getForEditing();
52
+ editable.config.membershipTypes = [
53
+ PlatformMembershipType.create({ id: '1', name: 'Hey there' }),
54
+ ];
55
+ await editable.save();
56
+
57
+ const structAfter = await Platform.getSharedStruct();
58
+ const privateStructAfter = await Platform.getSharedPrivateStruct();
59
+ const sharedModelAfter = await Platform.getShared();
60
+
61
+ expect(structAfter.config.membershipTypes[0].name).toEqual('Hey there');
62
+ expect(privateStructAfter.config.membershipTypes[0].name).toEqual('Hey there');
63
+ expect(sharedModelAfter.config.membershipTypes[0].name).toEqual('Hey there');
64
+
65
+ // Test before state not altered
66
+ expect(structBefore.config.membershipTypes).toHaveLength(0);
67
+ expect(privateStructBefore.config.membershipTypes).toHaveLength(0);
68
+ expect(sharedModelBefore.config.membershipTypes).toHaveLength(0);
69
+ });
70
+ });
71
+
72
+ describe('Creating fresh platform', () => {
73
+ beforeEach(async () => {
74
+ await Database.delete('DELETE FROM platform');
75
+ await Platform.clearCacheWithoutRefresh();
76
+ if (await Platform.getByID('1')) {
77
+ throw new Error('Platform 1 should not exist');
78
+ }
79
+ });
80
+
81
+ test('when requesting getForEditing', async () => {
82
+ const editable = await Platform.getForEditing();
83
+ expect(editable.id).toBe('1');
84
+
85
+ expect(await Platform.getByID('1')).toEqual(editable);
86
+ });
87
+
88
+ test('when requesting getShared', async () => {
89
+ const shared = await Platform.getShared();
90
+ expect(shared.id).toBe('1');
91
+
92
+ expect(await Platform.getByID('1')).toMatchObject(shared);
93
+ });
94
+
95
+ test('when requesting getSharedPrivateStruct', async () => {
96
+ const editable = await Platform.getSharedPrivateStruct();
97
+ expect(editable).toBeDefined();
98
+ expect(await Platform.getByID('1')).toBeDefined();
99
+ });
100
+
101
+ test('when requesting getSharedStruct', async () => {
102
+ const editable = await Platform.getSharedStruct();
103
+ expect(editable).toBeDefined();
104
+ expect(await Platform.getByID('1')).toBeDefined();
105
+ });
106
+ });
107
+ });
@@ -2,6 +2,7 @@ import { column } from '@simonbackx/simple-database';
2
2
  import { QueueHandler } from '@stamhoofd/queues';
3
3
  import { QueryableModel } from '@stamhoofd/sql';
4
4
  import { PlatformConfig, PlatformPrivateConfig, PlatformServerConfig, Platform as PlatformStruct } from '@stamhoofd/structures';
5
+ import { deepFreeze } from '@stamhoofd/utility';
5
6
  import { v4 as uuidv4 } from 'uuid';
6
7
  import { RegistrationPeriod } from './RegistrationPeriod';
7
8
 
@@ -36,15 +37,20 @@ export class Platform extends QueryableModel {
36
37
  @column({ type: 'json', decoder: PlatformServerConfig })
37
38
  serverConfig: PlatformServerConfig = PlatformServerConfig.create({});
38
39
 
39
- static sharedStruct: PlatformStruct | null = null;
40
+ private static shared: Platform | null = null;
41
+ private static sharedPrivateStruct: PlatformStruct & { privateConfig: PlatformPrivateConfig } | null = null;
42
+ private static sharedStruct: PlatformStruct | null = null;
40
43
 
41
44
  static async getSharedStruct(): Promise<PlatformStruct> {
42
- const struct: PlatformStruct = await this.getSharedPrivateStruct();
43
- const clone = struct.clone();
44
- clone.privateConfig = null;
45
- clone.setShared();
45
+ if (!this.sharedStruct) {
46
+ await this.loadCaches();
47
+ }
46
48
 
47
- return clone;
49
+ if (!this.sharedStruct || !!this.sharedStruct.privateConfig) {
50
+ throw new Error('[Platform] Failed to load platform shared struct');
51
+ }
52
+
53
+ return this.sharedStruct;
48
54
  }
49
55
 
50
56
  async setPreviousPeriodId() {
@@ -53,28 +59,24 @@ export class Platform extends QueryableModel {
53
59
  }
54
60
 
55
61
  static async getSharedPrivateStruct(): Promise<PlatformStruct & { privateConfig: PlatformPrivateConfig }> {
56
- if (this.sharedStruct && this.sharedStruct.privateConfig) {
57
- return this.sharedStruct as any;
62
+ if (!this.sharedPrivateStruct) {
63
+ await this.loadCaches();
58
64
  }
59
65
 
60
- return await QueueHandler.schedule('Platform.getSharedStruct', async () => {
61
- const model = await this.getShared();
62
- const period = await RegistrationPeriod.getByID(model.periodId);
63
- const struct = PlatformStruct.create({
64
- ...model,
65
- period: period?.getStructure() ?? undefined,
66
- });
67
- this.sharedStruct = struct;
66
+ if (!this.sharedPrivateStruct || !this.sharedPrivateStruct.privateConfig) {
67
+ throw new Error('[Platform] Failed to load platform shared private struct');
68
+ }
68
69
 
69
- return struct as any;
70
- });
70
+ return this.sharedPrivateStruct;
71
71
  }
72
72
 
73
- static async getShared(): Promise<Platform> {
74
- return QueueHandler.schedule('Platform.getShared', async () => {
73
+ static async getForEditing(): Promise<Platform> {
74
+ return QueueHandler.schedule('Platform.getModel', async () => {
75
75
  // Build a new one
76
76
  let model = await this.getByID('1');
77
77
  if (!model) {
78
+ console.info('[Platform] Creating new platform');
79
+
78
80
  // Create a new platform
79
81
  model = new Platform();
80
82
  model.id = '1';
@@ -86,16 +88,75 @@ export class Platform extends QueryableModel {
86
88
  });
87
89
  }
88
90
 
89
- static clearCache() {
90
- this.sharedStruct = null;
91
+ static async getShared(): Promise<Readonly<Platform> & { save: never }> {
92
+ return QueueHandler.schedule('Platform.getShared', async () => {
93
+ if (this.shared) {
94
+ return this.shared;
95
+ }
96
+
97
+ // Build a new one
98
+ const model = await this.getForEditing();
99
+ deepFreeze(model);
100
+ this.shared = model;
101
+ return model as any;
102
+ });
103
+ }
104
+
105
+ static async loadCaches(): Promise<void> {
106
+ await QueueHandler.schedule('Platform.loadCaches', async () => {
107
+ if (this.sharedPrivateStruct && this.sharedStruct) {
108
+ // Already loaded (possible if multiple calls to loadCaches were made)
109
+ return;
110
+ }
111
+ // Build a new one
112
+ const model = await this.getForEditing();
113
+ await this.setCachesFromModel(model);
114
+ });
115
+ }
116
+
117
+ private static async setCachesFromModel(model: Platform) {
118
+ // Set structure cache
119
+ const period = await RegistrationPeriod.getByID(model.periodId);
120
+ const struct = PlatformStruct.create({
121
+ ...model,
122
+ period: period?.getStructure() ?? undefined,
123
+ });
124
+
125
+ // We clone to avoid the chance of updating the platform model
126
+ this.sharedPrivateStruct = struct.clone() as PlatformStruct & { privateConfig: PlatformPrivateConfig };
127
+
128
+ const clone = struct.clone();
129
+ clone.privateConfig = null;
130
+ clone.setShared();
131
+ this.sharedStruct = clone;
132
+ }
133
+
134
+ static async clearCache() {
135
+ await this.clearCacheWithoutRefresh();
136
+ await this.loadCaches();
137
+ }
138
+
139
+ static async clearCacheWithoutRefresh() {
140
+ await QueueHandler.schedule('Platform.loadCaches', async () => {
141
+ this.sharedStruct = null;
142
+ this.sharedPrivateStruct = null;
143
+ });
144
+ await QueueHandler.schedule('Platform.getShared', async () => {
145
+ this.shared = null;
146
+ });
91
147
  }
92
148
 
93
149
  async save() {
150
+ let update = false;
151
+ if (this.existsInDatabase) {
152
+ update = true;
153
+ }
94
154
  const s = await super.save();
95
- Platform.clearCache();
96
155
 
97
- // Force update cache immediately
98
- await Platform.getSharedStruct();
156
+ if (update) {
157
+ // Force update cache immediately
158
+ await Platform.clearCache();
159
+ }
99
160
 
100
161
  return s;
101
162
  }
@@ -2,7 +2,7 @@ export { Organization } from './Organization';
2
2
  export { User } from './User';
3
3
  export { Payment } from './Payment';
4
4
  export { Registration } from './Registration';
5
- export { Member, RegistrationWithMember, MemberWithRegistrations } from './Member';
5
+ export * from './Member';
6
6
  export { MergedMember } from './MergedMember';
7
7
 
8
8
  export * from './EmailVerificationCode';