@stamhoofd/backend 2.110.0 → 2.111.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.110.0",
3
+ "version": "2.111.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -48,17 +48,17 @@
48
48
  "@bwip-js/node": "^4.5.1",
49
49
  "@mollie/api-client": "4.3.3",
50
50
  "@simonbackx/simple-database": "1.34.0",
51
- "@simonbackx/simple-encoding": "2.22.0",
51
+ "@simonbackx/simple-encoding": "2.23.1",
52
52
  "@simonbackx/simple-endpoints": "1.20.1",
53
53
  "@simonbackx/simple-logging": "^1.0.1",
54
- "@stamhoofd/backend-i18n": "2.110.0",
55
- "@stamhoofd/backend-middleware": "2.110.0",
56
- "@stamhoofd/email": "2.110.0",
57
- "@stamhoofd/models": "2.110.0",
58
- "@stamhoofd/queues": "2.110.0",
59
- "@stamhoofd/sql": "2.110.0",
60
- "@stamhoofd/structures": "2.110.0",
61
- "@stamhoofd/utility": "2.110.0",
54
+ "@stamhoofd/backend-i18n": "2.111.0",
55
+ "@stamhoofd/backend-middleware": "2.111.0",
56
+ "@stamhoofd/email": "2.111.0",
57
+ "@stamhoofd/models": "2.111.0",
58
+ "@stamhoofd/queues": "2.111.0",
59
+ "@stamhoofd/sql": "2.111.0",
60
+ "@stamhoofd/structures": "2.111.0",
61
+ "@stamhoofd/utility": "2.111.0",
62
62
  "archiver": "^7.0.1",
63
63
  "axios": "^1.13.2",
64
64
  "cookie": "^0.7.0",
@@ -76,5 +76,5 @@
76
76
  "publishConfig": {
77
77
  "access": "public"
78
78
  },
79
- "gitHead": "b43604081c29f7d909dac1b47a2d1633b66670e5"
79
+ "gitHead": "21344c902f356539e4034b499b94f54adbe10e50"
80
80
  }
@@ -0,0 +1,56 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { OrganizationFactory, UserFactory } from '@stamhoofd/models';
3
+
4
+ import { NewUser, PermissionRole, Permissions, UserPermissions } from '@stamhoofd/structures';
5
+ import { testServer } from '../../../tests/helpers/TestServer.js';
6
+ import { initAdmin } from '../../../tests/init/initAdmin.js';
7
+ import { PatchUserEndpoint } from './PatchUserEndpoint.js';
8
+
9
+ describe('Endpoint.PatchUser', () => {
10
+ // Test endpoint
11
+ const endpoint = new PatchUserEndpoint();
12
+
13
+ test('[Regression] Sending a patch for organization permissions that does not exist', async () => {
14
+ // Case: User A does not have permissions for organization A.
15
+ // You send a patch to change User A's permissions for organization A
16
+ // In the past, this caused data corruption because the way simple-encoding was implemented
17
+
18
+ const organization = await new OrganizationFactory({}).create();
19
+ const user = await new UserFactory({ organization }).create();
20
+
21
+ const { adminToken } = await initAdmin({ organization });
22
+
23
+ // Try to request members for this group
24
+ const userPermissions = UserPermissions.patch({});
25
+ const permissionsPatch = Permissions.patch({});
26
+ permissionsPatch.roles.addPut(PermissionRole.create({
27
+ id: 'test',
28
+ name: 'Test role',
29
+ }));
30
+ userPermissions.organizationPermissions.set(organization.id, permissionsPatch);
31
+
32
+ const request = Request.patch({
33
+ path: `/user/${user.id}`,
34
+ host: organization.getApiHost(),
35
+ headers: {
36
+ authorization: 'Bearer ' + adminToken.accessToken,
37
+ },
38
+ body: NewUser.patch({
39
+ id: user.id,
40
+ permissions: userPermissions,
41
+ }),
42
+ });
43
+
44
+ const response = await testServer.test(endpoint, request);
45
+ expect(response.status).toBe(200);
46
+
47
+ // This threw in the past when something was wrong
48
+ await user.refresh();
49
+
50
+ expect(user.permissions?.organizationPermissions.size).toEqual(1);
51
+
52
+ expect(user.permissions?.organizationPermissions.get(organization.id)).toBeDefined();
53
+ expect(user.permissions?.organizationPermissions.get(organization.id)?.roles.length).toEqual(1);
54
+ expect(user.permissions?.organizationPermissions.get(organization.id)?.roles[0].id).toEqual('test');
55
+ });
56
+ });
@@ -53,15 +53,7 @@ export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body
53
53
  template.year = put.year;
54
54
  template.organizationId = organization.id;
55
55
 
56
- if (await this.doesYearAlreadyHaveFiscalDocument(template)) {
57
- throw new SimpleError({
58
- code: 'double_fiscal_document',
59
- field: 'year',
60
- message: 'This year already has a fiscal document',
61
- human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
62
-
63
- });
64
- }
56
+ await this.throwErrorIfAddMoreThanOneFiscalDocumentInYear(template);
65
57
 
66
58
  await template.save();
67
59
 
@@ -113,13 +105,8 @@ export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body
113
105
  template.year = patch.year;
114
106
  }
115
107
 
116
- if (shouldCheckIfAlreadyHasFiscalDocument && await this.doesYearAlreadyHaveFiscalDocument(template)) {
117
- throw new SimpleError({
118
- code: 'double_fiscal_document',
119
- field: 'year',
120
- message: 'This year already has a fiscal document',
121
- human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
122
- });
108
+ if (shouldCheckIfAlreadyHasFiscalDocument) {
109
+ await this.throwErrorIfAddMoreThanOneFiscalDocumentInYear(template);
123
110
  }
124
111
 
125
112
  await template.save();
@@ -149,7 +136,11 @@ export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body
149
136
  );
150
137
  }
151
138
 
152
- private async doesYearAlreadyHaveFiscalDocument(template: DocumentTemplate) {
139
+ private async throwErrorIfAddMoreThanOneFiscalDocumentInYear(template: DocumentTemplate): Promise<void> {
140
+ if (template.privateSettings.templateDefinition.type !== 'fiscal') {
141
+ return;
142
+ }
143
+
153
144
  let query = SQL.select().from(SQL.table(DocumentTemplate.table))
154
145
  .where(SQL.column('organizationId'), template.organizationId)
155
146
  .where(SQL.column('year'), template.year)
@@ -162,6 +153,13 @@ export class PatchDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body
162
153
 
163
154
  const result = await query.limit(1).count();
164
155
 
165
- return result > 0;
156
+ if (result > 0) {
157
+ throw new SimpleError({
158
+ code: 'double_fiscal_document',
159
+ field: 'year',
160
+ message: 'This year already has a fiscal document',
161
+ human: $t('475f5f96-86bf-4124-a005-9904aaf72b37'),
162
+ });
163
+ }
166
164
  }
167
165
  }
@@ -5,14 +5,14 @@ import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform
5
5
  import { BuckarooSettings, Company, MemberResponsibility, OrganizationMetaData, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, PermissionsResourceType, ResourcePermissions, UitpasClientCredentialsStatus } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
- import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
- import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
- import { Context } from '../../../../helpers/Context';
11
- import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer';
12
- import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
13
- import { TagHelper } from '../../../../helpers/TagHelper';
14
- import { ViesHelper } from '../../../../helpers/ViesHelper';
15
- import { UitpasService } from '../../../../services/uitpas/UitpasService';
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
9
+ import { BuckarooHelper } from '../../../../helpers/BuckarooHelper.js';
10
+ import { Context } from '../../../../helpers/Context.js';
11
+ import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer.js';
12
+ import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater.js';
13
+ import { TagHelper } from '../../../../helpers/TagHelper.js';
14
+ import { ViesHelper } from '../../../../helpers/ViesHelper.js';
15
+ import { UitpasService } from '../../../../services/uitpas/UitpasService.js';
16
16
 
17
17
  type Params = Record<string, never>;
18
18
  type Query = undefined;
@@ -0,0 +1,114 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { BalanceItem, BalanceItemPayment, Invoice, Organization, Payment } from '@stamhoofd/models';
3
+ import { InvoicedBalanceItem as InvoicedBalanceItemStruct, Invoice as InvoiceStruct } from '@stamhoofd/structures';
4
+ import { Formatter } from '@stamhoofd/utility';
5
+
6
+ export class InvoiceService {
7
+ static async invoicePayment(payment: Payment) {
8
+ if (!payment.customer) {
9
+ throw new SimpleError({
10
+ code: 'missing_customer',
11
+ message: 'Missing customer',
12
+ field: 'customer',
13
+ human: $t('Er kan geen factuur aangemaakt worden omdat de klantgegevens ontbreken voor deze betaling'),
14
+ });
15
+ }
16
+
17
+ if (payment.price % 100 !== 0) {
18
+ throw new SimpleError({
19
+ code: 'invalid_price_decimals',
20
+ message: 'Cannot invoice a payment with a price having more than two decimals',
21
+ });
22
+ }
23
+
24
+ const organization = payment.organizationId ? await Organization.getByID(payment.organizationId) : null;
25
+ if (!organization) {
26
+ throw new SimpleError({
27
+ code: 'missing_organization',
28
+ message: 'Cannot invoice a payment without corresponding organization',
29
+ statusCode: 500,
30
+ });
31
+ }
32
+
33
+ // Find default company
34
+ const seller = organization.meta.companies[0];
35
+
36
+ if (!seller) {
37
+ throw new SimpleError({
38
+ code: 'missing_company',
39
+ message: 'Missing invoice settings (companies)',
40
+ human: $t('Het is niet mogelijk om facturen uit te schrijven omdat er nog geen facturatiegegevens zijn ingesteld voor {organization-name}', {
41
+ 'organization-name': organization.name,
42
+ }),
43
+ });
44
+ }
45
+
46
+ const items: InvoicedBalanceItemStruct[] = [];
47
+ const balanceItemPayments = await BalanceItemPayment.select().where('paymentId', payment.id).fetch();
48
+ const balanceItems = await BalanceItem.getByIDs(...Formatter.uniqueArray(balanceItemPayments.map(d => d.balanceItemId)));
49
+
50
+ for (const balanceItemPayment of balanceItemPayments) {
51
+ const balanceItem = balanceItems.find(b => b.id === balanceItemPayment.balanceItemId);
52
+ if (!balanceItem) {
53
+ throw new SimpleError({
54
+ code: 'missing_balance_item',
55
+ message: 'Balance item missing for balanceItemPayment ' + balanceItemPayment.id,
56
+ statusCode: 500,
57
+ });
58
+ }
59
+
60
+ const item = InvoicedBalanceItemStruct.createFor(balanceItem.getStructure(), balanceItemPayment.price);
61
+ items.push(item);
62
+ }
63
+
64
+ const struct = InvoiceStruct.create({
65
+ organizationId: organization.id,
66
+ seller,
67
+ customer: payment.customer,
68
+ payingOrganizationId: payment.payingOrganizationId,
69
+ items,
70
+ });
71
+
72
+ struct.calculateVAT();
73
+
74
+ if (struct.totalBalanceInvoicedAmount <= 0) {
75
+ throw new SimpleError({
76
+ code: 'invalid_invoiced_amount',
77
+ message: 'Unexpected 0 totalBalanceInvoicedAmount',
78
+ });
79
+ }
80
+
81
+ if (struct.totalWithVAT % 100 !== 0) {
82
+ throw new SimpleError({
83
+ code: 'invalid_price_decimals',
84
+ message: 'Unexpected invoice total price with more than two decimals',
85
+ statusCode: 500,
86
+ });
87
+ }
88
+
89
+ // Update payableRoundingAmount to match payment price
90
+ const difference = payment.price - struct.totalWithVAT;
91
+ if (Math.abs(difference) > 10_00) {
92
+ // Difference of more than 10 cent!
93
+ throw new SimpleError({
94
+ code: 'unexpected_price_difference',
95
+ message: 'The invoice and payment amounts differ by more than €0.10.',
96
+ statusCode: 500,
97
+ });
98
+ }
99
+
100
+ if (difference % 100 !== 0) {
101
+ throw new SimpleError({
102
+ code: 'invalid_price_decimals',
103
+ message: 'Unexpected payableRoundingAmount with more than two decimals',
104
+ statusCode: 500,
105
+ });
106
+ }
107
+
108
+ struct.payableRoundingAmount = difference;
109
+
110
+ // Todo: check we are not invoicing more than maximum invoiceable for these items
111
+
112
+ return await Invoice.createFrom(organization, struct);
113
+ }
114
+ }