@stamhoofd/backend 2.106.1 → 2.107.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/index.ts +6 -1
- package/package.json +17 -11
- package/src/boot.ts +28 -22
- package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +89 -0
- package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +30 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +3 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +109 -109
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +7 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -7
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +7 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +9 -2
- package/src/excel-loaders/payments.ts +5 -5
- package/src/excel-loaders/receivable-balances.ts +7 -7
- package/src/helpers/AdminPermissionChecker.ts +20 -0
- package/src/helpers/BuckarooHelper.ts +1 -1
- package/src/helpers/ServiceFeeHelper.ts +8 -4
- package/src/helpers/StripeHelper.ts +20 -35
- package/src/seeds/1752848561-groups-registration-periods.ts +35 -0
- package/src/services/BalanceItemService.ts +15 -2
- package/src/services/PaymentReallocationService.test.ts +298 -128
- package/src/services/PaymentReallocationService.ts +46 -16
- package/src/services/PaymentService.ts +49 -2
- package/src/services/uitpas/getSocialTariffForEvent.ts +2 -2
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +2 -2
- package/src/services/uitpas/registerTicketSales.ts +2 -2
- package/tests/e2e/bundle-discounts.test.ts +415 -391
- package/tests/e2e/documents.test.ts +21 -21
- package/tests/e2e/register.test.ts +93 -93
- package/tests/e2e/stock.test.ts +4 -4
|
@@ -36,7 +36,7 @@ export const PaymentReallocationService = {
|
|
|
36
36
|
const didMerge: BalanceItem[] = [];
|
|
37
37
|
|
|
38
38
|
// First try to merge balance items that are the same and have canceled variants
|
|
39
|
-
|
|
39
|
+
for (const balanceItem of balanceItems) {
|
|
40
40
|
if (balanceItem.status !== BalanceItemStatus.Due) {
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
@@ -53,7 +53,7 @@ export const PaymentReallocationService = {
|
|
|
53
53
|
await this.mergeBalanceItems(balanceItem, similarCanceledItems);
|
|
54
54
|
didMerge.push(balanceItem, ...similarCanceledItems);
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
|
57
57
|
|
|
58
58
|
if (didMerge.length) {
|
|
59
59
|
// Update outstanding
|
|
@@ -79,6 +79,13 @@ export const PaymentReallocationService = {
|
|
|
79
79
|
const negativeItems = balanceItems.filter(b => b.priceOpen < 0).map(balanceItem => ({ balanceItem, remaining: balanceItem.priceOpen }));
|
|
80
80
|
const positiveItems = balanceItems.filter(b => b.priceOpen > 0).map(balanceItem => ({ balanceItem, remaining: balanceItem.priceOpen }));
|
|
81
81
|
|
|
82
|
+
// If the total remaining is zero, we'll just merge everything together. This removes the restriction that two merged balance items should have the same remaining amount.
|
|
83
|
+
const canReachZero = (negativeItems.reduce((p, i) => p + i.remaining, 0) === -positiveItems.reduce((p, i) => p + i.remaining, 0));
|
|
84
|
+
|
|
85
|
+
// Rules:
|
|
86
|
+
// Total outstanding amount should be zero to merge all items into one payment reallocation
|
|
87
|
+
// otherwise, only swap if the amounts match exactly
|
|
88
|
+
|
|
82
89
|
if (negativeItems.length === 0 || positiveItems.length === 0) {
|
|
83
90
|
return;
|
|
84
91
|
}
|
|
@@ -88,48 +95,58 @@ export const PaymentReallocationService = {
|
|
|
88
95
|
|
|
89
96
|
const matchMethods: { alterPayments: boolean; match: (negativeItem: BalanceItemWithRemaining, positive: BalanceItemWithRemaining) => boolean }[] = [
|
|
90
97
|
{
|
|
91
|
-
// Priority 1: same relation ids, same amount
|
|
98
|
+
// Priority 1: same type, same relation ids, same amount
|
|
92
99
|
alterPayments: true,
|
|
93
100
|
match: (negativeItem, p) => {
|
|
94
101
|
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
95
102
|
},
|
|
96
103
|
},
|
|
97
104
|
{
|
|
98
|
-
// Priority 1: same relation ids, same amount
|
|
105
|
+
// Priority 1: same type, same relation ids, same amount
|
|
99
106
|
alterPayments: false,
|
|
100
107
|
match: (negativeItem, p) => {
|
|
101
108
|
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
102
109
|
},
|
|
103
110
|
},
|
|
104
111
|
{
|
|
105
|
-
// Priority 2: same relation ids, different amount
|
|
112
|
+
// Priority 2: same type, same relation ids, different amount
|
|
106
113
|
alterPayments: true,
|
|
107
114
|
match: (negativeItem, p) => {
|
|
108
115
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
109
116
|
},
|
|
110
117
|
},
|
|
111
118
|
{
|
|
112
|
-
// Priority 2: same relation ids, different amount
|
|
119
|
+
// Priority 2: same type, same relation ids, different amount
|
|
113
120
|
alterPayments: false,
|
|
114
121
|
match: (negativeItem, p) => {
|
|
115
122
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 0);
|
|
116
123
|
},
|
|
117
124
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
{
|
|
126
|
+
// Priority 3: same type, one mismatching relation id, same amount
|
|
127
|
+
alterPayments: false,
|
|
128
|
+
match: (negativeItem, p) => {
|
|
129
|
+
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 1);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
121
133
|
// Priority 3: same type, one mismatching relation id
|
|
122
134
|
alterPayments: false,
|
|
123
135
|
match: (negativeItem, p) => {
|
|
124
|
-
// todo: maybe do allow this, but only if the amount is the same
|
|
125
136
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 1);
|
|
126
137
|
},
|
|
127
138
|
},
|
|
139
|
+
{
|
|
140
|
+
// Priority 4: same type, two mismatching relation ids, same amount
|
|
141
|
+
alterPayments: false,
|
|
142
|
+
match: (negativeItem, p) => {
|
|
143
|
+
return p.remaining === -negativeItem.remaining && p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 2);
|
|
144
|
+
},
|
|
145
|
+
},
|
|
128
146
|
{
|
|
129
147
|
// Priority 4: same type, two mismatching relation ids
|
|
130
148
|
alterPayments: false,
|
|
131
149
|
match: (negativeItem, p) => {
|
|
132
|
-
// todo: maybe do allow this, but only if the amount is the same
|
|
133
150
|
return p.balanceItem.type === negativeItem.balanceItem.type && doBalanceItemRelationsMatch(p.balanceItem.relations, negativeItem.balanceItem.relations, 2);
|
|
134
151
|
},
|
|
135
152
|
},
|
|
@@ -139,8 +156,15 @@ export const PaymentReallocationService = {
|
|
|
139
156
|
match: (negativeItem, p) => {
|
|
140
157
|
return p.balanceItem.type === negativeItem.balanceItem.type && p.remaining === -negativeItem.remaining;
|
|
141
158
|
},
|
|
142
|
-
},
|
|
143
|
-
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
// Priority 6: same amount
|
|
162
|
+
alterPayments: false,
|
|
163
|
+
match: (negativeItem, p) => {
|
|
164
|
+
return p.remaining === -negativeItem.remaining;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
144
168
|
// Priority 6: same type
|
|
145
169
|
alterPayments: false,
|
|
146
170
|
match: (negativeItem, p) => {
|
|
@@ -150,10 +174,10 @@ export const PaymentReallocationService = {
|
|
|
150
174
|
{
|
|
151
175
|
// Priority: any
|
|
152
176
|
alterPayments: false,
|
|
153
|
-
match: () => {
|
|
177
|
+
match: (negativeItem, p) => {
|
|
154
178
|
return true;
|
|
155
179
|
},
|
|
156
|
-
},
|
|
180
|
+
},
|
|
157
181
|
];
|
|
158
182
|
|
|
159
183
|
for (const matchMethod of matchMethods) {
|
|
@@ -172,7 +196,7 @@ export const PaymentReallocationService = {
|
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
const match = positiveItems.find((p) => {
|
|
175
|
-
return p.remaining > 0 && matchMethod.match(negativeItem, p);
|
|
199
|
+
return p.remaining > 0 && matchMethod.match(negativeItem, p) && (matchMethod.alterPayments || canReachZero || p.remaining === -negativeItem.remaining);
|
|
176
200
|
});
|
|
177
201
|
|
|
178
202
|
if (!match) {
|
|
@@ -183,6 +207,12 @@ export const PaymentReallocationService = {
|
|
|
183
207
|
await this.swapPayments(negativeItem, match);
|
|
184
208
|
}
|
|
185
209
|
else {
|
|
210
|
+
if (match.remaining !== -negativeItem.remaining) {
|
|
211
|
+
if (!canReachZero) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
// Add to pending payment
|
|
187
217
|
const moveAmount = Math.min(match.remaining, -negativeItem.remaining);
|
|
188
218
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import createMollieClient, { PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
|
|
2
2
|
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
|
|
3
3
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
-
import { AuditLogSource, PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
4
|
+
import { AuditLogSource, BalanceItemRelation, BalanceItemStatus, BalanceItemType, PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
5
5
|
import { BuckarooHelper } from '../helpers/BuckarooHelper';
|
|
6
6
|
import { StripeHelper } from '../helpers/StripeHelper';
|
|
7
|
-
import { BalanceItemPaymentService } from './BalanceItemPaymentService';
|
|
8
7
|
import { AuditLogService } from './AuditLogService';
|
|
8
|
+
import { BalanceItemPaymentService } from './BalanceItemPaymentService';
|
|
9
9
|
import { BalanceItemService } from './BalanceItemService';
|
|
10
10
|
|
|
11
11
|
export const PaymentService = {
|
|
@@ -331,4 +331,51 @@ export const PaymentService = {
|
|
|
331
331
|
}
|
|
332
332
|
return false;
|
|
333
333
|
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Say the total amount to pay is 15,238 because (e.g. because of VAT). In that case,
|
|
337
|
+
* we'll need to round the payment to 1 cent. That can cause issues in the financial statements because
|
|
338
|
+
* the total amount of balances does not match the total amount received/paid.
|
|
339
|
+
*
|
|
340
|
+
* To fix that, we create an extra balance item with the difference. So the rounding always matches.
|
|
341
|
+
*/
|
|
342
|
+
async round(payment: Payment) {
|
|
343
|
+
if (!payment.organizationId) {
|
|
344
|
+
throw new Error('Cannot round payments without organizationId')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const amount = payment.price;
|
|
348
|
+
const rounded = Payment.roundPrice(payment.price)
|
|
349
|
+
const difference = rounded - amount;
|
|
350
|
+
|
|
351
|
+
if (difference === 0) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (difference > 100) {
|
|
356
|
+
throw new Error('Unexpected rounding difference.')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const balanceItem = new BalanceItem();
|
|
360
|
+
balanceItem.type = BalanceItemType.Rounding;
|
|
361
|
+
balanceItem.userId = payment.payingUserId;
|
|
362
|
+
balanceItem.payingOrganizationId = payment.payingOrganizationId
|
|
363
|
+
balanceItem.unitPrice = difference;
|
|
364
|
+
balanceItem.pricePaid = 0;
|
|
365
|
+
balanceItem.organizationId = payment.organizationId;
|
|
366
|
+
balanceItem.status = BalanceItemStatus.Hidden;
|
|
367
|
+
await balanceItem.save();
|
|
368
|
+
|
|
369
|
+
// Create balance item payment
|
|
370
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
371
|
+
balanceItemPayment.organizationId = payment.organizationId;
|
|
372
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
373
|
+
balanceItemPayment.price = difference;
|
|
374
|
+
balanceItemPayment.paymentId = payment.id;
|
|
375
|
+
await balanceItemPayment.save();
|
|
376
|
+
|
|
377
|
+
// Change payment total price
|
|
378
|
+
payment.price += difference
|
|
379
|
+
await payment.save();
|
|
380
|
+
}
|
|
334
381
|
};
|
|
@@ -29,7 +29,7 @@ function assertsIsStaticSocialTariffResponse(json: unknown): asserts json is Sta
|
|
|
29
29
|
export async function getSocialTariffForEvent(access_token: string, basePrice: number, uitpasEventUrl: string) {
|
|
30
30
|
const baseUrl = 'https://api-test.uitpas.be/tariffs/static';
|
|
31
31
|
const params = new URLSearchParams();
|
|
32
|
-
params.append('regularPrice', (basePrice /
|
|
32
|
+
params.append('regularPrice', (basePrice / 10_000).toFixed(2));
|
|
33
33
|
const eventId = uitpasEventUrl.split('/').pop(); // Extract the event ID from the URL
|
|
34
34
|
if (!eventId) {
|
|
35
35
|
throw new SimpleError({
|
|
@@ -86,5 +86,5 @@ export async function getSocialTariffForEvent(access_token: string, basePrice: n
|
|
|
86
86
|
if (json.available.length > 1) {
|
|
87
87
|
console.warn('Multiple social tariffs available for event', eventId, '(used ', json.available[0].price, ' as base price. All options:', json.available);
|
|
88
88
|
}
|
|
89
|
-
return Math.round(json.available[0].price * 100);
|
|
89
|
+
return Math.round(json.available[0].price * 100) * 100;
|
|
90
90
|
}
|
|
@@ -54,7 +54,7 @@ function isSocialTariffErrorResponse(
|
|
|
54
54
|
async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber: string, basePrice: number, uitpasEventUrl: string) {
|
|
55
55
|
const baseUrl = 'https://api-test.uitpas.be/tariffs';
|
|
56
56
|
const params = new URLSearchParams();
|
|
57
|
-
params.append('regularPrice', (basePrice /
|
|
57
|
+
params.append('regularPrice', (basePrice / 1_0000).toFixed(2));
|
|
58
58
|
const eventId = uitpasEventUrl.split('/').pop();
|
|
59
59
|
if (!eventId) {
|
|
60
60
|
throw new SimpleError({
|
|
@@ -156,7 +156,7 @@ async function getSocialTariffForUitpasNumber(access_token: string, uitpasNumber
|
|
|
156
156
|
console.log('Social tariff for UiTPAS number', uitpasNumber, 'with event id', uitpasEventUrl, 'is', json.available[0].price, 'euros');
|
|
157
157
|
return UitpasNumberAndPrice.create({
|
|
158
158
|
uitpasNumber,
|
|
159
|
-
price: Math.round((json.available[0].price) * 100),
|
|
159
|
+
price: Math.round((json.available[0].price) * 100) * 100,
|
|
160
160
|
uitpasTariffId: json.available[0].id,
|
|
161
161
|
});
|
|
162
162
|
}
|
|
@@ -76,7 +76,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
76
76
|
return {
|
|
77
77
|
uitpasNumber: ticketSale.uitpasNumber,
|
|
78
78
|
eventId,
|
|
79
|
-
regularPrice: (ticketSale.basePrice /
|
|
79
|
+
regularPrice: (ticketSale.basePrice / 10000).toFixed(2), // Convert from 4 decimals to 0 decimals
|
|
80
80
|
regularPriceLabel: ticketSale.basePriceLabel,
|
|
81
81
|
tariff: {
|
|
82
82
|
id: ticketSale.uitpasTariffId,
|
|
@@ -131,7 +131,7 @@ export async function registerTicketSales(access_token: string, registerTicketSa
|
|
|
131
131
|
}
|
|
132
132
|
results.set(request, {
|
|
133
133
|
ticketSaleId: ticketSale.id,
|
|
134
|
-
reducedPriceUitpas: Math.round(ticketSale.tariff.price * 100),
|
|
134
|
+
reducedPriceUitpas: Math.round(ticketSale.tariff.price * 100) * 100,
|
|
135
135
|
registeredAt: now,
|
|
136
136
|
});
|
|
137
137
|
}
|