@stamhoofd/backend 2.143.0 → 2.144.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 +17 -17
- package/src/boot.ts +1 -0
- package/src/crons/fake-settlements.test.ts +13 -11
- package/src/crons/fake-settlements.ts +22 -38
- package/src/endpoints/global/files/UploadFile.ts +53 -25
- package/src/endpoints/global/files/UploadImage.ts +32 -6
- package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
- package/src/helpers/Context.ts +1 -15
- package/src/helpers/MollieSettlementSync.test.ts +94 -41
- package/src/helpers/MollieSettlementSync.ts +174 -78
- package/src/helpers/StripeSettlementSync.test.ts +0 -85
- package/src/helpers/StripeSettlementSync.ts +37 -63
- package/src/helpers/ViesHelper.ts +18 -11
- package/src/helpers/streamForBreakdown.ts +1 -1
- package/src/services/SettlementService.test.ts +13 -85
- package/src/services/SettlementService.ts +5 -113
- package/src/sql-filters/balance-items.ts +10 -0
- package/tests/helpers/MollieMocker.ts +89 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.144.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -65,20 +65,20 @@
|
|
|
65
65
|
"@simonbackx/simple-errors": "1.5.0",
|
|
66
66
|
"@simonbackx/simple-logging": "1.0.1",
|
|
67
67
|
"@simplewebauthn/server": "13.3.2",
|
|
68
|
-
"@stamhoofd/backend-env": "2.
|
|
69
|
-
"@stamhoofd/backend-i18n": "2.
|
|
70
|
-
"@stamhoofd/backend-middleware": "2.
|
|
71
|
-
"@stamhoofd/crons": "2.
|
|
72
|
-
"@stamhoofd/email": "2.
|
|
73
|
-
"@stamhoofd/excel-writer": "2.
|
|
74
|
-
"@stamhoofd/logging": "2.
|
|
75
|
-
"@stamhoofd/models": "2.
|
|
76
|
-
"@stamhoofd/object-differ": "2.
|
|
77
|
-
"@stamhoofd/queues": "2.
|
|
78
|
-
"@stamhoofd/sql": "2.
|
|
79
|
-
"@stamhoofd/structures": "2.
|
|
80
|
-
"@stamhoofd/types": "2.
|
|
81
|
-
"@stamhoofd/utility": "2.
|
|
68
|
+
"@stamhoofd/backend-env": "2.144.0",
|
|
69
|
+
"@stamhoofd/backend-i18n": "2.144.0",
|
|
70
|
+
"@stamhoofd/backend-middleware": "2.144.0",
|
|
71
|
+
"@stamhoofd/crons": "2.144.0",
|
|
72
|
+
"@stamhoofd/email": "2.144.0",
|
|
73
|
+
"@stamhoofd/excel-writer": "2.144.0",
|
|
74
|
+
"@stamhoofd/logging": "2.144.0",
|
|
75
|
+
"@stamhoofd/models": "2.144.0",
|
|
76
|
+
"@stamhoofd/object-differ": "2.144.0",
|
|
77
|
+
"@stamhoofd/queues": "2.144.0",
|
|
78
|
+
"@stamhoofd/sql": "2.144.0",
|
|
79
|
+
"@stamhoofd/structures": "2.144.0",
|
|
80
|
+
"@stamhoofd/types": "2.144.0",
|
|
81
|
+
"@stamhoofd/utility": "2.144.0",
|
|
82
82
|
"archiver": "7.0.1",
|
|
83
83
|
"argon2": "0.44.0",
|
|
84
84
|
"axios": "1.18.1",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"uuid": "14.0.1"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@stamhoofd/test-utils": "2.
|
|
105
|
+
"@stamhoofd/test-utils": "2.144.0",
|
|
106
106
|
"@types/cookie": "0.6.0",
|
|
107
107
|
"@types/luxon": "3.7.2",
|
|
108
108
|
"@types/mailparser": "3.4.6",
|
|
@@ -117,5 +117,5 @@
|
|
|
117
117
|
"publishConfig": {
|
|
118
118
|
"access": "public"
|
|
119
119
|
},
|
|
120
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "31bc31b2a56d9516e8bc6be5779bc58af0022a88"
|
|
121
121
|
}
|
package/src/boot.ts
CHANGED
|
@@ -90,8 +90,8 @@ describe('Cron.fake-settlements', () => {
|
|
|
90
90
|
settledAt: Formatter.luxon(inWeek(2)).startOf('week').plus({ weeks: 1, days: 2 }).toJSDate(),
|
|
91
91
|
|
|
92
92
|
// Refunds are taken out of the payout, just like the provider does, and so are the
|
|
93
|
-
// fake Mollie
|
|
94
|
-
amount: 65_0000 -
|
|
93
|
+
// fake Mollie fees: 3 payments x 0.363
|
|
94
|
+
amount: 65_0000 - 3 * 36_30,
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
// All payments of the week point to the same payout
|
|
@@ -135,8 +135,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
135
135
|
];
|
|
136
136
|
|
|
137
137
|
// Same week, but four accounts: four payouts, each holding only its own money. The Mollie
|
|
138
|
-
// payouts are reduced by the fake
|
|
139
|
-
// carry no fees here
|
|
138
|
+
// payouts are reduced by the fake fee of one payment; the Stripe payments carry no fees here
|
|
140
139
|
expect(settlements.map(s => s!.amount)).toEqual([50_0000 - 36_30, 30_0000 - 36_30, 20_0000, 15_0000]);
|
|
141
140
|
expect(new Set(settlements.map(s => s!.id)).size).toBe(4);
|
|
142
141
|
expect(new Set(settlements.map(s => s!.reference)).size).toBe(4);
|
|
@@ -192,7 +191,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
192
191
|
return await Settlement.select().where('externalId', reference!.id).first(true);
|
|
193
192
|
};
|
|
194
193
|
|
|
195
|
-
test('A Mollie payout stores payment lines and
|
|
194
|
+
test('A Mollie payout stores payment lines and fee rows, and reconciles to zero', async () => {
|
|
196
195
|
const first = await createPayment({ price: 50_0000 });
|
|
197
196
|
const second = await createPayment({ price: -10_0000, type: PaymentType.Refund });
|
|
198
197
|
|
|
@@ -201,7 +200,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
201
200
|
const settlement = await getSettlementRow(first);
|
|
202
201
|
expect(settlement.provider).toBe(PaymentProvider.Mollie);
|
|
203
202
|
expect(settlement.organizationId).toBe(organization.id);
|
|
204
|
-
expect(settlement.amount).toBe(40_0000 -
|
|
203
|
+
expect(settlement.amount).toBe(40_0000 - 2 * 36_30);
|
|
205
204
|
expect(settlement.unexplainedAmount).toBe(0);
|
|
206
205
|
expect(settlement.syncedAt).not.toBeNull();
|
|
207
206
|
|
|
@@ -211,12 +210,15 @@ describe('Cron.fake-settlements', () => {
|
|
|
211
210
|
[second.id, -10_0000],
|
|
212
211
|
].sort());
|
|
213
212
|
|
|
214
|
-
const settledMonth = settlement.settledAt.getFullYear() + '-' + (settlement.settledAt.getMonth() + 1).toString().padStart(2, '0');
|
|
215
213
|
const charges = await SettlementCharge.select().where('settlementId', settlement.id).fetch();
|
|
216
|
-
expect(charges.map(c =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
]);
|
|
214
|
+
expect(charges.map(c => [c.paymentId, c.type, c.amount]).sort()).toEqual([
|
|
215
|
+
[first.id, SettlementChargeType.ProviderTransactionFee, -36_30],
|
|
216
|
+
[second.id, SettlementChargeType.ProviderTransactionFee, -36_30],
|
|
217
|
+
].sort());
|
|
218
|
+
|
|
219
|
+
// Each settled payment holds its own fee
|
|
220
|
+
expect((await Payment.getByID(first.id))!.transferFee).toBe(36_30);
|
|
221
|
+
expect((await Payment.getByID(second.id))!.transferFee).toBe(36_30);
|
|
220
222
|
});
|
|
221
223
|
|
|
222
224
|
test('A Stripe application fee is mirrored on the payout and a monthly platform payout', async () => {
|
|
@@ -34,10 +34,9 @@ const PAYOUT_DELAY_DAYS = 2;
|
|
|
34
34
|
const BATCH_SIZE = 1000;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Fake Mollie transaction fee per settled payment,
|
|
37
|
+
* Fake Mollie transaction fee per settled payment, including VAT.
|
|
38
38
|
*/
|
|
39
|
-
const MOLLIE_FEE_PER_PAYMENT =
|
|
40
|
-
const MOLLIE_FEE_VAT_PERCENTAGE = 21;
|
|
39
|
+
const MOLLIE_FEE_PER_PAYMENT = 36_30;
|
|
41
40
|
|
|
42
41
|
export async function createFakeSettlements() {
|
|
43
42
|
if (STAMHOOFD.environment !== 'development') {
|
|
@@ -183,8 +182,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
|
|
|
183
182
|
return total + serviceFee + transferFee;
|
|
184
183
|
}, 0);
|
|
185
184
|
} else if (provider === PaymentProvider.Mollie) {
|
|
186
|
-
|
|
187
|
-
fees = net + Math.round(net * MOLLIE_FEE_VAT_PERCENTAGE / 100);
|
|
185
|
+
fees = MOLLIE_FEE_PER_PAYMENT * group.length;
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
const grossAmount = group.reduce((total, payment) => total + payment.price, 0);
|
|
@@ -223,7 +221,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
|
|
|
223
221
|
if (provider === PaymentProvider.Stripe) {
|
|
224
222
|
await createStripeFeeRows(settlement, group, settledAt, platformOrganizationId);
|
|
225
223
|
} else if (provider === PaymentProvider.Mollie) {
|
|
226
|
-
await
|
|
224
|
+
await createMollieFeeRows(settlement, group);
|
|
227
225
|
}
|
|
228
226
|
|
|
229
227
|
await finishFakeSync(settlement);
|
|
@@ -322,43 +320,29 @@ async function createStripeFeeRows(settlement: Settlement, group: Payment[], set
|
|
|
322
320
|
|
|
323
321
|
// A dev database can contain fee payments (e.g. copied history): keep their derived lines
|
|
324
322
|
// in sync like a real payout walk does
|
|
325
|
-
await SettlementService.
|
|
323
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...invoicedFeeBalanceItemIds]);
|
|
326
324
|
|
|
327
325
|
await finishFakeSync(platformSettlement);
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
/**
|
|
331
|
-
* Mollie deducts its
|
|
332
|
-
* the settlement reconciles to 0
|
|
329
|
+
* Mollie deducts its fee per transaction: one fake fee charge per settled payment, linked to it
|
|
330
|
+
* like the real balance transaction walk stores, so the settlement reconciles to 0.
|
|
333
331
|
*/
|
|
334
|
-
async function
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
amount: -net,
|
|
347
|
-
settlementId: settlement.id,
|
|
348
|
-
organizationId: settlement.organizationId,
|
|
349
|
-
providerInvoiceId,
|
|
350
|
-
description: 'DEV Mollie transactiekosten',
|
|
351
|
-
occurredAt: settledAt,
|
|
352
|
-
});
|
|
332
|
+
async function createMollieFeeRows(settlement: Settlement, group: Payment[]) {
|
|
333
|
+
for (const payment of group) {
|
|
334
|
+
await SettlementService.upsertCharge({
|
|
335
|
+
type: SettlementChargeType.ProviderTransactionFee,
|
|
336
|
+
externalId: 'fake-baltr-' + payment.id,
|
|
337
|
+
amount: -MOLLIE_FEE_PER_PAYMENT,
|
|
338
|
+
settlementId: settlement.id,
|
|
339
|
+
paymentId: payment.id,
|
|
340
|
+
organizationId: settlement.organizationId,
|
|
341
|
+
description: 'DEV Mollie transactiekosten',
|
|
342
|
+
occurredAt: payment.paidAt!,
|
|
343
|
+
});
|
|
353
344
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
amount: -vat,
|
|
358
|
-
settlementId: settlement.id,
|
|
359
|
-
organizationId: settlement.organizationId,
|
|
360
|
-
providerInvoiceId,
|
|
361
|
-
description: 'DEV BTW op Mollie transactiekosten',
|
|
362
|
-
occurredAt: settledAt,
|
|
363
|
-
});
|
|
345
|
+
payment.transferFee = MOLLIE_FEE_PER_PAYMENT;
|
|
346
|
+
await payment.save();
|
|
347
|
+
}
|
|
364
348
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PutObjectCommand
|
|
1
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3'; // ES Modules import
|
|
2
2
|
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -11,7 +11,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
11
11
|
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
12
12
|
import { AutoEncoder, BooleanDecoder, field } from '@simonbackx/simple-encoding';
|
|
13
13
|
import { Context } from '../../../helpers/Context.js';
|
|
14
|
-
import { limiter } from './UploadImage.js';
|
|
14
|
+
import { limiter, organizationLimiter } from './UploadImage.js';
|
|
15
|
+
import type { Organization } from '@stamhoofd/models';
|
|
15
16
|
import { Image } from '@stamhoofd/models';
|
|
16
17
|
|
|
17
18
|
type Params = Record<string, never>;
|
|
@@ -58,11 +59,20 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
61
|
-
await Context.setOptionalOrganizationScope();
|
|
62
|
-
const { user } = await Context.
|
|
62
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
63
|
+
const { user } = await Context.optionalAuthenticate();
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// Webshops allow for file upload, but don't have a user attached
|
|
66
|
+
if (user) {
|
|
67
|
+
if (!Context.auth?.canUpload({ private: request.query.isPrivate })) {
|
|
68
|
+
throw Context.auth.error();
|
|
69
|
+
}
|
|
70
|
+
} else if (!organization) {
|
|
71
|
+
throw new SimpleError({
|
|
72
|
+
code: 'permission_denied',
|
|
73
|
+
message: 'Endpoints needs organization if no user is present',
|
|
74
|
+
human: $t('%ZlM'),
|
|
75
|
+
});
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
|
|
@@ -77,7 +87,14 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
77
87
|
throw new Error('Not supported without real request');
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
if (user) {
|
|
91
|
+
limiter.track(user.id, 1);
|
|
92
|
+
} else {
|
|
93
|
+
limiter.track(request.request.getIP(), 1);
|
|
94
|
+
if (organization) {
|
|
95
|
+
organizationLimiter.track(organization.id, 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
81
98
|
|
|
82
99
|
const form = formidable({ maxFileSize: 20 * 1024 * 1024, maxFields: 1, keepExtensions: true });
|
|
83
100
|
const file = await new Promise<FormidableFile>((resolve, reject) => {
|
|
@@ -89,34 +106,43 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
89
106
|
}));
|
|
90
107
|
return;
|
|
91
108
|
}
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
return;
|
|
109
|
+
try {
|
|
110
|
+
if (request.request.request?.destroyed) {
|
|
111
|
+
throw new Error('Request destroyed before parsing');
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
form.parse(request.request.request, (err, fields, files) => {
|
|
114
|
+
console.log('form.parse', err, fields, files);
|
|
115
|
+
if (err) {
|
|
116
|
+
reject(err);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!files.file || !Array.isArray(files.file) || files.file.length !== 1) {
|
|
121
|
+
reject(new SimpleError({
|
|
122
|
+
code: 'missing_field',
|
|
123
|
+
message: 'Missing file',
|
|
124
|
+
field: 'file',
|
|
125
|
+
}));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resolve(files.file[0]);
|
|
130
|
+
});
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error(e);
|
|
133
|
+
reject(e);
|
|
134
|
+
}
|
|
109
135
|
});
|
|
110
136
|
|
|
111
137
|
try {
|
|
112
|
-
return await this.upload(request, file, user);
|
|
138
|
+
return await this.upload(request, file, organization, user);
|
|
113
139
|
} finally {
|
|
114
140
|
// Formidable wrote the upload to a temporary file
|
|
115
141
|
await fs.rm(file.filepath, { force: true }).catch(() => { /* we can't do anything about this */ });
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
private async upload(request: DecodedRequest<Params, Query, Body>, file: FormidableFile,
|
|
145
|
+
private async upload(request: DecodedRequest<Params, Query, Body>, file: FormidableFile, organization: Organization | null, user?: { id: string }) {
|
|
120
146
|
if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
|
|
121
147
|
throw new SimpleError({
|
|
122
148
|
code: 'not_available',
|
|
@@ -156,6 +182,8 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
156
182
|
if (request.query.isPrivate && user) {
|
|
157
183
|
// Private files
|
|
158
184
|
prefix += 'users/' + user.id + '/';
|
|
185
|
+
} else if (request.query.isPrivate && !user && organization) {
|
|
186
|
+
prefix += 'anonymous/' + organization.id + '/';
|
|
159
187
|
} else {
|
|
160
188
|
// Public files
|
|
161
189
|
prefix += 'p/';
|
|
@@ -3,6 +3,7 @@ import { AutoEncoder, BooleanDecoder, field, ObjectData } from '@simonbackx/simp
|
|
|
3
3
|
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import type { Organization } from '@stamhoofd/models';
|
|
6
7
|
import { Image, RateLimiter } from '@stamhoofd/models';
|
|
7
8
|
import { Image as ImageStruct, ResolutionRequest, supportedImageTypes } from '@stamhoofd/structures';
|
|
8
9
|
import formidable from 'formidable';
|
|
@@ -44,6 +45,15 @@ export const limiter = new RateLimiter({
|
|
|
44
45
|
},
|
|
45
46
|
],
|
|
46
47
|
});
|
|
48
|
+
export const organizationLimiter = new RateLimiter({
|
|
49
|
+
limits: [
|
|
50
|
+
{
|
|
51
|
+
// Max 200 per day
|
|
52
|
+
limit: 200,
|
|
53
|
+
duration: 60 * 1000 * 60 * 24,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
47
57
|
|
|
48
58
|
export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
49
59
|
queryDecoder = Query as Decoder<Query>;
|
|
@@ -63,11 +73,20 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
66
|
-
await Context.setOptionalOrganizationScope();
|
|
67
|
-
const { user } = await Context.
|
|
76
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
77
|
+
const { user } = await Context.optionalAuthenticate();
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
// Webshops allow for file upload, but don't have a user attached
|
|
80
|
+
if (user) {
|
|
81
|
+
if (!Context.auth?.canUpload({ private: request.query.isPrivate })) {
|
|
82
|
+
throw Context.auth.error();
|
|
83
|
+
}
|
|
84
|
+
} else if (!organization) {
|
|
85
|
+
throw new SimpleError({
|
|
86
|
+
code: 'permission_denied',
|
|
87
|
+
message: 'Endpoints needs organization if no user is present',
|
|
88
|
+
human: $t('%ZlM'),
|
|
89
|
+
});
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
|
|
@@ -82,7 +101,14 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
82
101
|
throw new Error('Not supported without real request');
|
|
83
102
|
}
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
if (user) {
|
|
105
|
+
limiter.track(user.id, 1);
|
|
106
|
+
} else {
|
|
107
|
+
limiter.track(request.request.getIP(), 1);
|
|
108
|
+
if (organization) {
|
|
109
|
+
organizationLimiter.track(organization.id, 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
86
112
|
|
|
87
113
|
const form = formidable({
|
|
88
114
|
maxFileSize: 5 * 1024 * 1024,
|
|
@@ -147,7 +173,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
const fileContent = await fs.readFile(file.filepath);
|
|
150
|
-
const image = await Image.create(fileContent, imageType.contentType, resolutions, request.query.isPrivate, user);
|
|
176
|
+
const image = await Image.create(fileContent, imageType.contentType, resolutions, request.query.isPrivate, user, organization as Organization | null | undefined);
|
|
151
177
|
return new Response(ImageStruct.create(image));
|
|
152
178
|
} finally {
|
|
153
179
|
// Formidable wrote the upload to a temporary file
|
|
@@ -340,7 +340,7 @@ export class ApplicationFeeInvoicer {
|
|
|
340
340
|
// Paid now, not in the billed month: the invoices cron only picks up recent payments, so a
|
|
341
341
|
// month that is billed late would otherwise never end up on an invoice
|
|
342
342
|
await PaymentService.handlePaymentStatusUpdate(payment, sellingOrganization, PaymentStatus.Succeeded, new Date());
|
|
343
|
-
await SettlementService.
|
|
343
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeePayment(payment);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
/**
|
package/src/helpers/Context.ts
CHANGED
|
@@ -47,11 +47,6 @@ export const apiUserRateLimiter = new RateLimiter({
|
|
|
47
47
|
],
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
export class AuthorizationPostBody extends AutoEncoder {
|
|
51
|
-
@field({ decoder: StringDecoder })
|
|
52
|
-
header_authorization: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
50
|
export class ContextInstance {
|
|
56
51
|
request: Request;
|
|
57
52
|
queries: { query: string; time?: number }[] = [];
|
|
@@ -241,16 +236,7 @@ export class ContextInstance {
|
|
|
241
236
|
async authenticate(options: { allowMFASetupToken: true; allowWithoutAccount?: boolean; allowUnscoped?: boolean }): Promise<{ user: User; token: Token } | { user: User; setupToken: MFAToken }>;
|
|
242
237
|
async authenticate(options?: { allowMFASetupToken?: false | undefined; allowWithoutAccount?: boolean; allowUnscoped?: boolean }): Promise<{ user: User; token: Token }>;
|
|
243
238
|
async authenticate({ allowWithoutAccount = false, allowUnscoped = false, allowMFASetupToken = false }: { allowMFASetupToken?: boolean; allowWithoutAccount?: boolean; allowUnscoped?: boolean } = {}): Promise<{ user: User; token: Token } | { user: User; setupToken: MFAToken }> {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!header && this.request.method === 'POST') {
|
|
247
|
-
try {
|
|
248
|
-
const decoded = await DecodedRequest.fromRequest(this.request, undefined, undefined, AuthorizationPostBody as Decoder<AuthorizationPostBody>);
|
|
249
|
-
header = decoded.body.header_authorization;
|
|
250
|
-
} catch (e) {
|
|
251
|
-
// Ignore: failed to read from body
|
|
252
|
-
}
|
|
253
|
-
}
|
|
239
|
+
const header = this.request.headers.authorization;
|
|
254
240
|
|
|
255
241
|
if (!header) {
|
|
256
242
|
throw new SimpleError({
|