@stamhoofd/backend 2.120.3 → 2.120.4

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.120.3",
3
+ "version": "2.120.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -62,17 +62,17 @@
62
62
  "@bwip-js/node": "^4.5.1",
63
63
  "@mollie/api-client": "4.3.3",
64
64
  "@simonbackx/simple-database": "1.36.12",
65
- "@simonbackx/simple-encoding": "2.26.5",
66
- "@simonbackx/simple-endpoints": "1.21.0",
65
+ "@simonbackx/simple-encoding": "2.26.6",
66
+ "@simonbackx/simple-endpoints": "1.21.1",
67
67
  "@simonbackx/simple-logging": "^1.0.1",
68
- "@stamhoofd/backend-i18n": "2.120.3",
69
- "@stamhoofd/backend-middleware": "2.120.3",
70
- "@stamhoofd/email": "2.120.3",
71
- "@stamhoofd/models": "2.120.3",
72
- "@stamhoofd/queues": "2.120.3",
73
- "@stamhoofd/sql": "2.120.3",
74
- "@stamhoofd/structures": "2.120.3",
75
- "@stamhoofd/utility": "2.120.3",
68
+ "@stamhoofd/backend-i18n": "2.120.4",
69
+ "@stamhoofd/backend-middleware": "2.120.4",
70
+ "@stamhoofd/email": "2.120.4",
71
+ "@stamhoofd/models": "2.120.4",
72
+ "@stamhoofd/queues": "2.120.4",
73
+ "@stamhoofd/sql": "2.120.4",
74
+ "@stamhoofd/structures": "2.120.4",
75
+ "@stamhoofd/utility": "2.120.4",
76
76
  "archiver": "^7.0.1",
77
77
  "axios": "^1.13.2",
78
78
  "cookie": "^0.7.0",
@@ -90,5 +90,5 @@
90
90
  "publishConfig": {
91
91
  "access": "public"
92
92
  },
93
- "gitHead": "640ab6cea8c65bc276d4ca7ec8a1d172a23e9a06"
93
+ "gitHead": "4f2f25a4c56dae59c26368cf61e314b3f57ba204"
94
94
  }
@@ -1,7 +1,7 @@
1
1
  import { registerCron } from '@stamhoofd/crons';
2
2
  import { CachedBalance, Email, EmailRecipient, Organization, User } from '@stamhoofd/models';
3
- import type { IterableSQLSelect} from '@stamhoofd/sql';
4
- import { readDynamicSQLExpression, SQL, SQLCalculation, SQLPlusSign } from '@stamhoofd/sql';
3
+ import type { IterableSQLSelect } from '@stamhoofd/sql';
4
+ import { readDynamicSQLExpression, SQL } from '@stamhoofd/sql';
5
5
  import type { OrganizationEmail, StamhoofdFilter } from '@stamhoofd/structures';
6
6
  import { EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientSubfilter, EmailTemplateType, ReceivableBalanceType } from '@stamhoofd/structures';
7
7
  import { ContextInstance } from '../helpers/Context.js';
@@ -220,11 +220,8 @@ async function sendTemplate({
220
220
  .set('lastReminderAmountOpen', SQL.column('amountOpen'))
221
221
  .set(
222
222
  'reminderEmailCount',
223
- new SQLCalculation(
224
- SQL.column('reminderEmailCount'),
225
- new SQLPlusSign(),
226
- readDynamicSQLExpression(1),
227
- ),
223
+ SQL.calculation(SQL.column('reminderEmailCount'))
224
+ .add(readDynamicSQLExpression(1))
228
225
  )
229
226
  .where('id', balanceItemIds)
230
227
  .where('organizationId', organization.id)
@@ -0,0 +1,124 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { OrganizationFactory, STPackageFactory } from '@stamhoofd/models';
3
+ import { AccessRight, OrganizationPackagesStatus, STPackage, STPackageBundle, STPackageBundleHelper } from '@stamhoofd/structures';
4
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
+ import { testServer } from '../../../../tests/helpers/TestServer.js';
6
+ import { initAdmin, initPlatformAdmin } from '../../../../tests/init/index.js';
7
+ import { PatchPackagesEndpoint } from './PatchPackagesEndpoint.js';
8
+ import { PatchableArray } from '@simonbackx/simple-encoding';
9
+
10
+ const baseUrl = `/organization/packages`;
11
+ const endpoint = new PatchPackagesEndpoint();
12
+
13
+ describe('Endpoint.PatchPackagesEndpoint', () => {
14
+ beforeEach(async () => {
15
+ TestUtils.setEnvironment('userMode', 'organization');
16
+ });
17
+
18
+ describe('Permission checking', () => {
19
+ test('Cannot patch organization packages as finance director', async () => {
20
+ const organization = await new OrganizationFactory({}).create();
21
+
22
+ const package1 = await new STPackageFactory({
23
+ organization,
24
+ bundle: STPackageBundle.Members,
25
+ }).create();
26
+
27
+ const package2 = await new STPackageFactory({
28
+ organization,
29
+ bundle: STPackageBundle.TrialWebshops,
30
+ }).create();
31
+
32
+ const { adminToken } = await initAdmin({
33
+ organization,
34
+ accessRights: [AccessRight.OrganizationFinanceDirector],
35
+ });
36
+
37
+ // Try to request all members at organization
38
+ const request = Request.patch({
39
+ path: baseUrl,
40
+ host: organization.getApiHost(),
41
+ body: OrganizationPackagesStatus.patch({
42
+
43
+ }),
44
+ headers: {
45
+ authorization: 'Bearer ' + adminToken.accessToken,
46
+ },
47
+ });
48
+
49
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
50
+ });
51
+ });
52
+
53
+ test('Can create a new package', async () => {
54
+ const organization = await new OrganizationFactory({}).create();
55
+
56
+ const { adminToken } = await initPlatformAdmin();
57
+
58
+ const patch = OrganizationPackagesStatus.patch({});
59
+
60
+ const pack = STPackageBundleHelper.getCurrentPackage(STPackageBundle.Webshops, new Date())
61
+ pack.validAt = new Date(); // ignored by backend for now
62
+ patch.packages.addPut(pack);
63
+
64
+ // Try to request all members at organization
65
+ const request = Request.patch({
66
+ path: baseUrl,
67
+ host: organization.getApiHost(),
68
+ body: patch,
69
+ headers: {
70
+ authorization: 'Bearer ' + adminToken.accessToken,
71
+ },
72
+ });
73
+
74
+ const response = await testServer.test(endpoint, request);
75
+ expect(response.status).toBe(200);
76
+ expect(response.body.packages).toHaveLength(1);
77
+
78
+ const reference = response.body.packages[0];
79
+
80
+ // Ignore created at
81
+ pack.createdAt = reference.createdAt;
82
+ pack.updatedAt = reference.updatedAt;
83
+ pack.validAt = reference.validAt;
84
+ expect(reference).toEqual(pack);
85
+ });
86
+
87
+ test('Can edit a package', async () => {
88
+ const organization = await new OrganizationFactory({}).create();
89
+
90
+ const { adminToken } = await initPlatformAdmin();
91
+
92
+ const package1 = await new STPackageFactory({
93
+ organization,
94
+ bundle: STPackageBundle.Members,
95
+ }).create();
96
+
97
+ const patch = OrganizationPackagesStatus.patch({});
98
+ const removeAt = new Date();
99
+ removeAt.setMilliseconds(0)
100
+ patch.packages.addPatch(STPackage.patch({
101
+ id: package1.id,
102
+ removeAt
103
+ }));
104
+
105
+ // Try to request all members at organization
106
+ const request = Request.patch({
107
+ path: baseUrl,
108
+ host: organization.getApiHost(),
109
+ body: patch,
110
+ headers: {
111
+ authorization: 'Bearer ' + adminToken.accessToken,
112
+ },
113
+ });
114
+
115
+ const response = await testServer.test(endpoint, request);
116
+ expect(response.status).toBe(200);
117
+ expect(response.body.packages).toHaveLength(1);
118
+
119
+ const reference = response.body.packages[0];
120
+
121
+ // Ignore created at
122
+ expect(reference.removeAt).toEqual(removeAt);
123
+ });
124
+ });
@@ -0,0 +1,94 @@
1
+ import type { AutoEncoderPatchType, 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 { STPackage } from '@stamhoofd/models';
6
+ import { OrganizationPackagesStatus, STPackage as STPackageStruct } from '@stamhoofd/structures';
7
+ import { Context } from '../../../helpers/Context.js';
8
+ import { STPackageService } from '../../../services/STPackageService.js';
9
+
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type ResponseBody = OrganizationPackagesStatus;
13
+ type Body = AutoEncoderPatchType<OrganizationPackagesStatus>;
14
+
15
+ export class PatchPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ bodyDecoder = OrganizationPackagesStatus.patchType() as Decoder<AutoEncoderPatchType<OrganizationPackagesStatus>>
17
+
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method !== 'PATCH') {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, '/organization/packages', {});
24
+
25
+ if (params) {
26
+ return [true, params as Params];
27
+ }
28
+ return [false];
29
+ }
30
+
31
+ async handle(request: DecodedRequest<Params, Query, Body>) {
32
+ const organization = await Context.setOrganizationScope();
33
+ await Context.authenticate();
34
+
35
+ // If the user has permission, we'll also search if he has access to the organization's key
36
+ if (!Context.auth.hasPlatformFullAccess()) {
37
+ throw Context.auth.error();
38
+ }
39
+ let updatePackages = false
40
+ const packages = await STPackageService.getValidPackagesWithExpired(organization.id);
41
+
42
+ // Do patches
43
+ if (request.body.packages) {
44
+ for (const patch of request.body.packages.getPatches()) {
45
+ const pack = packages.find(p => p.id === patch.id)
46
+ if (!pack) {
47
+ throw new SimpleError({
48
+ code: 'not_found',
49
+ message: 'Package not found with id '+patch.id
50
+ })
51
+ }
52
+
53
+ if (patch.meta !== undefined) {
54
+ pack.meta.patchOrPut(patch.meta)
55
+ }
56
+
57
+ if (patch.validUntil !== undefined) {
58
+ pack.validUntil = patch.validUntil
59
+ }
60
+
61
+ if (patch.removeAt !== undefined) {
62
+ pack.removeAt = patch.removeAt
63
+ }
64
+
65
+ await pack.save()
66
+ updatePackages = true
67
+ }
68
+
69
+ for (const put of request.body.packages.getPuts()) {
70
+ const pack = new STPackage()
71
+ pack.id = put.put.id;
72
+ pack.validAt = new Date()
73
+ pack.organizationId = organization.id
74
+
75
+ pack.meta = put.put.meta
76
+ pack.validUntil = put.put.validUntil
77
+ pack.removeAt = put.put.removeAt
78
+
79
+ await pack.save()
80
+ updatePackages = true
81
+
82
+ packages.push(pack);
83
+ }
84
+ }
85
+
86
+ if (updatePackages) {
87
+ await STPackageService.updateOrganizationPackages(organization.id)
88
+ }
89
+
90
+ return new Response(OrganizationPackagesStatus.create({
91
+ packages: packages.map(p => STPackageStruct.create(p)),
92
+ }));
93
+ }
94
+ }
@@ -32,7 +32,7 @@ export class GetPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseB
32
32
  throw Context.auth.error();
33
33
  }
34
34
 
35
- const packages = await STPackageService.getActivePackages(organization.id);
35
+ const packages = Context.auth.hasPlatformFullAccess() ? await STPackageService.getValidPackagesWithExpired(organization.id) : await STPackageService.getActivePackages(organization.id);
36
36
 
37
37
  return new Response(OrganizationPackagesStatus.create({
38
38
  packages: packages.map(p => STPackageStruct.create(p)),
@@ -5,6 +5,7 @@ import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
5
5
  import { Webshop } from '@stamhoofd/models';
6
6
  import { PermissionLevel, PermissionsResourceType, PrivateWebshop, ResourcePermissions, Version, WebshopPrivateMetaData } from '@stamhoofd/structures';
7
7
  import { Formatter } from '@stamhoofd/utility';
8
+ import { v4 as uuidv4 } from 'uuid';
8
9
 
9
10
  import { Context } from '../../../../helpers/Context.js';
10
11
 
@@ -46,7 +47,7 @@ export class CreateWebshopEndpoint extends Endpoint<Params, Query, Body, Respons
46
47
 
47
48
  const webshop = new Webshop();
48
49
 
49
- webshop.id = request.body.id;
50
+ webshop.id = uuidv4()
50
51
  webshop.meta = request.body.meta;
51
52
  webshop.meta.domainActive = false;
52
53
  webshop.privateMeta = request.body.privateMeta;
@@ -1,6 +1,6 @@
1
1
  import { CachedBalance, Registration } from '@stamhoofd/models';
2
- import type { SQLNamedExpression} from '@stamhoofd/sql';
3
- import { SQL, SQLAlias, SQLCalculation, SQLPlusSign, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
2
+ import type { SQLNamedExpression } from '@stamhoofd/sql';
3
+ import { SQL, SQLAlias, SQLSelectAs, SQLSum } from '@stamhoofd/sql';
4
4
 
5
5
  export const memberCachedBalanceForOrganizationJoin = SQL.leftJoin(
6
6
  SQL.select('objectId', 'organizationId',
@@ -24,23 +24,16 @@ export const registrationCachedBalanceJoin = SQL.leftJoin(
24
24
  SQL.select('objectId', 'organizationId',
25
25
  new SQLSelectAs(
26
26
  new SQLSum(
27
- new SQLCalculation(
28
- SQL.column('amountOpen'),
29
- new SQLPlusSign(),
30
- SQL.column('amountPending'),
31
- ),
27
+ SQL.calculation(SQL.column('amountOpen'))
28
+ .add(SQL.column('amountPending'))
32
29
  ),
33
30
  new SQLAlias('toPay'),
34
31
  ),
35
32
  new SQLSelectAs(
36
33
  new SQLSum(
37
- new SQLCalculation(
38
- SQL.column('amountOpen'),
39
- new SQLPlusSign(),
40
- SQL.column('amountPaid'),
41
- new SQLPlusSign(),
42
- SQL.column('amountPending'),
43
- ),
34
+ SQL.calculation(SQL.column('amountOpen'))
35
+ .add(SQL.column('amountPaid'))
36
+ .add(SQL.column('amountPending'))
44
37
  ),
45
38
  new SQLAlias('price'),
46
39
  ),
@@ -16,6 +16,15 @@ export class STPackageService {
16
16
  SQL.where('removeAt', null)
17
17
  .or('removeAt', '>', new Date()),
18
18
  )
19
+ .orderBy('validAt', 'DESC')
20
+ .fetch();
21
+ }
22
+
23
+ static async getValidPackagesWithExpired(organizationId: string) {
24
+ return await STPackage.select()
25
+ .where('organizationId', organizationId)
26
+ .andWhere('validAt', '!=', null)
27
+ .orderBy('validAt', 'DESC')
19
28
  .fetch();
20
29
  }
21
30
 
@@ -1,5 +1,8 @@
1
- import type { SQLFilterDefinitions} from '@stamhoofd/sql';
2
- import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createWildcardColumnFilter, SQL, SQLCast, SQLConcat, SQLJsonExtract, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
1
+ import { Payment } from '@stamhoofd/models';
2
+ import type { SQLExpression, SQLFilterDefinitions } from '@stamhoofd/sql';
3
+ import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createWildcardColumnFilter, SQL, SQLCast, SQLConcat, SQLJsonExtract, SQLJsonUnquote, SQLScalar, SQLSum, SQLValueType } from '@stamhoofd/sql';
4
+ import { PaymentStatus } from '@stamhoofd/structures';
5
+ import { paymentFilterCompilers } from './payments.js';
3
6
 
4
7
  export const orderFilterCompilers: SQLFilterDefinitions = {
5
8
  ...baseSQLFilterCompilers,
@@ -56,6 +59,11 @@ export const orderFilterCompilers: SQLFilterDefinitions = {
56
59
  type: SQLValueType.JSONString,
57
60
  nullable: false,
58
61
  }),
62
+ checkoutMethodId: createColumnFilter({
63
+ expression: SQL.jsonExtract(SQL.column('data'), '$.value.checkoutMethod.id'),
64
+ type: SQLValueType.JSONString,
65
+ nullable: true,
66
+ }),
59
67
  checkoutMethod: createColumnFilter({
60
68
  expression: SQL.jsonExtract(SQL.column('data'), '$.value.checkoutMethod.type'),
61
69
  type: SQLValueType.JSONString,
@@ -107,7 +115,14 @@ export const orderFilterCompilers: SQLFilterDefinitions = {
107
115
  type: SQLValueType.Datetime,
108
116
  nullable: true,
109
117
  }),
110
-
118
+ discountCodes: {
119
+ ...baseSQLFilterCompilers,
120
+ code: createColumnFilter({
121
+ expression: SQL.jsonExtract(SQL.column('data'), '$.value.discountCodes[*].code'),
122
+ type: SQLValueType.JSONArray,
123
+ nullable: true,
124
+ })
125
+ },
111
126
  items: createExistsFilter(
112
127
  /**
113
128
  * There is a bug in MySQL 8 that is fixed in 9.3
@@ -203,4 +218,60 @@ export const orderFilterCompilers: SQLFilterDefinitions = {
203
218
  }),
204
219
  }),
205
220
  ),
221
+ payments: createExistsFilter(
222
+ // should equal payments on structure
223
+ SQL.select()
224
+ .from(
225
+ SQL.table('balance_items'),
226
+ )
227
+ .join(
228
+ SQL.join(
229
+ SQL.table('balance_item_payments'),
230
+ ).where(
231
+ SQL.column('balance_items', 'id'),
232
+ SQL.column('balance_item_payments', 'balanceItemId'),
233
+ )
234
+ )
235
+ .join(
236
+ SQL.join(
237
+ SQL.table('payments'),
238
+ ).where(
239
+ SQL.column('payments', 'id'),
240
+ SQL.column('balance_item_payments', 'paymentId')
241
+ ),
242
+ )
243
+ .where(
244
+ SQL.column('balance_items', 'orderId'),
245
+ SQL.column('webshop_orders', 'id'),
246
+ )
247
+ // payments on structure filter away failed payments -> also filter out failed payments in backend
248
+ .whereNot(
249
+ SQL.column('payments', 'status'),
250
+ PaymentStatus.Failed
251
+ ),
252
+ {
253
+ ...baseSQLFilterCompilers,
254
+ paidAt: createColumnFilter({
255
+ expression: SQL.column(Payment.table, 'paidAt'),
256
+ type: SQLValueType.Datetime,
257
+ nullable: false,
258
+ }),
259
+ method: paymentFilterCompilers.method,
260
+ status: paymentFilterCompilers.status,
261
+ price: paymentFilterCompilers.price,
262
+ transferDescription: paymentFilterCompilers.transferDescription,
263
+ },
264
+ ),
265
+ // not same as open balance (balance can be negative)
266
+ amountToPay: createColumnFilter({
267
+ expression: SQL.calculation(SQL.column('totalPrice')).subtract(getPricePaidSubQuery()),
268
+ type: SQLValueType.Number,
269
+ nullable: false,
270
+ }),
206
271
  };
272
+
273
+ function getPricePaidSubQuery(): SQLExpression {
274
+ return SQL.subQuery(SQL.select(new SQLSum(SQL.column('balance_items', 'pricePaid')))
275
+ .from(SQL.table('balance_items'))
276
+ .where(SQL.column('balance_items', 'orderId'), SQL.column('webshop_orders', 'id')));
277
+ }
@@ -1,4 +1,5 @@
1
- import type { SQLFilterDefinitions} from '@stamhoofd/sql';
1
+ import { Payment } from '@stamhoofd/models';
2
+ import type { SQLFilterDefinitions } from '@stamhoofd/sql';
2
3
  import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
3
4
  import { balanceItemPaymentsCompilers } from './balance-item-payments.js';
4
5
 
@@ -13,7 +14,7 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
13
14
  nullable: false,
14
15
  }),
15
16
  method: createColumnFilter({
16
- expression: SQL.column('method'),
17
+ expression: SQL.column(Payment.table, 'method'),
17
18
  type: SQLValueType.String,
18
19
  nullable: false,
19
20
  }),
@@ -23,7 +24,7 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
23
24
  nullable: false,
24
25
  }),
25
26
  status: createColumnFilter({
26
- expression: SQL.column('status'),
27
+ expression: SQL.column(Payment.table, 'status'),
27
28
  type: SQLValueType.String,
28
29
  nullable: false,
29
30
  }),
@@ -53,7 +54,7 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
53
54
  nullable: true,
54
55
  }),
55
56
  price: createColumnFilter({
56
- expression: SQL.column('price'),
57
+ expression: SQL.column(Payment.table, 'price'),
57
58
  type: SQLValueType.Number,
58
59
  nullable: false,
59
60
  }),
@@ -63,7 +64,7 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
63
64
  nullable: true,
64
65
  }),
65
66
  transferDescription: createColumnFilter({
66
- expression: SQL.column('transferDescription'),
67
+ expression: SQL.column(Payment.table, 'transferDescription'),
67
68
  type: SQLValueType.String,
68
69
  nullable: true,
69
70
  }),