@stamhoofd/backend 2.124.2 → 2.125.1
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/boot.ts +1 -0
- package/src/crons/balance-emails.ts +2 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/update-organization-future-events.ts +42 -0
- package/src/email-recipient-loaders/documents.ts +2 -1
- package/src/email-recipient-loaders/members.ts +2 -1
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +78 -0
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/email-recipient-loaders/receivable-balances.ts +2 -1
- package/src/email-recipient-loaders/registrations.ts +2 -1
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +12 -2
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +12 -3
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +64 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +14 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +8 -9
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +27 -48
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +57 -2
- package/src/helpers/AdminPermissionChecker.ts +17 -6
- package/src/helpers/AuthenticatedStructures.ts +1 -3
- package/src/seeds/1782728609-update-organization-future-events.ts +11 -0
- package/src/seeds/1782737437-fill-order-payment-customer.ts +59 -0
- package/src/sql-filters/payments.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.125.1",
|
|
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.125.1",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.125.1",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.125.1",
|
|
63
|
+
"@stamhoofd/crons": "2.125.1",
|
|
64
|
+
"@stamhoofd/email": "2.125.1",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.125.1",
|
|
66
|
+
"@stamhoofd/logging": "2.125.1",
|
|
67
|
+
"@stamhoofd/models": "2.125.1",
|
|
68
|
+
"@stamhoofd/object-differ": "2.125.1",
|
|
69
|
+
"@stamhoofd/queues": "2.125.1",
|
|
70
|
+
"@stamhoofd/sql": "2.125.1",
|
|
71
|
+
"@stamhoofd/structures": "2.125.1",
|
|
72
|
+
"@stamhoofd/types": "2.125.1",
|
|
73
|
+
"@stamhoofd/utility": "2.125.1",
|
|
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.125.1",
|
|
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": "7a5fada26c2b7e4d0cd47cd5f2ee95adceae8391"
|
|
111
111
|
}
|
package/src/boot.ts
CHANGED
|
@@ -165,6 +165,7 @@ export const boot = async (options: { killProcess: boolean }) => {
|
|
|
165
165
|
await import('./excel-loaders/registrations.js');
|
|
166
166
|
await import('./email-recipient-loaders/documents.js');
|
|
167
167
|
await import ('./email-recipient-loaders/payments.js');
|
|
168
|
+
await import ('./email-recipient-loaders/organizations.js');
|
|
168
169
|
|
|
169
170
|
productionLog('Opening port...');
|
|
170
171
|
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
@@ -3,8 +3,9 @@ import { CachedBalance, Email, EmailRecipient, Organization, User } from '@stamh
|
|
|
3
3
|
import type { IterableSQLSelect } from '@stamhoofd/sql';
|
|
4
4
|
import { readDynamicSQLExpression, SQL } from '@stamhoofd/sql';
|
|
5
5
|
import type { OrganizationEmail, StamhoofdFilter } from '@stamhoofd/structures';
|
|
6
|
-
import { EmailRecipientFilter,
|
|
6
|
+
import { EmailRecipientFilter, EmailRecipientSubfilter, EmailTemplateType, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
7
7
|
import { ContextInstance } from '../helpers/Context.js';
|
|
8
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
8
9
|
|
|
9
10
|
registerCron('balanceEmails', balanceEmails);
|
|
10
11
|
|
package/src/crons/index.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recalculate the hasFutureEvents flag for all organizations.
|
|
3
|
+
*
|
|
4
|
+
* The flag can go stale over time: an event's end date passes without anyone
|
|
5
|
+
* touching the organization, so it keeps reporting future events it no longer
|
|
6
|
+
* has (or vice versa). Recomputing it nightly keeps it accurate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
10
|
+
import { Organization } from '@stamhoofd/models';
|
|
11
|
+
|
|
12
|
+
let lastRunDate: number | null = null;
|
|
13
|
+
|
|
14
|
+
registerCron('updateOrganizationFutureEvents', updateOrganizationFutureEvents);
|
|
15
|
+
|
|
16
|
+
function shouldRun() {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
|
|
19
|
+
if (now.getDate() === lastRunDate) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hour = now.getHours();
|
|
24
|
+
|
|
25
|
+
// between 3 and 5 AM - except in development
|
|
26
|
+
if ((hour < 3 || hour >= 5) && STAMHOOFD.environment !== 'development') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function updateOrganizationFutureEvents() {
|
|
34
|
+
if (!shouldRun()) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lastRunDate = new Date().getDate();
|
|
39
|
+
|
|
40
|
+
console.log('Updating hasFutureEvents for all organizations...');
|
|
41
|
+
await Organization.updateFutureEventsForOrganizations('all');
|
|
42
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Email, Member } from '@stamhoofd/models';
|
|
2
2
|
import { SQL } from '@stamhoofd/sql';
|
|
3
3
|
import type { LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
4
|
-
import { EmailRecipient,
|
|
4
|
+
import { EmailRecipient, PaginatedResponse, Replacement } from '@stamhoofd/structures';
|
|
5
5
|
import { GetDocumentsEndpoint } from '../endpoints/organization/dashboard/documents/GetDocumentsEndpoint.js';
|
|
6
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
6
7
|
|
|
7
8
|
async function fetch(query: LimitedFilteredRequest) {
|
|
8
9
|
const result = await GetDocumentsEndpoint.buildData(query);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Email } from '@stamhoofd/models';
|
|
2
2
|
import { SQL } from '@stamhoofd/sql';
|
|
3
3
|
import type { EmailRecipient, LimitedFilteredRequest, MembersBlob } from '@stamhoofd/structures';
|
|
4
|
-
import {
|
|
4
|
+
import { PaginatedResponse, mergeFilters } from '@stamhoofd/structures';
|
|
5
5
|
import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint.js';
|
|
6
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
6
7
|
|
|
7
8
|
async function getRecipients(result: PaginatedResponse<MembersBlob, LimitedFilteredRequest>, type: 'member' | 'parents' | 'unverified') {
|
|
8
9
|
const recipients: EmailRecipient[] = [];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Email, Webshop } from '@stamhoofd/models';
|
|
2
2
|
import type { EmailRecipient, LimitedFilteredRequest, WebshopPreview } from '@stamhoofd/structures';
|
|
3
|
-
import {
|
|
3
|
+
import { mergeFilters, PaginatedResponse } from '@stamhoofd/structures';
|
|
4
4
|
import { GetWebshopOrdersEndpoint } from '../endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.js';
|
|
5
5
|
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
|
|
6
6
|
import { Context } from '../helpers/Context.js';
|
|
7
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
7
8
|
|
|
8
9
|
Email.recipientLoaders.set(EmailRecipientFilterType.Orders, {
|
|
9
10
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Organization, User } from '@stamhoofd/models';
|
|
2
|
+
import { Platform } from '@stamhoofd/models';
|
|
3
|
+
import { Email } from '@stamhoofd/models';
|
|
4
|
+
import type { InMemoryFilterDefinitions, LimitedFilteredRequest, StamhoofdFilter, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
5
|
+
import { baseInMemoryFilterCompilers, compileToInMemoryFilter, createInMemoryFilterCompiler, EmailRecipient, Replacement } from '@stamhoofd/structures';
|
|
6
|
+
import { PaginatedResponse } from '@stamhoofd/structures';
|
|
7
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
8
|
+
import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint.js';
|
|
9
|
+
|
|
10
|
+
function userToFilterableAdmin(user: User, platform: PlatformStruct, organization: Organization) {
|
|
11
|
+
return {
|
|
12
|
+
id: user.id,
|
|
13
|
+
permissions: user.permissions?.forOrganization(organization, platform),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const filterableAdminFilterCompilers: InMemoryFilterDefinitions = {
|
|
17
|
+
...baseInMemoryFilterCompilers,
|
|
18
|
+
permissions: createInMemoryFilterCompiler(['permissions'], {
|
|
19
|
+
...baseInMemoryFilterCompilers,
|
|
20
|
+
level: createInMemoryFilterCompiler('level'),
|
|
21
|
+
accessRights: createInMemoryFilterCompiler('accessRights'),
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.Organizations, {
|
|
26
|
+
fetch: fetchRecipients,
|
|
27
|
+
|
|
28
|
+
count: async (query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) => {
|
|
29
|
+
const q = await GetOrganizationsEndpoint.buildQuery(query);
|
|
30
|
+
const base = await q.count();
|
|
31
|
+
|
|
32
|
+
if (base < 100) {
|
|
33
|
+
// Do full scan
|
|
34
|
+
query.limit = 100;
|
|
35
|
+
const result = await fetchRecipients(query, subfilter);
|
|
36
|
+
return result.results.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return base;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function fetchRecipients(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
|
|
44
|
+
const result = await GetOrganizationsEndpoint.buildModels(query);
|
|
45
|
+
const compiledFilter = compileToInMemoryFilter(subfilter, filterableAdminFilterCompilers);
|
|
46
|
+
const platform = await Platform.getSharedStruct();
|
|
47
|
+
|
|
48
|
+
// Map recipients to admins
|
|
49
|
+
const recipients: EmailRecipient[] = [];
|
|
50
|
+
for (const organization of result.results) {
|
|
51
|
+
// todo: filter admins
|
|
52
|
+
const users = await organization.getAdmins();
|
|
53
|
+
const filteredUsers = users.filter((user) => {
|
|
54
|
+
const filterable = userToFilterableAdmin(user, platform, organization);
|
|
55
|
+
return compiledFilter(filterable);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
recipients.push(
|
|
59
|
+
...organization.adminsToRecipients(filteredUsers).map((r) => {
|
|
60
|
+
return EmailRecipient.create({
|
|
61
|
+
...r,
|
|
62
|
+
replacements: [
|
|
63
|
+
...r.replacements,
|
|
64
|
+
Replacement.create({
|
|
65
|
+
token: 'organizationName',
|
|
66
|
+
value: organization.name,
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new PaginatedResponse({
|
|
75
|
+
results: recipients,
|
|
76
|
+
next: result.next,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { RecipientLoader } from '@stamhoofd/models';
|
|
2
|
-
import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord,
|
|
2
|
+
import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Organization, Payment, User } from '@stamhoofd/models';
|
|
3
3
|
import { compileToSQLFilter, SQL } from '@stamhoofd/sql';
|
|
4
4
|
import type { LimitedFilteredRequest, PaymentGeneral, StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
|
-
import { CountFilteredRequest, EmailRecipient,
|
|
5
|
+
import { CountFilteredRequest, EmailRecipient, PaginatedResponse, PaymentMethod } from '@stamhoofd/structures';
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
import { GetPaymentsEndpoint } from '../endpoints/organization/dashboard/payments/GetPaymentsEndpoint.js';
|
|
8
8
|
import { memberResponsibilityRecordFilterCompilers } from '../sql-filters/member-responsibility-records.js';
|
|
9
9
|
import type { ReplacementsOptions } from '../email-replacements/getEmailReplacementsForPayment.js';
|
|
10
10
|
import { buildReplacementOptions, getEmailReplacementsForPayment } from '../email-replacements/getEmailReplacementsForPayment.js';
|
|
11
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
11
12
|
|
|
12
13
|
type BeforeFetchAllResult = {
|
|
13
14
|
doesIncludePaymentWithoutOrders: boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CachedBalance, Email } from '@stamhoofd/models';
|
|
2
2
|
import type { LimitedFilteredRequest, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
-
import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient,
|
|
3
|
+
import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, ReceivableBalanceType, Replacement } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
5
|
import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.js';
|
|
6
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
6
7
|
|
|
7
8
|
async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter | null) {
|
|
8
9
|
const result = await GetReceivableBalancesEndpoint.buildData(query);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Email, Member } from '@stamhoofd/models';
|
|
2
2
|
import { SQL } from '@stamhoofd/sql';
|
|
3
3
|
import type { EmailRecipient, LimitedFilteredRequest, RegistrationsBlob } from '@stamhoofd/structures';
|
|
4
|
-
import {
|
|
4
|
+
import { PaginatedResponse, mergeFilters } from '@stamhoofd/structures';
|
|
5
5
|
import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
|
|
6
6
|
import { memberJoin } from '../sql-filters/registrations.js';
|
|
7
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
7
8
|
|
|
8
9
|
async function getRecipients(result: PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>, type: 'member' | 'parents' | 'unverified') {
|
|
9
10
|
const recipients: EmailRecipient[] = [];
|
|
@@ -5,6 +5,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
5
5
|
import { Organization } from '@stamhoofd/models';
|
|
6
6
|
import { SQL, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
7
7
|
import type { CountFilteredRequest, Organization as OrganizationStruct, StamhoofdFilter } from '@stamhoofd/structures';
|
|
8
|
+
import { UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
8
9
|
import { LimitedFilteredRequest, PaginatedResponse, PermissionLevel, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
9
10
|
|
|
10
11
|
import type { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
@@ -130,7 +131,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
130
131
|
return query;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
static async
|
|
134
|
+
static async buildModels(requestQuery: LimitedFilteredRequest): Promise<UnencodeablePaginatedResponse<Organization[], LimitedFilteredRequest>> {
|
|
134
135
|
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
135
136
|
|
|
136
137
|
if (requestQuery.limit > maxLimit) {
|
|
@@ -187,8 +188,17 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
return new UnencodeablePaginatedResponse<Organization[], LimitedFilteredRequest>({
|
|
192
|
+
results: organizations,
|
|
193
|
+
next,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static async buildData(requestQuery: LimitedFilteredRequest): Promise<PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>> {
|
|
198
|
+
const { results, next } = await this.buildModels(requestQuery);
|
|
199
|
+
|
|
190
200
|
return new PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>({
|
|
191
|
-
results: await AuthenticatedStructures.organizations(
|
|
201
|
+
results: await AuthenticatedStructures.organizations(results),
|
|
192
202
|
next,
|
|
193
203
|
});
|
|
194
204
|
}
|
|
@@ -44,10 +44,15 @@ class Body extends AutoEncoder {
|
|
|
44
44
|
export const unblockLimiter = new RateLimiter({
|
|
45
45
|
limits: [
|
|
46
46
|
{
|
|
47
|
-
// Max
|
|
48
|
-
limit:
|
|
47
|
+
// Max 25 per week
|
|
48
|
+
limit: 25,
|
|
49
49
|
duration: 24 * 60 * 1000 * 60 * 7,
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
// Max 50 per month
|
|
53
|
+
limit: 50,
|
|
54
|
+
duration: 24 * 60 * 1000 * 60 * 30,
|
|
55
|
+
},
|
|
51
56
|
],
|
|
52
57
|
});
|
|
53
58
|
|
|
@@ -129,12 +134,16 @@ export class ManageEmailAddressEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
129
134
|
for (const email of emails) {
|
|
130
135
|
const wasBlocked = email.unsubscribedAll || email.unsubscribedMarketing || email.hardBounce || email.markedAsSpam;
|
|
131
136
|
|
|
132
|
-
if (email.organizationId === null || (organization && email.organizationId
|
|
137
|
+
if (email.organizationId === null || (organization && email.organizationId !== organization.id)) {
|
|
133
138
|
if (Context.auth.hasPlatformFullAccess()) {
|
|
134
139
|
// Only allowed as platform admins
|
|
140
|
+
// This is important, otherwise an individual organization can resubscribe to platform emails
|
|
135
141
|
email.unsubscribedAll = request.body.unsubscribedAll ?? email.unsubscribedAll;
|
|
136
142
|
email.unsubscribedMarketing = request.body.unsubscribedMarketing ?? email.unsubscribedMarketing;
|
|
137
143
|
}
|
|
144
|
+
} else {
|
|
145
|
+
email.unsubscribedAll = request.body.unsubscribedAll ?? email.unsubscribedAll;
|
|
146
|
+
// unsubscribed marketing not allowed for now
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
email.hardBounce = request.body.hardBounce ?? email.hardBounce;
|
|
@@ -2,10 +2,11 @@ import type { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
|
2
2
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
4
4
|
import { Email, EmailRecipient, GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
-
import { AccessRight, EmailRecipientFilter,
|
|
5
|
+
import { AccessRight, EmailRecipientFilter, EmailRecipientSubfilter, EmailStatus, Email as EmailStruct, OrganizationEmail, Parent, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserPermissions, Version } from '@stamhoofd/structures';
|
|
6
6
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
7
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
8
8
|
import { PatchEmailEndpoint } from './PatchEmailEndpoint.js';
|
|
9
|
+
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
9
10
|
|
|
10
11
|
// Import recipient loaders to initialize them
|
|
11
12
|
import { Formatter } from '@stamhoofd/utility';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Database } from '@simonbackx/simple-database';
|
|
1
2
|
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
3
|
import type { Endpoint } from '@simonbackx/simple-endpoints';
|
|
3
4
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
4
|
-
import type {
|
|
5
|
-
import { EventFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, PlatformEventTypeFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
+
import type { User } from '@stamhoofd/models';
|
|
6
|
+
import { EventFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, PlatformEventTypeFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
6
7
|
import { AccessRight, Event, Group, GroupSettings, GroupType, OrganizationEventType, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, TranslatedString } from '@stamhoofd/structures';
|
|
7
8
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
8
9
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
@@ -90,6 +91,67 @@ describe('Endpoint.PatchEventsEndpoint', () => {
|
|
|
90
91
|
});
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
describe('hasFutureEvents recomputation', () => {
|
|
95
|
+
// A global event (organizationId === null) counts towards every organization,
|
|
96
|
+
// so make sure leftover events from other tests don't influence these assertions.
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
await Database.delete('DELETE FROM `events`');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Creating a future event sets hasFutureEvents to true on the organization', async () => {
|
|
102
|
+
const organization = await new OrganizationFactory({}).create();
|
|
103
|
+
organization.hasFutureEvents = false;
|
|
104
|
+
await organization.save();
|
|
105
|
+
|
|
106
|
+
const user = await new UserFactory({
|
|
107
|
+
organization,
|
|
108
|
+
permissions: minimumUserPermissions,
|
|
109
|
+
}).create();
|
|
110
|
+
|
|
111
|
+
const body: Body = new PatchableArray();
|
|
112
|
+
body.addPut(Event.create({
|
|
113
|
+
organizationId: organization.id,
|
|
114
|
+
typeId: (await new PlatformEventTypeFactory({}).create()).id,
|
|
115
|
+
name: 'test event',
|
|
116
|
+
startDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
|
|
117
|
+
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const result = await TestRequest.patch({ body, user, organization });
|
|
121
|
+
expect(result.status).toBe(200);
|
|
122
|
+
|
|
123
|
+
const updated = await Organization.getByID(organization.id, true);
|
|
124
|
+
expect(updated.hasFutureEvents).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('Creating an event that already ended does not set hasFutureEvents to true', async () => {
|
|
128
|
+
const organization = await new OrganizationFactory({}).create();
|
|
129
|
+
organization.hasFutureEvents = false;
|
|
130
|
+
await organization.save();
|
|
131
|
+
|
|
132
|
+
const user = await new UserFactory({
|
|
133
|
+
organization,
|
|
134
|
+
permissions: minimumUserPermissions,
|
|
135
|
+
}).create();
|
|
136
|
+
|
|
137
|
+
const body: Body = new PatchableArray();
|
|
138
|
+
// Ended well before the future-events cutoff (~2 months ago), so it should not count.
|
|
139
|
+
body.addPut(Event.create({
|
|
140
|
+
organizationId: organization.id,
|
|
141
|
+
typeId: (await new PlatformEventTypeFactory({}).create()).id,
|
|
142
|
+
name: 'test event',
|
|
143
|
+
startDate: new Date(Date.now() - 100 * 24 * 60 * 60 * 1000),
|
|
144
|
+
endDate: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const result = await TestRequest.patch({ body, user, organization });
|
|
148
|
+
expect(result.status).toBe(200);
|
|
149
|
+
|
|
150
|
+
const updated = await Organization.getByID(organization.id, true);
|
|
151
|
+
expect(updated.hasFutureEvents).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
93
155
|
test('A normal user with write access cannot create a global event', async () => {
|
|
94
156
|
const organization = await new OrganizationFactory({}).create();
|
|
95
157
|
const user = await new UserFactory({
|
|
@@ -149,6 +149,14 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
149
149
|
await event.save();
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Creating an event can only add a future event, so it can only flip
|
|
153
|
+
// hasFutureEvents from false to true. Only recompute when it is still false.
|
|
154
|
+
// Best effort only
|
|
155
|
+
if (eventOrganization && !eventOrganization.hasFutureEvents) {
|
|
156
|
+
await eventOrganization.updateFutureEvents();
|
|
157
|
+
await eventOrganization.save();
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
events.push(event);
|
|
153
161
|
}
|
|
154
162
|
|
|
@@ -327,6 +335,12 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
327
335
|
await event.delete();
|
|
328
336
|
}
|
|
329
337
|
|
|
338
|
+
if (request.body.getDeletes().length && organization && organization.hasFutureEvents) {
|
|
339
|
+
// Best effort only
|
|
340
|
+
await organization.updateFutureEvents();
|
|
341
|
+
await organization.save();
|
|
342
|
+
}
|
|
343
|
+
|
|
330
344
|
const structures = await AuthenticatedStructures.events(events);
|
|
331
345
|
return new Response(
|
|
332
346
|
structures,
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { DetailedReceivableBalance, PaymentStatus, PermissionLevel, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
|
+
import type { MemberWithUsersAndRegistrations } from '@stamhoofd/models';
|
|
5
6
|
import { BalanceItem, BalanceItemPayment, CachedBalance, Member, MemberUser, Payment, Registration } from '@stamhoofd/models';
|
|
6
7
|
import { Context } from '../../../../helpers/Context.js';
|
|
7
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
|
|
@@ -36,20 +37,18 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
|
|
|
36
37
|
await Context.authenticate();
|
|
37
38
|
|
|
38
39
|
if (!await Context.auth.canManageFinances(organization.id)) {
|
|
39
|
-
|
|
40
|
-
if (request.params.type === ReceivableBalanceType.
|
|
41
|
-
const member = await Member.getByIdWithUsersAndRegistrations(request.params.id);
|
|
42
|
-
if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
|
|
43
|
-
throw Context.auth.error();
|
|
44
|
-
}
|
|
45
|
-
} else if (request.params.type === ReceivableBalanceType.registration) {
|
|
40
|
+
let member: MemberWithUsersAndRegistrations | null = null;
|
|
41
|
+
if (request.params.type === ReceivableBalanceType.registration) {
|
|
46
42
|
const registration = await Registration.select().where('id', request.params.id).first(false);
|
|
47
43
|
if (!registration) throw Context.auth.error();
|
|
48
|
-
|
|
44
|
+
member = await Member.getByIdWithUsersAndRegistrations(registration.memberId);
|
|
45
|
+
} else if (request.params.type === ReceivableBalanceType.member) {
|
|
46
|
+
member = await Member.getByIdWithUsersAndRegistrations(request.params.id);
|
|
49
47
|
if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
|
|
50
48
|
throw Context.auth.error();
|
|
51
49
|
}
|
|
52
|
-
}
|
|
50
|
+
}
|
|
51
|
+
if (!member || !await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, organization.id)) {
|
|
53
52
|
throw Context.auth.error();
|
|
54
53
|
}
|
|
55
54
|
}
|
|
@@ -565,29 +565,16 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
565
565
|
if (patch.waitingList === null) {
|
|
566
566
|
// delete
|
|
567
567
|
if (model.waitingListId) {
|
|
568
|
-
// for now don't delete, as waiting lists can be shared between multiple groups
|
|
569
|
-
// await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
|
|
570
568
|
model.waitingListId = null;
|
|
571
569
|
}
|
|
572
570
|
} else if (patch.waitingList.isPatch()) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
message: 'Cannot patch waiting list before it is created',
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
patch.waitingList.id = model.waitingListId;
|
|
581
|
-
patch.waitingList.type = GroupType.WaitingList;
|
|
582
|
-
await throwIfUpdateWaitingListPeriodWithMultipleGroups(patch.waitingList.id);
|
|
583
|
-
await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList, period, {
|
|
584
|
-
allowPatchWaitingListPeriod: shouldUpdatePeriodIds,
|
|
585
|
-
isPatchingEvent,
|
|
571
|
+
throw new SimpleError({
|
|
572
|
+
code: 'invalid_field',
|
|
573
|
+
field: 'waitingList',
|
|
574
|
+
message: 'Cannot patch waitingList',
|
|
586
575
|
});
|
|
587
576
|
} else {
|
|
588
577
|
if (model.waitingListId) {
|
|
589
|
-
// for now don't delete, as waiting lists can be shared between multiple groups
|
|
590
|
-
// await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
|
|
591
578
|
model.waitingListId = null;
|
|
592
579
|
}
|
|
593
580
|
patch.waitingList.type = GroupType.WaitingList;
|
|
@@ -613,29 +600,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
613
600
|
|
|
614
601
|
model.waitingListId = existing.id;
|
|
615
602
|
} else {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
throw new SimpleError({
|
|
624
|
-
code: 'invalid_period',
|
|
625
|
-
message: 'Period has different organization id',
|
|
626
|
-
statusCode: 400,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
631
|
-
patch.waitingList,
|
|
632
|
-
model.organizationId,
|
|
633
|
-
requiredPeriod,
|
|
634
|
-
{
|
|
635
|
-
allowedIds: [patch.waitingList.id],
|
|
636
|
-
},
|
|
637
|
-
);
|
|
638
|
-
model.waitingListId = group.id;
|
|
603
|
+
throw new SimpleError({
|
|
604
|
+
code: 'invalid_waiting_list',
|
|
605
|
+
field: 'waitingList',
|
|
606
|
+
message: 'Waiting list not found',
|
|
607
|
+
human: $t('%ZZb'),
|
|
608
|
+
statusCode: 404,
|
|
609
|
+
});
|
|
639
610
|
}
|
|
640
611
|
}
|
|
641
612
|
} else if (shouldUpdatePeriodIds && model.waitingListId && period) {
|
|
@@ -761,16 +732,24 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
761
732
|
});
|
|
762
733
|
}
|
|
763
734
|
|
|
735
|
+
if (existing.periodId !== model.periodId) {
|
|
736
|
+
throw new SimpleError({
|
|
737
|
+
code: 'invalid_field',
|
|
738
|
+
field: 'waitingList',
|
|
739
|
+
message: 'Waiting list group is already used in another period',
|
|
740
|
+
human: $t(`%F9`),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
764
744
|
model.waitingListId = existing.id;
|
|
765
745
|
} else {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
);
|
|
773
|
-
model.waitingListId = group.id;
|
|
746
|
+
throw new SimpleError({
|
|
747
|
+
code: 'invalid_waiting_list',
|
|
748
|
+
field: 'waitingList',
|
|
749
|
+
message: 'Waiting list not found',
|
|
750
|
+
human: $t('%ZZb'),
|
|
751
|
+
statusCode: 404,
|
|
752
|
+
});
|
|
774
753
|
}
|
|
775
754
|
}
|
|
776
755
|
|
|
@@ -158,6 +158,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
158
158
|
PaymentService.roundPayment(payment);
|
|
159
159
|
payment.paidAt = null;
|
|
160
160
|
payment.adminUserId = user.id;
|
|
161
|
+
payment.customer = order.data.customer.toPaymentCustomer();
|
|
161
162
|
|
|
162
163
|
// Determine the payment provider (always null because no online payments here)
|
|
163
164
|
payment.provider = null;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
1
3
|
import type { Response } from '@simonbackx/simple-endpoints';
|
|
2
4
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
5
|
import type { Organization, StripeAccount, User } from '@stamhoofd/models';
|
|
4
|
-
import { OrganizationFactory, Token, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
|
|
6
|
+
import { Order, OrganizationFactory, Payment, Token, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
|
|
5
7
|
import type { OrderResponse } from '@stamhoofd/structures';
|
|
6
|
-
import { Address, Cart, CartItem, CartItemOption, Customer, Option, OptionMenu, OrderData, PaymentConfiguration, PaymentMethod, PermissionLevel, Permissions, PrivatePaymentConfiguration, Product, ProductPrice, ProductType, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, WebshopAuthType, WebshopDeliveryMethod, WebshopMetaData, WebshopOnSiteMethod, WebshopPrivateMetaData, WebshopTakeoutMethod, WebshopTimeSlot } from '@stamhoofd/structures';
|
|
8
|
+
import { Address, Cart, CartItem, CartItemOption, Customer, Option, OptionMenu, OrderData, PaymentConfiguration, PaymentMethod, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, Product, ProductPrice, ProductType, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, WebshopAuthType, WebshopDeliveryMethod, WebshopMetaData, WebshopOnSiteMethod, WebshopPrivateMetaData, WebshopTakeoutMethod, WebshopTimeSlot } from '@stamhoofd/structures';
|
|
7
9
|
import { STExpect } from '@stamhoofd/test-utils';
|
|
8
10
|
import { Country } from '@stamhoofd/types/Country';
|
|
9
11
|
import sinon from 'sinon';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
13
|
|
|
11
14
|
import { StripeMocker } from '../../../../tests/helpers/StripeMocker.js';
|
|
12
15
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
@@ -518,4 +521,56 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
|
|
|
518
521
|
}
|
|
519
522
|
});
|
|
520
523
|
});
|
|
524
|
+
|
|
525
|
+
describe('Manually created orders', () => {
|
|
526
|
+
async function createManualOrder(paymentMethod: PaymentMethod): Promise<PrivateOrder> {
|
|
527
|
+
const orderData = OrderData.create({
|
|
528
|
+
paymentMethod,
|
|
529
|
+
checkoutMethod: takeoutMethod,
|
|
530
|
+
timeSlot: slot1,
|
|
531
|
+
cart: Cart.create({
|
|
532
|
+
items: [
|
|
533
|
+
CartItem.create({
|
|
534
|
+
product,
|
|
535
|
+
productPrice: productPrice1,
|
|
536
|
+
amount: 2,
|
|
537
|
+
}),
|
|
538
|
+
],
|
|
539
|
+
}),
|
|
540
|
+
customer,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const patchArray: PatchableArrayAutoEncoder<PrivateOrder> = new PatchableArray();
|
|
544
|
+
patchArray.addPut(PrivateOrder.create({
|
|
545
|
+
id: uuidv4(),
|
|
546
|
+
data: orderData,
|
|
547
|
+
webshopId: webshop.id,
|
|
548
|
+
}));
|
|
549
|
+
|
|
550
|
+
const r = Request.buildJson('PATCH', `/webshop/${webshop.id}/orders`, organization.getApiHost(), patchArray);
|
|
551
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
552
|
+
|
|
553
|
+
const response = await testServer.test(patchWebshopOrdersEndpoint, r);
|
|
554
|
+
expect(response.body).toHaveLength(1);
|
|
555
|
+
return response.body[0];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
test.each([PaymentMethod.Transfer, PaymentMethod.PointOfSale])('Stores the payment customer for a manually added %s order', async (paymentMethod) => {
|
|
559
|
+
const order = await createManualOrder(paymentMethod);
|
|
560
|
+
|
|
561
|
+
// The persisted payment should contain the billing details of the customer
|
|
562
|
+
const orderModel = (await Order.getByID(order.id))!;
|
|
563
|
+
expect(orderModel.paymentId).not.toBeNull();
|
|
564
|
+
|
|
565
|
+
const payment = (await Payment.getByID(orderModel.paymentId!))!;
|
|
566
|
+
expect(payment.customer).not.toBeNull();
|
|
567
|
+
expect(payment.customer!.firstName).toEqual(customer.firstName);
|
|
568
|
+
expect(payment.customer!.lastName).toEqual(customer.lastName);
|
|
569
|
+
expect(payment.customer!.email).toEqual(customer.email);
|
|
570
|
+
expect(payment.customer!.phone).toEqual(customer.phone);
|
|
571
|
+
|
|
572
|
+
// The payment customer should match the order's customer billing details
|
|
573
|
+
expect(payment.customer!.equals(orderModel.data.customer.toPaymentCustomer())).toBe(true);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
521
576
|
});
|
|
@@ -702,12 +702,6 @@ export class AdminPermissionChecker {
|
|
|
702
702
|
|
|
703
703
|
if (this.user.permissions) {
|
|
704
704
|
// We grant permission for a whole payment when the user has at least permission for a part of that payment.
|
|
705
|
-
for (const registration of registrations) {
|
|
706
|
-
if (await this.canAccessRegistration(registration, permissionLevel)) {
|
|
707
|
-
return true;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
705
|
const webshopCache: Map<string, Webshop> = new Map();
|
|
712
706
|
|
|
713
707
|
for (const order of orders) {
|
|
@@ -720,6 +714,23 @@ export class AdminPermissionChecker {
|
|
|
720
714
|
}
|
|
721
715
|
}
|
|
722
716
|
}
|
|
717
|
+
|
|
718
|
+
for (const registration of registrations) {
|
|
719
|
+
if (await this.canAccessRegistration(registration, permissionLevel)) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const seenMemberIds = new Set<string>();
|
|
725
|
+
|
|
726
|
+
for (const balanceItem of balanceItems) {
|
|
727
|
+
if (!balanceItem.memberId || seenMemberIds.has(balanceItem.memberId)) continue;
|
|
728
|
+
seenMemberIds.add(balanceItem.memberId);
|
|
729
|
+
const member = await Member.getByIdWithUsersAndRegistrations(balanceItem.memberId);
|
|
730
|
+
if (member && await this.hasFinancialMemberAccess(member, permissionLevel)) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
723
734
|
}
|
|
724
735
|
|
|
725
736
|
if (permissionLevel === PermissionLevel.Read) {
|
|
@@ -32,10 +32,8 @@ export class AuthenticatedStructures {
|
|
|
32
32
|
const { balanceItemPayments, balanceItems } = await Payment.loadBalanceItems(payments);
|
|
33
33
|
|
|
34
34
|
if (checkPermissions) {
|
|
35
|
-
const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
|
|
36
|
-
|
|
37
35
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
38
|
-
if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read
|
|
36
|
+
if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read))) {
|
|
39
37
|
throw new SimpleError({
|
|
40
38
|
code: 'permission_denied',
|
|
41
39
|
message: 'Permission denied',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Organization } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment === 'test') {
|
|
6
|
+
console.log('skipped in tests');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
await Organization.updateFutureEventsForOrganizations('all');
|
|
11
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Order, Payment } from '@stamhoofd/models';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
// The customer is filled in directly when the payment is created (see PatchWebshopOrdersEndpoint),
|
|
9
|
+
// which is covered by PlaceOrderEndpoint.test.ts.
|
|
10
|
+
console.log('skipped in tests');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Note: unlike 1780915001-fill-payment-customer, there is no platformName guard here.
|
|
15
|
+
// Manually added webshop orders never stored a payment customer on any platform, so every
|
|
16
|
+
// platform needs this fix.
|
|
17
|
+
|
|
18
|
+
console.log('Start filling in payment customers for orders.');
|
|
19
|
+
|
|
20
|
+
let filled = 0;
|
|
21
|
+
|
|
22
|
+
const result = await SeedTools.loopBatched({
|
|
23
|
+
query: Payment.select().where('customer', null),
|
|
24
|
+
batchSize: 1000,
|
|
25
|
+
batchAction: async (payments: Payment[]) => {
|
|
26
|
+
// Load the balance items linked to the payments so we can find their orders in bulk.
|
|
27
|
+
const { balanceItemPayments, balanceItems } = await Payment.loadBalanceItems(payments);
|
|
28
|
+
|
|
29
|
+
const orderIds = Formatter.uniqueArray(balanceItems.map(b => b.orderId).filter((id): id is string => id !== null));
|
|
30
|
+
const orders = orderIds.length ? await Order.getByIDs(...orderIds) : [];
|
|
31
|
+
|
|
32
|
+
for (const payment of payments) {
|
|
33
|
+
const paymentBalanceItemIds = balanceItemPayments.filter(bip => bip.paymentId === payment.id).map(bip => bip.balanceItemId);
|
|
34
|
+
const linkedOrderIds = Formatter.uniqueArray(
|
|
35
|
+
balanceItems
|
|
36
|
+
.filter(b => paymentBalanceItemIds.includes(b.id))
|
|
37
|
+
.map(b => b.orderId)
|
|
38
|
+
.filter((id): id is string => id !== null),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Only fill in the customer based on a linked order with usable billing details.
|
|
42
|
+
for (const orderId of linkedOrderIds) {
|
|
43
|
+
const order = orders.find(o => o.id === orderId);
|
|
44
|
+
if (order && (order.data.customer.email || order.data.customer.name)) {
|
|
45
|
+
payment.customer = order.data.customer.toPaymentCustomer();
|
|
46
|
+
await payment.save({
|
|
47
|
+
skipMarkSaved: true,
|
|
48
|
+
skipSendEvents: true,
|
|
49
|
+
});
|
|
50
|
+
filled++;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(`Finished filling in order payment customers: set ${filled} of ${result.total} payments.`);
|
|
59
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Payment } from '@stamhoofd/models';
|
|
2
2
|
import type { SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
3
|
-
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
3
|
+
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, createJoinedRelationFilter, SQL, SQLCast, SQLConcat, SQLJsonUnquote, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
4
4
|
import { balanceItemPaymentsCompilers } from './balance-item-payments.js';
|
|
5
|
+
import { organizationFilterCompilers } from './organizations.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Defines how to filter payments in the database from StamhoofdFilter objects
|
|
@@ -38,6 +39,10 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
|
|
|
38
39
|
type: SQLValueType.String,
|
|
39
40
|
nullable: true,
|
|
40
41
|
}),
|
|
42
|
+
payingOrganization: createJoinedRelationFilter(
|
|
43
|
+
SQL.join('organizations').where(SQL.column('organizations', 'id'), SQL.column(Payment.table, 'payingOrganizationId')),
|
|
44
|
+
organizationFilterCompilers,
|
|
45
|
+
),
|
|
41
46
|
invoiceId: createColumnFilter({
|
|
42
47
|
expression: SQL.column('invoiceId'),
|
|
43
48
|
type: SQLValueType.String,
|