@stamhoofd/backend 2.120.5 → 2.121.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 (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
@@ -0,0 +1,308 @@
1
+ import type { Decoder } from '@simonbackx/simple-encoding';
2
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
3
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { BalanceItem, Organization, Platform, STPackage } from '@stamhoofd/models';
6
+ import { BalanceItemStatus, BalanceItemType, CheckoutResponse, OrganizationPackagesStatus, OrganizationCheckout, STPackageStruct } from '@stamhoofd/structures';
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { PaymentService } from '../../../../services/PaymentService.js';
10
+ import { STPackageService } from '../../../../services/STPackageService.js';
11
+ import { CreateMandateSettings } from '@stamhoofd/structures/checkout/CreateMandateSettings.js';
12
+ import { BalanceItemService } from '../../../../services/BalanceItemService.js';
13
+
14
+ type Params = { sellingOrganizationId: string };
15
+ type Query = undefined;
16
+ type Body = OrganizationCheckout;
17
+ type ResponseBody = CheckoutResponse;
18
+
19
+ /**
20
+ * B2B billing (outstanding amount) and activating packages
21
+ */
22
+ export class OrganizationCheckoutEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
+ bodyDecoder = OrganizationCheckout as Decoder<Body>;
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method !== 'POST') {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, '/billing/@sellingOrganizationId/checkout', {sellingOrganizationId: String});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ const organization = await Context.setOrganizationScope();
40
+ const { user } = await Context.authenticate();
41
+
42
+ // If the user has permission, we'll also search if he has access to the organization's key
43
+ if (!await Context.auth.canActivatePackages(organization.id)) {
44
+ throw Context.auth.error();
45
+ }
46
+ const checkout = request.body;
47
+
48
+ // Validate company
49
+ if (checkout.customer && checkout.customer.company) {
50
+ // Search company id
51
+ // this avoids needing to check the VAT number every time
52
+ const id = checkout.customer.company.id;
53
+ const foundCompany = organization.meta.companies.find(c => c.id === id);
54
+
55
+ if (!foundCompany) {
56
+ throw new SimpleError({
57
+ code: 'invalid_data',
58
+ message: $t(`%w1`),
59
+ });
60
+ }
61
+ }
62
+
63
+ const id = request.params.sellingOrganizationId;
64
+ if (!id) {
65
+ throw new SimpleError({
66
+ code: 'unavailable',
67
+ message: 'This is temporarily unavailable',
68
+ human: $t('%1Rz')
69
+ })
70
+ }
71
+
72
+ const sellingOrganization = await Organization.getByID(id);
73
+ if (!sellingOrganization || !sellingOrganization.active) {
74
+ throw new SimpleError({
75
+ statusCode: 404,
76
+ code: 'not_found',
77
+ message: 'Selling organization not found',
78
+ human: $t('%1R5'),
79
+ field: 'sellingOrganization'
80
+ })
81
+ }
82
+
83
+ if (sellingOrganization.id === organization.id) {
84
+ throw new SimpleError({
85
+ code: 'unavailable',
86
+ message: 'Cannot purchase from yourself',
87
+ });
88
+ }
89
+
90
+ const packages: STPackageStruct[] = [];
91
+ const balanceItems: Map<BalanceItem, number> = new Map();
92
+ const models: STPackage[] = [];
93
+
94
+ const membershipOrganizationId = (await Platform.getShared()).membershipOrganizationId;
95
+
96
+ if (STAMHOOFD.userMode === 'organization' && sellingOrganization.id === membershipOrganizationId) {
97
+ const currentPackages = await STPackageService.getActivePackages(organization.id);
98
+ const packageStatus = OrganizationPackagesStatus.create({
99
+ packages: currentPackages.map(p => STPackageStruct.create(p)),
100
+ });
101
+
102
+ packages.push(...checkout.purchases.getPackages(packageStatus));
103
+ }
104
+
105
+ // Create the real models for each package
106
+ // calculate the price for these packages and create a hidden balance item
107
+ for (const pack of packages) {
108
+ const model = new STPackage();
109
+ model.id = pack.id;
110
+ model.meta = pack.meta;
111
+ model.validUntil = pack.validUntil;
112
+ model.removeAt = pack.removeAt;
113
+
114
+ // Not yet valid / active (ignored until valid)
115
+ model.validAt = null;
116
+ model.organizationId = organization.id;
117
+
118
+ if (request.body.proForma) {
119
+ // Security addition
120
+ model.disableSave();
121
+ }
122
+
123
+ const balanceItem = await STPackageService.chargePackage(model, undefined, checkout.customer ?? undefined);
124
+ if (balanceItem) {
125
+ balanceItem.VATExcempt = PaymentService.getVATExcempt({
126
+ customer: checkout.customer,
127
+ sellingOrganization
128
+ });
129
+ balanceItems.set(balanceItem, balanceItem.priceWithVAT);
130
+ }
131
+
132
+ if (!request.body.proForma) {
133
+ await model.save();
134
+ await balanceItem?.save();
135
+ } else {
136
+ // Security addition
137
+ if (balanceItem && balanceItem.id) {
138
+ throw new Error('Unexpected balance item save')
139
+ }
140
+ balanceItem?.disableSave()
141
+ }
142
+
143
+ models.push(model);
144
+
145
+ if (model.meta.requiresMandate) {
146
+ if (checkout.proForma) {
147
+ if (!checkout.createMandate) {
148
+ checkout.createMandate = CreateMandateSettings.create({saveAsDefault: true})
149
+ }
150
+ } else {
151
+ // setting checkout.mandate is not enough - we also need to set it as default.
152
+
153
+ if (!checkout.createMandate) {
154
+ throw new SimpleError({
155
+ code: '',
156
+ message: $t('%1Rh')
157
+ })
158
+ }
159
+
160
+ if (!checkout.createMandate.saveAsDefault) {
161
+ throw new SimpleError({
162
+ code: '',
163
+ message: $t('%1Qp')
164
+ })
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Add pending items (balance items in request)
171
+ // Update balances before we start
172
+ await BalanceItemService.flushCaches(organization.id);
173
+
174
+ // Validate balance items (can only happen serverside)
175
+ const balanceItemIds = [...request.body.balances.keys()]
176
+ let balanceItemsModels: BalanceItem[] = [];
177
+ if (balanceItemIds.length > 0) {
178
+ balanceItemsModels = await BalanceItem.select().where('id', balanceItemIds).andWhere('organizationId', sellingOrganization.id).limit(balanceItemIds.length).fetch();
179
+
180
+ if (balanceItemsModels.length !== balanceItemIds.length) {
181
+ throw new SimpleError({
182
+ code: 'balance_changed',
183
+ message: $t(`%vg`),
184
+ });
185
+ }
186
+
187
+ for (const [id, amount] of request.body.balances) {
188
+ if (amount === 0) {
189
+ continue;
190
+ }
191
+
192
+ const item = balanceItemsModels.find(b => b.id === id);
193
+ if (!item) {
194
+ throw new SimpleError({
195
+ code: 'balance_changed',
196
+ message: $t(`%vg`),
197
+ });
198
+ }
199
+
200
+ if (item.priceOpen === 0) {
201
+ throw new SimpleError({
202
+ code: 'balance_changed',
203
+ message: $t(`%vg`),
204
+ });
205
+ }
206
+
207
+ if (item.priceOpen > 0 && amount < 0 || amount > item.priceOpen) {
208
+ throw new SimpleError({
209
+ code: 'balance_changed',
210
+ message: $t(`%vg`),
211
+ });
212
+ }
213
+
214
+ if (item.priceOpen < 0 && amount > 0 || amount < item.priceOpen) {
215
+ throw new SimpleError({
216
+ code: 'balance_changed',
217
+ message: $t(`%vg`),
218
+ });
219
+ }
220
+ balanceItems.set(item, amount)
221
+ }
222
+ }
223
+
224
+ // If still zero payment
225
+ const { price: totalPrice, roundingAmount } = PaymentService.calculateTotalPrice({
226
+ balanceItems,
227
+ organization: sellingOrganization
228
+ })
229
+ const minimumAmount = 2_00;
230
+
231
+ if (totalPrice < minimumAmount && checkout.createMandate && !checkout.mandate) {
232
+ const item = new BalanceItem();
233
+ item.type = BalanceItemType.AdministrationFee;
234
+ item.description = $t('%1Q4')
235
+ item.payingOrganizationId = organization.id;
236
+ item.organizationId = sellingOrganization.id;
237
+ item.VATPercentage = 21;
238
+ item.VATExcempt = PaymentService.getVATExcempt({
239
+ customer: checkout.customer,
240
+ sellingOrganization
241
+ });
242
+ item.VATIncluded = !item.VATExcempt; // Makes sure price with VAT always matches unitPrice
243
+ item.quantity = 1;
244
+ item.unitPrice = minimumAmount - (totalPrice - roundingAmount);
245
+ item.createdAt = new Date();
246
+ item.status = BalanceItemStatus.Hidden;
247
+
248
+ if (!request.body.proForma) {
249
+ await item.save();
250
+ } else {
251
+ item.disableSave()
252
+ }
253
+
254
+ balanceItems.set(item, item.priceWithVAT);
255
+ }
256
+
257
+ if (checkout.proForma) {
258
+ const result = await PaymentService.createProForma({
259
+ balanceItems,
260
+ checkout,
261
+ user,
262
+ organization: sellingOrganization,
263
+ payingOrganization: organization,
264
+ });
265
+
266
+ return new Response(CheckoutResponse.create({
267
+ payment: result.payment,
268
+ invoice: result.invoice
269
+ }));
270
+ }
271
+
272
+ const result = await PaymentService.createPayment({
273
+ balanceItems,
274
+ checkout,
275
+ user,
276
+ organization: sellingOrganization,
277
+ payingOrganization: organization,
278
+ serviceFeeType: 'system',
279
+ createMandate: checkout.createMandate,
280
+ useMandate: checkout.mandate,
281
+ paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
282
+ privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration
283
+ });
284
+
285
+ if (!result) {
286
+ // No payment needed
287
+ throw new SimpleError({
288
+ code: 'missing_data',
289
+ message: 'Checkout was empty',
290
+ human: $t('%1L2'),
291
+ });
292
+ }
293
+
294
+ for (const pack of models) {
295
+ await pack.save();
296
+ }
297
+
298
+ const { payment, paymentUrl, paymentQRCode } = result;
299
+
300
+ return new Response(CheckoutResponse.create({
301
+ payment: payment ? await AuthenticatedStructures.paymentGeneral(payment) : null,
302
+ paymentUrl,
303
+ paymentQRCode,
304
+ }));
305
+ }
306
+
307
+ setPayment
308
+ }
@@ -0,0 +1,94 @@
1
+ import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
+ import { PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { Organization } from '@stamhoofd/models';
7
+ import { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
8
+ import { Context } from '../../../../helpers/Context.js';
9
+ import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
10
+
11
+ type Params = { sellingOrganizationId: string };
12
+ type Query = undefined;
13
+ type Body = PatchableArrayAutoEncoder<PaymentMandate>;
14
+ type ResponseBody = PaymentMandate[]
15
+
16
+ export class DeleteOrganizationMandateEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ bodyDecoder = new PatchableArrayDecoder(
18
+ PaymentMandate as Decoder<PaymentMandate>,
19
+ PaymentMandate.patchType() as Decoder<AutoEncoderPatchType<PaymentMandate>>,
20
+ StringDecoder
21
+ ) as Decoder<PatchableArrayAutoEncoder<PaymentMandate>>;
22
+
23
+ protected doesMatch(request: Request): [true, Params] | [false] {
24
+ if (request.method !== 'PATCH') {
25
+ return [false];
26
+ }
27
+
28
+ const params = Endpoint.parseParameters(request.url, '/billing/@sellingOrganizationId/mandates', {sellingOrganizationId: String});
29
+
30
+ if (params) {
31
+ return [true, params as Params];
32
+ }
33
+ return [false];
34
+ }
35
+
36
+ async handle(request: DecodedRequest<Params, Query, Body>) {
37
+ const payingOrganization = await Context.setOrganizationScope();
38
+ const { user } = await Context.authenticate();
39
+
40
+ const id = request.params.sellingOrganizationId;
41
+ if (!id) {
42
+ throw new SimpleError({
43
+ code: 'unavailable',
44
+ message: 'This is temporarily unavailable',
45
+ human: $t('%1Rz')
46
+ })
47
+ }
48
+
49
+ const sellingOrganization = await Organization.getByID(id);
50
+ if (!sellingOrganization || !sellingOrganization.active) {
51
+ throw new SimpleError({
52
+ statusCode: 404,
53
+ code: 'not_found',
54
+ message: 'Selling organization not found',
55
+ human: $t('%1R5'),
56
+ field: 'sellingOrganization'
57
+ })
58
+ }
59
+
60
+ const mandates = await PaymentMandateService.getMandates({
61
+ sellingOrganization,
62
+ user,
63
+ payingOrganization
64
+ });
65
+
66
+ for (const patch of request.body.getPatches()) {
67
+ const mandate = mandates.find(m => m.id === patch.id);
68
+ if (!mandate) {
69
+ throw new SimpleError({
70
+ code: 'not_found',
71
+ message: 'This payment mandate is not found',
72
+ human: $t('%1R8')
73
+ })
74
+ }
75
+
76
+ if (patch.isDefault === true) {
77
+ await PaymentMandateService.setDefaultMandate({
78
+ mandateId: mandate.id,
79
+ sellingOrganization,
80
+ payingOrganizationId: payingOrganization,
81
+ payingUserId: null
82
+ })
83
+ }
84
+ }
85
+
86
+ const updatedMandates = await PaymentMandateService.getMandates({
87
+ sellingOrganization,
88
+ user,
89
+ payingOrganization
90
+ })
91
+
92
+ return new Response(PaymentMandateService.groupByMandate(updatedMandates).mandates);
93
+ }
94
+ }
@@ -11,6 +11,7 @@ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStruct
11
11
  import { Context } from '../../../../helpers/Context.js';
12
12
  import { invoiceFilterCompilers } from '../../../../sql-filters/invoices.js';
13
13
  import { invoiceSorters } from '../../../../sql-sorters/invoices.js';
14
+ import { InvoiceService } from '../../../../services/InvoiceService.js';
14
15
 
15
16
  type Params = Record<string, never>;
16
17
  type Query = LimitedFilteredRequest;
@@ -186,6 +187,12 @@ export class GetInvoicesEndpoint extends Endpoint<Params, Query, Body, ResponseB
186
187
  }
187
188
  }
188
189
 
190
+ for (const invoice of invoices) {
191
+ if (invoice.number && STAMHOOFD.environment === 'development') {
192
+ await InvoiceService.generatePdf(invoice)
193
+ }
194
+ }
195
+
189
196
  return new PaginatedResponse<InvoiceStruct[], LimitedFilteredRequest>({
190
197
  results: await AuthenticatedStructures.invoices(invoices),
191
198
  next,
@@ -6,6 +6,7 @@ import { CheckMollieResponse, PermissionLevel } from '@stamhoofd/structures';
6
6
 
7
7
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
8
8
  import { Context } from '../../../../helpers/Context.js';
9
+ import { MollieService } from '../../../../services/MollieService.js';
9
10
 
10
11
  type Params = Record<string, never>;
11
12
  type Body = undefined;
@@ -36,9 +37,9 @@ export class CheckMollieEndpoint extends Endpoint<Params, Query, Body, ResponseB
36
37
  throw Context.auth.error();
37
38
  }
38
39
 
39
- const mollie = await MollieToken.getTokenFor(organization.id);
40
+ const service = await MollieService.create({sellingOrganization: organization })
40
41
 
41
- if (!mollie) {
42
+ if (!service) {
42
43
  organization.privateMeta.mollieOnboarding = null;
43
44
  organization.privateMeta.mollieProfile = null;
44
45
  await organization.save();
@@ -52,9 +53,9 @@ export class CheckMollieEndpoint extends Endpoint<Params, Query, Body, ResponseB
52
53
  profiles: [],
53
54
  }));
54
55
  }
55
- const profiles = await mollie.getProfiles();
56
+ const profiles = await service.getProfiles();
56
57
 
57
- const status = await mollie.getOnboardingStatus();
58
+ const status = await service.getOnboardingStatus();
58
59
  organization.privateMeta.mollieOnboarding = status;
59
60
 
60
61
  // Check profile is still valid
@@ -9,6 +9,7 @@ import { PermissionLevel } from '@stamhoofd/structures';
9
9
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
10
10
  import { checkMollieSettlementsFor } from '../../../../helpers/CheckSettlements.js';
11
11
  import { Context } from '../../../../helpers/Context.js';
12
+ import { MollieService } from '../../../../services/MollieService.js';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
 
@@ -47,9 +48,13 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
47
48
  }
48
49
 
49
50
  const mollieToken = await MollieToken.create(organization, request.body.code);
51
+ const service = await MollieService.create({sellingOrganization: organization})
52
+ if (service) {
53
+ await service.setupOnboarding()
50
54
 
51
- // Check settlements after linking (shouldn't block)
52
- checkMollieSettlementsFor(mollieToken.accessToken, true).catch(console.error);
55
+ // Check settlements after linking (shouldn't block)
56
+ checkMollieSettlementsFor(mollieToken.accessToken, true).catch(console.error);
57
+ }
53
58
 
54
59
  return new Response(await AuthenticatedStructures.organization(organization));
55
60
  }
@@ -1,10 +1,10 @@
1
- import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder} from '@simonbackx/simple-encoding';
1
+ import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
2
  import { cloneObject, isPatchableArray, isPatchMap, ObjectData, PatchableArray, PatchMap, patchObject } from '@simonbackx/simple-encoding';
3
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
3
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
4
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
5
  import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
6
6
  import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
7
- import type { Company, PermissionsResourceType, ResourcePermissions} from '@stamhoofd/structures';
7
+ import type { Company, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
8
8
  import { BuckarooSettings, MemberResponsibility, OrganizationMetaData, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, UitpasClientCredentialsStatus } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
@@ -12,6 +12,7 @@ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStruct
12
12
  import { BuckarooHelper } from '../../../../helpers/BuckarooHelper.js';
13
13
  import { Context } from '../../../../helpers/Context.js';
14
14
  import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer.js';
15
+ import { RecordAnswerHelper } from '../../../../helpers/RecordAnswerHelper.js';
15
16
  import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater.js';
16
17
  import { TagHelper } from '../../../../helpers/TagHelper.js';
17
18
  import { ViesHelper } from '../../../../helpers/ViesHelper.js';
@@ -158,6 +159,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
158
159
  organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey;
159
160
  organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
160
161
  organization.privateMeta.balanceNotificationSettings = patchObject(organization.privateMeta.balanceNotificationSettings, request.body.privateMeta.balanceNotificationSettings);
162
+ organization.privateMeta.invoiceSettings = patchObject(organization.privateMeta.invoiceSettings, request.body.privateMeta.invoiceSettings);
161
163
  organization.privateMeta.recordAnswers = request.body.privateMeta.recordAnswers.applyTo(organization.privateMeta.recordAnswers);
162
164
 
163
165
  if (request.body.privateMeta.responsibilities || request.body.privateMeta.roles) {
@@ -251,16 +253,23 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
251
253
  }
252
254
  }
253
255
 
254
- // Allow admin patches (permissions only atm). No put atm
255
- if (request.body.admins) {
256
- throw new Error('Temporary removed to keep code cleaner. Please use different endpoints.');
257
- }
258
-
259
256
  if (request.body.meta) {
260
257
  if (request.body.meta.companies) {
261
258
  await this.validateCompanies(organization, request.body.meta.companies);
262
259
  shouldUpdateSetupSteps = true;
263
260
  }
261
+
262
+ if (request.body.meta.recordsConfiguration?.recordCategories) {
263
+ RecordAnswerHelper.throwIfPatchOrPutIsInvalid(organization.meta.recordsConfiguration.recordCategories, request.body.meta.recordsConfiguration.recordCategories);
264
+ }
265
+
266
+ if (request.body.meta.financialSupport) {
267
+ if (STAMHOOFD.userMode === 'platform') {
268
+ // can only set financial support settings on platform if userMode platform
269
+ request.body.meta.financialSupport = null;
270
+ }
271
+ }
272
+
264
273
  const savedPackages = organization.meta.packages;
265
274
  organization.meta.patchOrPut(request.body.meta);
266
275
  organization.meta.packages = savedPackages;
@@ -166,7 +166,7 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
166
166
  // Check total price
167
167
  const totalPrice = balanceItemPayments.reduce((total, item) => total + item.price, 0);
168
168
  payment.price = totalPrice;
169
- PaymentService.round(payment);
169
+ PaymentService.roundPayment(payment);
170
170
 
171
171
  switch (payment.type) {
172
172
  case PaymentType.Payment: {
@@ -301,8 +301,8 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
301
301
  payment.paidAt = patch.paidAt;
302
302
  }
303
303
 
304
- if (patch.customer) {
305
- // payment.customer = patchObject(payment.customer, patch.customer, { getDefaultValue: () => PaymentCustomer.create({}) });
304
+ if (patch.customer !== undefined) {
305
+ payment.customer = patchObject(payment.customer, patch.customer, { getDefaultValue: () => PaymentCustomer.create({}) });
306
306
  }
307
307
 
308
308
  const payingOrganizationId = patch.payingOrganizationId ?? patch.payingOrganization?.id ?? null;