@stamhoofd/backend 2.83.5 → 2.84.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/index.ts +19 -4
- package/package.json +18 -14
- package/src/crons/amazon-ses.ts +26 -5
- package/src/crons/balance-emails.ts +18 -17
- package/src/email-recipient-loaders/registrations.ts +87 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
- package/src/endpoints/global/files/UploadFile.ts +11 -16
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +233 -232
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +153 -0
- package/src/helpers/AdminPermissionChecker.ts +65 -37
- package/src/helpers/AuthenticatedStructures.ts +43 -3
- package/src/helpers/Context.ts +29 -1
- package/src/helpers/GlobalHelper.ts +3 -1
- package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
- package/src/helpers/GroupedThrottledQueue.ts +108 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
- package/src/helpers/MemberCharger.ts +0 -5
- package/src/helpers/MembershipCharger.ts +3 -9
- package/src/helpers/OrganizationCharger.ts +0 -5
- package/src/helpers/ThrottledQueue.test.ts +194 -0
- package/src/helpers/ThrottledQueue.ts +145 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/services/BalanceItemPaymentService.ts +1 -33
- package/src/services/BalanceItemService.ts +167 -48
- package/src/services/FileSignService.ts +18 -13
- package/src/services/MemberRecordStore.ts +28 -19
- package/src/services/PaymentReallocationService.test.ts +25 -14
- package/src/services/PaymentReallocationService.ts +29 -10
- package/src/services/PaymentService.ts +4 -16
- package/src/services/PlatformMembershipService.ts +8 -4
- package/src/services/RegistrationService.ts +66 -2
- package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
- package/src/sql-filters/groups.ts +67 -0
- package/src/sql-filters/members.ts +33 -58
- package/src/sql-filters/organization-registration-periods.ts +8 -0
- package/src/sql-filters/registration-periods.ts +8 -0
- package/src/sql-filters/registrations.ts +11 -22
- package/src/sql-sorters/groups.ts +24 -0
- package/src/sql-sorters/organization-registration-periods.ts +24 -0
- package/src/sql-sorters/registration-periods.ts +47 -0
- package/src/sql-sorters/registrations.ts +77 -0
- package/tests/actions/patchOrganizationMember.ts +27 -0
- package/tests/actions/patchPaymentStatus.ts +45 -0
- package/tests/actions/patchUserMember.ts +27 -0
- package/tests/assertions/assertBalances.ts +49 -0
- package/tests/e2e/api-rate-limits.test.ts +5 -5
- package/tests/e2e/bundle-discounts.test.ts +4060 -0
- package/tests/e2e/charge-members.test.ts +27 -24
- package/tests/e2e/documents.test.ts +398 -0
- package/tests/e2e/register.test.ts +292 -312
- package/tests/helpers/PayconiqMocker.ts +55 -0
- package/tests/init/index.ts +5 -0
- package/tests/init/initAdmin.ts +14 -0
- package/tests/init/initBundleDiscount.ts +47 -0
- package/tests/init/initPayconiq.ts +9 -0
- package/tests/init/initPlatformAdmin.ts +13 -0
- package/tests/init/initStripe.ts +21 -0
- package/tests/jest.setup.ts +29 -0
- package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PayconiqAccount } from '@stamhoofd/structures';
|
|
2
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
3
|
+
import nock from 'nock';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
export class PayconiqMocker {
|
|
7
|
+
static infect() {
|
|
8
|
+
if (STAMHOOFD.environment !== 'test') {
|
|
9
|
+
throw new Error('PayconiqMocker can only be used in test environment');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
TestUtils.addBeforeAll(async () => {
|
|
13
|
+
this.setup();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
TestUtils.addAfterEach(() => {
|
|
17
|
+
this.reset();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static generateTestAccount() {
|
|
22
|
+
// Todo: store the api key somewhere so we can validate it in the API calls
|
|
23
|
+
return PayconiqAccount.create({
|
|
24
|
+
id: uuidv4(),
|
|
25
|
+
apiKey: 'testKey',
|
|
26
|
+
merchantId: 'test',
|
|
27
|
+
profileId: 'test',
|
|
28
|
+
name: 'test',
|
|
29
|
+
iban: 'BE56587127952688', // = random IBAN
|
|
30
|
+
callbackUrl: 'https://www.example.com',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static setup() {
|
|
35
|
+
nock('https://api.ext.payconiq.com')
|
|
36
|
+
.persist()
|
|
37
|
+
.post('/v3/payments')
|
|
38
|
+
.reply((uri, body) => {
|
|
39
|
+
// todo: do something smarter with the body
|
|
40
|
+
|
|
41
|
+
return [200, {
|
|
42
|
+
paymentId: uuidv4(),
|
|
43
|
+
_links: {
|
|
44
|
+
checkout: {
|
|
45
|
+
href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static reset() {
|
|
53
|
+
// todo: clean up any state if needed
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Organization, Token, UserFactory } from '@stamhoofd/models';
|
|
2
|
+
import { PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export async function initAdmin({ organization }: { organization: Organization }) {
|
|
5
|
+
const admin = await new UserFactory({
|
|
6
|
+
organization,
|
|
7
|
+
permissions: Permissions.create({
|
|
8
|
+
level: PermissionLevel.Full,
|
|
9
|
+
}),
|
|
10
|
+
}).create();
|
|
11
|
+
|
|
12
|
+
const adminToken = await Token.createToken(admin);
|
|
13
|
+
return { admin, adminToken };
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Group, OrganizationRegistrationPeriod } from '@stamhoofd/models';
|
|
2
|
+
import { GroupPriceDiscountType, BundleDiscount, TranslatedString, GroupPriceDiscount, ReduceablePrice, GroupPrice, BundleDiscountGroupPriceSettings } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
function createBundleDiscount({
|
|
5
|
+
name = 'Bundle discount',
|
|
6
|
+
countWholeFamily = true,
|
|
7
|
+
countPerGroup = false,
|
|
8
|
+
discounts = [
|
|
9
|
+
{ value: 10_00, type: GroupPriceDiscountType.Fixed },
|
|
10
|
+
{ value: 15_00, type: GroupPriceDiscountType.Fixed },
|
|
11
|
+
],
|
|
12
|
+
}: {
|
|
13
|
+
name?: string;
|
|
14
|
+
countWholeFamily?: boolean;
|
|
15
|
+
countPerGroup?: boolean;
|
|
16
|
+
discounts?: Array<{ value: number; type: GroupPriceDiscountType; reducedValue?: number }>;
|
|
17
|
+
} = {}) {
|
|
18
|
+
return BundleDiscount.create({
|
|
19
|
+
name: new TranslatedString(name),
|
|
20
|
+
countWholeFamily,
|
|
21
|
+
countPerGroup,
|
|
22
|
+
discounts: discounts.map(d => GroupPriceDiscount.create({
|
|
23
|
+
value: ReduceablePrice.create({
|
|
24
|
+
price: d.value,
|
|
25
|
+
reducedPrice: d.reducedValue ?? null,
|
|
26
|
+
}),
|
|
27
|
+
type: d.type,
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function enableDiscount({ group, groupPrice, bundleDiscount }: { group: Group; groupPrice: GroupPrice; bundleDiscount: BundleDiscount }) {
|
|
33
|
+
groupPrice.bundleDiscounts.set(bundleDiscount.id, BundleDiscountGroupPriceSettings.create({
|
|
34
|
+
name: bundleDiscount.name,
|
|
35
|
+
}));
|
|
36
|
+
await group.save();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a bundle discount
|
|
41
|
+
*/
|
|
42
|
+
export async function initBundleDiscount({ organizationRegistrationPeriod, discount }: { organizationRegistrationPeriod: OrganizationRegistrationPeriod; discount: Parameters<typeof createBundleDiscount>[0] }) {
|
|
43
|
+
const bundleDiscount = createBundleDiscount(discount);
|
|
44
|
+
organizationRegistrationPeriod.settings.bundleDiscounts.push(bundleDiscount);
|
|
45
|
+
await organizationRegistrationPeriod.save();
|
|
46
|
+
return bundleDiscount;
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PaymentMethod } from '@stamhoofd/structures';
|
|
2
|
+
import { PayconiqMocker } from '../helpers/PayconiqMocker';
|
|
3
|
+
import { Organization } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export async function initPayconiq({ organization }: { organization: Organization }) {
|
|
6
|
+
organization.meta.registrationPaymentConfiguration.paymentMethods.push(PaymentMethod.Payconiq);
|
|
7
|
+
organization.privateMeta.payconiqAccounts = [PayconiqMocker.generateTestAccount()];
|
|
8
|
+
await organization.save();
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Token, UserFactory } from '@stamhoofd/models';
|
|
2
|
+
import { PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export async function initPlatformAdmin() {
|
|
5
|
+
const admin = await new UserFactory({
|
|
6
|
+
globalPermissions: Permissions.create({
|
|
7
|
+
level: PermissionLevel.Full,
|
|
8
|
+
}),
|
|
9
|
+
}).create();
|
|
10
|
+
|
|
11
|
+
const adminToken = await Token.createToken(admin);
|
|
12
|
+
return { admin, adminToken };
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Organization } from '@stamhoofd/models';
|
|
2
|
+
import { StripeMocker } from '../helpers/StripeMocker';
|
|
3
|
+
import { PaymentMethod } from '@stamhoofd/structures';
|
|
4
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
5
|
+
|
|
6
|
+
export async function initStripe({ organization }: { organization: Organization }) {
|
|
7
|
+
const stripeMocker = new StripeMocker();
|
|
8
|
+
const stripeAccount = await stripeMocker.createStripeAccount(organization.id);
|
|
9
|
+
|
|
10
|
+
stripeMocker.start();
|
|
11
|
+
|
|
12
|
+
TestUtils.scheduleAfterThisTest(() => {
|
|
13
|
+
stripeMocker.stop();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
organization.meta.registrationPaymentConfiguration.paymentMethods.push(PaymentMethod.Bancontact, PaymentMethod.CreditCard, PaymentMethod.iDEAL);
|
|
17
|
+
organization.privateMeta.registrationPaymentConfiguration.stripeAccountId = stripeAccount.id;
|
|
18
|
+
await organization.save();
|
|
19
|
+
|
|
20
|
+
return { stripeMocker, stripeAccount };
|
|
21
|
+
}
|
package/tests/jest.setup.ts
CHANGED
|
@@ -11,6 +11,10 @@ import { GlobalHelper } from '../src/helpers/GlobalHelper';
|
|
|
11
11
|
import * as jose from 'jose';
|
|
12
12
|
import { TestUtils } from '@stamhoofd/test-utils';
|
|
13
13
|
import './toMatchMap';
|
|
14
|
+
import { PayconiqMocker } from './helpers/PayconiqMocker';
|
|
15
|
+
import { BalanceItemService } from '../src/services/BalanceItemService';
|
|
16
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
17
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
14
18
|
|
|
15
19
|
// Set version of saved structures
|
|
16
20
|
Column.setJSONVersion(Version);
|
|
@@ -75,6 +79,26 @@ beforeAll(async () => {
|
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
afterAll(async () => {
|
|
82
|
+
// Call twice to also wait on items that are scheduled withing scheduled tasks
|
|
83
|
+
await BalanceItemService.flushAll();
|
|
84
|
+
await BalanceItemService.flushAll();
|
|
85
|
+
QueueHandler.abortAll(
|
|
86
|
+
new SimpleError({
|
|
87
|
+
code: 'SHUTDOWN',
|
|
88
|
+
message: 'Shutting down',
|
|
89
|
+
statusCode: 503,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
await QueueHandler.awaitAll();
|
|
93
|
+
QueueHandler.abortAll(
|
|
94
|
+
new SimpleError({
|
|
95
|
+
code: 'SHUTDOWN',
|
|
96
|
+
message: 'Shutting down',
|
|
97
|
+
statusCode: 503,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
await QueueHandler.awaitAll();
|
|
101
|
+
|
|
78
102
|
// Wait for email queue etc
|
|
79
103
|
while (Email.currentQueue.length > 0) {
|
|
80
104
|
console.info('Emails still in queue. Waiting...');
|
|
@@ -83,5 +107,10 @@ afterAll(async () => {
|
|
|
83
107
|
await Database.end();
|
|
84
108
|
});
|
|
85
109
|
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
await QueueHandler.awaitAll();
|
|
112
|
+
});
|
|
113
|
+
|
|
86
114
|
TestUtils.setup();
|
|
87
115
|
EmailMocker.infect();
|
|
116
|
+
PayconiqMocker.infect();
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
-
import { BalanceItem, BalanceItemPayment, Organization, Payment } from '@stamhoofd/models';
|
|
4
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
5
|
-
import { AuditLogSource, PaymentStatus } from '@stamhoofd/structures';
|
|
6
|
-
import { AuditLogService } from '../services/AuditLogService';
|
|
7
|
-
import { BalanceItemPaymentService } from '../services/BalanceItemPaymentService';
|
|
8
|
-
|
|
9
|
-
export default new Migration(async () => {
|
|
10
|
-
if (STAMHOOFD.environment == 'test') {
|
|
11
|
-
console.log('skipped in tests');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
process.stdout.write('\n');
|
|
16
|
-
let c = 0;
|
|
17
|
-
|
|
18
|
-
await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
|
|
19
|
-
const q = Payment.select()
|
|
20
|
-
.where('status', PaymentStatus.Succeeded)
|
|
21
|
-
.where('createdAt', '>=', new Date('2024-12-12'))
|
|
22
|
-
.limit(100);
|
|
23
|
-
for await (const payment of q.all()) {
|
|
24
|
-
await fix(payment);
|
|
25
|
-
|
|
26
|
-
c += 1;
|
|
27
|
-
|
|
28
|
-
if (c % 1000 === 0) {
|
|
29
|
-
process.stdout.write('.');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
console.log('Updated ' + c + ' payments');
|
|
35
|
-
|
|
36
|
-
// Do something here
|
|
37
|
-
return Promise.resolve();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
async function fix(payment: Payment) {
|
|
41
|
-
if (payment.status !== PaymentStatus.Succeeded) {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!payment.organizationId) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const organization = await Organization.getByID(payment.organizationId);
|
|
50
|
-
|
|
51
|
-
if (!organization) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await AuditLogService.setContext({ fallbackUserId: payment.payingUserId, source: AuditLogSource.Payment, fallbackOrganizationId: payment.organizationId }, async () => {
|
|
56
|
-
// Prevent concurrency issues
|
|
57
|
-
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
58
|
-
const unloaded = (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment));
|
|
59
|
-
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
60
|
-
unloaded,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
for (const balanceItemPayment of balanceItemPayments) {
|
|
64
|
-
await BalanceItemPaymentService.markPaidRepeated(balanceItemPayment, organization);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|