@stamhoofd/models 2.116.0 → 2.117.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 (59) hide show
  1. package/dist/src/helpers/Handlebars.d.ts.map +1 -1
  2. package/dist/src/helpers/Handlebars.js +4 -0
  3. package/dist/src/helpers/Handlebars.js.map +1 -1
  4. package/dist/src/index.d.ts +0 -1
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +0 -1
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/migrations/1771510518-events-index-organization-id-start-date.sql +2 -0
  9. package/dist/src/migrations/1771918307-membership-active-filter-index.sql +2 -0
  10. package/dist/src/migrations/1772033555-balance-item-package-id.sql +3 -0
  11. package/dist/src/migrations/1772033715-balance-item-start-end-date.sql +3 -0
  12. package/dist/src/models/AuditLog.d.ts +1 -0
  13. package/dist/src/models/AuditLog.d.ts.map +1 -1
  14. package/dist/src/models/AuditLog.js +8 -0
  15. package/dist/src/models/AuditLog.js.map +1 -1
  16. package/dist/src/models/BalanceItem.d.ts +22 -2
  17. package/dist/src/models/BalanceItem.d.ts.map +1 -1
  18. package/dist/src/models/BalanceItem.js +32 -0
  19. package/dist/src/models/BalanceItem.js.map +1 -1
  20. package/dist/src/models/Invoice.d.ts +1 -4
  21. package/dist/src/models/Invoice.d.ts.map +1 -1
  22. package/dist/src/models/Invoice.js +0 -45
  23. package/dist/src/models/Invoice.js.map +1 -1
  24. package/dist/src/models/Organization.d.ts +19 -1
  25. package/dist/src/models/Organization.d.ts.map +1 -1
  26. package/dist/src/models/Organization.js +27 -3
  27. package/dist/src/models/Organization.js.map +1 -1
  28. package/dist/src/models/Registration.d.ts +1 -2
  29. package/dist/src/models/Registration.d.ts.map +1 -1
  30. package/dist/src/models/Registration.js +11 -87
  31. package/dist/src/models/Registration.js.map +1 -1
  32. package/dist/src/models/RegistrationPeriod.d.ts +1 -0
  33. package/dist/src/models/RegistrationPeriod.d.ts.map +1 -1
  34. package/dist/src/models/RegistrationPeriod.js +5 -0
  35. package/dist/src/models/RegistrationPeriod.js.map +1 -1
  36. package/dist/src/models/STPackage.d.ts +1 -5
  37. package/dist/src/models/STPackage.d.ts.map +1 -1
  38. package/dist/src/models/STPackage.js +0 -41
  39. package/dist/src/models/STPackage.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +2 -2
  42. package/src/helpers/Handlebars.ts +4 -0
  43. package/src/index.ts +0 -1
  44. package/src/migrations/1771510518-events-index-organization-id-start-date.sql +2 -0
  45. package/src/migrations/1771918307-membership-active-filter-index.sql +2 -0
  46. package/src/migrations/1772033555-balance-item-package-id.sql +3 -0
  47. package/src/migrations/1772033715-balance-item-start-end-date.sql +3 -0
  48. package/src/models/AuditLog.ts +10 -1
  49. package/src/models/BalanceItem.ts +32 -2
  50. package/src/models/Invoice.ts +0 -57
  51. package/src/models/Organization.ts +32 -3
  52. package/src/models/Registration.ts +20 -98
  53. package/src/models/RegistrationPeriod.ts +6 -0
  54. package/src/models/STPackage.ts +0 -50
  55. package/dist/src/helpers/GroupBuilder.d.ts +0 -9
  56. package/dist/src/helpers/GroupBuilder.d.ts.map +0 -1
  57. package/dist/src/helpers/GroupBuilder.js +0 -382
  58. package/dist/src/helpers/GroupBuilder.js.map +0 -1
  59. package/src/helpers/GroupBuilder.ts +0 -415
@@ -1,7 +1,7 @@
1
1
  import { column } from '@simonbackx/simple-database';
2
2
  import { ArrayDecoder, Decoder, MapDecoder, StringDecoder, StringOrNumberDecoder } from '@simonbackx/simple-encoding';
3
3
  import { QueryableModel } from '@stamhoofd/sql';
4
- import { AuditLogPatchItem, AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
4
+ import { AuditLogPatchItem, AuditLogReplacement, AuditLogSource, AuditLogType, getAuditLogTypeReplacements } from '@stamhoofd/structures';
5
5
  import { v7 as uuidv7 } from 'uuid';
6
6
 
7
7
  export class AuditLog extends QueryableModel {
@@ -70,4 +70,13 @@ export class AuditLog extends QueryableModel {
70
70
  },
71
71
  })
72
72
  createdAt: Date;
73
+
74
+ validate() {
75
+ const replacements = getAuditLogTypeReplacements(this.type);
76
+ for (const replacement of replacements) {
77
+ if (!this.replacements.has(replacement)) {
78
+ throw new Error(`Missing replacement ${replacement}`);
79
+ }
80
+ }
81
+ }
73
82
  }
@@ -48,6 +48,12 @@ export class BalanceItem extends QueryableModel {
48
48
  @column({ type: 'string', nullable: true })
49
49
  orderId: string | null = null;
50
50
 
51
+ /**
52
+ * The STPackage ID that is linked to this balance item
53
+ */
54
+ @column({ type: 'string', nullable: true })
55
+ packageId: string | null = null;
56
+
51
57
  /**
52
58
  * The depending balance item ID that is linked to this balance item
53
59
  * -> as soon as this balance item is paid, we'll mark this balance item as pending if it is still hidden
@@ -65,6 +71,20 @@ export class BalanceItem extends QueryableModel {
65
71
  @column({ type: 'string' })
66
72
  description = '';
67
73
 
74
+ /**
75
+ * In case this balance item is associated with an item that was charged for a certain period, the startDate is saved here.
76
+ * e.g. startDate of the registration.
77
+ */
78
+ @column({ type: 'datetime', nullable: true })
79
+ startDate: Date | null = null;
80
+
81
+ /**
82
+ * In case this balance item is associated with an item that was charged for a certain period, the endDate is saved here.
83
+ * e.g. endDate of the registration.
84
+ */
85
+ @column({ type: 'datetime', nullable: true })
86
+ endDate: Date | null = null;
87
+
68
88
  /**
69
89
  * @deprecated Use quantity
70
90
  */
@@ -306,6 +326,16 @@ export class BalanceItem extends QueryableModel {
306
326
  return this.priceWithVAT - this.pricePaid - this.pricePending;
307
327
  }
308
328
 
329
+ /**
330
+ * Returns zero if the balance item is canceled, otherwise priceWithVAT
331
+ */
332
+ get priceDue() {
333
+ if (this.status === BalanceItemStatus.Canceled) {
334
+ return 0;
335
+ }
336
+ return this.priceWithVAT;
337
+ }
338
+
309
339
  /**
310
340
  * price minus pricePaid
311
341
  */
@@ -513,7 +543,7 @@ export class BalanceItem extends QueryableModel {
513
543
  await Database.update(query, params);
514
544
  }
515
545
 
516
- static async loadPayments(items: BalanceItem[]) {
546
+ static async loadPayments(items: (BalanceItem | { id: string })[]) {
517
547
  if (items.length == 0) {
518
548
  return { balanceItemPayments: [], payments: [] };
519
549
  }
@@ -535,7 +565,7 @@ export class BalanceItem extends QueryableModel {
535
565
  });
536
566
  }
537
567
 
538
- static async getStructureWithPayments(items: BalanceItem[]): Promise<BalanceItemWithPayments[]> {
568
+ static async getStructureWithPayments(items: (BalanceItem | BalanceItemStruct)[]): Promise<BalanceItemWithPayments[]> {
539
569
  if (items.length === 0) {
540
570
  return [];
541
571
  }
@@ -153,63 +153,6 @@ export class Invoice extends QueryableModel {
153
153
  })
154
154
  updatedAt: Date;
155
155
 
156
- static async createFrom(organization: { id: string }, struct: InvoiceStruct) {
157
- const model = new Invoice();
158
- model.customer = struct.customer;
159
- model.seller = struct.seller;
160
- model.organizationId = organization.id;
161
- model.payingOrganizationId = struct.payingOrganizationId;
162
-
163
- model.payableRoundingAmount = struct.payableRoundingAmount;
164
- model.VATTotal = struct.VATTotal;
165
- model.VATTotalAmount = struct.VATTotalAmount;
166
- model.totalWithoutVAT = struct.totalWithoutVAT;
167
- model.totalWithVAT = struct.totalWithVAT;
168
- model.totalBalanceInvoicedAmount = struct.totalBalanceInvoicedAmount;
169
-
170
- model.ipAddress = struct.ipAddress;
171
- model.userAgent = struct.userAgent;
172
-
173
- model.stripeAccountId = struct.stripeAccountId;
174
- model.reference = struct.reference;
175
-
176
- model.negativeInvoiceId = struct.negativeInvoiceId;
177
-
178
- await model.save();
179
-
180
- // Create balances
181
- try {
182
- for (const item of struct.items) {
183
- const invoiced = new InvoicedBalanceItem();
184
- invoiced.invoiceId = model.id;
185
- invoiced.organizationId = model.organizationId;
186
- invoiced.balanceItemId = item.balanceItemId;
187
-
188
- invoiced.VATExcempt = item.VATExcempt;
189
- invoiced.VATPercentage = item.VATPercentage;
190
- invoiced.VATIncluded = item.VATIncluded;
191
-
192
- invoiced.unitPrice = item.unitPrice;
193
- invoiced.quantity = item.quantity;
194
-
195
- invoiced.balanceInvoicedAmount = item.balanceInvoicedAmount;
196
-
197
- invoiced.totalWithoutVAT = item.totalWithoutVAT;
198
-
199
- await invoiced.save();
200
- }
201
- }
202
- catch (e) {
203
- try {
204
- await model.delete();
205
- }
206
- catch (ee) {
207
- console.error('Error while trying to delete invoice because of fail save', ee, 'Deleting because of error', e);
208
- }
209
- throw e;
210
- }
211
- }
212
-
213
156
  static async loadBalanceItems(invoices: Invoice[]) {
214
157
  if (invoices.length === 0) {
215
158
  return { invoicedBalanceItems: [] };
@@ -210,7 +210,7 @@ export class Organization extends QueryableModel {
210
210
  /**
211
211
  * Potentially includes a path
212
212
  */
213
- getHost(i18n?: I18n): string {
213
+ getRegistrationHost(i18n?: { language: Language; locale: string }): string {
214
214
  if (this.registerDomain) {
215
215
  let d = this.registerDomain;
216
216
 
@@ -220,10 +220,10 @@ export class Organization extends QueryableModel {
220
220
 
221
221
  return d;
222
222
  }
223
- return this.getDefaultHost(i18n);
223
+ return this.getDefaultRegistrationHost(i18n);
224
224
  }
225
225
 
226
- getDefaultHost(i18n?: I18n): string {
226
+ getDefaultRegistrationHost(i18n?: { language: Language; locale: string }): string {
227
227
  if (!STAMHOOFD.domains.registration) {
228
228
  return STAMHOOFD.domains.dashboard + '/' + (i18n?.locale ?? this.i18n.locale) + '/' + appToUri('registration') + '/' + this.uri;
229
229
  }
@@ -236,6 +236,26 @@ export class Organization extends QueryableModel {
236
236
  return this.uri + '.' + defaultDomain;
237
237
  }
238
238
 
239
+ get registerUrl() {
240
+ return 'https://' + this.getRegistrationHost();
241
+ }
242
+
243
+ /**
244
+ * @deprecated
245
+ * use getRegistrationHost
246
+ */
247
+ getHost(i18n?: I18n): string {
248
+ return this.getRegistrationHost(i18n);
249
+ }
250
+
251
+ /**
252
+ * @deprecated
253
+ * Use getDefaultRegistrationHost
254
+ */
255
+ getDefaultHost(i18n?: I18n): string {
256
+ return this.getDefaultRegistrationHost(i18n);
257
+ }
258
+
239
259
  get marketingDomain(): string {
240
260
  return STAMHOOFD.domains.marketing[this.address.country] ?? STAMHOOFD.domains.marketing[''];
241
261
  }
@@ -940,4 +960,13 @@ export class Organization extends QueryableModel {
940
960
 
941
961
  return providers;
942
962
  }
963
+
964
+ override async delete(): Promise<void> {
965
+ // Clear periodID first to remove circular dependency
966
+ if (this.periodId) {
967
+ await Organization.update().where('id', this.id).set('periodId', null).update();
968
+ }
969
+
970
+ return await super.delete();
971
+ }
943
972
  }
@@ -1,12 +1,11 @@
1
- import { column, Database, ManyToOneRelation } from '@simonbackx/simple-database';
2
- import { AppliedRegistrationDiscount, EmailTemplateType, GroupPrice, PaymentMethod, PaymentMethodHelper, Recipient, RecordAnswer, RecordAnswerDecoder, RegisterItemOption, Registration as RegistrationStructure, Replacement, StockReservation } from '@stamhoofd/structures';
3
- import { Formatter } from '@stamhoofd/utility';
1
+ import { column, ManyToOneRelation } from '@simonbackx/simple-database';
2
+ import { AppliedRegistrationDiscount, EmailTemplateType, GroupPrice, Recipient, RecordAnswer, RecordAnswerDecoder, RegisterItemOption, Registration as RegistrationStructure, Replacement, StockReservation } from '@stamhoofd/structures';
4
3
  import { v4 as uuidv4 } from 'uuid';
5
4
 
6
5
  import { ArrayDecoder, MapDecoder, StringDecoder } from '@simonbackx/simple-encoding';
7
- import { QueryableModel } from '@stamhoofd/sql';
6
+ import { QueryableModel, SQL } from '@stamhoofd/sql';
8
7
  import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
9
- import { Group, Organization, User } from './index.js';
8
+ import { Group, Organization } from './index.js';
10
9
 
11
10
  export class Registration extends QueryableModel {
12
11
  static table = 'registrations';
@@ -169,21 +168,23 @@ export class Registration extends QueryableModel {
169
168
  * This is used for billing
170
169
  */
171
170
  static async getActiveMembers(organizationId: string): Promise<number> {
172
- const query = `
173
- SELECT COUNT(DISTINCT \`${Registration.table}\`.memberId) as c FROM \`${Registration.table}\`
174
- JOIN \`groups\` ON \`groups\`.id = \`${Registration.table}\`.groupId
175
- WHERE \`groups\`.organizationId = ? AND \`${Registration.table}\`.cycle = \`groups\`.cycle AND \`groups\`.deletedAt is null AND \`${Registration.table}\`.registeredAt is not null AND \`${Registration.table}\`.deactivatedAt is null`;
176
-
177
- const [results] = await Database.select(query, [organizationId]);
178
- const count = results[0]['']['c'];
179
-
180
- if (Number.isInteger(count)) {
181
- return count as number;
182
- }
183
- else {
184
- console.error('Unexpected result for occupancy', results);
185
- throw new Error('Query failed');
171
+ const organization = await Organization.getByID(organizationId);
172
+ if (!organization) {
173
+ return 0;
186
174
  }
175
+
176
+ return await this.select()
177
+ .join(
178
+ SQL.join(Group.table)
179
+ .where(SQL.column('id'), SQL.parentColumn('groupId')),
180
+ )
181
+ .where('periodId', organization.periodId)
182
+ .where('deactivatedAt', null)
183
+ .where('registeredAt', '!=', null)
184
+ .where(SQL.column(Group.table, 'deletedAt'), null)
185
+ .count(
186
+ SQL.distinct(SQL.column('memberId')),
187
+ );
187
188
  }
188
189
 
189
190
  async getRecipients(organization: Organization, group: import('./').Group) {
@@ -251,85 +252,6 @@ export class Registration extends QueryableModel {
251
252
  });
252
253
  }
253
254
 
254
- static async sendTransferEmail(user: User, organization: Organization, payment: import('./').Payment) {
255
- const paymentGeneral = await payment.getGeneralStructure();
256
- const groupIds = paymentGeneral.groupIds;
257
-
258
- const recipients = [
259
- Recipient.create({
260
- firstName: user.firstName,
261
- lastName: user.lastName,
262
- email: user.email,
263
- userId: user.id,
264
- replacements: [
265
- Replacement.create({
266
- token: 'priceToPay',
267
- value: Formatter.price(payment.price),
268
- }),
269
- Replacement.create({
270
- token: 'paymentMethod',
271
- value: PaymentMethodHelper.getName(payment.method ?? PaymentMethod.Unknown),
272
- }),
273
- Replacement.create({
274
- token: 'transferDescription',
275
- value: (payment.transferDescription ?? ''),
276
- }),
277
- Replacement.create({
278
- token: 'transferBankAccount',
279
- value: payment.transferSettings?.iban ?? '',
280
- }),
281
- Replacement.create({
282
- token: 'transferBankCreditor',
283
- value: payment.transferSettings?.creditor ?? organization.name,
284
- }),
285
- Replacement.create({
286
- token: 'overviewContext',
287
- value: $t(`01d5fd7e-2960-4eb4-ab3a-2ac6dcb2e39c`) + ' ' + paymentGeneral.memberNames,
288
- }),
289
- Replacement.create({
290
- token: 'memberNames',
291
- value: paymentGeneral.memberNames,
292
- }),
293
- Replacement.create({
294
- token: 'overviewTable',
295
- value: '',
296
- html: paymentGeneral.getDetailsHTMLTable(),
297
- }),
298
- Replacement.create({
299
- token: 'paymentTable',
300
- value: '',
301
- html: paymentGeneral.getHTMLTable(),
302
- }),
303
- Replacement.create({
304
- token: 'registerUrl',
305
- value: 'https://' + organization.getHost(),
306
- }),
307
- Replacement.create({
308
- token: 'organizationName',
309
- value: organization.name,
310
- }),
311
- ],
312
- }),
313
- ];
314
-
315
- let group: Group | undefined | null = null;
316
-
317
- if (groupIds.length == 1) {
318
- const Group = (await import('./index.js')).Group;
319
- group = await Group.getByID(groupIds[0]);
320
- }
321
-
322
- // Create e-mail builder
323
- await sendEmailTemplate(organization, {
324
- template: {
325
- type: EmailTemplateType.RegistrationTransferDetails,
326
- group,
327
- },
328
- type: 'transactional',
329
- recipients,
330
- });
331
- }
332
-
333
255
  shouldIncludeStock() {
334
256
  return (this.registeredAt !== null && this.deactivatedAt === null) || this.canRegister || (this.reservedUntil && this.reservedUntil > new Date());
335
257
  }
@@ -68,6 +68,12 @@ export class RegistrationPeriod extends QueryableModel {
68
68
  return RegistrationPeriodStruct.create(this);
69
69
  }
70
70
 
71
+ configureForNewOrganization() {
72
+ this.settings = RegistrationPeriodSettings.create({});
73
+ this.startDate = new Date();
74
+ this.endDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 31); // 1 month
75
+ }
76
+
71
77
  static async getByDate(date: Date, organizationId: string | null): Promise<RegistrationPeriod | null> {
72
78
  if (STAMHOOFD.userMode === 'organization' && organizationId === null) {
73
79
  throw new SimpleError({
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid';
6
6
 
7
7
  import { QueryableModel } from '@stamhoofd/sql';
8
8
  import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
9
- import { GroupBuilder } from '../helpers/GroupBuilder.js';
10
9
  import { Organization } from './index.js';
11
10
 
12
11
  export class STPackage extends QueryableModel {
@@ -66,55 +65,6 @@ export class STPackage extends QueryableModel {
66
65
  @column({ type: 'datetime', nullable: true })
67
66
  lastEmailAt: Date | null = null;
68
67
 
69
- static async getForOrganization(organizationId: string) {
70
- const pack1 = await STPackage.where({ organizationId, validAt: { sign: '!=', value: null }, removeAt: { sign: '>', value: new Date() } });
71
- const pack2 = await STPackage.where({ organizationId, validAt: { sign: '!=', value: null }, removeAt: null });
72
-
73
- return [...pack1, ...pack2];
74
- }
75
-
76
- static async getForOrganizationIncludingExpired(organizationId: string) {
77
- return await STPackage.where({ organizationId, validAt: { sign: '!=', value: null } }, { sort: [{ column: 'validAt', direction: 'DESC' }] });
78
- }
79
-
80
- static async getOrganizationPackagesMap(organizationId: string): Promise<Map<STPackageType, STPackageStatus>> {
81
- const packages = await this.getForOrganizationIncludingExpired(organizationId);
82
-
83
- const map = new Map<STPackageType, STPackageStatus>();
84
- for (const pack of packages) {
85
- const exist = map.get(pack.meta.type);
86
- if (exist) {
87
- exist.merge(pack.createStatus());
88
- }
89
- else {
90
- map.set(pack.meta.type, pack.createStatus());
91
- }
92
- }
93
-
94
- return map;
95
- }
96
-
97
- static async updateOrganizationPackages(organizationId: string) {
98
- console.log('Updating packages for organization ' + organizationId);
99
- const map = await this.getOrganizationPackagesMap(organizationId);
100
-
101
- const organization = await Organization.getByID(organizationId);
102
- if (organization) {
103
- const didUseMembers = organization.meta.packages.useMembers && organization.meta.packages.useActivities;
104
- organization.meta.packages.packages = map;
105
- await organization.save();
106
-
107
- if (!didUseMembers && organization.meta.packages.useMembers && organization.meta.packages.useActivities) {
108
- console.log('Building groups and categories for ' + organization.id);
109
- const builder = new GroupBuilder(organization);
110
- await builder.build();
111
- }
112
- }
113
- else {
114
- console.error("Couldn't find organization when updating packages " + organizationId);
115
- }
116
- }
117
-
118
68
  async activate() {
119
69
  if (this.validAt !== null) {
120
70
  return;
@@ -1,9 +0,0 @@
1
- import { Group, Organization } from '../models/index.js';
2
- export declare class GroupBuilder {
3
- organization: Organization;
4
- constructor(organization: Organization);
5
- build(): Promise<void>;
6
- createSGVGroups(): Promise<Group[]>;
7
- createChiroGroups(): Promise<void>;
8
- }
9
- //# sourceMappingURL=GroupBuilder.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"GroupBuilder.d.ts","sourceRoot":"","sources":["../../../src/helpers/GroupBuilder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEzD,qBAAa,YAAY;IACrB,YAAY,EAAE,YAAY,CAAC;gBAEf,YAAY,EAAE,YAAY;IAIhC,KAAK;IAgEL,eAAe;IA2Kf,iBAAiB;CAyK1B"}