@stamhoofd/backend 2.64.0 → 2.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +7 -1
  3. package/src/audit-logs/ModelLogger.ts +17 -2
  4. package/src/crons/balance-emails.ts +232 -0
  5. package/src/crons/index.ts +1 -0
  6. package/src/email-recipient-loaders/members.ts +14 -4
  7. package/src/email-recipient-loaders/receivable-balances.ts +29 -15
  8. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +47 -12
  9. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -18
  10. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +15 -1
  11. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +1 -1
  13. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +0 -10
  14. package/src/helpers/EmailResumer.ts +1 -5
  15. package/src/helpers/MemberUserSyncer.ts +22 -1
  16. package/src/helpers/MembershipCharger.ts +1 -0
  17. package/src/helpers/TagHelper.ts +7 -14
  18. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +4 -14
  19. package/src/seeds/1729253172-update-orders.ts +7 -18
  20. package/src/seeds/{1726494420-update-cached-outstanding-balance-from-items.ts → 1735577912-update-cached-outstanding-balance-from-items.ts} +1 -14
  21. package/src/services/PlatformMembershipService.ts +5 -5
  22. package/src/sql-filters/members.ts +1 -0
  23. package/src/sql-filters/receivable-balances.ts +15 -1
  24. package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +19 -0
  25. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +0 -253
  26. package/src/helpers/ModelHelper.ts +0 -32
  27. package/src/seeds/1726055544-balance-item-paid.ts +0 -11
  28. package/src/seeds/1726055545-balance-item-pending.ts +0 -11
  29. package/src/seeds/1726494419-update-cached-outstanding-balance.ts +0 -53
  30. package/src/seeds/1728928973-balance-item-pending.ts +0 -11
  31. package/src/seeds/1734700082-update-cached-outstanding-balance-from-items.ts +0 -40
@@ -1,9 +1,10 @@
1
- import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
1
+ import { CachedBalance, Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
3
  import { AuditLogSource, MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
4
4
  import crypto from 'crypto';
5
5
  import basex from 'base-x';
6
6
  import { AuditLogService } from '../services/AuditLogService';
7
+ import { Formatter } from '@stamhoofd/utility';
7
8
 
8
9
  const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
9
10
  const customBase = basex(ALPHABET);
@@ -185,6 +186,9 @@ export class MemberUserSyncerStatic {
185
186
  console.log('Removing access for ' + user.id + ' to member ' + member.id);
186
187
  await Member.users.reverse('members').unlink(user, member);
187
188
 
189
+ // Update balance of this user, as it could have changed
190
+ await this.updateUserBalance(user.id, member.id);
191
+
188
192
  if (user.memberId === member.id) {
189
193
  user.memberId = null;
190
194
  }
@@ -305,6 +309,23 @@ export class MemberUserSyncerStatic {
305
309
  if (!member.users.find(u => u.id === user.id)) {
306
310
  await Member.users.reverse('members').link(user, [member]);
307
311
  member.users.push(user);
312
+
313
+ // Update balance of this user, as it could have changed
314
+ await this.updateUserBalance(user.id, member.id);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Update the balance after making a change in linked member/users
320
+ */
321
+ async updateUserBalance(userId: string, memberId: string) {
322
+ // Update balance of this user, as it could have changed
323
+ const memberBalances = await CachedBalance.getForObjects([memberId]);
324
+ if (memberBalances.length > 0) {
325
+ const organizationIds = Formatter.uniqueArray(memberBalances.map(b => b.organizationId));
326
+ for (const organizationId of organizationIds) {
327
+ await CachedBalance.updateForUsers(organizationId, [userId]);
328
+ }
308
329
  }
309
330
  }
310
331
  }
@@ -36,6 +36,7 @@ export const MembershipCharger = {
36
36
  .where('balanceItemId', null)
37
37
  .where('deletedAt', null)
38
38
  .whereNot('organizationId', chargeVia)
39
+ .where(SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date()))
39
40
  .limit(chunkSize)
40
41
  .orderBy(
41
42
  new SQLOrderBy({
@@ -1,7 +1,6 @@
1
1
  import { Organization, Platform } from '@stamhoofd/models';
2
2
  import { QueueHandler } from '@stamhoofd/queues';
3
3
  import { AuditLogSource, OrganizationTag, TagHelper as SharedTagHelper } from '@stamhoofd/structures';
4
- import { ModelHelper } from './ModelHelper';
5
4
  import { AuditLogService } from '../services/AuditLogService';
6
5
 
7
6
  export class TagHelper extends SharedTagHelper {
@@ -14,17 +13,15 @@ export class TagHelper extends SharedTagHelper {
14
13
  let platform = await Platform.getShared();
15
14
 
16
15
  const tagCounts = new Map<string, number>();
17
- await this.loopOrganizations(async (organizations) => {
18
- for (const organization of organizations) {
19
- organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
20
16
 
21
- for (const tag of organization.meta.tags) {
22
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
23
- }
24
- }
17
+ for await (const organization of Organization.select().all()) {
18
+ organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
25
19
 
26
- await Promise.all(organizations.map(organization => organization.save()));
27
- });
20
+ for (const tag of organization.meta.tags) {
21
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
22
+ }
23
+ await organization.save();
24
+ }
28
25
 
29
26
  // Reload platform to avoid race conditions
30
27
  platform = await Platform.getShared();
@@ -39,10 +36,6 @@ export class TagHelper extends SharedTagHelper {
39
36
  });
40
37
  }
41
38
 
42
- private static async loopOrganizations(onBatchReceived: (batch: Organization[]) => Promise<void>) {
43
- await ModelHelper.loop(Organization, 'id', onBatchReceived, { limit: 10 });
44
- }
45
-
46
39
  /**
47
40
  * Removes child tag ids that do not exist and sorts the tags.
48
41
  * @param platformTags
@@ -2,6 +2,9 @@ import { Migration } from '@simonbackx/simple-database';
2
2
  import { logger } from '@simonbackx/simple-logging';
3
3
  import { BalanceItem } from '@stamhoofd/models';
4
4
 
5
+ /**
6
+ * This migration is required to keep '1733994455-balance-item-status-open' working
7
+ */
5
8
  export default new Migration(async () => {
6
9
  if (STAMHOOFD.environment == 'test') {
7
10
  console.log('skipped in tests');
@@ -10,26 +13,13 @@ export default new Migration(async () => {
10
13
 
11
14
  process.stdout.write('\n');
12
15
  let c = 0;
13
- let id: string = '';
14
16
 
15
17
  await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
16
- while (true) {
17
- const items = await BalanceItem.where({
18
- id: {
19
- value: id,
20
- sign: '>',
21
- },
22
- }, { limit: 1000, sort: ['id'] });
23
-
18
+ for await (const items of BalanceItem.select().limit(1000).allBatched()) {
24
19
  await BalanceItem.updateOutstanding(items);
25
20
 
26
21
  c += items.length;
27
22
  process.stdout.write('.');
28
-
29
- if (items.length < 1000) {
30
- break;
31
- }
32
- id = items[items.length - 1].id;
33
23
  }
34
24
  });
35
25
 
@@ -1,7 +1,5 @@
1
1
  import { Migration } from '@simonbackx/simple-database';
2
2
  import { Order } from '@stamhoofd/models';
3
- import { sleep } from '@stamhoofd/utility';
4
- import { ModelHelper } from '../helpers/ModelHelper';
5
3
 
6
4
  export default new Migration(async () => {
7
5
  if (STAMHOOFD.environment === 'test') {
@@ -11,22 +9,13 @@ export default new Migration(async () => {
11
9
 
12
10
  console.log('Start saving orders.');
13
11
 
14
- const limit = 100;
15
- let count = limit;
12
+ const batchSize = 100;
13
+ let count = 0;
16
14
 
17
- await ModelHelper.loop(Order, 'id', async (batch: Order[]) => {
18
- console.log('Saving orders...', `(${count})`);
19
-
20
- // save all orders to update the new columns
21
- for (const order of batch) {
22
- await order.save();
23
- }
24
-
25
- count += limit;
26
- },
27
- { limit });
28
-
29
- await sleep(1000);
15
+ for await (const order of Order.select().limit(batchSize).all()) {
16
+ await order.save();
17
+ count += 1;
18
+ }
30
19
 
31
- console.log('Finished saving orders.');
20
+ console.log('Finished saving ' + count + ' orders.');
32
21
  });
@@ -10,26 +10,13 @@ export default new Migration(async () => {
10
10
 
11
11
  process.stdout.write('\n');
12
12
  let c = 0;
13
- let id: string = '';
14
13
 
15
14
  await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
16
- while (true) {
17
- const items = await BalanceItem.where({
18
- id: {
19
- value: id,
20
- sign: '>',
21
- },
22
- }, { limit: 1000, sort: ['id'] });
23
-
15
+ for await (const items of BalanceItem.select().limit(1000).allBatched()) {
24
16
  await BalanceItem.updateOutstanding(items);
25
17
 
26
18
  c += items.length;
27
19
  process.stdout.write('.');
28
-
29
- if (items.length < 1000) {
30
- break;
31
- }
32
- id = items[items.length - 1].id;
33
20
  }
34
21
  });
35
22
 
@@ -225,12 +225,12 @@ export class PlatformMembershipService {
225
225
 
226
226
  const shouldApplyReducedPrice = me.details.shouldApplyReducedPrice;
227
227
 
228
- const cheapestMembership = defaultMembershipsWithOrganization.sort(({ membership: a, registration: ar, organization: ao }, { membership: b, registration: br, organization: bo }) => {
229
- const tagIdsA = ao?.meta.tags ?? [];
230
- const tagIdsB = bo?.meta.tags ?? [];
231
- const diff = a.getPrice(period.id, ar.startDate ?? ar.registeredAt ?? now, tagIdsA, shouldApplyReducedPrice)! - b.getPrice(period.id, ar.startDate ?? ar.registeredAt ?? now, tagIdsB, shouldApplyReducedPrice)!;
228
+ const cheapestMembership = defaultMembershipsWithOrganization.sort((a, b) => {
229
+ const tagIdsA = a.organization?.meta.tags ?? [];
230
+ const tagIdsB = b.organization?.meta.tags ?? [];
231
+ const diff = a.membership.getPrice(period.id, a.registration.startDate ?? a.registration.registeredAt ?? now, tagIdsA, shouldApplyReducedPrice)! - b.membership.getPrice(period.id, a.registration.startDate ?? a.registration.registeredAt ?? now, tagIdsB, shouldApplyReducedPrice)!;
232
232
  if (diff === 0) {
233
- return Sorter.byDateValue(br.createdAt, ar.createdAt);
233
+ return Sorter.byDateValue(b.registration.startDate ?? b.registration.createdAt, a.registration.startDate ?? a.registration.createdAt);
234
234
  }
235
235
  return diff;
236
236
  })[0];
@@ -242,6 +242,7 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
242
242
  startDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'startDate')),
243
243
  endDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'endDate')),
244
244
  expireDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'expireDate')),
245
+ trialUntil: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'trialUntil')),
245
246
  },
246
247
  ),
247
248
 
@@ -1,4 +1,5 @@
1
- import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler } from '@stamhoofd/sql';
1
+ import { SQL, SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler } from '@stamhoofd/sql';
2
+ import { EmailRelationFilterCompilers } from './shared/EmailRelationFilterCompilers';
2
3
 
3
4
  /**
4
5
  * Defines how to filter cached balance items in the database from StamhoofdFilter objects
@@ -11,4 +12,17 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
11
12
  amountOpen: createSQLColumnFilterCompiler('amountOpen'),
12
13
  amountPending: createSQLColumnFilterCompiler('amountPending'),
13
14
  nextDueAt: createSQLColumnFilterCompiler('nextDueAt'),
15
+ lastReminderEmail: createSQLColumnFilterCompiler('lastReminderEmail'),
16
+ reminderEmailCount: createSQLColumnFilterCompiler('reminderEmailCount'),
17
+ reminderAmountIncreased: createSQLExpressionFilterCompiler(
18
+ SQL.if(
19
+ SQL.column('amountOpen'),
20
+ '>',
21
+ SQL.column('lastReminderAmountOpen'),
22
+ ).then(1).else(0),
23
+ { isJSONValue: false, isJSONObject: false },
24
+ ),
25
+
26
+ // Allowed to filter by recent emails
27
+ ...EmailRelationFilterCompilers,
14
28
  };
@@ -0,0 +1,19 @@
1
+ import { createSQLRelationFilterCompiler, SQL, SQLParentNamespace, baseSQLFilterCompilers, createSQLColumnFilterCompiler } from '@stamhoofd/sql';
2
+
3
+ export const EmailRelationFilterCompilers = {
4
+ emails: createSQLRelationFilterCompiler(
5
+ SQL.select()
6
+ .from(
7
+ SQL.table('email_recipients'),
8
+ )
9
+ .where(
10
+ SQL.column(SQLParentNamespace, 'id'),
11
+ SQL.column('objectId'),
12
+ ),
13
+ {
14
+ ...baseSQLFilterCompilers,
15
+ emailType: createSQLColumnFilterCompiler('emailType'),
16
+ sentAt: createSQLColumnFilterCompiler('sentAt'),
17
+ },
18
+ ),
19
+ };
@@ -1,253 +0,0 @@
1
- import { Decoder } from '@simonbackx/simple-encoding';
2
- import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { SimpleError } from '@simonbackx/simple-errors';
4
- import { Email } from '@stamhoofd/email';
5
- import { getEmailBuilder, RateLimiter } from '@stamhoofd/models';
6
- import { EmailRequest, Recipient } from '@stamhoofd/structures';
7
-
8
- import { Context } from '../../../../helpers/Context';
9
-
10
- type Params = Record<string, never>;
11
- type Query = undefined;
12
- type Body = EmailRequest;
13
- type ResponseBody = undefined;
14
-
15
- export const paidEmailRateLimiter = new RateLimiter({
16
- limits: [
17
- {
18
- // Max 5.000 emails a day
19
- limit: 5000,
20
- duration: 24 * 60 * 1000 * 60,
21
- },
22
- {
23
- // 10.000 requests per week
24
- limit: 10000,
25
- duration: 24 * 60 * 1000 * 60 * 7,
26
- },
27
- ],
28
- });
29
-
30
- export const freeEmailRateLimiter = new RateLimiter({
31
- limits: [
32
- {
33
- // Max 100 a day
34
- limit: 100,
35
- duration: 24 * 60 * 1000 * 60,
36
- },
37
- {
38
- // Max 200 a week
39
- limit: 200,
40
- duration: 7 * 24 * 60 * 1000 * 60,
41
- },
42
- ],
43
- });
44
-
45
- /**
46
- * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
47
- */
48
-
49
- export class EmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
50
- bodyDecoder = EmailRequest as Decoder<EmailRequest>;
51
-
52
- protected doesMatch(request: Request): [true, Params] | [false] {
53
- if (request.method !== 'POST') {
54
- return [false];
55
- }
56
-
57
- const params = Endpoint.parseParameters(request.url, '/email/legacy', {});
58
-
59
- if (params) {
60
- return [true, params as Params];
61
- }
62
- return [false];
63
- }
64
-
65
- async handle(request: DecodedRequest<Params, Query, Body>) {
66
- const organization = await Context.setOrganizationScope();
67
- const { user } = await Context.authenticate();
68
-
69
- if (!Context.auth.canSendEmails()) {
70
- throw Context.auth.error();
71
- }
72
-
73
- if (request.body.recipients.length > 5000) {
74
- throw new SimpleError({
75
- code: 'too_many_recipients',
76
- message: 'Too many recipients',
77
- human: 'Je kan maar een mail naar maximaal 5000 personen tergelijk versturen. Contacteer ons om deze limiet te verhogen indien dit nodig is.',
78
- field: 'recipients',
79
- });
80
- }
81
-
82
- // For non paid organizations, the limit is 10
83
- if (request.body.recipients.length > 10 && !organization.meta.packages.isPaid) {
84
- throw new SimpleError({
85
- code: 'too_many_emails',
86
- message: 'Too many e-mails',
87
- human: 'Zolang je de demo versie van Stamhoofd gebruikt kan je maar maximaal een email sturen naar 10 emailadressen. Als je het pakket aankoopt zal deze limiet er niet zijn. Dit is om misbruik te voorkomen met spammers die spam email versturen via Stamhoofd.',
88
- field: 'recipients',
89
- });
90
- }
91
-
92
- const limiter = organization.meta.packages.isPaid ? paidEmailRateLimiter : freeEmailRateLimiter;
93
-
94
- try {
95
- limiter.track(organization.id, request.body.recipients.length);
96
- }
97
- catch (e) {
98
- Email.sendWebmaster({
99
- subject: '[Limiet] Limiet bereikt voor aantal e-mails',
100
- text: 'Beste, \nDe limiet werd bereikt voor het aantal e-mails per dag. \nVereniging: ' + organization.id + ' (' + organization.name + ')' + '\n\n' + e.message + '\n\nStamhoofd',
101
- });
102
-
103
- throw new SimpleError({
104
- code: 'too_many_emails_period',
105
- message: 'Too many e-mails limited',
106
- human: 'Oeps! Om spam te voorkomen limiteren we het aantal emails die je per dag/week kan versturen. Neem contact met ons op om deze limiet te verhogen.',
107
- field: 'recipients',
108
- });
109
- }
110
-
111
- // Validate email
112
- const sender = organization.privateMeta.emails.find(e => e.id == request.body.emailId);
113
- if (!sender) {
114
- throw new SimpleError({
115
- code: 'invalid_field',
116
- message: 'Invalid emailId',
117
- human: 'Het e-mailadres waarvan je wilt versturen bestaat niet (meer). Kijk je het na?',
118
- field: 'emailId',
119
- });
120
- }
121
-
122
- // Validate attachments
123
- const size = request.body.attachments.reduce((value: number, attachment) => {
124
- return value + attachment.content.length;
125
- }, 0);
126
-
127
- if (size > 9.5 * 1024 * 1024) {
128
- throw new SimpleError({
129
- code: 'too_big_attachments',
130
- message: 'Too big attachments',
131
- human: 'Jouw bericht is te groot. Grote bijlages verstuur je beter niet via e-mail, je plaatst dan best een link naar de locatie in bv. Google Drive. De maximale grootte van een e-mail is 10MB, inclusief het bericht. Als je grote bestanden verstuurt kan je ze ook proberen te verkleinen.',
132
- field: 'attachments',
133
- });
134
- }
135
-
136
- const safeContentTypes = [
137
- 'application/msword',
138
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
139
- 'application/vnd.ms-excel',
140
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
141
- 'application/pdf',
142
- 'image/jpeg',
143
- 'image/png',
144
- 'image/gif',
145
- ];
146
-
147
- for (const attachment of request.body.attachments) {
148
- if (attachment.contentType && !safeContentTypes.includes(attachment.contentType)) {
149
- throw new SimpleError({
150
- code: 'content_type_not_supported',
151
- message: 'Content-Type not supported',
152
- human: 'Het bestandstype van jouw bijlage wordt niet ondersteund of is onveilig om in een e-mail te plaatsen. Overweeg om je bestand op bv. Google Drive te zetten en de link in jouw e-mail te zetten.',
153
- field: 'attachments',
154
- });
155
- }
156
- }
157
-
158
- const attachments = request.body.attachments.map((attachment, index) => {
159
- let filename = 'bijlage-' + index;
160
-
161
- if (attachment.contentType == 'application/pdf') {
162
- // tmp solution for pdf only
163
- filename += '.pdf';
164
- }
165
-
166
- // Correct file name if needed
167
- if (attachment.filename) {
168
- filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
169
- }
170
-
171
- return {
172
- filename: filename,
173
- content: attachment.content,
174
- contentType: attachment.contentType ?? undefined,
175
- encoding: 'base64',
176
- };
177
- });
178
-
179
- let from = organization.getDefaultFrom(request.i18n, false, 'broadcast');
180
- let replyTo: string | undefined = sender.email;
181
-
182
- // Can we send from this e-mail or reply-to?
183
- if (organization.privateMeta.mailDomain && organization.privateMeta.mailDomainActive && sender.email.endsWith('@' + organization.privateMeta.mailDomain)) {
184
- from = sender.email;
185
- replyTo = undefined;
186
- }
187
-
188
- // Include name in form field
189
- if (sender.name) {
190
- from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + from + '>';
191
- }
192
- else {
193
- from = '"' + organization.name.replaceAll('"', '\\"') + '" <' + from + '>';
194
- }
195
-
196
- const email = request.body;
197
-
198
- if (!email.html) {
199
- throw new SimpleError({
200
- code: 'missing_field',
201
- message: 'Missing html',
202
- human: 'Je hebt geen inhoud ingevuld voor je e-mail. Vul een bericht in en probeer opnieuw.',
203
- field: 'html',
204
- });
205
- }
206
-
207
- if (!email.subject) {
208
- throw new SimpleError({
209
- code: 'missing_field',
210
- message: 'Missing subject',
211
- human: 'Je hebt geen onderwerp ingevuld voor je e-mail. Vul een onderwerp in en probeer opnieuw.',
212
- field: 'subject',
213
- });
214
- }
215
-
216
- // Create e-mail builder
217
- const builder = await getEmailBuilder(organization, {
218
- subject: email.subject,
219
- html: email.html,
220
- recipients: email.recipients,
221
- from,
222
- replyTo,
223
- attachments,
224
- defaultReplacements: request.body.defaultReplacements ?? [],
225
- });
226
-
227
- Email.schedule(builder);
228
-
229
- // Also send a copy
230
- const recipient = Recipient.create(email.recipients[0]);
231
- recipient.email = sender.email;
232
- recipient.firstName = sender.name ?? null;
233
- recipient.lastName = null;
234
- recipient.userId = null;
235
-
236
- const prefix = '<p><i>Kopie e-mail verzonden door ' + user.firstName + ' ' + user.lastName + '</i><br /><br /></p>';
237
- const builder2 = await getEmailBuilder(organization, {
238
- ...email,
239
- subject: '[KOPIE] ' + email.subject,
240
- html: email.html.replace('<body>', '<body>' + prefix),
241
- recipients: [
242
- recipient,
243
- ],
244
- from,
245
- replyTo,
246
- attachments,
247
- });
248
-
249
- Email.schedule(builder2);
250
-
251
- return new Response(undefined);
252
- }
253
- }
@@ -1,32 +0,0 @@
1
- import { Model } from '@simonbackx/simple-database';
2
-
3
- // todo: move for reuse?
4
- type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
5
-
6
- export class ModelHelper {
7
- static async loop<M extends typeof Model>(m: M, idKey: KeysMatching<InstanceType<M>, string> & string, onBatchReceived: (batch: InstanceType<M>[]) => Promise<void>, options: { limit?: number } = {}) {
8
- let lastId = '';
9
- const limit = options.limit ?? 10;
10
-
11
- while (true) {
12
- const models = await m.where(
13
- { [idKey]: { sign: '>', value: lastId } },
14
- { limit, sort: [idKey] });
15
-
16
- if (models.length === 0) {
17
- break;
18
- }
19
-
20
- await onBatchReceived(models);
21
-
22
- if (models.length < limit) {
23
- break;
24
- }
25
-
26
- lastId
27
- = models[
28
- models.length - 1
29
- ][idKey] as string;
30
- }
31
- }
32
- }
@@ -1,11 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { BalanceItem } from '@stamhoofd/models';
3
-
4
- export default new Migration(async () => {
5
- if (STAMHOOFD.environment == 'test') {
6
- console.log('skipped in tests');
7
- return;
8
- }
9
-
10
- await BalanceItem.updatePricePaid('all');
11
- });
@@ -1,11 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { BalanceItem } from '@stamhoofd/models';
3
-
4
- export default new Migration(async () => {
5
- if (STAMHOOFD.environment == 'test') {
6
- console.log('skipped in tests');
7
- return;
8
- }
9
-
10
- await BalanceItem.updatePricePending('all');
11
- });
@@ -1,53 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { logger } from '@simonbackx/simple-logging';
3
- import { BalanceItem, BalanceItemPayment, Payment } from '@stamhoofd/models';
4
-
5
- export default new Migration(async () => {
6
- if (STAMHOOFD.environment == 'test') {
7
- console.log('skipped in tests');
8
- return;
9
- }
10
-
11
- process.stdout.write('\n');
12
- let c = 0;
13
- let id: string = '';
14
-
15
- await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
16
- while (true) {
17
- const payments = await Payment.where({
18
- id: {
19
- value: id,
20
- sign: '>',
21
- },
22
- }, { limit: 100, sort: ['id'] });
23
-
24
- for (const payment of payments) {
25
- const unloaded = (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment));
26
- const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
27
- unloaded,
28
- );
29
-
30
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
31
-
32
- c++;
33
-
34
- if (c % 100 === 0) {
35
- process.stdout.write('.');
36
- }
37
- if (c % 10000 === 0) {
38
- process.stdout.write('\n');
39
- }
40
- }
41
-
42
- if (payments.length < 100) {
43
- break;
44
- }
45
- id = payments[payments.length - 1].id;
46
- }
47
- });
48
-
49
- console.log('Updated outstanding balance for ' + c + ' payments');
50
-
51
- // Do something here
52
- return Promise.resolve();
53
- });
@@ -1,11 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { BalanceItem } from '@stamhoofd/models';
3
-
4
- export default new Migration(async () => {
5
- if (STAMHOOFD.environment == 'test') {
6
- console.log('skipped in tests');
7
- return;
8
- }
9
-
10
- await BalanceItem.updatePricePending('all');
11
- });