@stamhoofd/backend 2.91.0 → 2.93.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 +10 -10
- package/src/audit-logs/EmailLogger.ts +6 -6
- package/src/crons/amazon-ses.ts +100 -4
- package/src/crons/balance-emails.ts +1 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/email-recipient-loaders/receivable-balances.ts +3 -1
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +37 -7
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +205 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +81 -26
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/helpers/EmailResumer.ts +2 -2
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
- package/src/seeds/1755876819-remove-duplicate-members.ts +145 -0
- package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/email-recipients.ts +59 -0
- package/src/sql-filters/emails.ts +95 -0
- package/src/sql-filters/members.ts +42 -1
- package/src/sql-filters/registration-periods.ts +5 -0
- package/src/sql-sorters/email-recipients.ts +69 -0
- package/src/sql-sorters/emails.ts +47 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Email, Organization, Platform } 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
|
+
console.log('Start setting senderId of emails.');
|
|
11
|
+
|
|
12
|
+
const batchSize = 100;
|
|
13
|
+
let count = 0;
|
|
14
|
+
const platform = await Platform.getShared();
|
|
15
|
+
|
|
16
|
+
for await (const email of Email.select()
|
|
17
|
+
.where('senderId', null).limit(batchSize).all()) {
|
|
18
|
+
if (!email.fromAddress) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const organization = email.organizationId ? await Organization.getByID(email.organizationId) : null;
|
|
22
|
+
if (!organization) {
|
|
23
|
+
const sender = platform.privateConfig.emails.find(s => s.email === email.fromAddress);
|
|
24
|
+
if (sender) {
|
|
25
|
+
email.senderId = sender.id;
|
|
26
|
+
await email.save();
|
|
27
|
+
count += 1;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.warn(`No sender found for email ${email.fromAddress} in platform config`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const sender = organization.privateMeta.emails.find(s => s.email === email.fromAddress);
|
|
35
|
+
if (sender) {
|
|
36
|
+
email.senderId = sender.id;
|
|
37
|
+
await email.save();
|
|
38
|
+
count += 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`No sender found for email ${email.fromAddress} in organization ${organization.id}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('Finished saving ' + count + ' emails.');
|
|
47
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
3
|
+
import { EmailRecipient } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
function stringToError(message: string) {
|
|
6
|
+
if (message === 'Recipient has hard bounced') {
|
|
7
|
+
return new SimpleErrors(
|
|
8
|
+
new SimpleError({
|
|
9
|
+
code: 'email_skipped_hard_bounce',
|
|
10
|
+
message: 'The recipient has hard bounced. This means that the email address is invalid or no longer exists.',
|
|
11
|
+
human: $t(`af49a569-ce88-48d9-ac37-81e594e16c03`),
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (message === 'Recipient has marked as spam') {
|
|
17
|
+
return new SimpleErrors(
|
|
18
|
+
new SimpleError({
|
|
19
|
+
code: 'email_skipped_spam',
|
|
20
|
+
message: 'Recipient has marked as spam',
|
|
21
|
+
human: $t(`e6523f56-397e-4127-8bf7-8396f6f25a62`),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (message === 'Recipient has unsubscribed') {
|
|
27
|
+
return new SimpleErrors(
|
|
28
|
+
new SimpleError({
|
|
29
|
+
code: 'email_skipped_unsubscribed',
|
|
30
|
+
message: 'Recipient has unsubscribed',
|
|
31
|
+
human: $t('De ontvanger heeft zich afgemeld voor e-mails'),
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (message === 'Recipient has unsubscribed from marketing') {
|
|
37
|
+
return new SimpleErrors(
|
|
38
|
+
new SimpleError({
|
|
39
|
+
code: 'email_skipped_unsubscribed',
|
|
40
|
+
message: 'Recipient has unsubscribed from marketing',
|
|
41
|
+
human: $t('De ontvanger heeft zich afgemeld voor e-mails'),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (message === 'All recipients are filtered due to hard bounce or spam') {
|
|
47
|
+
return new SimpleErrors(
|
|
48
|
+
new SimpleError({
|
|
49
|
+
code: 'all_filtered',
|
|
50
|
+
message: 'All recipients are filtered due to hard bounce or spam',
|
|
51
|
+
human: $t('Deze ontvanger komt voor op de gedeelde bounce of spamlijst. De ontvanger was eerder permanent onbereikbaar of heeft eerder een e-mail als spam gemarkeerd.'),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (message === 'Invalid email address') {
|
|
57
|
+
return new SimpleErrors(
|
|
58
|
+
new SimpleError({
|
|
59
|
+
code: 'invalid_email_address',
|
|
60
|
+
message: 'Invalid email address',
|
|
61
|
+
human: $t(`cbbff442-758c-4f76-b8c2-26bb176fefcc`),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new SimpleErrors(
|
|
67
|
+
new SimpleError({
|
|
68
|
+
code: 'unknown_error',
|
|
69
|
+
message: message,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default new Migration(async () => {
|
|
75
|
+
if (STAMHOOFD.environment === 'test') {
|
|
76
|
+
console.log('skipped in tests');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('Start setting failError object on email recipients.');
|
|
81
|
+
|
|
82
|
+
const batchSize = 100;
|
|
83
|
+
let count = 0;
|
|
84
|
+
|
|
85
|
+
for await (const r of EmailRecipient.select()
|
|
86
|
+
.whereNot('failErrorMessage', null).limit(batchSize).all()) {
|
|
87
|
+
if (!r.failErrorMessage) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
r.failError = stringToError(r.failErrorMessage);
|
|
91
|
+
await r.save();
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('Finished saving ' + count + ' recipients with error object.');
|
|
96
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Migration, SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member, mergeTwoMembers } from '@stamhoofd/models';
|
|
3
|
+
import { SQL, SQLSelect } from '@stamhoofd/sql';
|
|
4
|
+
import { PatchOrganizationMembersEndpoint } from '../endpoints/global/members/PatchOrganizationMembersEndpoint';
|
|
5
|
+
|
|
6
|
+
type MergeType = {
|
|
7
|
+
a: {
|
|
8
|
+
id: string;
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
};
|
|
13
|
+
b: {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default new Migration(async () => {
|
|
20
|
+
if (STAMHOOFD.environment === 'test') {
|
|
21
|
+
console.log('skipped in tests');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const q = new SQLSelect(
|
|
26
|
+
(row: SQLResultNamespacedRow): MergeType => {
|
|
27
|
+
return {
|
|
28
|
+
a: {
|
|
29
|
+
id: row['a'].id as string,
|
|
30
|
+
firstName: row['a'].firstName as string,
|
|
31
|
+
lastName: row['a'].lastName as string,
|
|
32
|
+
createdAt: row['a'].createdAt as Date,
|
|
33
|
+
},
|
|
34
|
+
b: {
|
|
35
|
+
id: row['b'].id as string,
|
|
36
|
+
createdAt: row['b'].createdAt as Date,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
SQL.wildcard('a'),
|
|
41
|
+
SQL.wildcard('b'),
|
|
42
|
+
)
|
|
43
|
+
.from(Member.table, 'a')
|
|
44
|
+
.join(
|
|
45
|
+
SQL.join(Member.table, 'b')
|
|
46
|
+
.where(
|
|
47
|
+
SQL.column('b', 'id'),
|
|
48
|
+
'!=',
|
|
49
|
+
SQL.column('a', 'id'),
|
|
50
|
+
)
|
|
51
|
+
.andWhere(
|
|
52
|
+
SQL.column('b', 'firstName'),
|
|
53
|
+
SQL.column('a', 'firstName'),
|
|
54
|
+
)
|
|
55
|
+
.andWhere(
|
|
56
|
+
SQL.column('b', 'lastName'),
|
|
57
|
+
SQL.column('a', 'lastName'),
|
|
58
|
+
)
|
|
59
|
+
.andWhere(
|
|
60
|
+
SQL.column('b', 'birthDay'),
|
|
61
|
+
SQL.column('a', 'birthDay'),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
.where(
|
|
65
|
+
SQL.where(
|
|
66
|
+
SQL.column('a', 'createdAt'),
|
|
67
|
+
'<',
|
|
68
|
+
SQL.column('b', 'createdAt'),
|
|
69
|
+
).or(
|
|
70
|
+
SQL.where(
|
|
71
|
+
SQL.column('b', 'createdAt'),
|
|
72
|
+
SQL.column('a', 'createdAt'),
|
|
73
|
+
).and(
|
|
74
|
+
SQL.column('a', 'id'),
|
|
75
|
+
'<',
|
|
76
|
+
SQL.column('b', 'id'),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
.orderBy(
|
|
81
|
+
SQL.column('a', 'createdAt'),
|
|
82
|
+
'ASC',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (STAMHOOFD.userMode === 'organization') {
|
|
86
|
+
q.where(
|
|
87
|
+
SQL.column('b', 'organizationId'),
|
|
88
|
+
SQL.column('organizationId'),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const duplicates = await q.fetch();
|
|
93
|
+
|
|
94
|
+
const deletedSet = new Set<string>();
|
|
95
|
+
const mergedIntoSet = new Set<string>();
|
|
96
|
+
|
|
97
|
+
for (const duplicate of duplicates) {
|
|
98
|
+
if (mergedIntoSet.has(duplicate.b.id)) {
|
|
99
|
+
console.log('Found chained duplicate in wrong order', duplicate.a.id, 'and', duplicate.b.id);
|
|
100
|
+
continue; // Already merged this one
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (deletedSet.has(duplicate.b.id)) {
|
|
104
|
+
continue; // Already deleted this one
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (deletedSet.has(duplicate.a.id)) {
|
|
108
|
+
console.log('Skipping duplicate', duplicate.a.id, 'because it was already deleted');
|
|
109
|
+
continue; // Already deleted this one
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
'Found duplicate member',
|
|
114
|
+
duplicate.a.id,
|
|
115
|
+
duplicate.a.createdAt,
|
|
116
|
+
'and',
|
|
117
|
+
duplicate.b.id,
|
|
118
|
+
duplicate.b.createdAt,
|
|
119
|
+
'with name',
|
|
120
|
+
duplicate.a.firstName, duplicate.a.lastName);
|
|
121
|
+
deletedSet.add(duplicate.b.id);
|
|
122
|
+
mergedIntoSet.add(duplicate.a.id);
|
|
123
|
+
|
|
124
|
+
// Run the merge
|
|
125
|
+
const [memberA] = await Member.getBlobByIds(duplicate.a.id);
|
|
126
|
+
const [memberB] = await Member.getBlobByIds(duplicate.b.id);
|
|
127
|
+
|
|
128
|
+
if (memberA.details.name !== memberB.details.name) {
|
|
129
|
+
console.warn('Member names do not match', memberA.details.name, 'and', memberB.details.name);
|
|
130
|
+
continue; // Names do not match, cannot merge
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (memberA.details.birthDayFormatted === null || memberA.details.birthDayFormatted !== memberB.details.birthDayFormatted) {
|
|
134
|
+
console.warn('Member birthday do not match', memberA.details.birthDayFormatted, 'and', memberB.details.birthDayFormatted);
|
|
135
|
+
continue; // Names do not match, cannot merge
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!PatchOrganizationMembersEndpoint.shouldCheckIfMemberIsDuplicate(memberA)) {
|
|
139
|
+
console.log('Skipping merge because not eligible for duplicate check');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await mergeTwoMembers(memberA, memberB);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Email } from '@stamhoofd/models';
|
|
3
|
+
import { EmailStatus } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment === 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { affectedRows } = await Email.delete()
|
|
12
|
+
.where('status', EmailStatus.Draft)
|
|
13
|
+
.where('createdAt', '<', new Date(Date.now() - 1000 * 60 * 60 * 24 * 30)) // older than 30 days
|
|
14
|
+
.delete();
|
|
15
|
+
console.log('Deleted ' + affectedRows + ' old draft emails.');
|
|
16
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Email, EmailRecipient } from '@stamhoofd/models';
|
|
3
|
+
import { SQL } from '@stamhoofd/sql';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment === 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
console.log('Start setting organizationId object on email recipients.');
|
|
12
|
+
|
|
13
|
+
const batchSize = 100;
|
|
14
|
+
let count = 0;
|
|
15
|
+
|
|
16
|
+
for await (const email of Email.select().where('organizationId', '!=', null).limit(batchSize).all()) {
|
|
17
|
+
if (!email.organizationId) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await SQL.update(EmailRecipient.table)
|
|
22
|
+
.set('organizationId', email.organizationId)
|
|
23
|
+
.where('emailId', email.id)
|
|
24
|
+
.update();
|
|
25
|
+
|
|
26
|
+
count++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('Finished saving email recipients of ' + count + ' emails with organization id.');
|
|
30
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model } from '@simonbackx/simple-database';
|
|
2
2
|
import { Order, WebshopUitpasNumber } from '@stamhoofd/models';
|
|
3
|
-
import { OrderStatus, Product, ProductPrice, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
3
|
+
import { Cart, OrderStatus, Product, ProductPrice, UitpasClientCredentialsStatus, UitpasOrganizersResponse } from '@stamhoofd/structures';
|
|
4
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
5
|
import { UitpasTokenRepository } from '../../helpers/UitpasTokenRepository';
|
|
6
6
|
import { searchUitpasOrganizers } from './searchUitpasOrganizers';
|
|
@@ -13,6 +13,7 @@ import { RegisterTicketSaleRequest, RegisterTicketSaleResponse, registerTicketSa
|
|
|
13
13
|
import { cancelTicketSales } from './cancelTicketSales';
|
|
14
14
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
15
15
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
16
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
16
17
|
|
|
17
18
|
type UitpasTicketSale = {
|
|
18
19
|
basePrice: number;
|
|
@@ -39,7 +40,7 @@ type InsertUitpasNumber = {
|
|
|
39
40
|
uitpasEventUrl: string | null; // null for non-official flow
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
43
|
+
export function shouldReserveUitpasNumbers(status: OrderStatus): boolean {
|
|
43
44
|
return status !== OrderStatus.Canceled && status !== OrderStatus.Deleted;
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -356,4 +357,72 @@ export class UitpasService {
|
|
|
356
357
|
// Clear the uitpas client credentials for the organization
|
|
357
358
|
await UitpasTokenRepository.clearClientCredentialsFor(organizationId);
|
|
358
359
|
}
|
|
360
|
+
|
|
361
|
+
static async areThereRegisteredTicketSales(webshopId: string): Promise<boolean> {
|
|
362
|
+
return await WebshopUitpasNumber.areThereRegisteredTicketSales(webshopId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
static async validateCart(organizationId: string, webshopId: string, cart: Cart, exisitingOrderId?: string): Promise<Cart> {
|
|
366
|
+
let access_token_org: string | null = null;
|
|
367
|
+
let access_token_platform: string | null = null;
|
|
368
|
+
for (const item of cart.items) {
|
|
369
|
+
if (item.uitpasNumbers.length === 0) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// verify the UiTPAS numbers are not already used for this product
|
|
374
|
+
const hasBeenUsed = await WebshopUitpasNumber.areUitpasNumbersUsed(webshopId, item.product.id, item.uitpasNumbers.map(p => p.uitpasNumber), item.product.uitpasEvent?.url, exisitingOrderId);
|
|
375
|
+
if (hasBeenUsed) {
|
|
376
|
+
throw new SimpleError({
|
|
377
|
+
code: 'uitpas_number_already_used',
|
|
378
|
+
message: 'One or more uitpas numbers are already used',
|
|
379
|
+
human: $t('f3daff19-a227-4e45-b19a-c770bd7a6687'),
|
|
380
|
+
field: 'uitpasNumbers',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (item.product.uitpasEvent) {
|
|
385
|
+
// official flow
|
|
386
|
+
const basePrice = item.product.prices.filter(price => price.id === item.productPrice.uitpasBaseProductPriceId)[0]?.price;
|
|
387
|
+
if (!basePrice) {
|
|
388
|
+
throw new SimpleError({
|
|
389
|
+
code: 'missing_uitpas_base_product_price',
|
|
390
|
+
message: `Missing UiTPAS base product price`,
|
|
391
|
+
human: $t(`3d08a166-11a7-4429-8ff7-84458bbe3e9a`),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
access_token_org = access_token_org ?? await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
396
|
+
const verified = await getSocialTariffForUitpasNumbers(access_token_org, item.uitpasNumbers.map(p => p.uitpasNumber), basePrice, item.product.uitpasEvent.url);
|
|
397
|
+
if (verified.length < item.uitpasNumbers.length) {
|
|
398
|
+
throw new SimpleError({
|
|
399
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
400
|
+
message: 'UiTPAS wrong number of prices returned',
|
|
401
|
+
human: $t('83c472b8-4bc5-4282-bbc9-1c6a2d382171'),
|
|
402
|
+
field: 'uitpasNumbers',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
for (let i = 0; i < verified.length; i++) {
|
|
406
|
+
if (item.uitpasNumbers[i].uitpasTariffId !== verified[i].uitpasTariffId) {
|
|
407
|
+
// silently update
|
|
408
|
+
item.uitpasNumbers[i].uitpasTariffId = verified[i].uitpasTariffId;
|
|
409
|
+
}
|
|
410
|
+
if (item.uitpasNumbers[i].price !== verified[i].price) {
|
|
411
|
+
throw new SimpleError({
|
|
412
|
+
code: 'uitpas_social_tariff_price_mismatch',
|
|
413
|
+
message: 'UiTPAS social tariff have a different price',
|
|
414
|
+
human: $t('9a0ad099-99e3-4341-beac-f14feb3fb9d1', { correctPrice: Formatter.price(verified[i].price), orderPrice: Formatter.price(item.uitpasNumbers[i].price) }),
|
|
415
|
+
field: 'uitpasNumbers.' + i.toString(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// non-official flow
|
|
422
|
+
access_token_platform = access_token_platform ?? await UitpasTokenRepository.getAccessTokenFor();
|
|
423
|
+
await checkUitpasNumbers(access_token_platform, item.uitpasNumbers.map(p => p.uitpasNumber));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return cart;
|
|
427
|
+
}
|
|
359
428
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLConcat, SQLFilterDefinitions, SQLScalar, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const emailRecipientsFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createColumnFilter({
|
|
6
|
+
expression: SQL.column('id'),
|
|
7
|
+
type: SQLValueType.String,
|
|
8
|
+
nullable: false,
|
|
9
|
+
}),
|
|
10
|
+
email: createColumnFilter({
|
|
11
|
+
expression: SQL.column('email'),
|
|
12
|
+
type: SQLValueType.String,
|
|
13
|
+
nullable: true,
|
|
14
|
+
}),
|
|
15
|
+
name: createColumnFilter({
|
|
16
|
+
expression: new SQLConcat(
|
|
17
|
+
SQL.column('firstName'),
|
|
18
|
+
new SQLScalar(' '),
|
|
19
|
+
SQL.column('lastName'),
|
|
20
|
+
),
|
|
21
|
+
type: SQLValueType.String,
|
|
22
|
+
nullable: true,
|
|
23
|
+
}),
|
|
24
|
+
organizationId: createColumnFilter({
|
|
25
|
+
expression: SQL.column('organizationId'),
|
|
26
|
+
type: SQLValueType.String,
|
|
27
|
+
nullable: true,
|
|
28
|
+
}),
|
|
29
|
+
emailId: createColumnFilter({
|
|
30
|
+
expression: SQL.column('emailId'),
|
|
31
|
+
type: SQLValueType.String,
|
|
32
|
+
nullable: false,
|
|
33
|
+
}),
|
|
34
|
+
sentAt: createColumnFilter({
|
|
35
|
+
expression: SQL.column('sentAt'),
|
|
36
|
+
type: SQLValueType.Datetime,
|
|
37
|
+
nullable: true,
|
|
38
|
+
}),
|
|
39
|
+
failError: createColumnFilter({
|
|
40
|
+
expression: SQL.column('failError'),
|
|
41
|
+
type: SQLValueType.JSONObject,
|
|
42
|
+
nullable: true,
|
|
43
|
+
}),
|
|
44
|
+
spamComplaintError: createColumnFilter({
|
|
45
|
+
expression: SQL.column('spamComplaintError'),
|
|
46
|
+
type: SQLValueType.String,
|
|
47
|
+
nullable: true,
|
|
48
|
+
}),
|
|
49
|
+
softBounceError: createColumnFilter({
|
|
50
|
+
expression: SQL.column('softBounceError'),
|
|
51
|
+
type: SQLValueType.String,
|
|
52
|
+
nullable: true,
|
|
53
|
+
}),
|
|
54
|
+
hardBounceError: createColumnFilter({
|
|
55
|
+
expression: SQL.column('hardBounceError'),
|
|
56
|
+
type: SQLValueType.String,
|
|
57
|
+
nullable: true,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createColumnFilter, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const emailFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createColumnFilter({
|
|
6
|
+
expression: SQL.column('id'),
|
|
7
|
+
type: SQLValueType.String,
|
|
8
|
+
nullable: false,
|
|
9
|
+
}),
|
|
10
|
+
organizationId: createColumnFilter({
|
|
11
|
+
expression: SQL.column('organizationId'),
|
|
12
|
+
type: SQLValueType.String,
|
|
13
|
+
nullable: true,
|
|
14
|
+
}),
|
|
15
|
+
userId: createColumnFilter({
|
|
16
|
+
expression: SQL.column('userId'),
|
|
17
|
+
type: SQLValueType.String,
|
|
18
|
+
nullable: true,
|
|
19
|
+
}),
|
|
20
|
+
emailType: createColumnFilter({
|
|
21
|
+
expression: SQL.column('emailType'),
|
|
22
|
+
type: SQLValueType.String,
|
|
23
|
+
nullable: true,
|
|
24
|
+
}),
|
|
25
|
+
subject: createColumnFilter({
|
|
26
|
+
expression: SQL.column('subject'),
|
|
27
|
+
type: SQLValueType.String,
|
|
28
|
+
nullable: true,
|
|
29
|
+
}),
|
|
30
|
+
fromAddress: createColumnFilter({
|
|
31
|
+
expression: SQL.column('fromAddress'),
|
|
32
|
+
type: SQLValueType.String,
|
|
33
|
+
nullable: true,
|
|
34
|
+
}),
|
|
35
|
+
text: createColumnFilter({
|
|
36
|
+
expression: SQL.column('text'),
|
|
37
|
+
type: SQLValueType.String,
|
|
38
|
+
nullable: true,
|
|
39
|
+
}),
|
|
40
|
+
status: createColumnFilter({
|
|
41
|
+
expression: SQL.column('status'),
|
|
42
|
+
type: SQLValueType.String,
|
|
43
|
+
nullable: false,
|
|
44
|
+
}),
|
|
45
|
+
recipientStatus: createColumnFilter({
|
|
46
|
+
expression: SQL.column('recipientStatus'),
|
|
47
|
+
type: SQLValueType.String,
|
|
48
|
+
nullable: false,
|
|
49
|
+
}),
|
|
50
|
+
emailRecipientsCount: createColumnFilter({
|
|
51
|
+
expression: SQL.column('emailRecipientsCount'),
|
|
52
|
+
type: SQLValueType.Number,
|
|
53
|
+
nullable: true,
|
|
54
|
+
}),
|
|
55
|
+
failedCount: createColumnFilter({
|
|
56
|
+
expression: SQL.column('failedCount'),
|
|
57
|
+
type: SQLValueType.Number,
|
|
58
|
+
nullable: false,
|
|
59
|
+
}),
|
|
60
|
+
softFailedCount: createColumnFilter({
|
|
61
|
+
expression: SQL.column('softFailedCount'),
|
|
62
|
+
type: SQLValueType.Number,
|
|
63
|
+
nullable: false,
|
|
64
|
+
}),
|
|
65
|
+
hardBouncesCount: createColumnFilter({
|
|
66
|
+
expression: SQL.column('hardBouncesCount'),
|
|
67
|
+
type: SQLValueType.Number,
|
|
68
|
+
nullable: false,
|
|
69
|
+
}),
|
|
70
|
+
softBouncesCount: createColumnFilter({
|
|
71
|
+
expression: SQL.column('softBouncesCount'),
|
|
72
|
+
type: SQLValueType.Number,
|
|
73
|
+
nullable: false,
|
|
74
|
+
}),
|
|
75
|
+
spamComplaintsCount: createColumnFilter({
|
|
76
|
+
expression: SQL.column('spamComplaintsCount'),
|
|
77
|
+
type: SQLValueType.Number,
|
|
78
|
+
nullable: false,
|
|
79
|
+
}),
|
|
80
|
+
createdAt: createColumnFilter({
|
|
81
|
+
expression: SQL.column('createdAt'),
|
|
82
|
+
type: SQLValueType.Datetime,
|
|
83
|
+
nullable: true,
|
|
84
|
+
}),
|
|
85
|
+
sentAt: createColumnFilter({
|
|
86
|
+
expression: SQL.column('sentAt'),
|
|
87
|
+
type: SQLValueType.Datetime,
|
|
88
|
+
nullable: true,
|
|
89
|
+
}),
|
|
90
|
+
senderId: createColumnFilter({
|
|
91
|
+
expression: SQL.column('senderId'),
|
|
92
|
+
type: SQLValueType.String,
|
|
93
|
+
nullable: true,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { Member } from '@stamhoofd/models';
|
|
2
|
+
import { Email, Member } from '@stamhoofd/models';
|
|
3
3
|
import { baseSQLFilterCompilers, createColumnFilter, createExistsFilter, SQL, SQLAge, SQLCast, SQLConcat, SQLFilterDefinitions, SQLValueType, SQLScalar, createWildcardColumnFilter, SQLJsonExtract } from '@stamhoofd/sql';
|
|
4
4
|
import { AccessRight } from '@stamhoofd/structures';
|
|
5
5
|
import { Context } from '../helpers/Context';
|
|
@@ -413,6 +413,47 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
413
413
|
),
|
|
414
414
|
organizationFilterCompilers,
|
|
415
415
|
),
|
|
416
|
+
'emails': createExistsFilter(
|
|
417
|
+
SQL.select()
|
|
418
|
+
.from(
|
|
419
|
+
SQL.table('email_recipients'),
|
|
420
|
+
).where(
|
|
421
|
+
SQL.column('memberId'),
|
|
422
|
+
SQL.column('members', 'id'),
|
|
423
|
+
),
|
|
424
|
+
{
|
|
425
|
+
...baseSQLFilterCompilers,
|
|
426
|
+
id: createColumnFilter({
|
|
427
|
+
expression: SQL.column('emailId'),
|
|
428
|
+
type: SQLValueType.String,
|
|
429
|
+
nullable: false,
|
|
430
|
+
checkPermission: async (filter) => {
|
|
431
|
+
if (typeof filter !== 'string') {
|
|
432
|
+
throw new SimpleError({
|
|
433
|
+
code: 'invalid_filter',
|
|
434
|
+
message: 'This filter structure is not supported here.',
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const id = filter;
|
|
438
|
+
const email = await Email.getByID(id);
|
|
439
|
+
if (!email) {
|
|
440
|
+
throw new SimpleError({
|
|
441
|
+
code: 'not_found',
|
|
442
|
+
message: 'This email does not exist.',
|
|
443
|
+
human: $t('Deze e-mail bestaat niet (meer)'),
|
|
444
|
+
statusCode: 404,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (!await Context.auth.canAccessEmail(email)) {
|
|
448
|
+
throw Context.auth.error({
|
|
449
|
+
message: 'No permissions to access this email.',
|
|
450
|
+
human: $t('Je hebt niet voldoende toegangsrechten om te filteren op deze e-mail'),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
},
|
|
456
|
+
),
|
|
416
457
|
'details': {
|
|
417
458
|
...baseSQLFilterCompilers,
|
|
418
459
|
recordAnswers: createWildcardColumnFilter(
|
|
@@ -7,6 +7,11 @@ export const registrationPeriodFilterCompilers: SQLFilterDefinitions = {
|
|
|
7
7
|
type: SQLValueType.String,
|
|
8
8
|
nullable: false,
|
|
9
9
|
}),
|
|
10
|
+
organizationId: createColumnFilter({
|
|
11
|
+
expression: SQL.column('organizationId'),
|
|
12
|
+
type: SQLValueType.String,
|
|
13
|
+
nullable: false,
|
|
14
|
+
}),
|
|
10
15
|
startDate: createColumnFilter({
|
|
11
16
|
expression: SQL.column('startDate'),
|
|
12
17
|
type: SQLValueType.Datetime,
|