@stamhoofd/backend 2.143.1 → 2.145.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 +19 -19
- package/src/crons/fake-settlements.test.ts +13 -11
- package/src/crons/fake-settlements.ts +22 -38
- package/src/crons/mollie-chargebacks.ts +31 -29
- package/src/crons/mollie-refunds.ts +61 -59
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
- package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
- package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
- package/src/endpoints/auth/MFA.security.test.ts +9 -7
- package/src/endpoints/auth/MFA.test.ts +42 -15
- package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.ts +53 -25
- package/src/endpoints/global/files/UploadImage.test.ts +2 -1
- package/src/endpoints/global/files/UploadImage.ts +32 -6
- package/src/endpoints/global/files/upload-security.test.ts +3 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
- package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
- package/src/helpers/Context.ts +14 -21
- package/src/helpers/ContextStorage.ts +5 -0
- 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 +47 -70
- package/src/helpers/TwoFactorHelper.ts +21 -22
- package/src/helpers/streamForBreakdown.ts +1 -1
- package/src/services/AdminSessionService.ts +5 -3
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/MollieService.ts +168 -126
- package/src/services/PaymentService.ts +2 -2
- package/src/services/SSOService.test.ts +25 -1
- package/src/services/SSOService.ts +17 -6
- package/src/services/SessionService.test.ts +319 -0
- package/src/services/SessionService.ts +357 -0
- package/src/services/SettlementService.test.ts +13 -85
- package/src/services/SettlementService.ts +5 -113
- package/src/services/WebshopCrowdfundingService.test.ts +4 -3
- package/src/sql-filters/balance-items.ts +10 -0
- package/src/sql-filters/registrations.ts +59 -1
- package/tests/actions/patchUserMember.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +2 -1
- package/tests/e2e/bundle-discounts.test.ts +4 -3
- package/tests/e2e/charge-members.test.ts +6 -5
- package/tests/e2e/documents.test.ts +4 -3
- package/tests/e2e/private-files.test.ts +9 -8
- package/tests/e2e/register.test.ts +4 -3
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +4 -3
- package/tests/helpers/ExportSlice.ts +2 -1
- package/tests/helpers/MollieMocker.ts +173 -38
- package/tests/init/initAdmin.ts +2 -1
- package/tests/init/initPlatformAdmin.ts +2 -1
- package/src/helpers/ViesHelper.test.ts +0 -401
- package/src/helpers/ViesHelper.ts +0 -215
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
|
@@ -662,91 +662,6 @@ describe('StripeSettlementSync', () => {
|
|
|
662
662
|
expect(after).toEqual(rowIds);
|
|
663
663
|
});
|
|
664
664
|
|
|
665
|
-
test('a payment that moved to another payout is swept', async () => {
|
|
666
|
-
const payment = await createPayment();
|
|
667
|
-
const otherPayment = await createPayment({ price: 50_00_00 });
|
|
668
|
-
const payout = stripeMocker.createPayout({ amount: 15000, arrivalDate, stripeAccount: stripeAccount.accountId });
|
|
669
|
-
|
|
670
|
-
stripeMocker.createBalanceTransaction({
|
|
671
|
-
type: 'payment',
|
|
672
|
-
amount: 10000,
|
|
673
|
-
created,
|
|
674
|
-
payout: payout.id,
|
|
675
|
-
stripeAccount: stripeAccount.accountId,
|
|
676
|
-
source: stripeMocker.createChargeObject({ metadata: { payment: payment.id } }),
|
|
677
|
-
});
|
|
678
|
-
const moved = stripeMocker.createBalanceTransaction({
|
|
679
|
-
type: 'payment',
|
|
680
|
-
amount: 5000,
|
|
681
|
-
created,
|
|
682
|
-
payout: payout.id,
|
|
683
|
-
stripeAccount: stripeAccount.accountId,
|
|
684
|
-
source: stripeMocker.createChargeObject({ metadata: { payment: otherPayment.id } }),
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
const sync = createConnectedSync();
|
|
688
|
-
expect(await sync.syncPayouts({ start })).toEqual({ synced: 1, skipped: 0, failed: 0 });
|
|
689
|
-
|
|
690
|
-
const settlement = await getSettlement(payout);
|
|
691
|
-
expect(await PaymentSettlement.select().where('settlementId', settlement.id).count()).toBe(2);
|
|
692
|
-
|
|
693
|
-
// Stripe moved the second payment to a later payout
|
|
694
|
-
moved.payout = stripeMocker.createId('po');
|
|
695
|
-
payout.amount = 10000;
|
|
696
|
-
|
|
697
|
-
expect(await sync.syncPayouts({ start, force: true })).toEqual({ synced: 1, skipped: 0, failed: 0 });
|
|
698
|
-
|
|
699
|
-
const lines = await PaymentSettlement.select().where('settlementId', settlement.id).fetch();
|
|
700
|
-
expect(lines).toHaveLength(1);
|
|
701
|
-
expect(lines[0].paymentId).toBe(payment.id);
|
|
702
|
-
expect((await getSettlement(payout)).unexplainedAmount).toBe(0);
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
test('an application fee that moved to another payout is only unlinked', async () => {
|
|
706
|
-
const payment = await createPayment();
|
|
707
|
-
const payout = stripeMocker.createPayout({ amount: 250, arrivalDate });
|
|
708
|
-
const movedFee = stripeMocker.createBalanceTransaction({
|
|
709
|
-
type: 'application_fee',
|
|
710
|
-
amount: 250,
|
|
711
|
-
created,
|
|
712
|
-
payout: payout.id,
|
|
713
|
-
source: stripeMocker.createApplicationFee({
|
|
714
|
-
amount: 250,
|
|
715
|
-
account: stripeAccount.accountId,
|
|
716
|
-
originatingTransaction: stripeMocker.createChargeObject({ metadata: { payment: payment.id, serviceFee: '30' } }),
|
|
717
|
-
}),
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
const sync = createSync();
|
|
721
|
-
expect(await sync.syncPayouts({ start })).toEqual({ synced: 1, skipped: 0, failed: 0 });
|
|
722
|
-
|
|
723
|
-
const settlement = await getSettlement(payout);
|
|
724
|
-
const applicationFeeId = ((movedFee.source as StripeObject)).id;
|
|
725
|
-
expect(await ApplicationFee.select().where('settlementId', settlement.id).count()).toBe(2);
|
|
726
|
-
|
|
727
|
-
movedFee.payout = stripeMocker.createId('po');
|
|
728
|
-
payout.amount = 0;
|
|
729
|
-
|
|
730
|
-
expect(await sync.syncPayouts({ start, force: true })).toEqual({ synced: 1, skipped: 0, failed: 0 });
|
|
731
|
-
|
|
732
|
-
// The fee rows outlive the payout: they are only unlinked, and so are the deduction
|
|
733
|
-
// charges they point at (a foreign key forbids deleting those)
|
|
734
|
-
const fees = await ApplicationFee.select().where('externalId', applicationFeeId).fetch();
|
|
735
|
-
expect(fees).toHaveLength(2);
|
|
736
|
-
for (const fee of fees) {
|
|
737
|
-
expect(fee.settlementId).toBeNull();
|
|
738
|
-
}
|
|
739
|
-
const deductions = await SettlementCharge.select().where('applicationFeeId', applicationFeeId).fetch();
|
|
740
|
-
expect(deductions).toHaveLength(2);
|
|
741
|
-
for (const row of deductions) {
|
|
742
|
-
expect(row.settlementId).toBeNull();
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const after = await getSettlement(payout);
|
|
746
|
-
expect(after.unexplainedAmount).toBe(0);
|
|
747
|
-
expect(after.pendingFees).toBe(0);
|
|
748
|
-
});
|
|
749
|
-
|
|
750
665
|
describe('Payers we can no longer reach', () => {
|
|
751
666
|
/**
|
|
752
667
|
* A platform payout that only receives one application fee, plus the payout transaction.
|
|
@@ -12,10 +12,10 @@ import { SettlementStatus } from '@stamhoofd/structures/settlements/SettlementSt
|
|
|
12
12
|
import Stripe from 'stripe';
|
|
13
13
|
|
|
14
14
|
import { ApplicationFeeService } from '../services/ApplicationFeeService.js';
|
|
15
|
-
import {
|
|
15
|
+
import { SettlementService } from '../services/SettlementService.js';
|
|
16
16
|
import { ApplicationFeeDetails } from './ApplicationFeeDetails.js';
|
|
17
|
-
import { passthroughFetch } from './passthroughFetch.js';
|
|
18
17
|
import { getPaymentIdForStripeCharge } from './getPaymentIdForStripeCharge.js';
|
|
18
|
+
import { passthroughFetch } from './passthroughFetch.js';
|
|
19
19
|
import { WebmasterReport } from './WebmasterReport.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -109,8 +109,7 @@ export class StripeSettlementSync {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Sync all paid payouts that arrived in the window.
|
|
113
|
-
* skipped so the other payouts still sync; the summary tells the caller how bad it was.
|
|
112
|
+
* Sync all paid payouts that arrived in the window.
|
|
114
113
|
*/
|
|
115
114
|
async syncPayouts({ start, end, force = false, abort }: { start: Date; end?: Date; force?: boolean; abort?: AbortSignal }): Promise<{ synced: number; skipped: number; failed: number }> {
|
|
116
115
|
const result = { synced: 0, skipped: 0, failed: 0 };
|
|
@@ -180,7 +179,7 @@ export class StripeSettlementSync {
|
|
|
180
179
|
|
|
181
180
|
// Storing a fee can link it to a balance item right away (a month the legacy invoicer
|
|
182
181
|
// billed). When it is already paid out, its payout needs the derived line for it
|
|
183
|
-
const
|
|
182
|
+
const settledApplicationFeeBalanceItems = new Set<string>();
|
|
184
183
|
|
|
185
184
|
try {
|
|
186
185
|
for await (const transaction of this.stripe.balanceTransactions.list({
|
|
@@ -195,7 +194,7 @@ export class StripeSettlementSync {
|
|
|
195
194
|
abort?.throwIfAborted();
|
|
196
195
|
|
|
197
196
|
try {
|
|
198
|
-
await this.#handleApplicationFee(transaction, {
|
|
197
|
+
await this.#handleApplicationFee(transaction, { settledApplicationFeeBalanceItems });
|
|
199
198
|
} catch (e) {
|
|
200
199
|
// The month is only invoiced after a walk without errors, so an interrupted
|
|
201
200
|
// walk has to stop the walk itself instead of joining the errors of its
|
|
@@ -209,11 +208,11 @@ export class StripeSettlementSync {
|
|
|
209
208
|
} catch (e) {
|
|
210
209
|
// The payouts of the fees stored so far still need their derived lines, but failing to
|
|
211
210
|
// update them may not hide what broke the walk
|
|
212
|
-
await SettlementService.
|
|
211
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]).catch(console.error);
|
|
213
212
|
throw e;
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
await SettlementService.
|
|
215
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]);
|
|
217
216
|
|
|
218
217
|
if (errors.length > 0) {
|
|
219
218
|
throw new SimpleError({
|
|
@@ -234,7 +233,7 @@ export class StripeSettlementSync {
|
|
|
234
233
|
* be updated afterwards. Filled while storing, not from the return value: a fee that is
|
|
235
234
|
* stored before a later one throws still needs its derived line.
|
|
236
235
|
*/
|
|
237
|
-
|
|
236
|
+
settledApplicationFeeBalanceItems?: Set<string>;
|
|
238
237
|
} = {}): Promise<{ fees: ApplicationFee[]; charges: SettlementCharge[] }> {
|
|
239
238
|
const fee = transaction.source as Stripe.ApplicationFee;
|
|
240
239
|
const payingAccountId = typeof fee.account === 'string' ? fee.account : fee.account.id;
|
|
@@ -317,7 +316,7 @@ export class StripeSettlementSync {
|
|
|
317
316
|
// An invoiced fee is explained by the derived line of its fee payment instead of by
|
|
318
317
|
// itself, so the payouts it sits in have to rebuild those lines
|
|
319
318
|
if (storedFee.balanceItemId && storedFee.settlementId) {
|
|
320
|
-
options.
|
|
319
|
+
options.settledApplicationFeeBalanceItems?.add(storedFee.balanceItemId);
|
|
321
320
|
}
|
|
322
321
|
}
|
|
323
322
|
|
|
@@ -467,12 +466,11 @@ export class StripeSettlementSync {
|
|
|
467
466
|
}
|
|
468
467
|
|
|
469
468
|
async #walkPayout(payout: Stripe.Payout, settlement: Settlement, abort: AbortSignal) {
|
|
470
|
-
const reported = new ReportedRows();
|
|
471
469
|
let transactionCount = 0;
|
|
472
470
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
const
|
|
471
|
+
// Keep track of all balance items linked to (settled) application fees that are created or updated.
|
|
472
|
+
// So we can update the settlement status of the associated payments
|
|
473
|
+
const settledApplicationFeeBalanceItems = new Set<string>();
|
|
476
474
|
|
|
477
475
|
try {
|
|
478
476
|
for await (const transaction of this.stripe.balanceTransactions.list({
|
|
@@ -482,67 +480,51 @@ export class StripeSettlementSync {
|
|
|
482
480
|
? ['data.source', 'data.source.application_fee', 'data.source.application_fee.originating_transaction', 'data.source.charge']
|
|
483
481
|
: ['data.source', 'data.source.originating_transaction', 'data.source.charge'],
|
|
484
482
|
})) {
|
|
485
|
-
// Between two transactions is a safe point to stop: only a complete walk sweeps
|
|
486
|
-
// and marks the settlement synced, so an interrupted one is re-walked from scratch
|
|
487
483
|
abort.throwIfAborted();
|
|
488
484
|
|
|
489
485
|
transactionCount += 1;
|
|
490
|
-
await this.#handleTransaction(transaction, settlement,
|
|
486
|
+
await this.#handleTransaction(transaction, settlement, settledApplicationFeeBalanceItems);
|
|
491
487
|
}
|
|
492
488
|
|
|
493
|
-
// Stripe reported nothing for money that did move: storing that as a complete sync
|
|
494
|
-
// would silently hide the whole payout
|
|
495
489
|
if (transactionCount === 0 && settlement.amount !== 0) {
|
|
496
490
|
throw new SimpleError({
|
|
497
491
|
code: 'empty_payout',
|
|
498
492
|
message: 'Payout ' + payout.id + ' of ' + settlement.amount + ' has no balance transactions',
|
|
499
493
|
});
|
|
500
494
|
}
|
|
501
|
-
|
|
502
|
-
// Only a complete walk may sweep: it can't tell a row that moved away from one this
|
|
503
|
-
// walk never reached
|
|
504
|
-
const { unlinkedFees } = await SettlementService.sweepSettlement(settlement, reported);
|
|
505
|
-
for (const fee of unlinkedFees) {
|
|
506
|
-
if (fee.balanceItemId) {
|
|
507
|
-
invoicedFeeBalanceItemIds.add(fee.balanceItemId);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
495
|
} catch (e) {
|
|
511
496
|
// The fee payments still follow the fees this walk linked before it broke (a fee may
|
|
512
497
|
// never sit in a payout that has no line for it), but failing to update them may not
|
|
513
498
|
// hide what broke the walk
|
|
514
|
-
await SettlementService.
|
|
499
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]).catch(console.error);
|
|
515
500
|
throw e;
|
|
516
501
|
}
|
|
517
502
|
|
|
518
|
-
await SettlementService.
|
|
503
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...settledApplicationFeeBalanceItems]);
|
|
519
504
|
await SettlementService.finishSync(settlement, { transactionCount });
|
|
520
505
|
}
|
|
521
506
|
|
|
522
|
-
async #handleTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement,
|
|
507
|
+
async #handleTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, settledApplicationFeeBalanceItems: Set<string>) {
|
|
523
508
|
const occurredAt = new Date(transaction.created * 1000);
|
|
524
509
|
|
|
525
|
-
// A plain string switch: the pinned SDK's type union misses some real-world types
|
|
526
|
-
// (e.g. network_cost)
|
|
527
510
|
switch (transaction.type as string) {
|
|
528
511
|
case 'charge':
|
|
529
512
|
case 'payment': {
|
|
530
513
|
const payment = await this.#resolvePayment(transaction);
|
|
531
514
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
|
|
515
|
+
if (!payment || payment.organizationId !== settlement.organizationId) {
|
|
516
|
+
// Destination charge via the platform
|
|
517
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
|
|
536
518
|
return;
|
|
537
519
|
}
|
|
538
520
|
|
|
539
|
-
|
|
521
|
+
await SettlementService.upsertPaymentLine(settlement, {
|
|
540
522
|
paymentId: payment.id,
|
|
541
523
|
amount: transaction.amount * 100,
|
|
542
524
|
externalId: transaction.id,
|
|
543
525
|
occurredAt,
|
|
544
|
-
})
|
|
545
|
-
await this.#storePaidFeesForTransaction(transaction, settlement,
|
|
526
|
+
});
|
|
527
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: payment.id });
|
|
546
528
|
await this.#updateTransferFee(transaction, settlement, payment);
|
|
547
529
|
await SettlementService.updateLegacySettlementReference(payment);
|
|
548
530
|
return;
|
|
@@ -559,30 +541,28 @@ export class StripeSettlementSync {
|
|
|
559
541
|
const isReturned = transaction.type === 'refund_failure';
|
|
560
542
|
const refunded = await this.#resolveRefundedPayment(transaction);
|
|
561
543
|
|
|
562
|
-
// The reverse of the pass-through above: refunding another organization's payment
|
|
563
|
-
// only moves the money back through our balance
|
|
564
544
|
if (refunded.organizationId !== settlement.organizationId) {
|
|
565
|
-
|
|
545
|
+
// Destination charge via the platform
|
|
546
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
|
|
566
547
|
return;
|
|
567
548
|
}
|
|
568
549
|
|
|
569
550
|
const payment = await this.#resolveReversingPayment(transaction, refunded, { negated: isReturned });
|
|
570
|
-
|
|
551
|
+
await SettlementService.upsertPaymentLine(settlement, {
|
|
571
552
|
paymentId: payment.id,
|
|
572
553
|
amount: transaction.amount * 100,
|
|
573
554
|
externalId: transaction.id,
|
|
574
555
|
occurredAt,
|
|
575
|
-
})
|
|
576
|
-
await this.#storePaidFeesForTransaction(transaction, settlement,
|
|
556
|
+
});
|
|
557
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: payment.id });
|
|
577
558
|
await SettlementService.updateLegacySettlementReference(payment);
|
|
578
559
|
return;
|
|
579
560
|
}
|
|
580
561
|
|
|
581
562
|
case 'application_fee': {
|
|
582
563
|
// The fee rows, now linked to the platform payout that contains them
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
|
|
564
|
+
await this.#handleApplicationFee(transaction, { settlementId: settlement.id, settledApplicationFeeBalanceItems });
|
|
565
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
|
|
586
566
|
return;
|
|
587
567
|
}
|
|
588
568
|
|
|
@@ -612,7 +592,7 @@ export class StripeSettlementSync {
|
|
|
612
592
|
case 'stripe_fee':
|
|
613
593
|
case 'network_cost':
|
|
614
594
|
case 'tax_fee': {
|
|
615
|
-
|
|
595
|
+
await SettlementService.upsertCharge({
|
|
616
596
|
type: transaction.type === 'tax_fee' ? SettlementChargeType.Tax : SettlementChargeType.ProviderAccountFee,
|
|
617
597
|
externalId: transaction.id,
|
|
618
598
|
amount: transaction.amount * 100,
|
|
@@ -622,7 +602,6 @@ export class StripeSettlementSync {
|
|
|
622
602
|
description: transaction.description ?? '',
|
|
623
603
|
occurredAt,
|
|
624
604
|
});
|
|
625
|
-
reported.charge(charge);
|
|
626
605
|
return;
|
|
627
606
|
}
|
|
628
607
|
|
|
@@ -630,7 +609,7 @@ export class StripeSettlementSync {
|
|
|
630
609
|
case 'reserved_funds':
|
|
631
610
|
case 'reserve_hold':
|
|
632
611
|
case 'reserve_release': {
|
|
633
|
-
|
|
612
|
+
await SettlementService.upsertCharge({
|
|
634
613
|
type: SettlementChargeType.Reserve,
|
|
635
614
|
externalId: transaction.id,
|
|
636
615
|
amount: transaction.amount * 100,
|
|
@@ -639,14 +618,13 @@ export class StripeSettlementSync {
|
|
|
639
618
|
description: transaction.description ?? '',
|
|
640
619
|
occurredAt,
|
|
641
620
|
});
|
|
642
|
-
reported.charge(charge);
|
|
643
621
|
return;
|
|
644
622
|
}
|
|
645
623
|
|
|
646
624
|
case 'adjustment':
|
|
647
625
|
case 'payment_reversal': {
|
|
648
626
|
const paymentId = await this.#tryResolveAdjustmentPayment(transaction, settlement);
|
|
649
|
-
|
|
627
|
+
await SettlementService.upsertCharge({
|
|
650
628
|
type: SettlementChargeType.Adjustment,
|
|
651
629
|
externalId: transaction.id,
|
|
652
630
|
amount: transaction.amount * 100,
|
|
@@ -657,8 +635,7 @@ export class StripeSettlementSync {
|
|
|
657
635
|
description: transaction.description ?? '',
|
|
658
636
|
occurredAt,
|
|
659
637
|
});
|
|
660
|
-
|
|
661
|
-
await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId });
|
|
638
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId });
|
|
662
639
|
return;
|
|
663
640
|
}
|
|
664
641
|
|
|
@@ -672,7 +649,7 @@ export class StripeSettlementSync {
|
|
|
672
649
|
case 'connect_collection_transfer':
|
|
673
650
|
case 'stripe_balance_payment_debit':
|
|
674
651
|
case 'stripe_balance_payment_debit_reversal': {
|
|
675
|
-
|
|
652
|
+
await SettlementService.upsertCharge({
|
|
676
653
|
type: SettlementChargeType.BalanceMovement,
|
|
677
654
|
externalId: transaction.id,
|
|
678
655
|
amount: transaction.amount * 100,
|
|
@@ -681,8 +658,7 @@ export class StripeSettlementSync {
|
|
|
681
658
|
description: transaction.description ?? transaction.type,
|
|
682
659
|
occurredAt,
|
|
683
660
|
});
|
|
684
|
-
|
|
685
|
-
await this.#storePaidFeesForTransaction(transaction, settlement, reported, { paymentId: null });
|
|
661
|
+
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
|
|
686
662
|
return;
|
|
687
663
|
}
|
|
688
664
|
|
|
@@ -703,7 +679,7 @@ export class StripeSettlementSync {
|
|
|
703
679
|
* connected account also the application fee we charged. The amounts always come from the
|
|
704
680
|
* balance transaction itself.
|
|
705
681
|
*/
|
|
706
|
-
async #storePaidFeesForTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement,
|
|
682
|
+
async #storePaidFeesForTransaction(transaction: Stripe.BalanceTransaction, settlement: Settlement, { paymentId }: { paymentId: string | null }) {
|
|
707
683
|
const occurredAt = new Date(transaction.created * 1000);
|
|
708
684
|
|
|
709
685
|
for (const [index, detail] of (transaction.fee_details ?? []).entries()) {
|
|
@@ -712,11 +688,11 @@ export class StripeSettlementSync {
|
|
|
712
688
|
}
|
|
713
689
|
|
|
714
690
|
if (detail.type === 'application_fee') {
|
|
715
|
-
await this.#storePaidApplicationFeeForTransaction(transaction, detail, settlement,
|
|
691
|
+
await this.#storePaidApplicationFeeForTransaction(transaction, detail, settlement, { paymentId });
|
|
716
692
|
continue;
|
|
717
693
|
}
|
|
718
694
|
|
|
719
|
-
|
|
695
|
+
await SettlementService.upsertCharge({
|
|
720
696
|
type: getFeeDetailType(detail, transaction),
|
|
721
697
|
externalId: transaction.id + ':fee:' + index,
|
|
722
698
|
amount: -detail.amount * 100,
|
|
@@ -729,7 +705,6 @@ export class StripeSettlementSync {
|
|
|
729
705
|
description: detail.description ?? '',
|
|
730
706
|
occurredAt,
|
|
731
707
|
});
|
|
732
|
-
reported.charge(charge);
|
|
733
708
|
}
|
|
734
709
|
}
|
|
735
710
|
|
|
@@ -740,7 +715,7 @@ export class StripeSettlementSync {
|
|
|
740
715
|
* applicationFeeId both sides of one kind sum to zero. This walk fills their settlementId; the
|
|
741
716
|
* rows themselves usually already exist (created by the fee walk).
|
|
742
717
|
*/
|
|
743
|
-
async #storePaidApplicationFeeForTransaction(transaction: Stripe.BalanceTransaction, detail: Stripe.BalanceTransaction.FeeDetail, settlement: Settlement,
|
|
718
|
+
async #storePaidApplicationFeeForTransaction(transaction: Stripe.BalanceTransaction, detail: Stripe.BalanceTransaction.FeeDetail, settlement: Settlement, { paymentId }: { paymentId: string | null }) {
|
|
744
719
|
if (!this.stripeAccount) {
|
|
745
720
|
throw new SimpleError({
|
|
746
721
|
code: 'unexpected_application_fee_detail',
|
|
@@ -803,7 +778,6 @@ export class StripeSettlementSync {
|
|
|
803
778
|
stripeAccountId: this.stripeAccount.id,
|
|
804
779
|
occurredAt,
|
|
805
780
|
});
|
|
806
|
-
reported.charge(charge);
|
|
807
781
|
|
|
808
782
|
// What the organization pays here is what we receive on the other side. The two are
|
|
809
783
|
// written from different Stripe transactions, so a divergence would silently bill the
|
|
@@ -852,7 +826,7 @@ export class StripeSettlementSync {
|
|
|
852
826
|
await payment.save();
|
|
853
827
|
}
|
|
854
828
|
|
|
855
|
-
async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment> {
|
|
829
|
+
async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment | null> {
|
|
856
830
|
const source = transaction.source;
|
|
857
831
|
if (!source || typeof source === 'string' || source.object !== 'charge') {
|
|
858
832
|
throw new SimpleError({
|
|
@@ -874,10 +848,13 @@ export class StripeSettlementSync {
|
|
|
874
848
|
|
|
875
849
|
const payment = await Payment.getByID(paymentId);
|
|
876
850
|
if (!payment) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
851
|
+
// Probably deleted
|
|
852
|
+
|
|
853
|
+
return null;
|
|
854
|
+
// throw new SimpleError({
|
|
855
|
+
// code: 'payment_not_found',
|
|
856
|
+
// message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
|
|
857
|
+
// });
|
|
881
858
|
}
|
|
882
859
|
|
|
883
860
|
this.#assertPaymentScope(payment, source.id);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
2
|
import type { I18n } from '@stamhoofd/backend-i18n/I18n';
|
|
3
|
-
import type { User } from '@stamhoofd/models';
|
|
4
|
-
import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter,
|
|
3
|
+
import type { User, Token } from '@stamhoofd/models';
|
|
4
|
+
import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, WebauthnCredential } from '@stamhoofd/models';
|
|
5
5
|
import type { User as UserStruct } from '@stamhoofd/structures';
|
|
6
|
-
import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
|
+
import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, SessionLoginMethod, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
|
|
9
9
|
|
|
10
10
|
import { PasswordForgotService } from '../services/PasswordForgotService.js';
|
|
11
|
+
import { SessionService } from '../services/SessionService.js';
|
|
11
12
|
import { RecoveryCodeHelper } from './RecoveryCodeHelper.js';
|
|
12
13
|
import { WebauthnHelper } from './WebauthnHelper.js';
|
|
13
14
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
@@ -133,33 +134,33 @@ export class TwoFactorHelper {
|
|
|
133
134
|
* credential was accepted.
|
|
134
135
|
*
|
|
135
136
|
* `loginMethod` describes the credential that was just verified:
|
|
136
|
-
* -
|
|
137
|
+
* - Password: the account password. A single credential that says nothing about
|
|
137
138
|
* whether the user still reads the email address on the account, so a required
|
|
138
139
|
* second factor must be set up here if the user does not have one yet — and a
|
|
139
140
|
* long-inactive admin has to confirm their email address before they may.
|
|
140
|
-
* -
|
|
141
|
+
* - Email: a password token or email verification code. Also a single credential,
|
|
141
142
|
* but one that only reaches someone who reads the account's email, so it is the way
|
|
142
143
|
* out of that email confirmation.
|
|
143
|
-
* -
|
|
144
|
+
* - SSO: an external identity provider already authenticated the user, and is
|
|
144
145
|
* trusted to apply its own second factor. An enrolled factor is still verified
|
|
145
146
|
* (the user asked us to protect their account), but we do not force enrollment —
|
|
146
147
|
* unless the account ALSO has a password, because then the password remains a way
|
|
147
148
|
* in that bypasses whatever the provider enforces.
|
|
148
149
|
*/
|
|
149
|
-
static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod:
|
|
150
|
+
static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<SecondFactorRequirement> {
|
|
150
151
|
if (await TwoFactorHelper.userHasFactors(user.id)) {
|
|
151
|
-
return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user) };
|
|
152
|
+
return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user, { loginMethod }) };
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
if (loginMethod ===
|
|
155
|
+
if (loginMethod === SessionLoginMethod.SSO && !user.hasPasswordBasedAccount()) {
|
|
155
156
|
return { type: 'none' };
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
if (await TwoFactorHelper.isTwoFactorRequired(user, organization)) {
|
|
159
|
-
if (loginMethod ===
|
|
160
|
+
if (loginMethod === SessionLoginMethod.Password && TwoFactorHelper.isInactiveForEnrollment(user)) {
|
|
160
161
|
return { type: 'confirm-email' };
|
|
161
162
|
}
|
|
162
|
-
return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup') };
|
|
163
|
+
return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup', { loginMethod }) };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
return { type: 'none' };
|
|
@@ -173,16 +174,14 @@ export class TwoFactorHelper {
|
|
|
173
174
|
static async sendEnrollmentConfirmationEmail(user: User, organization: Organization | null, i18n: I18n): Promise<void> {
|
|
174
175
|
try {
|
|
175
176
|
enrollmentConfirmationRateLimiter.track(user.id);
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
177
|
+
} catch {
|
|
178
178
|
// Sent often enough already.
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
try {
|
|
183
183
|
await PasswordForgotService.sendPasswordRecoveryEmail(user, organization, i18n);
|
|
184
|
-
}
|
|
185
|
-
catch (e) {
|
|
184
|
+
} catch (e) {
|
|
186
185
|
console.error('Could not send the two-factor enrollment confirmation email', e);
|
|
187
186
|
}
|
|
188
187
|
}
|
|
@@ -203,7 +202,7 @@ export class TwoFactorHelper {
|
|
|
203
202
|
* whoever holds the link could enroll one and get a session anyway, and the client
|
|
204
203
|
* needs a session to let the user choose a password before enrolling.
|
|
205
204
|
*/
|
|
206
|
-
static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod:
|
|
205
|
+
static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: SessionLoginMethod.Password | SessionLoginMethod.Email; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
|
|
207
206
|
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod });
|
|
208
207
|
|
|
209
208
|
if (requirement.type === 'confirm-email') {
|
|
@@ -228,7 +227,7 @@ export class TwoFactorHelper {
|
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
if (requirement.type === 'setup') {
|
|
231
|
-
const temporaryToken = allowTemporarySession ? new TokenStruct(await
|
|
230
|
+
const temporaryToken = allowTemporarySession ? new TokenStruct(await SessionService.createSession(user, { loginMethod, authenticatedAt: new Date() })) : null;
|
|
232
231
|
|
|
233
232
|
throw new SimpleError({
|
|
234
233
|
code: 'require_mfa_setup',
|
|
@@ -313,7 +312,7 @@ export class TwoFactorHelper {
|
|
|
313
312
|
/**
|
|
314
313
|
* Create a login MFA session token and the challenge payload returned to the client.
|
|
315
314
|
*/
|
|
316
|
-
static async createLoginChallenge(user: User): Promise<MFAChallengeResponse> {
|
|
315
|
+
static async createLoginChallenge(user: User, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<MFAChallengeResponse> {
|
|
317
316
|
const { methods, passkeys } = await TwoFactorHelper.getEnrolledMethods(user.id);
|
|
318
317
|
|
|
319
318
|
let webauthnOptions: PublicKeyCredentialRequestOptionsJSON | null = null;
|
|
@@ -323,7 +322,7 @@ export class TwoFactorHelper {
|
|
|
323
322
|
challenge = webauthnOptions.challenge;
|
|
324
323
|
}
|
|
325
324
|
|
|
326
|
-
const mfaToken = await MFAToken.createFor(user.id, 'login', challenge);
|
|
325
|
+
const mfaToken = await MFAToken.createFor(user.id, 'login', { webauthnChallenge: challenge, loginMethod });
|
|
327
326
|
|
|
328
327
|
return MFAChallengeResponse.create({
|
|
329
328
|
token: mfaToken.token,
|
|
@@ -376,12 +375,12 @@ export class TwoFactorHelper {
|
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
// Before minting the new session, so it is never signed out by its own enrollment.
|
|
379
|
-
await
|
|
378
|
+
await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
|
|
380
379
|
|
|
381
380
|
let token: TokenStruct | null = null;
|
|
382
381
|
if (setupToken) {
|
|
383
382
|
await setupToken.consume();
|
|
384
|
-
const t = await
|
|
383
|
+
const t = await SessionService.createSession(user, { loginMethod: setupToken.loginMethod, authenticatedAt: new Date() });
|
|
385
384
|
await user.markActive();
|
|
386
385
|
token = new TokenStruct(t);
|
|
387
386
|
}
|
|
@@ -403,7 +402,7 @@ export class TwoFactorHelper {
|
|
|
403
402
|
await MFARecoveryCode.deleteForUser(user.id);
|
|
404
403
|
}
|
|
405
404
|
|
|
406
|
-
await
|
|
405
|
+
await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
|
|
407
406
|
|
|
408
407
|
return await TwoFactorHelper.buildStatus(user);
|
|
409
408
|
}
|
|
@@ -14,7 +14,7 @@ import { fetchToAsyncIterator } from './fetchToAsyncIterator.js';
|
|
|
14
14
|
* They are read page by page and added up right away, so they are never all in memory at once. Above
|
|
15
15
|
* this many objects a breakdown takes too long to wait for, so we ask the user to narrow down instead.
|
|
16
16
|
*/
|
|
17
|
-
export const MAX_BREAKDOWN_OBJECTS =
|
|
17
|
+
export const MAX_BREAKDOWN_OBJECTS = 50_000;
|
|
18
18
|
|
|
19
19
|
const PAGE_SIZE = 100;
|
|
20
20
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { User } from '@stamhoofd/models';
|
|
2
|
+
|
|
3
|
+
import { SessionService } from './SessionService.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Signs out administrators in bulk.
|
|
@@ -25,7 +27,7 @@ export class AdminSessionService {
|
|
|
25
27
|
*/
|
|
26
28
|
static async signOutOrganizationAdmins(organizationId: string, keepAccessToken: string | null): Promise<number> {
|
|
27
29
|
const admins = await User.getAdmins(organizationId);
|
|
28
|
-
return await
|
|
30
|
+
return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -36,6 +38,6 @@ export class AdminSessionService {
|
|
|
36
38
|
*/
|
|
37
39
|
static async signOutPlatformAdmins(keepAccessToken: string | null): Promise<number> {
|
|
38
40
|
const admins = await User.getPlatformAdmins();
|
|
39
|
-
return await
|
|
41
|
+
return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -6,7 +6,7 @@ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
|
|
|
6
6
|
import type { Invoice as InvoiceStruct } from '@stamhoofd/structures';
|
|
7
7
|
import { EmailTemplateType, PaymentStatus, Recipient, Replacement } from '@stamhoofd/structures';
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
-
import {
|
|
9
|
+
import { ViesService } from '@stamhoofd/vies';
|
|
10
10
|
import { WebmasterReport } from '../helpers/WebmasterReport.js';
|
|
11
11
|
import { ApplicationFeeService } from './ApplicationFeeService.js';
|
|
12
12
|
import { BalanceItemService } from './BalanceItemService.js';
|
|
@@ -35,7 +35,7 @@ export class InvoiceService {
|
|
|
35
35
|
model.customer = struct.customer;
|
|
36
36
|
|
|
37
37
|
if (model.customer.company) {
|
|
38
|
-
await
|
|
38
|
+
await ViesService.checkCompany(model.customer.company, null, { forceValidation: true });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
struct.updatePrices();
|