@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 +17 -17
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +143 -0
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +189 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +26 -0
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +12 -5
- package/src/helpers/StripeInvoicer.ts +0 -4
- package/src/helpers/StripePayoutChecker.ts +0 -4
- package/src/helpers/ViesHelper.test.ts +145 -1
- package/src/helpers/ViesHelper.ts +39 -0
- package/src/seeds/1783520259-make-old-group-filters-readable.ts +454 -0
- package/src/services/InvoiceService.ts +21 -0
- package/src/services/InvoiceXMLService.test.ts +161 -0
- package/src/services/InvoiceXMLService.ts +11 -9
- package/src/services/PeppolDirectoryService.test.ts +139 -0
- package/src/services/PeppolDirectoryService.ts +111 -0
- package/src/services/uitpas/searchUitpasEvents.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.
|
|
63
|
-
"@stamhoofd/crons": "2.
|
|
64
|
-
"@stamhoofd/email": "2.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.
|
|
66
|
-
"@stamhoofd/logging": "2.
|
|
67
|
-
"@stamhoofd/models": "2.
|
|
68
|
-
"@stamhoofd/object-differ": "2.
|
|
69
|
-
"@stamhoofd/queues": "2.
|
|
70
|
-
"@stamhoofd/sql": "2.
|
|
71
|
-
"@stamhoofd/structures": "2.
|
|
72
|
-
"@stamhoofd/types": "2.
|
|
73
|
-
"@stamhoofd/utility": "2.
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
}
|
package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts
CHANGED
|
@@ -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 =
|
|
19
|
-
type Body =
|
|
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
|
-
|
|
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.
|
|
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
|
}
|