@stamhoofd/backend 2.112.0 → 2.114.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.112.0",
3
+ "version": "2.114.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -54,14 +54,14 @@
54
54
  "@simonbackx/simple-encoding": "2.23.1",
55
55
  "@simonbackx/simple-endpoints": "1.20.1",
56
56
  "@simonbackx/simple-logging": "^1.0.1",
57
- "@stamhoofd/backend-i18n": "2.112.0",
58
- "@stamhoofd/backend-middleware": "2.112.0",
59
- "@stamhoofd/email": "2.112.0",
60
- "@stamhoofd/models": "2.112.0",
61
- "@stamhoofd/queues": "2.112.0",
62
- "@stamhoofd/sql": "2.112.0",
63
- "@stamhoofd/structures": "2.112.0",
64
- "@stamhoofd/utility": "2.112.0",
57
+ "@stamhoofd/backend-i18n": "2.114.0",
58
+ "@stamhoofd/backend-middleware": "2.114.0",
59
+ "@stamhoofd/email": "2.114.0",
60
+ "@stamhoofd/models": "2.114.0",
61
+ "@stamhoofd/queues": "2.114.0",
62
+ "@stamhoofd/sql": "2.114.0",
63
+ "@stamhoofd/structures": "2.114.0",
64
+ "@stamhoofd/utility": "2.114.0",
65
65
  "archiver": "^7.0.1",
66
66
  "axios": "^1.13.2",
67
67
  "cookie": "^0.7.0",
@@ -79,5 +79,5 @@
79
79
  "publishConfig": {
80
80
  "access": "public"
81
81
  },
82
- "gitHead": "8734fff2c389052210c5da51bd3c4beaf149bfd1"
82
+ "gitHead": "89f437d82d2c21b1741c9c7efdbdafd6c8b98318"
83
83
  }
@@ -1,8 +1,8 @@
1
1
  import { Email, Member } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
3
  import { EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, RegistrationsBlob, mergeFilters } from '@stamhoofd/structures';
4
- import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint';
5
- import { memberJoin } from '../sql-filters/registrations';
4
+ import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
5
+ import { memberJoin } from '../sql-filters/registrations.js';
6
6
 
7
7
  async function getRecipients(result: PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>, type: 'member' | 'parents' | 'unverified') {
8
8
  const recipients: EmailRecipient[] = [];
@@ -81,7 +81,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.RegistrationUnverified, {
81
81
  const q = (await GetRegistrationsEndpoint.buildQuery(query)).join(memberJoin);
82
82
 
83
83
  return await q.sum(
84
- SQL.jsonLength(SQL.column('details'), '$.value.unverifiedEmails'),
84
+ SQL.jsonLength(SQL.column(Member.table, 'details'), '$.value.unverifiedEmails'),
85
85
  );
86
86
  },
87
87
  });
@@ -3,7 +3,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { Email, Platform, RateLimiter } from '@stamhoofd/models';
4
4
  import { EmailPreview, EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
5
5
 
6
- import { Context } from '../../../helpers/Context';
6
+ import { Context } from '../../../helpers/Context.js';
7
7
  import { SimpleError } from '@simonbackx/simple-errors';
8
8
 
9
9
  type Params = Record<string, never>;
@@ -128,7 +128,7 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
128
128
 
129
129
  await model.save();
130
130
  await model.buildExampleRecipient();
131
- model.updateCount();
131
+ await model.updateCount();
132
132
 
133
133
  if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
134
134
  if (!await Context.auth.canSendEmail(model)) {
@@ -1,10 +1,10 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { Email, Platform } from '@stamhoofd/models';
3
- import { EmailPreview, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
3
+ import { EmailPreview, EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
4
4
 
5
5
  import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
7
- import { Context } from '../../../helpers/Context';
7
+ import { Context } from '../../../helpers/Context.js';
8
8
 
9
9
  type Params = { id: string };
10
10
  type Query = undefined;
@@ -124,6 +124,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
124
124
  });
125
125
  }
126
126
 
127
+ if (model.recipientsStatus === EmailRecipientsStatus.Created) {
128
+ throw new SimpleError({
129
+ code: 'already_created',
130
+ message: 'Recipients already created',
131
+ human: $t(`457ecdaf-d1de-4136-9e82-682c18c5fa76`),
132
+ statusCode: 400,
133
+ });
134
+ }
135
+
127
136
  model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
128
137
  rebuild = true;
129
138
  }
@@ -168,10 +177,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
168
177
 
169
178
  if (rebuild) {
170
179
  await model.buildExampleRecipient();
171
-
172
- // Force null - because we have stale data
173
- model.emailRecipientsCount = null;
174
- model.updateCount();
180
+ await model.updateCount();
175
181
  }
176
182
 
177
183
  if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
@@ -1057,6 +1057,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
1057
1057
  payment.status = PaymentStatus.Created;
1058
1058
  payment.paidAt = null;
1059
1059
  payment.price = totalPrice;
1060
+ PaymentService.round(payment);
1061
+ totalPrice = payment.price;
1060
1062
 
1061
1063
  if (totalPrice === 0) {
1062
1064
  payment.status = PaymentStatus.Succeeded;
@@ -1,14 +1,15 @@
1
- import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { BalanceItem, BalanceItemPayment, Payment } from '@stamhoofd/models';
4
+ import { BalanceItem, BalanceItemPayment, Payment, User } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { PaymentGeneral, PaymentMethod, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel } from '@stamhoofd/structures';
6
+ import { PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel } from '@stamhoofd/structures';
7
7
 
8
- import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
- import { Context } from '../../../../helpers/Context';
10
- import { BalanceItemService } from '../../../../services/BalanceItemService';
11
- import { PaymentService } from '../../../../services/PaymentService';
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
9
+ import { Context } from '../../../../helpers/Context.js';
10
+ import { BalanceItemService } from '../../../../services/BalanceItemService.js';
11
+ import { PaymentService } from '../../../../services/PaymentService.js';
12
+ import { ViesHelper } from '../../../../helpers/ViesHelper.js';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
  type Query = undefined;
@@ -71,6 +72,38 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
71
72
  payment.status = PaymentStatus.Created;
72
73
  payment.method = put.method;
73
74
  payment.customer = put.customer;
75
+
76
+ const payingOrganizationId = put.payingOrganizationId ?? put.payingOrganization?.id ?? null;
77
+
78
+ if (payingOrganizationId) {
79
+ if (Context.auth.hasSomePlatformAccess()) {
80
+ if (await Context.auth.hasFullAccess(payingOrganizationId, PermissionLevel.Full)) {
81
+ payment.payingOrganizationId = payingOrganizationId;
82
+ }
83
+ else {
84
+ // silently ignore
85
+ }
86
+ }
87
+ }
88
+
89
+ if (put.payingUserId) {
90
+ const user = await User.getByID(put.payingUserId);
91
+ if (!user) {
92
+ throw new SimpleError({
93
+ code: 'user_not_found',
94
+ message: 'User not found',
95
+ field: 'payingUserId',
96
+ });
97
+ }
98
+ if (await Context.auth.canAccessUser(user, PermissionLevel.Full)) {
99
+ // Allowed
100
+ payment.payingUserId = put.payingUserId;
101
+ }
102
+ }
103
+
104
+ if (put.customer?.company) {
105
+ await ViesHelper.checkCompany(put.customer.company, put.customer.company);
106
+ }
74
107
  payment.type = put.type;
75
108
 
76
109
  if (payment.type === PaymentType.Reallocation) {
@@ -131,6 +164,7 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
131
164
  // Check total price
132
165
  const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0);
133
166
  payment.price = totalPrice;
167
+ PaymentService.round(payment);
134
168
 
135
169
  switch (payment.type) {
136
170
  case PaymentType.Payment: {
@@ -265,6 +299,38 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
265
299
  payment.paidAt = patch.paidAt;
266
300
  }
267
301
 
302
+ if (patch.customer) {
303
+ payment.customer = patchObject(payment.customer, patch.customer, { defaultValue: PaymentCustomer.create({}) });
304
+ }
305
+
306
+ const payingOrganizationId = patch.payingOrganizationId ?? patch.payingOrganization?.id ?? null;
307
+
308
+ if (payingOrganizationId) {
309
+ if (Context.auth.hasSomePlatformAccess()) {
310
+ if (await Context.auth.hasFullAccess(payingOrganizationId, PermissionLevel.Full)) {
311
+ payment.payingOrganizationId = payingOrganizationId;
312
+ }
313
+ else {
314
+ // silently ignore
315
+ }
316
+ }
317
+ }
318
+
319
+ if (patch.payingUserId) {
320
+ const user = await User.getByID(patch.payingUserId);
321
+ if (!user) {
322
+ throw new SimpleError({
323
+ code: 'user_not_found',
324
+ message: 'User not found',
325
+ field: 'payingUserId',
326
+ });
327
+ }
328
+ if (await Context.auth.canAccessUser(user, PermissionLevel.Full)) {
329
+ // Allowed
330
+ payment.payingUserId = patch.payingUserId;
331
+ }
332
+ }
333
+
268
334
  await payment.save();
269
335
 
270
336
  if (patch.status) {
@@ -5,10 +5,11 @@ import { BalanceItem, BalanceItemPayment, Order, Payment, Webshop, WebshopCounte
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
6
  import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, TranslatedString, Webshop as WebshopStruct, WebshopTicketType } from '@stamhoofd/structures';
7
7
 
8
- import { Context } from '../../../../helpers/Context';
9
- import { AuditLogService } from '../../../../services/AuditLogService';
10
- import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService';
11
- import { ServiceFeeHelper } from '../../../../helpers/ServiceFeeHelper';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { AuditLogService } from '../../../../services/AuditLogService.js';
10
+ import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService.js';
11
+ import { ServiceFeeHelper } from '../../../../helpers/ServiceFeeHelper.js';
12
+ import { PaymentService } from '../../../../services/PaymentService.js';
12
13
 
13
14
  type Params = { id: string };
14
15
  type Query = undefined;
@@ -154,6 +155,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
154
155
  payment.method = struct.data.paymentMethod;
155
156
  payment.status = PaymentStatus.Created;
156
157
  payment.price = totalPrice;
158
+ PaymentService.round(payment);
157
159
  payment.paidAt = null;
158
160
 
159
161
  // Determine the payment provider (always null because no online payments here)
@@ -8,12 +8,13 @@ import { QueueHandler } from '@stamhoofd/queues';
8
8
  import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct, WebshopTicketType } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
- import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
12
- import { Context } from '../../../helpers/Context';
13
- import { StripeHelper } from '../../../helpers/StripeHelper';
14
- import { AuditLogService } from '../../../services/AuditLogService';
15
- import { UitpasService } from '../../../services/uitpas/UitpasService';
16
- import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper';
11
+ import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
12
+ import { Context } from '../../../helpers/Context.js';
13
+ import { StripeHelper } from '../../../helpers/StripeHelper.js';
14
+ import { AuditLogService } from '../../../services/AuditLogService.js';
15
+ import { UitpasService } from '../../../services/uitpas/UitpasService.js';
16
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
17
+ import { PaymentService } from '../../../services/PaymentService.js';
17
18
 
18
19
  type Params = { id: string };
19
20
  type Query = undefined;
@@ -154,7 +155,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
154
155
  });
155
156
 
156
157
  // The order now is valid, the stock is reserved for now (until the payment fails or expires)
157
- const totalPrice = request.body.totalPrice;
158
+ let totalPrice = request.body.totalPrice;
158
159
 
159
160
  if (totalPrice % 100 !== 0) {
160
161
  throw new SimpleError({
@@ -178,6 +179,8 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
178
179
  payment.method = request.body.paymentMethod;
179
180
  payment.status = PaymentStatus.Created;
180
181
  payment.price = totalPrice;
182
+ PaymentService.round(payment);
183
+ totalPrice = payment.price;
181
184
  payment.paidAt = null;
182
185
  payment.customer = PaymentCustomer.create({
183
186
  firstName: request.body.customer.firstName,
@@ -131,7 +131,7 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
131
131
  name: $t(`030be384-9014-410c-87ba-e04920c26111`),
132
132
  width: 20,
133
133
  getValue: ({ patchedMember: object }: PlatformMember) => ({
134
- value: XlsxTransformerColumnHelper.formatBoolean(object.details.requiresFinancialSupport?.value),
134
+ value: XlsxTransformerColumnHelper.formatBoolean(object.details.hasFinancialSupportOrActiveUitpas),
135
135
  }),
136
136
  },
137
137
  {
@@ -56,7 +56,11 @@ export class AdminPermissionChecker {
56
56
  if (!result) {
57
57
  console.error('Unexpected missing organization in AdminPermissionChecker.getOrganization', id);
58
58
  this.organizationCache.delete(id);
59
- throw new Error('Unexpected missing organization in AdminPermissionChecker.getOrganization');
59
+
60
+ throw new SimpleError({
61
+ code: 'organization_not_found',
62
+ message: 'Organization not found',
63
+ });
60
64
  }
61
65
  this.organizationCache.set(id, result);
62
66
  return result;
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Company, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
  import { Sorter } from '@stamhoofd/utility';
5
5
 
6
6
  import { SQL } from '@stamhoofd/sql';
@@ -896,10 +896,67 @@ export class AuthenticatedStructures {
896
896
 
897
897
  const result: { balance: CachedBalance; object: ReceivableBalanceObject }[] = [];
898
898
 
899
+ function getMemberContacts(member: Member, balance: CachedBalance) {
900
+ const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
901
+ return [
902
+ ...(member.details.getMemberEmails().length
903
+ ? [
904
+ ReceivableBalanceObjectContact.create({
905
+ firstName: member.details.firstName ?? '',
906
+ lastName: member.details.lastName ?? '',
907
+ emails: member.details.getMemberEmails(),
908
+ meta: {
909
+ type: 'member',
910
+ responsibilityIds: [],
911
+ url,
912
+ },
913
+ }),
914
+ ]
915
+ : []),
916
+
917
+ ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
918
+ ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
919
+ firstName: p.firstName ?? '',
920
+ lastName: p.lastName ?? '',
921
+ emails: p.getEmails(),
922
+ meta: {
923
+ type: 'parent',
924
+ responsibilityIds: [],
925
+ url,
926
+ },
927
+ }))
928
+ : []),
929
+ ];
930
+ }
931
+
932
+ function getMemberCustomers(member: Member): PaymentCustomer[] {
933
+ return [
934
+ ...(member.details.defaultAge >= 14 || member.details.parents.length === 0 || !member.details.calculatedParentsHaveAccess
935
+ ? [
936
+ PaymentCustomer.create({
937
+ firstName: member.details.firstName ?? '',
938
+ lastName: member.details.lastName ?? '',
939
+ email: member.details.getMemberEmails()[0] ?? null,
940
+ phone: member.details.phone,
941
+ }),
942
+ ]
943
+ : []),
944
+
945
+ ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
946
+ ? member.details.parents.map(parent => PaymentCustomer.create({
947
+ firstName: parent.firstName ?? '',
948
+ lastName: parent.lastName ?? '',
949
+ email: parent.getEmails()[0] ?? null,
950
+ phone: parent.phone,
951
+ }))
952
+ : []),
953
+ ];
954
+ }
955
+
899
956
  for (const balance of balances) {
900
957
  let object = ReceivableBalanceObject.create({
901
958
  id: balance.objectId,
902
- name: 'Onbekend',
959
+ name: $t('6c3e777c-7cd6-4566-9540-8a829c26212f'),
903
960
  });
904
961
 
905
962
  if (balance.objectType === ReceivableBalanceType.organization) {
@@ -920,6 +977,7 @@ export class AuthenticatedStructures {
920
977
  id: balance.objectId,
921
978
  name: organization.name,
922
979
  uri: organization.uri,
980
+ customers: organization.defaultCompanies.map(company => PaymentCustomer.create({ company })),
923
981
  contacts: thisMembers.map(({ member, responsibilities }) => ReceivableBalanceObjectContact.create({
924
982
  firstName: member.firstName ?? '',
925
983
  lastName: member.lastName ?? '',
@@ -936,39 +994,11 @@ export class AuthenticatedStructures {
936
994
  else if (balance.objectType === ReceivableBalanceType.member) {
937
995
  const member = members.find(m => m.id === balance.objectId) ?? null;
938
996
  if (member) {
939
- const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
940
997
  object = ReceivableBalanceObject.create({
941
998
  id: balance.objectId,
942
999
  name: member.details.name,
943
- contacts: [
944
- ...(member.details.getMemberEmails().length
945
- ? [
946
- ReceivableBalanceObjectContact.create({
947
- firstName: member.details.firstName ?? '',
948
- lastName: member.details.lastName ?? '',
949
- emails: member.details.getMemberEmails(),
950
- meta: {
951
- type: 'member',
952
- responsibilityIds: [],
953
- url,
954
- },
955
- }),
956
- ]
957
- : []),
958
-
959
- ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
960
- ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
961
- firstName: p.firstName ?? '',
962
- lastName: p.lastName ?? '',
963
- emails: p.getEmails(),
964
- meta: {
965
- type: 'parent',
966
- responsibilityIds: [],
967
- url,
968
- },
969
- }))
970
- : []),
971
- ],
1000
+ customers: getMemberCustomers(member),
1001
+ contacts: getMemberContacts(member, balance),
972
1002
  });
973
1003
  }
974
1004
  }
@@ -979,39 +1009,11 @@ export class AuthenticatedStructures {
979
1009
  }
980
1010
  const member = members.find(m => m.id === registration.memberId) ?? null;
981
1011
  if (member) {
982
- const url = Context.organization && Context.organization.id === balance.organizationId ? 'https://' + Context.organization.getHost() : '';
983
1012
  object = ReceivableBalanceObject.create({
984
1013
  id: balance.objectId,
985
1014
  name: member.details.name,
986
- contacts: [
987
- ...(member.details.getMemberEmails().length
988
- ? [
989
- ReceivableBalanceObjectContact.create({
990
- firstName: member.details.firstName ?? '',
991
- lastName: member.details.lastName ?? '',
992
- emails: member.details.getMemberEmails(),
993
- meta: {
994
- type: 'member',
995
- responsibilityIds: [],
996
- url,
997
- },
998
- }),
999
- ]
1000
- : []),
1001
-
1002
- ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
1003
- ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
1004
- firstName: p.firstName ?? '',
1005
- lastName: p.lastName ?? '',
1006
- emails: p.getEmails(),
1007
- meta: {
1008
- type: 'parent',
1009
- responsibilityIds: [],
1010
- url,
1011
- },
1012
- }))
1013
- : []),
1014
- ],
1015
+ customers: getMemberCustomers(member),
1016
+ contacts: getMemberContacts(member, balance),
1015
1017
  });
1016
1018
  }
1017
1019
  }
@@ -1022,6 +1024,11 @@ export class AuthenticatedStructures {
1022
1024
  object = ReceivableBalanceObject.create({
1023
1025
  id: balance.objectId,
1024
1026
  name: user.name || user.email,
1027
+ customers: [PaymentCustomer.create({
1028
+ firstName: user.firstName,
1029
+ lastName: user.lastName,
1030
+ email: user.email,
1031
+ })],
1025
1032
  contacts: [
1026
1033
  ReceivableBalanceObjectContact.create({
1027
1034
  firstName: user.firstName ?? '',
@@ -2,7 +2,7 @@ import { MolliePayment, MollieToken, Order, Organization, PayconiqPayment, Payme
2
2
  import { Settlement } from '@stamhoofd/structures';
3
3
  import axios from 'axios';
4
4
 
5
- import { StripePayoutChecker } from './StripePayoutChecker';
5
+ import { StripePayoutChecker } from './StripePayoutChecker.js';
6
6
 
7
7
  type MollieSettlement = {
8
8
  id: string;
@@ -179,7 +179,7 @@ async function updateSettlement(token: string, settlement: MollieSettlement, fro
179
179
  id: settlement.id,
180
180
  reference: settlement.reference,
181
181
  settledAt: new Date(settlement.settledAt),
182
- amount: Math.round(parseFloat(settlement.amount.value) * 100),
182
+ amount: Math.round(parseFloat(settlement.amount.value) * 100) * 100,
183
183
  });
184
184
  const saved = await payment.save();
185
185
 
@@ -6,7 +6,7 @@ import { AsyncLocalStorage } from 'async_hooks';
6
6
 
7
7
  import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
8
8
  import { ApiUserRateLimits } from '@stamhoofd/structures';
9
- import { AdminPermissionChecker } from './AdminPermissionChecker';
9
+ import { AdminPermissionChecker } from './AdminPermissionChecker.js';
10
10
 
11
11
  export const apiUserRateLimiter = new RateLimiter({
12
12
  limits: [
@@ -171,8 +171,8 @@ export class StripePayoutChecker {
171
171
  return;
172
172
  }
173
173
 
174
- if (payment.price !== balanceItem.amount) {
175
- console.log('Amount mismatch for payment ' + payment.id + ': ' + payment.price + ' !== ' + balanceItem.amount);
174
+ if (payment.price !== balanceItem.amount * 100) {
175
+ console.log('Amount mismatch for payment ' + payment.id + ': ' + payment.price + ' !== ' + (balanceItem.amount * 100));
176
176
  return;
177
177
  }
178
178
 
@@ -180,13 +180,13 @@ export class StripePayoutChecker {
180
180
  id: payout.id,
181
181
  reference: payout.statement_descriptor ?? '',
182
182
  settledAt: new Date(payout.arrival_date * 1000),
183
- amount: payout.amount,
183
+ amount: payout.amount * 100,
184
184
  // Set only if application fee is witheld
185
- fee: totalFees,
185
+ fee: totalFees * 100,
186
186
  });
187
187
 
188
188
  payment.settlement = settlement;
189
- payment.transferFee = totalFees - payment.serviceFeePayout;
189
+ payment.transferFee = totalFees * 100 - payment.serviceFeePayout;
190
190
 
191
191
  // Force an updatedAt timestamp of the related order
192
192
  // Mark order as 'updated', or the frontend won't pull in the updates
@@ -98,7 +98,8 @@ export class UitpasTokenRepository {
98
98
  }
99
99
 
100
100
  private async getNewAccessToken() {
101
- const url = 'https://account-test.uitid.be/realms/uitid/protocol/openid-connect/token';
101
+ console.log('UITPAS: Fetching new access token for', this.uitpasClientCredential.organizationId);
102
+ const url = STAMHOOFD.UITPAS_API_URL?.includes('test') ? 'https://account-test.uitid.be/realms/uitid/protocol/openid-connect/token' : 'https://account.uitid.be/realms/uitid/protocol/openid-connect/token';
102
103
  const myHeaders = new Headers();
103
104
  myHeaders.append('Content-Type', 'application/x-www-form-urlencoded');
104
105
  const params = new URLSearchParams({
@@ -110,6 +111,7 @@ export class UitpasTokenRepository {
110
111
  method: 'POST',
111
112
  headers: myHeaders,
112
113
  body: params.toString(),
114
+ signal: AbortSignal.timeout(5000),
113
115
  };
114
116
  const response = await fetch(url, requestOptions).catch(() => {
115
117
  // Handle network errors
@@ -123,6 +125,7 @@ export class UitpasTokenRepository {
123
125
  if (response.status === 401) {
124
126
  // Unauthorized, credentials are invalid
125
127
  throw new SimpleError({
128
+ statusCode: this.uitpasClientCredential.organizationId === null ? 500 : 400, // Internal, non visible error in case it is a built in credential
126
129
  code: 'invalid_uitpas_client_credentials',
127
130
  message: `Invalid UiTPAS client credentials`,
128
131
  human: $t(`1086bb24-5df4-4faf-9dc0-ab5a955b0d8f`),
@@ -130,6 +133,7 @@ export class UitpasTokenRepository {
130
133
  }
131
134
  console.error(`Unsuccessful response when fetching UiTPAS token for organization with id ${this.uitpasClientCredential.organizationId}:`, response.statusText);
132
135
  throw new SimpleError({
136
+ statusCode: this.uitpasClientCredential.organizationId === null ? 500 : 400, // Internal, non visible error in case it is a built in credential
133
137
  code: 'unsuccessful_response_fetching_uitpas_token',
134
138
  message: `Unsuccesful response when fetching UiTPAS token`,
135
139
  human: $t(`dd9b30ca-860f-47aa-8cb1-527fd156d9ca`),
@@ -138,6 +142,7 @@ export class UitpasTokenRepository {
138
142
  const json: unknown = await response.json().catch(() => {
139
143
  // Handle JSON parsing errors
140
144
  throw new SimpleError({
145
+ statusCode: this.uitpasClientCredential.organizationId === null ? 500 : 400, // Internal, non visible error in case it is a built in credential
141
146
  code: 'invalid_json_fetching_uitpas_token',
142
147
  message: `Invalid json when fetching UiTPAS token`,
143
148
  human: $t(`8f217db0-c672-46f0-a8f7-6eba6f080947`),
@@ -5,7 +5,7 @@ import { Member } from '@stamhoofd/models';
5
5
  import { SQL } from '@stamhoofd/sql';
6
6
  import { UitpasSocialTariff, UitpasSocialTariffStatus } from '@stamhoofd/structures';
7
7
  import { sleep } from '@stamhoofd/utility';
8
- import { updateMemberDetailsUitpasNumber } from '../../helpers/updateMemberDetailsUitpasNumber.js';
8
+ import { updateMemberDetailsUitpasNumber } from '../helpers/updateMemberDetailsUitpasNumber.js';
9
9
 
10
10
  /**
11
11
  * Seed to update the social tariff of all uitpas numbers.
@@ -34,17 +34,15 @@ let idOfLastUpdatedMember: string | null = null;
34
34
 
35
35
  export async function migrateUitpasStatusOfAllMembers() {
36
36
  let query = Member.select()
37
- // where there is an uitpas number
38
- .where(SQL.jsonValue(SQL.column('details'), '$.value.uitpasNumberDetails'), '!=', null);
37
+ // where there is an uitpas number
38
+ .where(SQL.jsonValue(SQL.column('details'), '$.value.uitpasNumber'), '!=', null)
39
+ .orWhere(SQL.jsonValue(SQL.column('details'), '$.value.uitpasNumberDetails'), '!=', null);
39
40
 
40
41
  if (idOfLastUpdatedMember !== null) {
41
42
  console.log('Continue from member with id ', idOfLastUpdatedMember);
42
43
  query = query.where('id', '>', idOfLastUpdatedMember);
43
44
  }
44
-
45
- const total = await query.clone().count();
46
-
47
- console.log(`Start updating uitpas status of ${total} members.`);
45
+ console.log(`Start updating uitpas status members.`);
48
46
 
49
47
  let c = 0;
50
48
 
@@ -93,14 +91,9 @@ async function migrateMember(member: Member) {
93
91
  if (error.hasCode('https://api.publiq.be/probs/uitpas/pass-not-found') || error.hasCode('https://api.publiq.be/probs/uitpas/invalid-uitpas-number')) {
94
92
  console.log(`Uitpas number ${member.details.uitpasNumberDetails?.uitpasNumber} is not known by the uitpas api for member with id ${member.id}.`);
95
93
 
96
- // set updated at
97
- if (member.details.uitpasNumberDetails) {
98
- member.details.uitpasNumberDetails.socialTariff = UitpasSocialTariff.create({
99
- status: UitpasSocialTariffStatus.Unknown,
100
- });
101
- }
102
-
103
- // remove review
94
+ // remove the uitpas number
95
+ member.details.uitpasNumberDetails = null;
96
+ member.details.cleanData();
104
97
  member.details.reviewTimes.removeReview('uitpasNumber');
105
98
  await member.save();
106
99
 
@@ -115,6 +108,9 @@ async function migrateMember(member: Member) {
115
108
  // remove the uitpas number
116
109
  member.details.uitpasNumberDetails = null;
117
110
  member.details.cleanData();
111
+
112
+ // remove review
113
+ member.details.reviewTimes.removeReview('uitpasNumber');
118
114
  await member.save();
119
115
 
120
116
  // do not throw
@@ -341,7 +341,7 @@ export const PaymentService = {
341
341
  *
342
342
  * TODO: update this method to generate a virtual invoice and use the price of the invoice instead of the rounded payment price, so we don't get differences in calculation
343
343
  */
344
- async round(payment: Payment) {
344
+ round(payment: Payment) {
345
345
  const amount = payment.price;
346
346
  const rounded = Payment.roundPrice(payment.price);
347
347
  const difference = rounded - amount;
@@ -354,10 +354,9 @@ export const PaymentService = {
354
354
  throw new Error('Unexpected rounding difference of ' + difference + ' for payment ' + payment.id);
355
355
  }
356
356
 
357
- // payment.roundingAmount = difference;
357
+ payment.roundingAmount = difference;
358
358
 
359
359
  // Change payment total price
360
360
  payment.price += difference;
361
- await payment.save();
362
361
  },
363
362
  };
@@ -68,34 +68,20 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
68
68
  type: SQLValueType.String,
69
69
  nullable: false,
70
70
  }),
71
+ 'details.uitpasNumberDetails.socialTariff.status': createColumnFilter({
72
+ expression: SQL.jsonExtract(SQL.column(membersTable, 'details'), '$.value.uitpasNumberDetails.socialTariff.status'),
73
+ type: SQLValueType.JSONString,
74
+ nullable: true,
75
+ checkPermission: async () => {
76
+ await throwIfNoFinancialReadAccess();
77
+ },
78
+ }),
71
79
  'details.requiresFinancialSupport': createColumnFilter({
72
80
  expression: SQL.jsonExtract(SQL.column(membersTable, 'details'), '$.value.requiresFinancialSupport.value'),
73
81
  type: SQLValueType.JSONBoolean,
74
82
  nullable: true,
75
83
  checkPermission: async () => {
76
- const organization = Context.organization;
77
- if (!organization) {
78
- if (!Context.auth.hasPlatformFullAccess()) {
79
- throw new SimpleError({
80
- code: 'permission_denied',
81
- message: 'No permissions for financial support filter.',
82
- human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
83
- statusCode: 400,
84
- });
85
- }
86
- return;
87
- }
88
-
89
- const permissions = await Context.auth.getOrganizationPermissions(organization);
90
-
91
- if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
92
- throw new SimpleError({
93
- code: 'permission_denied',
94
- message: 'No permissions for financial support filter (organization scope).',
95
- human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
96
- statusCode: 400,
97
- });
98
- }
84
+ await throwIfNoFinancialReadAccess();
99
85
  },
100
86
  }),
101
87
  'email': createColumnFilter({
@@ -512,3 +498,29 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
512
498
  ),
513
499
  },
514
500
  };
501
+
502
+ async function throwIfNoFinancialReadAccess() {
503
+ const organization = Context.organization;
504
+ if (!organization) {
505
+ if (!Context.auth.hasPlatformFullAccess()) {
506
+ throw new SimpleError({
507
+ code: 'permission_denied',
508
+ message: 'No permissions for financial support filter.',
509
+ human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
510
+ statusCode: 400,
511
+ });
512
+ }
513
+ return;
514
+ }
515
+
516
+ const permissions = await Context.auth.getOrganizationPermissions(organization);
517
+
518
+ if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
519
+ throw new SimpleError({
520
+ code: 'permission_denied',
521
+ message: 'No permissions for financial support filter (organization scope).',
522
+ human: $t(`64d658fa-0727-4924-9448-b243fe8e10a1`),
523
+ statusCode: 400,
524
+ });
525
+ }
526
+ }
@@ -5,6 +5,7 @@ import { Formatter } from '@stamhoofd/utility';
5
5
  import { memberCachedBalanceForOrganizationJoin, registrationCachedBalanceJoin } from '../helpers/outstandingBalanceJoin.js';
6
6
  import { SQLTranslatedString } from '../helpers/SQLTranslatedString.js';
7
7
  import { groupJoin, memberJoin, organizationJoin } from '../sql-filters/registrations.js';
8
+ import { Context, ContextInstance } from '../helpers/Context.js';
8
9
 
9
10
  export class RegistrationSortData {
10
11
  readonly registration: RegistrationWithMemberBlob;
@@ -17,8 +18,11 @@ export class RegistrationSortData {
17
18
 
18
19
  get organization() {
19
20
  const organization = this.organizations.find(o => o.id === this.registration.organizationId);
21
+ if (!organization && ContextInstance.optional?.organization && ContextInstance.optional?.organization.id === this.registration.organizationId) {
22
+ return Context.organization!;
23
+ }
20
24
  if (!organization) {
21
- throw new Error('Organization not found for registration');
25
+ throw new Error('Organization not found for registration ' + this.registration.id);
22
26
  }
23
27
 
24
28
  return organization;