@stamhoofd/backend 2.132.0 → 2.133.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.132.0",
3
+ "version": "2.133.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -57,20 +57,20 @@
57
57
  "@simonbackx/simple-endpoints": "1.21.1",
58
58
  "@simonbackx/simple-errors": "1.5.0",
59
59
  "@simonbackx/simple-logging": "1.0.1",
60
- "@stamhoofd/backend-env": "2.132.0",
61
- "@stamhoofd/backend-i18n": "2.132.0",
62
- "@stamhoofd/backend-middleware": "2.132.0",
63
- "@stamhoofd/crons": "2.132.0",
64
- "@stamhoofd/email": "2.132.0",
65
- "@stamhoofd/excel-writer": "2.132.0",
66
- "@stamhoofd/logging": "2.132.0",
67
- "@stamhoofd/models": "2.132.0",
68
- "@stamhoofd/object-differ": "2.132.0",
69
- "@stamhoofd/queues": "2.132.0",
70
- "@stamhoofd/sql": "2.132.0",
71
- "@stamhoofd/structures": "2.132.0",
72
- "@stamhoofd/types": "2.132.0",
73
- "@stamhoofd/utility": "2.132.0",
60
+ "@stamhoofd/backend-env": "2.133.0",
61
+ "@stamhoofd/backend-i18n": "2.133.0",
62
+ "@stamhoofd/backend-middleware": "2.133.0",
63
+ "@stamhoofd/crons": "2.133.0",
64
+ "@stamhoofd/email": "2.133.0",
65
+ "@stamhoofd/excel-writer": "2.133.0",
66
+ "@stamhoofd/logging": "2.133.0",
67
+ "@stamhoofd/models": "2.133.0",
68
+ "@stamhoofd/object-differ": "2.133.0",
69
+ "@stamhoofd/queues": "2.133.0",
70
+ "@stamhoofd/sql": "2.133.0",
71
+ "@stamhoofd/structures": "2.133.0",
72
+ "@stamhoofd/types": "2.133.0",
73
+ "@stamhoofd/utility": "2.133.0",
74
74
  "archiver": "7.0.1",
75
75
  "axios": "1.16.0",
76
76
  "base-x": "3.0.11",
@@ -91,7 +91,7 @@
91
91
  "stripe": "16.12.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@stamhoofd/test-utils": "2.132.0",
94
+ "@stamhoofd/test-utils": "2.133.0",
95
95
  "@types/cookie": "0.6.0",
96
96
  "@types/luxon": "3.7.1",
97
97
  "@types/mailparser": "3.4.6",
@@ -107,5 +107,5 @@
107
107
  "publishConfig": {
108
108
  "access": "public"
109
109
  },
110
- "gitHead": "f7143688bb7be0cd9f73c209f4b55c9bacdd59b8"
110
+ "gitHead": "65d5f0a31f87b8e3b7a23349f08cf7943ef15bda"
111
111
  }
@@ -58,7 +58,7 @@ export class ChargeOrganizationsEndpoint extends Endpoint<Params, Query, Body, R
58
58
  filter: body.filter,
59
59
  limit: 100,
60
60
  }), {
61
- fetch: GetOrganizationsEndpoint.buildData,
61
+ fetch: request => GetOrganizationsEndpoint.buildData(request),
62
62
  });
63
63
 
64
64
  for await (const data of dataGenerator) {
@@ -133,7 +133,7 @@ export class GetGroupsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
133
133
  }
134
134
  }
135
135
 
136
- const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
136
+ const maxLimit = Context.optionalAuth?.hasSomePlatformAccess() ? 1000 : 100;
137
137
 
138
138
  if (request.query.limit > maxLimit) {
139
139
  throw new SimpleError({
@@ -0,0 +1,143 @@
1
+ import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
+ import { PatchableArray } from '@simonbackx/simple-encoding';
3
+ import { Request } from '@simonbackx/simple-endpoints';
4
+ import type { Organization, User } from '@stamhoofd/models';
5
+ import { BalanceItem, BalanceItemFactory, Invoice, InvoicedBalanceItem, OrganizationFactory, Payment, Token, UserFactory } from '@stamhoofd/models';
6
+ import type { InvoiceStruct } from '@stamhoofd/structures';
7
+ import { PaymentMethod, PaymentStatus, PermissionLevel, Permissions } from '@stamhoofd/structures';
8
+ import { STExpect } from '@stamhoofd/test-utils';
9
+ import { testServer } from '../../../../../tests/helpers/TestServer.js';
10
+ import { PatchInvoicesEndpoint } from './PatchInvoicesEndpoint.js';
11
+
12
+ describe('Endpoint.PatchInvoicesEndpoint', () => {
13
+ const endpoint = new PatchInvoicesEndpoint();
14
+
15
+ const createBalanceItem = async ({ organization, unitPrice = 10_00 }: { organization: Organization; unitPrice?: number }) => {
16
+ return await new BalanceItemFactory({
17
+ organizationId: organization.id,
18
+ amount: 1,
19
+ unitPrice,
20
+ }).create();
21
+ };
22
+
23
+ const createInvoice = async ({ organization, balanceItemIds, number = '1' }: { organization: Organization; balanceItemIds: string[]; number?: string | null }) => {
24
+ const invoice = new Invoice();
25
+ invoice.organizationId = organization.id;
26
+ invoice.number = number;
27
+ invoice.invoicedAt = number ? new Date() : null;
28
+ await invoice.save();
29
+
30
+ for (const balanceItemId of balanceItemIds) {
31
+ const item = new InvoicedBalanceItem();
32
+ item.organizationId = organization.id;
33
+ item.invoiceId = invoice.id;
34
+ item.balanceItemId = balanceItemId;
35
+ item.name = 'Test item';
36
+ item.unitPrice = 10_00;
37
+ item.balanceInvoicedAmount = 10_00;
38
+ await item.save();
39
+ }
40
+
41
+ // Make sure the invoiced cache of the balance items is up to date, like it would be for a real invoice.
42
+ await BalanceItem.updateInvoiced(balanceItemIds);
43
+
44
+ return invoice;
45
+ };
46
+
47
+ const createPayment = async ({ organization, invoice }: { organization: Organization; invoice: Invoice }) => {
48
+ const payment = new Payment();
49
+ payment.organizationId = organization.id;
50
+ payment.method = PaymentMethod.PointOfSale;
51
+ payment.status = PaymentStatus.Succeeded;
52
+ payment.price = 10_00;
53
+ payment.invoiceId = invoice.id;
54
+ await payment.save();
55
+ return payment;
56
+ };
57
+
58
+ const patchInvoices = async ({ body, organization, user }: { body: PatchableArrayAutoEncoder<InvoiceStruct>; organization: Organization; user: User }) => {
59
+ const token = await Token.createToken(user);
60
+ const request = Request.buildJson('PATCH', '/invoices', organization.getApiHost(), body);
61
+ request.headers.authorization = 'Bearer ' + token.accessToken;
62
+ return await testServer.test<InvoiceStruct[]>(endpoint, request);
63
+ };
64
+
65
+ describe('Deleting invoices', () => {
66
+ test('deletes the invoice, its invoiced balance items and unlinks the payments', async () => {
67
+ const organization = await new OrganizationFactory({}).create();
68
+ const user = await new UserFactory({
69
+ organization,
70
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
71
+ }).create();
72
+
73
+ const balanceItem = await createBalanceItem({ organization });
74
+ const invoice = await createInvoice({ organization, balanceItemIds: [balanceItem.id] });
75
+ const payment = await createPayment({ organization, invoice });
76
+
77
+ // Sanity check: the balance item is marked as invoiced
78
+ const before = await BalanceItem.getByID(balanceItem.id);
79
+ expect(before!.priceInvoiced).toBe(10_00);
80
+
81
+ const body = new PatchableArray() as PatchableArrayAutoEncoder<InvoiceStruct>;
82
+ body.addDelete(invoice.id);
83
+
84
+ const response = await patchInvoices({ body, organization, user });
85
+ expect(response.status).toBe(200);
86
+
87
+ // Invoice is gone
88
+ expect(await Invoice.getByID(invoice.id)).toBeUndefined();
89
+
90
+ // Invoiced balance items are cascade deleted
91
+ const remainingItems = await InvoicedBalanceItem.select().where('invoiceId', invoice.id).fetch();
92
+ expect(remainingItems).toHaveLength(0);
93
+
94
+ // Payment is kept but unlinked
95
+ const reloadedPayment = await Payment.getByID(payment.id);
96
+ expect(reloadedPayment).toBeDefined();
97
+ expect(reloadedPayment!.invoiceId).toBeNull();
98
+
99
+ // Invoiced cache of the balance item is recalculated
100
+ const after = await BalanceItem.getByID(balanceItem.id);
101
+ expect(after!.priceInvoiced).toBe(0);
102
+ });
103
+
104
+ test('user without full access cannot delete invoices', async () => {
105
+ const organization = await new OrganizationFactory({}).create();
106
+ const user = await new UserFactory({
107
+ organization,
108
+ permissions: Permissions.create({ level: PermissionLevel.Read }),
109
+ }).create();
110
+
111
+ const balanceItem = await createBalanceItem({ organization });
112
+ const invoice = await createInvoice({ organization, balanceItemIds: [balanceItem.id] });
113
+
114
+ const body = new PatchableArray() as PatchableArrayAutoEncoder<InvoiceStruct>;
115
+ body.addDelete(invoice.id);
116
+
117
+ await expect(patchInvoices({ body, organization, user })).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
118
+
119
+ // Invoice is untouched
120
+ expect(await Invoice.getByID(invoice.id)).toBeDefined();
121
+ });
122
+
123
+ test('cannot delete an invoice of another organization', async () => {
124
+ const organization = await new OrganizationFactory({}).create();
125
+ const otherOrganization = await new OrganizationFactory({}).create();
126
+ const user = await new UserFactory({
127
+ organization,
128
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
129
+ }).create();
130
+
131
+ const balanceItem = await createBalanceItem({ organization: otherOrganization });
132
+ const invoice = await createInvoice({ organization: otherOrganization, balanceItemIds: [balanceItem.id] });
133
+
134
+ const body = new PatchableArray() as PatchableArrayAutoEncoder<InvoiceStruct>;
135
+ body.addDelete(invoice.id);
136
+
137
+ await expect(patchInvoices({ body, organization, user })).rejects.toThrow(STExpect.errorWithCode('not_found'));
138
+
139
+ // Invoice is untouched
140
+ expect(await Invoice.getByID(invoice.id)).toBeDefined();
141
+ });
142
+ });
143
+ });
@@ -6,7 +6,7 @@ import { Invoice as InvoiceStruct } from '@stamhoofd/structures';
6
6
 
7
7
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
8
8
  import { Context } from '../../../../helpers/Context.js';
9
- import type { Invoice } from '@stamhoofd/models';
9
+ import { Invoice } from '@stamhoofd/models';
10
10
  import { SimpleError } from '@simonbackx/simple-errors';
11
11
  import { ViesHelper } from '../../../../helpers/ViesHelper.js';
12
12
  import { InvoiceService } from '../../../../services/InvoiceService.js';
@@ -57,6 +57,15 @@ export class PatchInvoicesEndpoint extends Endpoint<Params, Query, Body, Respons
57
57
  invoices.push(model);
58
58
  }
59
59
 
60
+ for (const id of request.body.getDeletes()) {
61
+ const model = await Invoice.getByID(id);
62
+ if (!model || model.organizationId !== organization.id) {
63
+ throw Context.auth.notFoundOrNoAccess($t('%ZcE'));
64
+ }
65
+
66
+ await InvoiceService.delete(model);
67
+ }
68
+
60
69
  return new Response(
61
70
  await AuthenticatedStructures.invoices(invoices, true),
62
71
  );
@@ -1,11 +1,13 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import type { Organization, User, Webshop } from '@stamhoofd/models';
3
3
  import { GroupFactory, OrganizationFactory, OrganizationRegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory, WebshopFactory } from '@stamhoofd/models';
4
- import { AccessRight, MemberResponsibility, OrganizationPrivateMetaData, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
4
+ import { AccessRight, Address, Company, MemberResponsibility, OrganizationMetaData, OrganizationPrivateMetaData, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PeppolEndointId, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
5
5
 
6
6
  import type { AutoEncoderPatchType, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
7
7
  import { PatchableArray, PatchMap } from '@simonbackx/simple-encoding';
8
8
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
9
+ import { Country } from '@stamhoofd/types/Country';
10
+ import nock from 'nock';
9
11
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
10
12
  import { PatchOrganizationEndpoint } from './PatchOrganizationEndpoint.js';
11
13
 
@@ -1282,6 +1284,192 @@ describe('Endpoint.PatchOrganization', () => {
1282
1284
  });
1283
1285
  });
1284
1286
 
1287
+ describe('custom PEPPOL endpoint id on companies', () => {
1288
+ const belgianAddress = () => Address.create({
1289
+ street: 'Demostraat',
1290
+ number: '12',
1291
+ city: 'Gent',
1292
+ postalCode: '9000',
1293
+ country: Country.Belgium,
1294
+ });
1295
+
1296
+ function mockDirectoryFound(schemeID: string, id: string, entityName = 'Directory Name') {
1297
+ return nock('https://directory.peppol.eu')
1298
+ .get('/search/1.0/json')
1299
+ .query({ participant: `iso6523-actorid-upis::${schemeID}:${id}` })
1300
+ .reply(200, {
1301
+ 'total-result-count': 1,
1302
+ matches: [{
1303
+ participantID: { scheme: 'iso6523-actorid-upis', value: `${schemeID}:${id}` },
1304
+ entities: [{ name: [{ name: entityName }] }],
1305
+ }],
1306
+ } as nock.Body);
1307
+ }
1308
+
1309
+ afterEach(() => {
1310
+ nock.cleanAll();
1311
+ });
1312
+
1313
+ const companiesPut = (company: Company): PatchableArrayAutoEncoder<Company> => {
1314
+ const arr: PatchableArrayAutoEncoder<Company> = new PatchableArray();
1315
+ arr.addPut(company);
1316
+ return arr;
1317
+ };
1318
+
1319
+ const companiesPatch = (patch: AutoEncoderPatchType<Company>): PatchableArrayAutoEncoder<Company> => {
1320
+ const arr: PatchableArrayAutoEncoder<Company> = new PatchableArray();
1321
+ arr.addPatch(patch);
1322
+ return arr;
1323
+ };
1324
+
1325
+ test('a normal organization admin cannot add a company with a custom PEPPOL endpoint id', async () => {
1326
+ const scope = mockDirectoryFound('0208', '0123456789');
1327
+ const organization = await new OrganizationFactory({}).create();
1328
+ const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1329
+ const token = await Token.createToken(user);
1330
+
1331
+ const patch = OrganizationStruct.patch({
1332
+ id: organization.id,
1333
+ meta: OrganizationMetaData.patch({
1334
+ companies: companiesPut(Company.create({
1335
+ name: 'Demo Company',
1336
+ address: belgianAddress(),
1337
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0208', id: '0123456789' }),
1338
+ })),
1339
+ }),
1340
+ });
1341
+
1342
+ await expect(patchOrganization({ patch, organization, token })).rejects.toThrow(
1343
+ STExpect.simpleError({ code: 'permission_denied', field: 'customPeppolEndpointId' }),
1344
+ );
1345
+
1346
+ // The directory must not have been contacted: the permission check happens first.
1347
+ expect(scope.isDone()).toBe(false);
1348
+ });
1349
+
1350
+ test('a normal organization admin cannot patch a custom PEPPOL endpoint id onto an existing company', async () => {
1351
+ const organization = await new OrganizationFactory({}).create();
1352
+ const company = Company.create({ name: 'Demo Company', address: belgianAddress() });
1353
+ organization.meta.companies = [company];
1354
+ await organization.save();
1355
+
1356
+ const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1357
+ const token = await Token.createToken(user);
1358
+
1359
+ const patch = OrganizationStruct.patch({
1360
+ id: organization.id,
1361
+ meta: OrganizationMetaData.patch({
1362
+ companies: companiesPatch(Company.patch({
1363
+ id: company.id,
1364
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0208', id: '0123456789' }),
1365
+ })),
1366
+ }),
1367
+ });
1368
+
1369
+ await expect(patchOrganization({ patch, organization, token })).rejects.toThrow(
1370
+ STExpect.simpleError({ code: 'permission_denied', field: 'customPeppolEndpointId' }),
1371
+ );
1372
+ });
1373
+
1374
+ test('a platform admin can set a custom PEPPOL endpoint id that is validated against the directory', async () => {
1375
+ const scope = mockDirectoryFound('0088', '5412345000013');
1376
+ const organization = await new OrganizationFactory({}).create();
1377
+ const user = await new UserFactory({ globalPermissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1378
+ const token = await Token.createToken(user);
1379
+
1380
+ const patch = OrganizationStruct.patch({
1381
+ id: organization.id,
1382
+ meta: OrganizationMetaData.patch({
1383
+ companies: companiesPut(Company.create({
1384
+ name: 'Demo Company',
1385
+ address: belgianAddress(),
1386
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0088', id: '5412345000013' }),
1387
+ })),
1388
+ }),
1389
+ });
1390
+
1391
+ const response = await patchOrganization({ patch, organization, token });
1392
+
1393
+ expect(scope.isDone()).toBe(true);
1394
+ expect(response.body.meta.companies).toHaveLength(1);
1395
+ expect(response.body.meta.companies[0].customPeppolEndpointId).toMatchObject({
1396
+ schemeID: '0088',
1397
+ id: '5412345000013',
1398
+ // The registered entity name from the directory is stored.
1399
+ entityName: 'Directory Name',
1400
+ });
1401
+ });
1402
+
1403
+ test('a client-supplied entityName is ignored and overwritten by the directory value', async () => {
1404
+ mockDirectoryFound('0088', '5412345000013', 'Directory Name');
1405
+ const organization = await new OrganizationFactory({}).create();
1406
+ const user = await new UserFactory({ globalPermissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1407
+ const token = await Token.createToken(user);
1408
+
1409
+ const patch = OrganizationStruct.patch({
1410
+ id: organization.id,
1411
+ meta: OrganizationMetaData.patch({
1412
+ companies: companiesPut(Company.create({
1413
+ name: 'Demo Company',
1414
+ address: belgianAddress(),
1415
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0088', id: '5412345000013', entityName: 'Spoofed name' }),
1416
+ })),
1417
+ }),
1418
+ });
1419
+
1420
+ const response = await patchOrganization({ patch, organization, token });
1421
+
1422
+ expect(response.body.meta.companies[0].customPeppolEndpointId?.entityName).toBe('Directory Name');
1423
+ });
1424
+
1425
+ test('a KBO custom PEPPOL endpoint id that does not match the company number is rejected', async () => {
1426
+ const organization = await new OrganizationFactory({}).create();
1427
+ const user = await new UserFactory({ globalPermissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1428
+ const token = await Token.createToken(user);
1429
+
1430
+ const patch = OrganizationStruct.patch({
1431
+ id: organization.id,
1432
+ meta: OrganizationMetaData.patch({
1433
+ companies: companiesPut(Company.create({
1434
+ name: 'Demo Company',
1435
+ address: belgianAddress(),
1436
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0208', id: '9999999999' }),
1437
+ })),
1438
+ }),
1439
+ });
1440
+
1441
+ await expect(patchOrganization({ patch, organization, token })).rejects.toThrow(
1442
+ STExpect.simpleError({ code: 'invalid_field', field: 'customPeppolEndpointId' }),
1443
+ );
1444
+ });
1445
+
1446
+ test('a platform admin gets an error when the custom PEPPOL endpoint id is unknown to the directory', async () => {
1447
+ nock('https://directory.peppol.eu')
1448
+ .get('/search/1.0/json')
1449
+ .query({ participant: 'iso6523-actorid-upis::0088:5412345000013' })
1450
+ .reply(200, { 'total-result-count': 0, matches: [] } as nock.Body);
1451
+
1452
+ const organization = await new OrganizationFactory({}).create();
1453
+ const user = await new UserFactory({ globalPermissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
1454
+ const token = await Token.createToken(user);
1455
+
1456
+ const patch = OrganizationStruct.patch({
1457
+ id: organization.id,
1458
+ meta: OrganizationMetaData.patch({
1459
+ companies: companiesPut(Company.create({
1460
+ name: 'Demo Company',
1461
+ address: belgianAddress(),
1462
+ customPeppolEndpointId: PeppolEndointId.create({ schemeID: '0088', id: '5412345000013' }),
1463
+ })),
1464
+ }),
1465
+ });
1466
+
1467
+ await expect(patchOrganization({ patch, organization, token })).rejects.toThrow(
1468
+ STExpect.simpleError({ code: 'invalid_field', field: 'customPeppolEndpointId' }),
1469
+ );
1470
+ });
1471
+ });
1472
+
1285
1473
  describe('userMode organization', () => {
1286
1474
  beforeEach(async () => {
1287
1475
  TestUtils.setEnvironment('userMode', 'organization');
@@ -660,6 +660,20 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
660
660
  return new Response(struct);
661
661
  }
662
662
 
663
+ /**
664
+ * A custom PEPPOL endpoint id may only be set by full platform admins for now.
665
+ */
666
+ private requirePeppolPermission() {
667
+ if (!Context.auth.hasPlatformFullAccess()) {
668
+ throw new SimpleError({
669
+ code: 'permission_denied',
670
+ message: 'Only platform admins can set a custom PEPPOL endpoint id',
671
+ human: $t('%Zcg'),
672
+ field: 'customPeppolEndpointId',
673
+ });
674
+ }
675
+ }
676
+
663
677
  async validateCompanies(organization: Organization, companies: PatchableArrayAutoEncoder<Company> | Company[]) {
664
678
  if (isPatchableArray(companies)) {
665
679
  for (const patch of companies.getPatches()) {
@@ -674,6 +688,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
674
688
  });
675
689
  }
676
690
 
691
+ if (patch.customPeppolEndpointId !== undefined) {
692
+ this.requirePeppolPermission();
693
+ }
694
+
677
695
  // Changed VAT number
678
696
  const prepatched = original.patch(patch);
679
697
  await ViesHelper.checkCompany(prepatched, patch);
@@ -692,6 +710,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
692
710
  });
693
711
  }
694
712
 
713
+ if (put.customPeppolEndpointId) {
714
+ this.requirePeppolPermission();
715
+ }
716
+
695
717
  await ViesHelper.checkCompany(put, put);
696
718
  }
697
719
  } else {
@@ -705,6 +727,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
705
727
  }
706
728
 
707
729
  for (const company of companies) {
730
+ if (company.customPeppolEndpointId) {
731
+ this.requirePeppolPermission();
732
+ }
733
+
708
734
  await ViesHelper.checkCompany(company, company);
709
735
  }
710
736
  }
@@ -6,7 +6,7 @@ import { CountFilteredRequest, CountResponse, PaymentCustomer, PaymentMethod, Pe
6
6
  import { Context } from '../../../../helpers/Context.js';
7
7
  import { GetReceivableBalancesEndpoint } from './GetReceivableBalancesEndpoint.js';
8
8
  import type { BalanceItem } from '@stamhoofd/models';
9
- import { Organization, User } from '@stamhoofd/models';
9
+ import { Organization, Platform, User } from '@stamhoofd/models';
10
10
  import { CachedBalance } from '@stamhoofd/models';
11
11
  import { PaymentService } from '../../../../services/PaymentService.js';
12
12
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -15,12 +15,12 @@ import { BalanceItemService } from '../../../../services/BalanceItemService.js';
15
15
  import { QueueHandler } from '@stamhoofd/queues';
16
16
 
17
17
  type Params = Record<string, never>;
18
- type Query = CountFilteredRequest;
19
- type Body = undefined;
18
+ type Query = undefined;
19
+ type Body = CountFilteredRequest;
20
20
  type ResponseBody = undefined;
21
21
 
22
22
  export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
- queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
23
+ bodyDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
24
24
 
25
25
  protected doesMatch(request: Request): [true, Params] | [false] {
26
26
  if (request.method !== 'POST') {
@@ -38,6 +38,7 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
38
38
  async handle(request: DecodedRequest<Params, Query, Body>) {
39
39
  const sellingOrganization = await Context.setOrganizationScope();
40
40
  const { user } = await Context.authenticate();
41
+ const platform = await Platform.getShared();
41
42
 
42
43
  if (!await Context.auth.canManageFinances(sellingOrganization.id)) {
43
44
  throw Context.auth.error();
@@ -53,7 +54,7 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
53
54
  }
54
55
 
55
56
  await QueueHandler.schedule(queueId, async () => {
56
- const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
57
+ const query = await GetReceivableBalancesEndpoint.buildQuery(request.body);
57
58
 
58
59
  for await (const cachedBalance of query.all()) {
59
60
  if (cachedBalance.organizationId !== sellingOrganization.id) {
@@ -80,6 +81,12 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
80
81
  continue;
81
82
  }
82
83
 
84
+ if (total <= 4_00_00 && sellingOrganization.id === platform.membershipOrganizationId && STAMHOOFD.userMode === 'organization') {
85
+ // Skip too small payments for Stamhoofd
86
+ console.error('Skipped charging too small payment for', cachedBalance.objectId, cachedBalance.objectType);
87
+ continue;
88
+ }
89
+
83
90
  let payingOrganization: Organization | null = null;
84
91
  let customerUser: User | null = null;
85
92
  if (cachedBalance.objectType === ReceivableBalanceType.organization) {
@@ -57,10 +57,6 @@ export class ApplicationFeeDetails {
57
57
  this.combine(ApplicationFeeDetails.fromStripe(fee));
58
58
  }
59
59
 
60
- remove(fee: Stripe.BalanceTransaction) {
61
- this.combine(ApplicationFeeDetails.fromStripe(fee));
62
- }
63
-
64
60
  combine(other: ApplicationFeeDetails) {
65
61
  this.serviceFee += other.serviceFee;
66
62
  this.transferFee += other.transferFee;
@@ -69,13 +69,9 @@ export class StripePayoutChecker {
69
69
  payout: payout.id,
70
70
  // Via the Application Fee object, we can get the original payment metadata
71
71
  expand: ['data.source', 'data.source.application_fee', 'data.source.application_fee.originating_transaction'],
72
- // TODO: ALSO DO CARDS! (type: 'charge')
73
- // type: 'payment'
74
72
  };
75
73
 
76
74
  for await (const balanceItem of this.stripe.balanceTransactions.list(params)) {
77
- // TODO
78
-
79
75
  if (balanceItem.type === 'charge' || balanceItem.type === 'payment') {
80
76
  await this.handleBalanceItem(payout, balanceItem);
81
77
  }