@stamhoofd/backend 2.55.1 → 2.56.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 +4 -0
- package/package.json +11 -10
- package/src/crons.ts +4 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +150 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +27 -9
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -9
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +5 -3
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +4 -306
- package/src/helpers/AdminPermissionChecker.ts +102 -1
- package/src/helpers/AuthenticatedStructures.ts +46 -2
- package/src/helpers/EmailResumer.ts +8 -3
- package/src/seeds/1732117645-move-rrn.ts +77 -0
- package/src/services/AuditLogService.ts +681 -0
- package/src/services/BalanceItemPaymentService.ts +45 -0
- package/src/services/BalanceItemService.ts +88 -0
- package/src/services/GroupService.ts +13 -0
- package/src/services/PaymentService.ts +308 -0
- package/src/services/RegistrationService.ts +78 -0
- package/src/sql-filters/audit-logs.ts +10 -0
- package/src/sql-sorters/audit-logs.ts +35 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ManyToOneRelation } from '@simonbackx/simple-database';
|
|
2
|
+
import { BalanceItemPayment, Organization } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItemStatus } from '@stamhoofd/structures';
|
|
4
|
+
import { BalanceItemService } from './BalanceItemService';
|
|
5
|
+
|
|
6
|
+
type Loaded<T> = (T) extends ManyToOneRelation<infer Key, infer Model> ? Record<Key, Model> : never;
|
|
7
|
+
|
|
8
|
+
export const BalanceItemPaymentService = {
|
|
9
|
+
async markPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
10
|
+
// Update cached amountPaid of the balance item (balanceItemPayment will get overwritten later, but we need it to calculate the status)
|
|
11
|
+
balanceItemPayment.balanceItem.pricePaid += balanceItemPayment.price;
|
|
12
|
+
|
|
13
|
+
// Update status
|
|
14
|
+
const old = balanceItemPayment.balanceItem.status;
|
|
15
|
+
balanceItemPayment.balanceItem.updateStatus();
|
|
16
|
+
await balanceItemPayment.balanceItem.save();
|
|
17
|
+
|
|
18
|
+
// Do logic of balance item
|
|
19
|
+
if (balanceItemPayment.balanceItem.status === BalanceItemStatus.Paid && old !== BalanceItemStatus.Paid) {
|
|
20
|
+
// Only call markPaid once (if it wasn't (partially) paid before)
|
|
21
|
+
await BalanceItemService.markPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await BalanceItemService.markUpdated(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Call balanceItemPayment once a earlier succeeded payment is no longer succeeded
|
|
30
|
+
*/
|
|
31
|
+
async undoPaid(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
32
|
+
await BalanceItemService.undoPaid(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async markFailed(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
36
|
+
// Do logic of balance item
|
|
37
|
+
await BalanceItemService.markFailed(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async undoFailed(balanceItemPayment: BalanceItemPayment & Loaded<typeof BalanceItemPayment.balanceItem> & Loaded<typeof BalanceItemPayment.payment>, organization: Organization) {
|
|
41
|
+
// Reactivate deleted items
|
|
42
|
+
await BalanceItemService.undoFailed(balanceItemPayment.balanceItem, balanceItemPayment.payment, organization);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BalanceItem, Order, Organization, Payment, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItemStatus, OrderStatus } from '@stamhoofd/structures';
|
|
3
|
+
import { RegistrationService } from './RegistrationService';
|
|
4
|
+
|
|
5
|
+
export const BalanceItemService = {
|
|
6
|
+
async markPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
|
|
7
|
+
if (balanceItem.status === BalanceItemStatus.Hidden) {
|
|
8
|
+
await BalanceItem.reactivateItems([balanceItem]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// status and pricePaid changes are handled inside balanceitempayment
|
|
12
|
+
if (balanceItem.dependingBalanceItemId) {
|
|
13
|
+
const depending = await BalanceItem.getByID(balanceItem.dependingBalanceItemId);
|
|
14
|
+
if (depending && depending.status === BalanceItemStatus.Hidden) {
|
|
15
|
+
await BalanceItem.reactivateItems([depending]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// If registration
|
|
20
|
+
if (balanceItem.registrationId) {
|
|
21
|
+
await RegistrationService.markValid(balanceItem.registrationId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If order
|
|
25
|
+
if (balanceItem.orderId) {
|
|
26
|
+
const order = await Order.getByID(balanceItem.orderId);
|
|
27
|
+
if (order) {
|
|
28
|
+
await order.markPaid(payment, organization);
|
|
29
|
+
|
|
30
|
+
// Save number in balance description
|
|
31
|
+
if (order.number !== null) {
|
|
32
|
+
const webshop = await Webshop.getByID(order.webshopId);
|
|
33
|
+
|
|
34
|
+
if (webshop) {
|
|
35
|
+
balanceItem.description = order.generateBalanceDescription(webshop);
|
|
36
|
+
await balanceItem.save();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async markUpdated(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
44
|
+
// For orders: mark order as changed (so they are refetched in front ends)
|
|
45
|
+
if (balanceItem.orderId) {
|
|
46
|
+
const order = await Order.getByID(balanceItem.orderId);
|
|
47
|
+
if (order) {
|
|
48
|
+
await order.paymentChanged(payment, organization);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async undoPaid(balanceItem: BalanceItem, payment: Payment | null, organization: Organization) {
|
|
54
|
+
// If order
|
|
55
|
+
if (balanceItem.orderId) {
|
|
56
|
+
const order = await Order.getByID(balanceItem.orderId);
|
|
57
|
+
if (order) {
|
|
58
|
+
await order.undoPaid(payment, organization);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async markFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
64
|
+
// If order
|
|
65
|
+
if (balanceItem.orderId) {
|
|
66
|
+
const order = await Order.getByID(balanceItem.orderId);
|
|
67
|
+
if (order) {
|
|
68
|
+
await order.onPaymentFailed(payment, organization);
|
|
69
|
+
|
|
70
|
+
if (order.status === OrderStatus.Deleted) {
|
|
71
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
72
|
+
await balanceItem.save();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async undoFailed(balanceItem: BalanceItem, payment: Payment, organization: Organization) {
|
|
79
|
+
// If order
|
|
80
|
+
if (balanceItem.orderId) {
|
|
81
|
+
const order = await Order.getByID(balanceItem.orderId);
|
|
82
|
+
if (order) {
|
|
83
|
+
await order.undoPaymentFailed(payment, organization);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Group } from '@stamhoofd/models';
|
|
2
|
+
|
|
3
|
+
export const GroupService = {
|
|
4
|
+
async updateOccupancy(groupId: string) {
|
|
5
|
+
const group = await Group.getByID(groupId);
|
|
6
|
+
if (group) {
|
|
7
|
+
// todo: implementation should move to the service
|
|
8
|
+
await group.updateOccupancy();
|
|
9
|
+
await group.save();
|
|
10
|
+
}
|
|
11
|
+
return group;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import createMollieClient, { PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
|
|
2
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
|
|
3
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
+
import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
5
|
+
import { BuckarooHelper } from '../helpers/BuckarooHelper';
|
|
6
|
+
import { StripeHelper } from '../helpers/StripeHelper';
|
|
7
|
+
import { BalanceItemPaymentService } from './BalanceItemPaymentService';
|
|
8
|
+
|
|
9
|
+
export const PaymentService = {
|
|
10
|
+
async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
11
|
+
if (payment.status === status) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (status === PaymentStatus.Succeeded) {
|
|
16
|
+
payment.status = PaymentStatus.Succeeded;
|
|
17
|
+
payment.paidAt = new Date();
|
|
18
|
+
await payment.save();
|
|
19
|
+
|
|
20
|
+
// Prevent concurrency issues
|
|
21
|
+
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
22
|
+
const unloaded = (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment));
|
|
23
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
24
|
+
unloaded,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
28
|
+
await BalanceItemPaymentService.markPaid(balanceItemPayment, organization);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const oldStatus = payment.status;
|
|
37
|
+
|
|
38
|
+
// Save before updating balance items
|
|
39
|
+
payment.status = status;
|
|
40
|
+
payment.paidAt = null;
|
|
41
|
+
await payment.save();
|
|
42
|
+
|
|
43
|
+
// If OLD status was succeeded, we need to revert the actions
|
|
44
|
+
if (oldStatus === PaymentStatus.Succeeded) {
|
|
45
|
+
// No longer succeeded
|
|
46
|
+
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
47
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
48
|
+
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
52
|
+
await BalanceItemPaymentService.undoPaid(balanceItemPayment, organization);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Moved to failed
|
|
60
|
+
if (status == PaymentStatus.Failed) {
|
|
61
|
+
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
62
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
63
|
+
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
67
|
+
await BalanceItemPaymentService.markFailed(balanceItemPayment, organization);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If OLD status was FAILED, we need to revert the actions
|
|
75
|
+
if (oldStatus === PaymentStatus.Failed) { // OLD FAILED!! -> NOW PENDING
|
|
76
|
+
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
77
|
+
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
78
|
+
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
for (const balanceItemPayment of balanceItemPayments) {
|
|
82
|
+
await BalanceItemPaymentService.undoFailed(balanceItemPayment, organization);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
|
|
92
|
+
*/
|
|
93
|
+
async pollStatus(paymentId: string, org: Organization | null, cancel = false): Promise<Payment | undefined> {
|
|
94
|
+
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
95
|
+
return await QueueHandler.schedule('payments/' + paymentId, async () => {
|
|
96
|
+
// Get a new copy of the payment (is required to prevent concurreny bugs)
|
|
97
|
+
const payment = await Payment.getByID(paymentId);
|
|
98
|
+
if (!payment) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!payment.organizationId) {
|
|
103
|
+
console.error('Payment without organization not supported', payment.id);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const organization = org ?? await Organization.getByID(payment.organizationId);
|
|
108
|
+
if (!organization) {
|
|
109
|
+
console.error('Organization not found for payment', payment.id);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const testMode = organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production';
|
|
114
|
+
|
|
115
|
+
if (payment.status === PaymentStatus.Pending || payment.status === PaymentStatus.Created || (payment.provider === PaymentProvider.Buckaroo && payment.status === PaymentStatus.Failed)) {
|
|
116
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
117
|
+
try {
|
|
118
|
+
let status = await StripeHelper.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment), testMode);
|
|
119
|
+
|
|
120
|
+
if (this.isManualExpired(status, payment)) {
|
|
121
|
+
console.error('Manually marking Stripe payment as expired', payment.id);
|
|
122
|
+
status = PaymentStatus.Failed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
console.error('Payment check failed Stripe', payment.id, e);
|
|
129
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
130
|
+
console.error('Manually marking Stripe payment as expired', payment.id);
|
|
131
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (payment.provider === PaymentProvider.Mollie) {
|
|
136
|
+
// check status via mollie
|
|
137
|
+
const molliePayments = await MolliePayment.where({ paymentId: payment.id }, { limit: 1 });
|
|
138
|
+
if (molliePayments.length == 1) {
|
|
139
|
+
const molliePayment = molliePayments[0];
|
|
140
|
+
// check status
|
|
141
|
+
const token = await MollieToken.getTokenFor(organization.id);
|
|
142
|
+
|
|
143
|
+
if (token) {
|
|
144
|
+
try {
|
|
145
|
+
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
146
|
+
const mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
|
|
147
|
+
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log(mollieData); // log to log files to check issues
|
|
151
|
+
|
|
152
|
+
const details = mollieData.details as any;
|
|
153
|
+
if (details?.consumerName) {
|
|
154
|
+
payment.ibanName = details.consumerName;
|
|
155
|
+
}
|
|
156
|
+
if (details?.consumerAccount) {
|
|
157
|
+
payment.iban = details.consumerAccount;
|
|
158
|
+
}
|
|
159
|
+
if (details?.cardHolder) {
|
|
160
|
+
payment.ibanName = details.cardHolder;
|
|
161
|
+
}
|
|
162
|
+
if (details?.cardNumber) {
|
|
163
|
+
payment.iban = 'xxxx xxxx xxxx ' + details.cardNumber;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (mollieData.status === MolliePaymentStatus.paid) {
|
|
167
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
|
|
168
|
+
}
|
|
169
|
+
else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
|
|
170
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
171
|
+
}
|
|
172
|
+
else if (this.isManualExpired(payment.status, payment)) {
|
|
173
|
+
// Mollie still returning pending after 1 day: mark as failed
|
|
174
|
+
console.error('Manually marking Mollie payment as expired', payment.id);
|
|
175
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
console.error('Payment check failed Mollie', payment.id, e);
|
|
180
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
181
|
+
console.error('Manually marking Mollie payment as expired', payment.id);
|
|
182
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.warn('Mollie payment is missing for organization ' + organization.id + ' while checking payment status...');
|
|
188
|
+
|
|
189
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
190
|
+
console.error('Manually marking payment without mollie token as expired', payment.id);
|
|
191
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
197
|
+
console.error('Manually marking payment without mollie payments as expired', payment.id);
|
|
198
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
203
|
+
const helper = new BuckarooHelper(organization.privateMeta.buckarooSettings?.key ?? '', organization.privateMeta.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
|
|
204
|
+
try {
|
|
205
|
+
let status = await helper.getStatus(payment);
|
|
206
|
+
|
|
207
|
+
if (this.isManualExpired(status, payment)) {
|
|
208
|
+
console.error('Manually marking Buckaroo payment as expired', payment.id);
|
|
209
|
+
status = PaymentStatus.Failed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
console.error('Payment check failed Buckaroo', payment.id, e);
|
|
216
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
217
|
+
console.error('Manually marking Buckaroo payment as expired', payment.id);
|
|
218
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (payment.provider == PaymentProvider.Payconiq) {
|
|
223
|
+
// Check status
|
|
224
|
+
|
|
225
|
+
const payconiqPayments = await PayconiqPayment.where({ paymentId: payment.id }, { limit: 1 });
|
|
226
|
+
if (payconiqPayments.length == 1) {
|
|
227
|
+
const payconiqPayment = payconiqPayments[0];
|
|
228
|
+
|
|
229
|
+
if (cancel) {
|
|
230
|
+
console.error('Cancelling Payconiq payment on request', payment.id);
|
|
231
|
+
await payconiqPayment.cancel(organization);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let status = await payconiqPayment.getStatus(organization);
|
|
235
|
+
|
|
236
|
+
if (!cancel && this.shouldTryToCancel(status, payment)) {
|
|
237
|
+
console.error('Manually cancelling Payconiq payment', payment.id);
|
|
238
|
+
if (await payconiqPayment.cancel(organization)) {
|
|
239
|
+
status = PaymentStatus.Failed;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (this.isManualExpired(status, payment)) {
|
|
244
|
+
console.error('Manually marking Payconiq payment as expired', payment.id);
|
|
245
|
+
status = PaymentStatus.Failed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.warn('Payconiq payment is missing for organization ' + organization.id + ' while checking payment status...');
|
|
252
|
+
|
|
253
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
254
|
+
console.error('Manually marking Payconiq payment as expired because not found', payment.id);
|
|
255
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.error('Invalid payment provider', payment.provider, 'for payment', payment.id);
|
|
261
|
+
if (this.isManualExpired(payment.status, payment)) {
|
|
262
|
+
console.error('Manually marking unknown payment as expired', payment.id);
|
|
263
|
+
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Do a manual update if needed
|
|
269
|
+
if (payment.status === PaymentStatus.Succeeded) {
|
|
270
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
271
|
+
// Update the status
|
|
272
|
+
await StripeHelper.getStatus(payment, false, testMode);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return payment;
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
isManualExpired(status: PaymentStatus, payment: Payment) {
|
|
281
|
+
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
282
|
+
// If payment is not succeeded after one day, mark as failed
|
|
283
|
+
if (payment.createdAt < new Date(new Date().getTime() - 60 * 1000 * 60 * 24)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Try to cancel a payment that is still pending
|
|
292
|
+
*/
|
|
293
|
+
shouldTryToCancel(status: PaymentStatus, payment: Payment): boolean {
|
|
294
|
+
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
295
|
+
let timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 2 : 60 * 1000 * 30;
|
|
296
|
+
|
|
297
|
+
// If payconiq and not yet 'identified' (scanned), cancel after 5 minutes
|
|
298
|
+
if (payment.provider === PaymentProvider.Payconiq && status === PaymentStatus.Created) {
|
|
299
|
+
timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 1 : 60 * 1000 * 5;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (payment.createdAt < new Date(new Date().getTime() - timeout)) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ManyToOneRelation } from '@simonbackx/simple-database';
|
|
2
|
+
import { Document, Group, Member, Registration } from '@stamhoofd/models';
|
|
3
|
+
import { AuditLogType, EmailTemplateType } from '@stamhoofd/structures';
|
|
4
|
+
import { GroupService } from './GroupService';
|
|
5
|
+
import { AuditLogService } from './AuditLogService';
|
|
6
|
+
|
|
7
|
+
export const RegistrationService = {
|
|
8
|
+
async markValid(registrationId: string) {
|
|
9
|
+
const registration = await Registration.getByID(registrationId);
|
|
10
|
+
if (!registration) {
|
|
11
|
+
throw new Error('Registration not found');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (registration.registeredAt !== null && registration.deactivatedAt === null) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
registration.reservedUntil = null;
|
|
19
|
+
registration.registeredAt = registration.registeredAt ?? new Date();
|
|
20
|
+
registration.deactivatedAt = null;
|
|
21
|
+
registration.canRegister = false;
|
|
22
|
+
await registration.save();
|
|
23
|
+
registration.scheduleStockUpdate();
|
|
24
|
+
|
|
25
|
+
await Member.updateMembershipsForId(registration.memberId);
|
|
26
|
+
|
|
27
|
+
await registration.sendEmailTemplate({
|
|
28
|
+
type: EmailTemplateType.RegistrationConfirmation,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const member = await Member.getByID(registration.memberId);
|
|
32
|
+
if (member) {
|
|
33
|
+
const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
|
|
34
|
+
registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
|
|
35
|
+
await Document.updateForRegistration(registration.setRelation(registrationMemberRelation, member));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Update group occupancy
|
|
39
|
+
const group = await GroupService.updateOccupancy(registration.groupId);
|
|
40
|
+
|
|
41
|
+
// Create a log
|
|
42
|
+
if (member && group) {
|
|
43
|
+
await AuditLogService.log({
|
|
44
|
+
type: AuditLogType.MemberRegistered,
|
|
45
|
+
member,
|
|
46
|
+
group,
|
|
47
|
+
registration,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async deactivate(registration: Registration, group?: Group, member?: Member) {
|
|
55
|
+
if (registration.deactivatedAt !== null) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Clear the registration
|
|
60
|
+
registration.deactivatedAt = new Date();
|
|
61
|
+
await registration.save();
|
|
62
|
+
registration.scheduleStockUpdate();
|
|
63
|
+
|
|
64
|
+
await Member.updateMembershipsForId(registration.memberId);
|
|
65
|
+
|
|
66
|
+
const fetchedMember = member ?? await Member.getByID(registration.memberId);
|
|
67
|
+
const fetchedGroup = group ?? await Group.getByID(registration.groupId);
|
|
68
|
+
|
|
69
|
+
if (fetchedMember && fetchedGroup) {
|
|
70
|
+
await AuditLogService.log({
|
|
71
|
+
type: AuditLogType.MemberUnregistered,
|
|
72
|
+
member: fetchedMember,
|
|
73
|
+
group: fetchedGroup,
|
|
74
|
+
registration,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const auditLogFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
6
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
7
|
+
type: createSQLColumnFilterCompiler('type'),
|
|
8
|
+
objectId: createSQLColumnFilterCompiler('objectId'),
|
|
9
|
+
createdAt: createSQLColumnFilterCompiler('createdAt'),
|
|
10
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuditLog } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
|
|
4
|
+
export const auditLogSorters: SQLSortDefinitions<AuditLog> = {
|
|
5
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
6
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
7
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
8
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
9
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
10
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
11
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
12
|
+
|
|
13
|
+
id: {
|
|
14
|
+
getValue(a) {
|
|
15
|
+
return a.id;
|
|
16
|
+
},
|
|
17
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
18
|
+
return new SQLOrderBy({
|
|
19
|
+
column: SQL.column('id'),
|
|
20
|
+
direction,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
createdAt: {
|
|
25
|
+
getValue(a) {
|
|
26
|
+
return a.createdAt;
|
|
27
|
+
},
|
|
28
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
29
|
+
return new SQLOrderBy({
|
|
30
|
+
column: SQL.column('createdAt'),
|
|
31
|
+
direction,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|