@stamhoofd/backend 2.72.0 → 2.73.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 (34) hide show
  1. package/index.ts +1 -0
  2. package/package.json +10 -10
  3. package/src/email-recipient-loaders/orders.ts +1 -1
  4. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  5. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
  7. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -4
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
  12. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
  13. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
  16. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  17. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
  19. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
  20. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
  21. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  22. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
  23. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
  24. package/src/excel-loaders/members.ts +8 -0
  25. package/src/excel-loaders/receivable-balances.ts +294 -0
  26. package/src/helpers/AuthenticatedStructures.ts +32 -6
  27. package/src/helpers/SetupStepUpdater.ts +10 -4
  28. package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
  29. package/src/services/PaymentReallocationService.ts +3 -2
  30. package/src/services/PaymentService.ts +17 -1
  31. package/src/sql-filters/members.ts +20 -1
  32. package/src/sql-filters/organizations.ts +1 -0
  33. package/src/sql-filters/receivable-balances.ts +53 -1
  34. package/src/sql-sorters/organizations.ts +11 -0
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { Context } from './Context';
@@ -399,7 +399,7 @@ export class AuthenticatedStructures {
399
399
  const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ deletedAt: null, memberId: { sign: 'IN', value: members.map(m => m.id) } }) : [];
400
400
 
401
401
  // Load missing organizations
402
- const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).filter(id => id !== null));
402
+ const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).concat(platformMemberships.map(r => r.organizationId)).filter(id => id !== null));
403
403
  for (const id of organizationIds) {
404
404
  if (includeContextOrganization || id !== Context.auth.organization?.id) {
405
405
  const found = organizations.get(id);
@@ -575,6 +575,10 @@ export class AuthenticatedStructures {
575
575
  }
576
576
 
577
577
  static async receivableBalances(balances: CachedBalance[]): Promise<ReceivableBalanceStruct[]> {
578
+ return (await this.receivableBalancesHelper(balances)).map(({ balance, object }) => ReceivableBalanceStruct.create({ ...balance, object }));
579
+ }
580
+
581
+ private static async receivableBalancesHelper(balances: CachedBalance[]): Promise< { balance: CachedBalance; object: ReceivableBalanceObject }[]> {
578
582
  if (balances.length === 0) {
579
583
  return [];
580
584
  }
@@ -608,7 +612,8 @@ export class AuthenticatedStructures {
608
612
  ]);
609
613
  const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
610
614
 
611
- const result: ReceivableBalanceStruct[] = [];
615
+ const result: { balance: CachedBalance; object: ReceivableBalanceObject }[] = [];
616
+
612
617
  for (const balance of balances) {
613
618
  let object = ReceivableBalanceObject.create({
614
619
  id: balance.objectId,
@@ -632,6 +637,7 @@ export class AuthenticatedStructures {
632
637
  object = ReceivableBalanceObject.create({
633
638
  id: balance.objectId,
634
639
  name: organization.name,
640
+ uri: organization.uri,
635
641
  contacts: thisMembers.map(({ member, responsibilities }) => ReceivableBalanceObjectContact.create({
636
642
  firstName: member.firstName ?? '',
637
643
  lastName: member.lastName ?? '',
@@ -749,15 +755,35 @@ export class AuthenticatedStructures {
749
755
  }
750
756
  }
751
757
 
752
- const struct = ReceivableBalanceStruct.create({
758
+ result.push({
759
+ balance,
760
+ object,
761
+ });
762
+ }
763
+
764
+ return result;
765
+ }
766
+
767
+ static async detailedReceivableBalances(organizationId: string, balances: CachedBalance[]): Promise<DetailedReceivableBalance[]> {
768
+ const items = await this.receivableBalancesHelper(balances);
769
+ const results: DetailedReceivableBalance[] = [];
770
+
771
+ for (const { balance, object } of items) {
772
+ const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType, true);
773
+ const balanceItemsWithPayments = await BalanceItem.getStructureWithPayments(balanceItems);
774
+
775
+ const result = DetailedReceivableBalance.create({
753
776
  ...balance,
754
777
  object,
778
+ balanceItems: balanceItemsWithPayments,
779
+ // todo!!!
780
+ payments: [],
755
781
  });
756
782
 
757
- result.push(struct);
783
+ results.push(result);
758
784
  }
759
785
 
760
- return result;
786
+ return results;
761
787
  }
762
788
 
763
789
  static async auditLogs(logs: AuditLog[]): Promise<AuditLogStruct[]> {
@@ -283,10 +283,16 @@ export class SetupStepUpdater {
283
283
  finishedSteps++;
284
284
  }
285
285
 
286
- setupSteps.update(SetupStepType.Premises, {
287
- totalSteps,
288
- finishedSteps,
289
- });
286
+ if (premiseTypes.length > 0) {
287
+ setupSteps.update(SetupStepType.Premises, {
288
+ totalSteps,
289
+ finishedSteps,
290
+ });
291
+ }
292
+ else {
293
+ // if no premise types, remove step
294
+ setupSteps.remove(SetupStepType.Premises);
295
+ }
290
296
  }
291
297
 
292
298
  private static updateStepGroups(
@@ -90,6 +90,14 @@ export class XlsxTransformerColumnHelper {
90
90
  value: getParent(member)?.email ?? '',
91
91
  }),
92
92
  },
93
+ {
94
+ id: getId('nationalRegisterNumber'),
95
+ name: getName('Rijksregisternummer'),
96
+ width: 20,
97
+ getValue: (member: PlatformMember) => ({
98
+ value: getParent(member)?.nationalRegisterNumber?.toString() ?? '',
99
+ }),
100
+ },
93
101
  XlsxTransformerColumnHelper.createAddressColumns<PlatformMember>({
94
102
  matchId: getId('address'),
95
103
  getAddress: member => getParent(member)?.address,
@@ -40,13 +40,13 @@ export const PaymentReallocationService = {
40
40
  continue;
41
41
  }
42
42
 
43
- const similarDueItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status === BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
43
+ const similarDueItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status === BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
44
44
 
45
45
  if (similarDueItems.length) {
46
46
  // Not possible to merge into one: there are 2 due items
47
47
  continue;
48
48
  }
49
- const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
49
+ const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
50
50
 
51
51
  if (similarCanceledItems.length) {
52
52
  await this.mergeBalanceItems(balanceItem, similarCanceledItems);
@@ -190,6 +190,7 @@ export const PaymentReallocationService = {
190
190
  payment.type = PaymentType.Reallocation;
191
191
  payment.method = PaymentMethod.Unknown;
192
192
  payment.status = PaymentStatus.Succeeded;
193
+ payment.paidAt = new Date();
193
194
  await payment.save();
194
195
 
195
196
  // Create balance item payments
@@ -161,7 +161,7 @@ export const PaymentService = {
161
161
  if (token) {
162
162
  try {
163
163
  const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
164
- const mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
164
+ let mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
165
165
  testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
166
166
  });
167
167
 
@@ -187,6 +187,22 @@ export const PaymentService = {
187
187
  else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
188
188
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
189
189
  }
190
+ else if ((cancel || this.shouldTryToCancel(payment.status, payment)) && mollieData.isCancelable) {
191
+ console.log('Cancelling Mollie payment on request', payment.id);
192
+ mollieData = await mollieClient.payments.cancel(molliePayment.mollieId);
193
+
194
+ if (mollieData.status === MolliePaymentStatus.paid) {
195
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
196
+ }
197
+ else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
198
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
199
+ }
200
+ else if (this.isManualExpired(payment.status, payment)) {
201
+ // Mollie still returning pending after 1 day: mark as failed
202
+ console.error('Manually marking Mollie payment as expired', payment.id);
203
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
204
+ }
205
+ }
190
206
  else if (this.isManualExpired(payment.status, payment)) {
191
207
  // Mollie still returning pending after 1 day: mark as failed
192
208
  console.error('Manually marking Mollie payment as expired', payment.id);
@@ -1,5 +1,8 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
1
2
  import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLScalar, SQLValueType, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler } from '@stamhoofd/sql';
3
+ import { AccessRight } from '@stamhoofd/structures';
2
4
  import { Formatter } from '@stamhoofd/utility';
5
+ import { Context } from '../helpers/Context';
3
6
  import { organizationFilterCompilers } from './organizations';
4
7
  import { registrationFilterCompilers } from './registrations';
5
8
 
@@ -44,7 +47,23 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
44
47
 
45
48
  'details.requiresFinancialSupport': createSQLExpressionFilterCompiler(
46
49
  SQL.jsonValue(SQL.column('details'), '$.value.requiresFinancialSupport.value'),
47
- { isJSONValue: true, type: SQLValueType.JSONBoolean },
50
+ { isJSONValue: true, type: SQLValueType.JSONBoolean, checkPermission: async () => {
51
+ const organization = Context.organization;
52
+ if (!organization) {
53
+ return;
54
+ }
55
+
56
+ const permissions = await Context.auth.getOrganizationPermissions(organization);
57
+
58
+ if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
59
+ throw new SimpleError({
60
+ code: 'permission_denied',
61
+ message: 'No permissions for financial support filter (organization scope).',
62
+ human: 'Je hebt geen toegangsrechten om deze filter te gebruiken.',
63
+ statusCode: 400,
64
+ });
65
+ }
66
+ } },
48
67
  ),
49
68
 
50
69
  'email': createSQLExpressionFilterCompiler(
@@ -22,6 +22,7 @@ import { SetupStepType } from '@stamhoofd/structures';
22
22
  export const organizationFilterCompilers: SQLFilterDefinitions = {
23
23
  ...baseSQLFilterCompilers,
24
24
  id: createSQLExpressionFilterCompiler(SQL.column('organizations', 'id')),
25
+ uriPadded: createSQLExpressionFilterCompiler(SQL.lpad(SQL.column('organizations', 'uri'), 100, '0')),
25
26
  uri: createSQLExpressionFilterCompiler(SQL.column('organizations', 'uri')),
26
27
  name: createSQLExpressionFilterCompiler(
27
28
  SQL.column('organizations', 'name'),
@@ -1,4 +1,6 @@
1
- import { SQL, SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler } from '@stamhoofd/sql';
1
+ import { SQL, SQLConcat, SQLFilterDefinitions, SQLScalar, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLRelationFilterCompiler } from '@stamhoofd/sql';
2
+ import { memberFilterCompilers } from './members';
3
+ import { organizationFilterCompilers } from './organizations';
2
4
  import { EmailRelationFilterCompilers } from './shared/EmailRelationFilterCompilers';
3
5
 
4
6
  /**
@@ -22,6 +24,56 @@ export const receivableBalanceFilterCompilers: SQLFilterDefinitions = {
22
24
  ).then(1).else(0),
23
25
  { isJSONValue: false, isJSONObject: false },
24
26
  ),
27
+ organizations: createSQLRelationFilterCompiler(
28
+ SQL.select()
29
+ .from(SQL.table('organizations'))
30
+ .where(
31
+ SQL.column(
32
+ 'organizations',
33
+ 'id',
34
+ ),
35
+ SQL.column('cached_outstanding_balances', 'objectId'),
36
+ ).where(
37
+ SQL.column('cached_outstanding_balances', 'objectType'),
38
+ 'organization'),
39
+ organizationFilterCompilers,
40
+ ),
41
+ members: createSQLRelationFilterCompiler(
42
+ SQL.select()
43
+ .from(SQL.table('members'))
44
+ .where(
45
+ SQL.column(
46
+ 'members',
47
+ 'id',
48
+ ),
49
+ SQL.column('cached_outstanding_balances', 'objectId'),
50
+ ).where(
51
+ SQL.column('cached_outstanding_balances', 'objectType'),
52
+ 'member'),
53
+ memberFilterCompilers,
54
+ ),
55
+ users: createSQLRelationFilterCompiler(
56
+ SQL.select()
57
+ .from(SQL.table('users'))
58
+ .where(
59
+ SQL.column(
60
+ 'users',
61
+ 'id',
62
+ ),
63
+ SQL.column('cached_outstanding_balances', 'objectId'),
64
+ ).where(
65
+ SQL.column('cached_outstanding_balances', 'objectType'),
66
+ 'user'),
67
+ {
68
+ name: createSQLExpressionFilterCompiler(
69
+ new SQLConcat(
70
+ SQL.column('firstName'),
71
+ new SQLScalar(' '),
72
+ SQL.column('lastName'),
73
+ ),
74
+ ),
75
+ },
76
+ ),
25
77
 
26
78
  // Allowed to filter by recent emails
27
79
  ...EmailRelationFilterCompilers,
@@ -43,6 +43,17 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
43
43
  });
44
44
  },
45
45
  },
46
+ uriPadded: {
47
+ getValue(a) {
48
+ return a.uri.padStart(100, '0');
49
+ },
50
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
51
+ return new SQLOrderBy({
52
+ column: SQL.lpad(SQL.column('uri'), 100, '0'),
53
+ direction,
54
+ });
55
+ },
56
+ },
46
57
  type: {
47
58
  getValue(a) {
48
59
  return a.meta.type;