@stamhoofd/backend 2.54.2 → 2.55.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.
package/index.ts CHANGED
@@ -91,6 +91,7 @@ const start = async () => {
91
91
  // Register Email Recipient loaders
92
92
  await import('./src/email-recipient-loaders/members');
93
93
  await import('./src/email-recipient-loaders/orders');
94
+ await import('./src/email-recipient-loaders/receivable-balances');
94
95
 
95
96
  routerServer.listen(STAMHOOFD.PORT ?? 9090);
96
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.54.2",
3
+ "version": "2.55.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -36,14 +36,14 @@
36
36
  "@simonbackx/simple-encoding": "2.16.6",
37
37
  "@simonbackx/simple-endpoints": "1.14.0",
38
38
  "@simonbackx/simple-logging": "^1.0.1",
39
- "@stamhoofd/backend-i18n": "2.54.2",
40
- "@stamhoofd/backend-middleware": "2.54.2",
41
- "@stamhoofd/email": "2.54.2",
42
- "@stamhoofd/models": "2.54.2",
43
- "@stamhoofd/queues": "2.54.2",
44
- "@stamhoofd/sql": "2.54.2",
45
- "@stamhoofd/structures": "2.54.2",
46
- "@stamhoofd/utility": "2.54.2",
39
+ "@stamhoofd/backend-i18n": "2.55.0",
40
+ "@stamhoofd/backend-middleware": "2.55.0",
41
+ "@stamhoofd/email": "2.55.0",
42
+ "@stamhoofd/models": "2.55.0",
43
+ "@stamhoofd/queues": "2.55.0",
44
+ "@stamhoofd/sql": "2.55.0",
45
+ "@stamhoofd/structures": "2.55.0",
46
+ "@stamhoofd/utility": "2.55.0",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
49
49
  "axios": "1.6.8",
@@ -63,5 +63,5 @@
63
63
  "publishConfig": {
64
64
  "access": "public"
65
65
  },
66
- "gitHead": "b2f48dffcf11a2386ea74b7c8c2061d4cc380452"
66
+ "gitHead": "0f1de4c457eb1d067a6df5621d8bb06e821f09e3"
67
67
  }
@@ -0,0 +1,59 @@
1
+ import { Email } from '@stamhoofd/models';
2
+ import { receivableBalanceObjectContactInMemoryFilterCompilers, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
3
+ import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
4
+ import { Formatter } from '@stamhoofd/utility';
5
+
6
+ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
7
+ const result = await GetReceivableBalancesEndpoint.buildData(query);
8
+
9
+ // Map all contacts to recipients
10
+ const compiledFilter = compileToInMemoryFilter(subfilter, receivableBalanceObjectContactInMemoryFilterCompilers);
11
+
12
+ return new PaginatedResponse({
13
+ results: result.results.flatMap((balance) => {
14
+ return balance.object.contacts.filter(c => compiledFilter(c)).flatMap((contact) => {
15
+ return contact.emails.map(email => EmailRecipient.create({
16
+ firstName: contact.firstName,
17
+ lastName: contact.lastName,
18
+ email,
19
+ replacements: [
20
+ Replacement.create({
21
+ token: 'organizationName',
22
+ value: balance.object.name,
23
+ }),
24
+ Replacement.create({
25
+ token: 'outstandingBalance',
26
+ value: Formatter.price(balance.amountOpen),
27
+ }),
28
+ ...(contact.meta && contact.meta.url && typeof contact.meta.url === 'string'
29
+ ? [Replacement.create({
30
+ token: 'paymentUrl',
31
+ value: contact.meta.url,
32
+ })]
33
+ : []),
34
+ ],
35
+ }));
36
+ });
37
+ }),
38
+ next: result.next,
39
+ });
40
+ }
41
+
42
+ Email.recipientLoaders.set(EmailRecipientFilterType.ReceivableBalances, {
43
+ fetch,
44
+
45
+ // For now: only count the number of organizations - not the amount of emails
46
+ count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
47
+ const q = await GetReceivableBalancesEndpoint.buildQuery(query);
48
+ const base = await q.count();
49
+
50
+ if (base < 1000) {
51
+ // Do full scan
52
+ query.limit = 1000;
53
+ const result = await fetch(query, subfilter);
54
+ return result.results.length;
55
+ }
56
+
57
+ return base;
58
+ },
59
+ });
@@ -51,6 +51,14 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
51
51
  throw new SimpleError({ code: 'not_found', message: 'Organization not found', statusCode: 404 });
52
52
  }
53
53
 
54
+ if (organization.id === (await Platform.getShared()).membershipOrganizationId) {
55
+ throw new SimpleError({
56
+ code: 'cannot_delete_membership_organization',
57
+ message: 'Cannot delete membership organization',
58
+ human: 'Je kan de hoofdgroep niet verwijderen. Als je dit toch wil doen, kan je eerst een andere vereniging instellen als hoofdgroep via \'Boekhouding en aansluitingen\'.',
59
+ });
60
+ }
61
+
54
62
  await organization.delete();
55
63
  }
56
64
 
@@ -446,16 +446,18 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
446
446
  .where('membershipTypeId', put.membershipTypeId)
447
447
  .where('periodId', put.periodId)
448
448
  .where(
449
- SQL.where('startDate', SQLWhereSign.LessEqual, put.startDate)
450
- .and('endDate', SQLWhereSign.GreaterEqual, put.startDate),
451
- )
452
- .orWhere(
453
- SQL.where('startDate', SQLWhereSign.LessEqual, put.endDate)
454
- .and('endDate', SQLWhereSign.GreaterEqual, put.endDate),
455
- )
456
- .orWhere(
457
- SQL.where('startDate', SQLWhereSign.GreaterEqual, put.startDate)
458
- .and('endDate', SQLWhereSign.LessEqual, put.endDate),
449
+ SQL.where(
450
+ SQL.where('startDate', SQLWhereSign.LessEqual, put.startDate)
451
+ .and('endDate', SQLWhereSign.GreaterEqual, put.startDate),
452
+ )
453
+ .or(
454
+ SQL.where('startDate', SQLWhereSign.LessEqual, put.endDate)
455
+ .and('endDate', SQLWhereSign.GreaterEqual, put.endDate),
456
+ )
457
+ .or(
458
+ SQL.where('startDate', SQLWhereSign.GreaterEqual, put.startDate)
459
+ .and('endDate', SQLWhereSign.LessEqual, put.endDate),
460
+ )
459
461
  )
460
462
  .first(false);
461
463
 
@@ -95,7 +95,7 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
95
95
  : []
96
96
  );
97
97
 
98
- const defaultTemplateTypes = organization ? types.filter(type => type !== EmailTemplateType.SavedMembersEmail) : types;
98
+ const defaultTemplateTypes = organization ? types.filter(type => EmailTemplateStruct.isSavedEmail(type)) : types;
99
99
  const defaultTemplates = defaultTemplateTypes.length === 0
100
100
  ? []
101
101
  : (await EmailTemplate.where({
@@ -121,6 +121,20 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
121
121
  }
122
122
  }
123
123
 
124
+ // Check if we have a category with groups and categories combined
125
+ if (patch.settings) {
126
+ for (const category of organizationPeriod.settings.categories) {
127
+ if (category.groupIds.length && category.categoryIds.length) {
128
+ throw new SimpleError({
129
+ code: 'invalid_field',
130
+ field: 'categories',
131
+ message: 'Cannot have groups and categories combined',
132
+ human: 'Een categorie kan niet zowel groepen als subcategorieën bevatten. Mogelijks zijn meerdere mensen tegelijk aanpassingen aan het maken aan de categorieën. Herlaadt de pagina en probeer opnieuw.',
133
+ })
134
+ }
135
+ }
136
+ }
137
+
124
138
  // #region handle locked categories
125
139
  if (!Context.auth.hasPlatformFullAccess()) {
126
140
  const categoriesAfterPatch = organizationPeriod.settings.categories;
@@ -560,10 +560,20 @@ export class AuthenticatedStructures {
560
560
 
561
561
  const organizationIds = Formatter.uniqueArray(balances.filter(b => b.objectType === ReceivableBalanceType.organization).map(b => b.objectId));
562
562
  const organizations = organizationIds.length > 0 ? await Organization.getByIDs(...organizationIds) : [];
563
- const admins = await User.getAdmins(organizationIds, { verified: true });
563
+
564
+ const responsibilities = organizationIds.length > 0
565
+ ? (await MemberResponsibilityRecord.select()
566
+ .where('organizationId', organizationIds)
567
+ .where('endDate', null)
568
+ .fetch())
569
+ : [];
570
+
564
571
  const organizationStructs = await this.organizations(organizations);
565
572
 
566
- const memberIds = Formatter.uniqueArray(balances.filter(b => b.objectType === ReceivableBalanceType.member).map(b => b.objectId));
573
+ const memberIds = Formatter.uniqueArray([
574
+ ...balances.filter(b => b.objectType === ReceivableBalanceType.member).map(b => b.objectId),
575
+ ...responsibilities.map(r => r.memberId),
576
+ ]);
567
577
  const members = memberIds.length > 0 ? await Member.getBlobByIds(...memberIds) : [];
568
578
 
569
579
  const result: ReceivableBalanceStruct[] = [];
@@ -576,14 +586,28 @@ export class AuthenticatedStructures {
576
586
  if (balance.objectType === ReceivableBalanceType.organization) {
577
587
  const organization = organizationStructs.find(o => o.id == balance.objectId) ?? null;
578
588
  if (organization) {
579
- const thisAdmins = admins.filter(a => a.permissions && a.permissions.forOrganization(organization)?.hasAccessRight(AccessRight.OrganizationFinanceDirector));
589
+ const theseResponsibilities = responsibilities.filter(r => r.organizationId === organization.id);
590
+ const thisMembers = members.flatMap((m) => {
591
+ const resp = theseResponsibilities.filter(r => r.memberId === m.id);
592
+ return resp.length > 0
593
+ ? [{
594
+ member: m,
595
+ responsibilities: resp,
596
+ }]
597
+ : [];
598
+ });
599
+
580
600
  object = ReceivableBalanceObject.create({
581
601
  id: balance.objectId,
582
602
  name: organization.name,
583
- contacts: thisAdmins.map(a => ReceivableBalanceObjectContact.create({
584
- firstName: a.firstName ?? '',
585
- lastName: a.lastName ?? '',
586
- emails: [a.email],
603
+ contacts: thisMembers.map(({ member, responsibilities }) => ReceivableBalanceObjectContact.create({
604
+ firstName: member.firstName ?? '',
605
+ lastName: member.lastName ?? '',
606
+ emails: member.details.getMemberEmails(),
607
+ meta: {
608
+ responsibilityIds: responsibilities.map(r => r.responsibilityId),
609
+ url: organization.dashboardUrl + '/boekhouding/openstaand/' + (Context.organization?.uri ?? ''),
610
+ },
587
611
  })),
588
612
  });
589
613
  }