IMP-251: Remove payouts page and revert "remove claims payouts" (#383)

This commit is contained in:
Rinat Arsaev 2024-09-02 13:11:05 +05:00 committed by GitHub
parent 3ddefcdfca
commit 5e3c5a5932
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 74 additions and 901 deletions

16
package-lock.json generated
View File

@ -21,11 +21,11 @@
"@angular/router": "18.0.5",
"@ngneat/input-mask": "6.0.0",
"@vality/deanonimus-proto": "2.0.1-2a02d87.0",
"@vality/domain-proto": "2.0.1-6051aa9.0",
"@vality/domain-proto": "2.0.1-e5d3c83.0",
"@vality/dominator-proto": "1.0.1-41bee97.0",
"@vality/fistful-proto": "2.0.1-88e69a5.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/magista-proto": "2.0.2-ec1bdb9.0",
"@vality/ng-core": "18.2.0",
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
@ -6571,9 +6571,9 @@
"integrity": "sha512-mokuK6w+ExASdDE6+L9bM2SaS0yVPSyBvXavY8rRtNzZgVdmj7KSRiyGmXz41grhS6jcvD8aR85Nj4o7/iogmQ=="
},
"node_modules/@vality/domain-proto": {
"version": "2.0.1-6051aa9.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.1-6051aa9.0.tgz",
"integrity": "sha512-QbiCNs316WvQHG/LTpkIhT3FTT6Mv/9vm1dKsEk0vPAQSPCvFZKZ6FKk/wEG9KCKzI1ESIJ1ClKo4n9dt8w3ew=="
"version": "2.0.1-e5d3c83.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-2.0.1-e5d3c83.0.tgz",
"integrity": "sha512-G6FpLCyx7kZZIox90PFUe0FsiAzUTtHZ42gqK7pTyHn2mOzampUuQyyn9MwPkQv6atGLXXiSzEY+qmZIWWkvHQ=="
},
"node_modules/@vality/dominator-proto": {
"version": "1.0.1-41bee97.0",
@ -6610,9 +6610,9 @@
"integrity": "sha512-HSK9WXE+cT+1skU5w3xI++GM/RO7YXaNDFL8mGMiE5Mga8hVUlb+yLBvvNM+zCslqiI+dENFQjviZeFROWH3Kw=="
},
"node_modules/@vality/magista-proto": {
"version": "2.0.2-28d11b9.0",
"resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-2.0.2-28d11b9.0.tgz",
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
"version": "2.0.2-ec1bdb9.0",
"resolved": "https://registry.npmjs.org/@vality/magista-proto/-/magista-proto-2.0.2-ec1bdb9.0.tgz",
"integrity": "sha512-XWF7qM/CARRAey0scGVhfGU6jNq+UdlGE2mg3jn4eIFDuIWQJqsT+Bah300RBUrl+XgFsmj95C6HWRfeA5Q8kw=="
},
"node_modules/@vality/ng-core": {
"version": "18.2.0",

View File

@ -29,11 +29,11 @@
"@angular/router": "18.0.5",
"@ngneat/input-mask": "6.0.0",
"@vality/deanonimus-proto": "2.0.1-2a02d87.0",
"@vality/domain-proto": "2.0.1-6051aa9.0",
"@vality/domain-proto": "2.0.1-e5d3c83.0",
"@vality/dominator-proto": "1.0.1-41bee97.0",
"@vality/fistful-proto": "2.0.1-88e69a5.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/magista-proto": "2.0.2-ec1bdb9.0",
"@vality/ng-core": "18.2.0",
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",

View File

@ -9,8 +9,6 @@ import {
StatPaymentResponse,
RefundSearchQuery,
StatRefundResponse,
PayoutSearchQuery,
StatPayoutResponse,
ChargebackSearchQuery,
} from '@vality/magista-proto/magista';
import { combineLatest, from, map, Observable, switchMap } from 'rxjs';
@ -58,11 +56,6 @@ export class MerchantStatisticsService {
return this.client$.pipe(switchMap((c) => c.SearchRefunds(refundSearchQuery)));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
SearchPayouts(payoutSearchQuery: PayoutSearchQuery): Observable<StatPayoutResponse> {
return this.client$.pipe(switchMap((c) => c.SearchPayouts(payoutSearchQuery)));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
SearchChargebacks(chargebackSearchQuery: ChargebackSearchQuery) {
return this.client$.pipe(switchMap((c) => c.SearchChargebacks(chargebackSearchQuery)));

View File

@ -9,7 +9,7 @@ import { AppAuthGuardService } from '@cc/app/shared/services';
[
{
path: '',
redirectTo: '/payouts',
redirectTo: '/payments',
pathMatch: 'full',
},
],

View File

@ -13,7 +13,6 @@ import { ROUTING_CONFIG as CLAIMS_ROUTING_CONFIG } from './sections/claims/routi
import { ROUTING_CONFIG as DEPOSITS_ROUTING_CONFIG } from './sections/deposits/routing-config';
import { ROUTING_CONFIG as DOMAIN_ROUTING_CONFIG } from './sections/domain/routing-config';
import { ROUTING_CONFIG as PAYMENTS_ROUTING_CONFIG } from './sections/payments/routing-config';
import { ROUTING_CONFIG as PAYOUTS_ROUTING_CONFIG } from './sections/payouts/payouts/routing-config';
import { ROUTING_CONFIG as REPAIRING_ROUTING_CONFIG } from './sections/repairing/routing-config';
import { ROUTING_CONFIG as PARTIES_ROUTING_CONFIG } from './sections/search-parties/routing-config';
import { SHOPS_ROUTING_CONFIG } from './sections/shops';
@ -120,11 +119,6 @@ export class AppComponent {
url: '/old-payments',
services: PAYMENTS_ROUTING_CONFIG.services,
},
{
label: 'Payouts',
url: '/payouts',
services: PAYOUTS_ROUTING_CONFIG.services,
},
{
label: 'Chargebacks',
url: '/chargebacks',

View File

@ -24,7 +24,6 @@ import { ToolbarComponent } from './core/components/toolbar/toolbar.component';
import { CoreModule } from './core/core.module';
import icons from './icons.json';
import { ClaimsModule } from './sections/claims/claims.module';
import { PayoutsModule } from './sections/payouts';
import { SearchPartiesModule } from './sections/search-parties/search-parties.module';
import { SectionsModule } from './sections/sections.module';
import { CandidateCardComponent } from './shared/components/candidate-card/candidate-card.component';
@ -60,7 +59,6 @@ registerLocaleData(localeRu);
SearchPartiesModule,
ClaimsModule,
KeycloakTokenInfoModule,
PayoutsModule,
SectionsModule,
SidenavInfoComponent,
ToolbarComponent,

View File

@ -153,6 +153,7 @@ export class CreateShopDialogComponent
this.form.value;
const contractorId = short().uuid();
const contractId = short().uuid();
const payoutToolId = short().generate();
const shopId = short().uuid();
this.claimManagementService
.UpdateClaim(
@ -184,6 +185,27 @@ export class CreateShopDialogComponent
},
},
},
{
party_modification: {
contract_modification: {
id: contractId,
modification: {
payout_tool_modification: {
payout_tool_id: payoutToolId,
modification: {
creation: {
currency: currency,
tool_info: {
russian_bank_account:
DEFAULT_RUSSIAN_BANK_ACCOUNT,
},
},
},
},
},
},
},
},
{
party_modification: {
shop_modification: {
@ -194,6 +216,7 @@ export class CreateShopDialogComponent
category: category,
location: DEFAULT_SHOP_LOCATION,
contract_id: contractId,
payout_tool_id: payoutToolId,
},
},
},

View File

@ -47,6 +47,12 @@ export const MODIFICATIONS_NAME_TREE: ModificationsNameTree<Modification> = {
creation: 'Contract Adjustment Creation',
},
},
payout_tool_modification: {
modification: {
creation: 'Contract Payout Tool Creation',
info_modification: 'Contract Payout Tool Info',
},
},
legal_agreement_binding: 'Contract Legal Agreement Binding',
report_preferences_modification: 'Contract Report Preferences',
contractor_modification: 'Contract Contractor',
@ -58,8 +64,10 @@ export const MODIFICATIONS_NAME_TREE: ModificationsNameTree<Modification> = {
category_modification: 'Shop Category',
details_modification: 'Shop Details',
contract_modification: 'Shop Contract',
payout_tool_modification: 'Shop Payout Tool',
location_modification: 'Shop Location',
shop_account_creation: 'Shop Account Creation',
payout_schedule_modification: 'Shop Schedule',
cash_register_modification_unit: {
modification: {
creation: 'Shop Cash Register Creation',

View File

@ -1 +0,0 @@
export * from './payouts.module';

View File

@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { PayoutDetailsComponent } from './payout-details.component';
import { ROUTING_CONFIG } from './routing-config';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: PayoutDetailsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
},
]),
],
exports: [RouterModule],
})
export class PayoutDetailsRoutingModule {}

View File

@ -1,70 +0,0 @@
<cc-page-layout [description]="(payout$ | async)?.id" [progress]="progress$ | async" title="Payout">
<cc-page-layout-actions *ngIf="payout$ | async as payout">
<button
[disabled]="canBeCancelled(payout.status | ngtUnionKey)"
color="warn"
mat-raised-button
(click)="cancel(payout.id)"
>
Cancel
</button>
<button
[disabled]="canBeConfirmed(payout.status | ngtUnionKey)"
color="primary"
mat-raised-button
(click)="confirm(payout.id)"
>
Confirm
</button>
</cc-page-layout-actions>
<ng-container *ngIf="payout$ | async as payout">
<mat-card>
<mat-card-content style="display: grid; gap: 16px">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px">
<cc-details-item title="Payout ID">{{ payout.payout_id }}</cc-details-item>
<cc-details-item title="Amount">
{{ payout.amount | ccFormatAmount }}
{{ payout.currency.symbolic_code | ccCurrency }}
</cc-details-item>
<cc-details-item title="Fee">
{{ payout.fee | ccFormatAmount }}
{{ payout.currency.symbolic_code | ccCurrency }}
</cc-details-item>
<cc-details-item title="Status">{{
payout.status | ngtUnionKey
}}</cc-details-item>
<cc-details-item *ngIf="payout.status.cancelled" title="Status Details">{{
payout.status.cancelled.details
}}</cc-details-item>
<cc-details-item title="Created At">{{
payout.created_at | date: 'dd.MM.yyyy HH:mm:ss'
}}</cc-details-item>
</div>
<ng-container *ngIf="party$ | async as party">
<div><mat-divider></mat-divider></div>
<h2 class="mat-headline-4">Party</h2>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px">
<cc-details-item title="ID">{{ payout.party_id }}</cc-details-item>
<cc-details-item title="Email">
{{ party.contact_info?.email }}
</cc-details-item>
</div>
</ng-container>
<div><mat-divider></mat-divider></div>
<h2 class="mat-headline-4">Shop</h2>
<cc-shop-details [shop]="shop$ | async"></cc-shop-details>
<ng-container *ngIf="payoutTool$ | async as payoutTool">
<div><mat-divider></mat-divider></div>
<h2 class="mat-headline-4">Payout Tool</h2>
<cc-payout-tool-details [payoutTool]="payoutTool"></cc-payout-tool-details>
</ng-container>
</mat-card-content>
</mat-card>
<h2 class="mat-h1 mat-no-margin">Cash Flow</h2>
<v-table [columns]="cashFlowColumns" [data]="payout.cash_flow"></v-table>
</ng-container>
</cc-page-layout>

View File

@ -1,93 +0,0 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PayoutID, PayoutStatus } from '@vality/magista-proto/magista';
import { Column, progressTo } from '@vality/ng-core';
import { getUnionKey, getUnionValue } from '@vality/ng-thrift';
import { FinalCashFlowPosting } from '@vality/payout-manager-proto/internal/proto/domain';
import startCase from 'lodash-es/startCase';
import { combineLatest, BehaviorSubject } from 'rxjs';
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { PayoutManagementService } from '@cc/app/api/payout-manager';
import { createCurrencyColumn } from '../../../shared';
import { PayoutActionsService } from '../services/payout-actions.service';
@Component({
selector: 'cc-payout-details',
templateUrl: './payout-details.component.html',
providers: [PayoutActionsService],
})
export class PayoutDetailsComponent {
progress$ = new BehaviorSubject(0);
payout$ = this.route.params.pipe(
startWith(this.route.snapshot.params),
map((p) => p?.payoutId),
switchMap((id: string) =>
this.payoutManagementService.GetPayout(id).pipe(progressTo(this.progress$)),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
shop$ = this.payout$.pipe(
switchMap(({ party_id, shop_id }) =>
this.partyManagementService.GetShop(party_id, shop_id),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
party$ = this.payout$.pipe(
switchMap(({ party_id }) => this.partyManagementService.Get(party_id)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
payoutTool$ = combineLatest([this.payout$, this.shop$]).pipe(
switchMap(([{ party_id, payout_tool_id }, { contract_id }]) =>
this.partyManagementService
.GetContract(party_id, contract_id)
.pipe(
map((contract) => contract.payout_tools.find((t) => t.id === payout_tool_id)),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
cashFlowColumns: Column<FinalCashFlowPosting>[] = [
{
field: 'source',
description: (d) => getUnionValue(d.source.account_type),
formatter: (d) => startCase(getUnionKey(d.source.account_type)),
},
{
field: 'destination',
description: (d) => getUnionValue(d.destination.account_type),
formatter: (d) => startCase(getUnionKey(d.destination.account_type)),
},
createCurrencyColumn(
'volume',
(d) => d.volume.amount,
(d) => d.volume.currency.symbolic_code,
),
'details',
];
constructor(
private route: ActivatedRoute,
private payoutManagementService: PayoutManagementService,
private partyManagementService: PartyManagementService,
private payoutActionsService: PayoutActionsService,
) {}
canBeConfirmed(status: keyof PayoutStatus) {
return this.payoutActionsService.canBeConfirmed(status);
}
canBeCancelled(status: keyof PayoutStatus) {
return this.payoutActionsService.canBeCancelled(status);
}
cancel(id: PayoutID) {
this.payoutActionsService.cancel(id);
}
confirm(id: PayoutID) {
this.payoutActionsService.confirm(id);
}
}

View File

@ -1,39 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatPaginatorModule } from '@angular/material/paginator';
import { ActionsModule, TableModule } from '@vality/ng-core';
import { ThriftPipesModule } from '@vality/ng-thrift';
import { ShopDetailsModule, PageLayoutModule } from '@cc/app/shared/components';
import { PayoutToolDetailsModule } from '@cc/app/shared/components/payout-tool-details/payout-tool-details.module';
import { CommonPipesModule } from '@cc/app/shared/pipes';
import { DetailsItemModule } from '@cc/components/details-item';
import { HeadlineModule } from '@cc/components/headline';
import { PayoutDetailsRoutingModule } from './payout-details-routing.module';
import { PayoutDetailsComponent } from './payout-details.component';
@NgModule({
declarations: [PayoutDetailsComponent],
imports: [
CommonModule,
PayoutDetailsRoutingModule,
HeadlineModule,
MatCardModule,
DetailsItemModule,
MatDividerModule,
CommonPipesModule,
ThriftPipesModule,
ShopDetailsModule,
PayoutToolDetailsModule,
MatPaginatorModule,
MatButtonModule,
ActionsModule,
TableModule,
PageLayoutModule,
],
})
export class PayoutDetailsModule {}

View File

@ -1,5 +0,0 @@
import { Services, RoutingConfig } from '@cc/app/shared/services';
export const ROUTING_CONFIG: RoutingConfig = {
services: [Services.PayoutManagement],
};

View File

@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'payouts',
loadChildren: () => import('./payouts/payouts.module').then((m) => m.PayoutsModule),
},
{
path: 'payouts/:payoutId',
loadChildren: () =>
import('./payout-details/payout-details.module').then(
(m) => m.PayoutDetailsModule,
),
},
]),
],
exports: [RouterModule],
})
export class PayoutsRoutingModule {}

View File

@ -1,9 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { PayoutsRoutingModule } from './payouts-routing.module';
@NgModule({
imports: [CommonModule, PayoutsRoutingModule],
})
export class PayoutsModule {}

View File

@ -1,16 +0,0 @@
<v-dialog [progress]="progress$ | async" title="Cancel payout">
<mat-form-field>
<mat-label>Details</mat-label>
<input [formControl]="detailsControl" matInput required type="text" />
</mat-form-field>
<v-dialog-actions>
<button
[disabled]="detailsControl.invalid || !!(progress$ | async)"
color="primary"
mat-button
(click)="accept()"
>
Accept
</button>
</v-dialog-actions>
</v-dialog>

View File

@ -1,46 +0,0 @@
import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';
import {
DialogResponseStatus,
DialogSuperclass,
NotifyLogService,
progressTo,
} from '@vality/ng-core';
import { PayoutID } from '@vality/payout-manager-proto/payout_manager';
import { BehaviorSubject } from 'rxjs';
import { PayoutManagementService } from '@cc/app/api/payout-manager';
@Component({
selector: 'cc-cancel-payout-dialog',
templateUrl: './cancel-payout-dialog.component.html',
})
export class CancelPayoutDialogComponent extends DialogSuperclass<
CancelPayoutDialogComponent,
{ id: PayoutID }
> {
detailsControl = new FormControl() as FormControl<string>;
progress$ = new BehaviorSubject(0);
constructor(
private payoutManagementService: PayoutManagementService,
private log: NotifyLogService,
private destroyRef: DestroyRef,
) {
super();
}
accept() {
this.payoutManagementService
.CancelPayout(this.dialogData.id, this.detailsControl.value)
.pipe(progressTo(this.progress$), takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.dialogRef.close({ status: DialogResponseStatus.Success });
this.log.success('Payout canceled successfully');
},
error: this.log.error,
});
}
}

View File

@ -1,45 +0,0 @@
<v-dialog [progress]="!!(progress$ | async)" title="Create payout">
<div [formGroup]="control" style="display: grid; grid-template-columns: 1fr; gap: 16px">
<cc-merchant-field formControlName="partyId" required></cc-merchant-field>
<cc-shop-field
[partyId]="this.control.value.partyId"
formControlName="shopId"
required
></cc-shop-field>
<div style="display: flex; gap: 24px">
<mat-form-field style="flex: 1">
<input
formControlName="amount"
matInput
placeholder="Amount"
required
type="number"
/>
</mat-form-field>
<mat-form-field style="flex: 1">
<input
formControlName="currency"
matInput
placeholder="Currency"
required
type="text"
/>
</mat-form-field>
</div>
<cc-payout-tool-field
[partyId]="this.control.value.partyId"
[shopId]="this.control.value.shopId"
formControlName="payoutToolId"
></cc-payout-tool-field>
</div>
<v-dialog-actions>
<button
[disabled]="control.invalid || !!(progress$ | async)"
color="primary"
mat-button
(click)="create()"
>
Create
</button>
</v-dialog-actions>
</v-dialog>

View File

@ -1,78 +0,0 @@
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder } from '@angular/forms';
import {
DialogResponseStatus,
DialogSuperclass,
NotifyLogService,
progressTo,
toMinor,
} from '@vality/ng-core';
import { PayoutParams } from '@vality/payout-manager-proto/payout_manager';
import isNil from 'lodash-es/isNil';
import omitBy from 'lodash-es/omitBy';
import { BehaviorSubject } from 'rxjs';
import { PayoutManagementService } from '@cc/app/api/payout-manager';
interface CreatePayoutDialogForm {
partyId: string;
shopId: string;
currency: string;
amount: number;
payoutToolId?: string;
}
@Component({
selector: 'cc-create-payout-dialog',
templateUrl: './create-payout-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreatePayoutDialogComponent extends DialogSuperclass<CreatePayoutDialogComponent> {
progress$ = new BehaviorSubject(0);
control = this.fb.group<CreatePayoutDialogForm>({
partyId: null,
shopId: null,
currency: null,
amount: null,
payoutToolId: null,
});
constructor(
private fb: FormBuilder,
private payoutManagementService: PayoutManagementService,
private log: NotifyLogService,
private destroyRef: DestroyRef,
) {
super();
}
create() {
const { value } = this.control;
this.payoutManagementService
.CreatePayout(
omitBy(
{
shop_params: {
shop_id: value.shopId,
party_id: value.partyId,
},
cash: {
amount: toMinor(value.amount, value.currency), // TODO use domain currencies refs
currency: { symbolic_code: value.currency },
},
payout_tool_id: value.payoutToolId,
},
isNil,
) as PayoutParams,
)
.pipe(takeUntilDestroyed(this.destroyRef), progressTo(this.progress$))
.subscribe({
next: () => {
this.log.success('Payout created successfully');
this.dialogRef.close({ status: DialogResponseStatus.Success });
},
error: this.log.error,
});
}
}

View File

@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { PayoutsComponent } from './payouts.component';
import { ROUTING_CONFIG } from './routing-config';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: PayoutsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
},
]),
],
exports: [RouterModule],
})
export class PayoutsRoutingModule {}

View File

@ -1,61 +0,0 @@
<cc-page-layout title="Payouts">
<cc-page-layout-actions>
<v-more-filters-button [filters]="filters"></v-more-filters-button>
</cc-page-layout-actions>
<v-filters #filters [active]="active$ | async" merge (clear)="filtersForm.reset()">
<ng-template [formGroup]="filtersForm">
<v-date-range-field formControlName="dateRange" required></v-date-range-field>
<mat-form-field>
<input
autocomplete="off"
formControlName="payoutId"
matInput
placeholder="Payout ID"
/>
</mat-form-field>
<mat-form-field>
<mat-select
formControlName="payoutStatusTypes"
multiple
placeholder="Payout Status Type"
>
<mat-option
*ngFor="let type of statusTypeEnum | enumKeys"
[value]="statusTypeEnum[type]"
>
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="payoutToolType" placeholder="Payout Tool Type">
<mat-option [value]="null">any</mat-option>
<mat-option
*ngFor="let type of payoutToolTypeEnum | enumKeys"
[value]="payoutToolTypeEnum[type]"
>
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
<cc-merchant-field formControlName="partyId"></cc-merchant-field>
<cc-shop-field
[partyId]="filtersForm.value.partyId"
formControlName="shops"
multiple
></cc-shop-field>
</ng-template>
</v-filters>
<v-table
[columns]="columns"
[data]="payouts$ | async"
[hasMore]="hasMore$ | async"
[progress]="inProgress$ | async"
(more)="more()"
(update)="reload($event)"
>
<v-table-actions>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</v-table-actions>
</v-table>
</cc-page-layout>

View File

@ -1,164 +0,0 @@
import { Component, OnInit, Inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NonNullableFormBuilder } from '@angular/forms';
import { Party, Shop, ShopID, PartyID } from '@vality/domain-proto/domain';
import { magista } from '@vality/magista-proto';
import { StatPayout } from '@vality/magista-proto/magista';
import {
DialogService,
QueryParamsService,
clean,
getValueChanges,
getNoTimeZoneIsoString,
createDateRangeToToday,
Column,
createOperationColumn,
DateRange,
UpdateOptions,
debounceTimeWithFirst,
countChanged,
} from '@vality/ng-core';
import { getUnionKey } from '@vality/ng-thrift';
import { endOfDay } from 'date-fns';
import startCase from 'lodash-es/startCase';
import { map, shareReplay } from 'rxjs/operators';
import { createCurrencyColumn, createPartyColumn, createShopColumn } from '../../../shared';
import { DATE_RANGE_DAYS, DEBOUNCE_TIME_MS } from '../../../tokens';
import { PayoutActionsService } from '../services/payout-actions.service';
import { CreatePayoutDialogComponent } from './components/create-payout-dialog/create-payout-dialog.component';
import { FetchPayoutsService } from './services/fetch-payouts.service';
interface PayoutsSearchForm {
payoutId: string;
partyId: Party['id'];
dateRange: DateRange;
shops: Shop['id'][];
payoutStatusTypes: magista.PayoutStatusType[];
payoutToolType: magista.PayoutToolType;
}
@Component({
selector: 'cc-payouts',
templateUrl: './payouts.component.html',
providers: [FetchPayoutsService, PayoutActionsService],
})
export class PayoutsComponent implements OnInit {
filtersForm = this.fb.group({
payoutId: null as string,
partyId: null as PartyID,
dateRange: createDateRangeToToday(this.dateRangeDays),
shops: [null as ShopID[]],
payoutStatusTypes: [null as magista.PayoutStatusType[]],
payoutToolType: [null as magista.PayoutToolType],
});
inProgress$ = this.fetchPayoutsService.isLoading$;
payouts$ = this.fetchPayoutsService.result$;
hasMore$ = this.fetchPayoutsService.hasMore$;
columns: Column<StatPayout>[] = [
{ field: 'id', link: (d) => `/payouts/${d.id}` },
createPartyColumn('party_id'),
createShopColumn('shop_id', (d) => d.party_id),
{ field: 'created_at', type: 'datetime' },
{
field: 'status',
type: 'tag',
formatter: (d) => getUnionKey(d.status),
typeParameters: {
label: (d) => startCase(getUnionKey(d.status)),
tags: {
unpaid: { color: 'pending' },
paid: { color: 'success' },
cancelled: { color: 'warn' },
confirmed: { color: 'success' },
},
},
},
createCurrencyColumn(
'amount',
(d) => d.amount,
(d) => d.currency_symbolic_code,
),
createCurrencyColumn(
'fee',
(d) => d.fee,
(d) => d.currency_symbolic_code,
),
{ field: 'payoutToolType', formatter: (d) => getUnionKey(d.payout_tool_info) },
createOperationColumn([
{
label: 'Cancel',
disabled: (d) => !this.payoutActionsService.canBeCancelled(getUnionKey(d.status)),
click: (d) => this.payoutActionsService.cancel(d.id),
},
{
label: 'Confirm',
disabled: (d) => !this.payoutActionsService.canBeConfirmed(getUnionKey(d.status)),
click: (d) => this.payoutActionsService.confirm(d.id),
},
]),
];
statusTypeEnum = magista.PayoutStatusType;
payoutToolTypeEnum = magista.PayoutToolType;
active$ = getValueChanges(this.filtersForm).pipe(
map((v) => countChanged(this.initFilters, v)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private initFilters = this.filtersForm.value;
constructor(
private fetchPayoutsService: FetchPayoutsService,
private qp: QueryParamsService<Partial<PayoutsSearchForm>>,
private dialogService: DialogService,
@Inject(DATE_RANGE_DAYS) private dateRangeDays: number,
private payoutActionsService: PayoutActionsService,
private fb: NonNullableFormBuilder,
private destroyRef: DestroyRef,
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
) {}
ngOnInit() {
this.filtersForm.patchValue(this.qp.params);
getValueChanges(this.filtersForm)
.pipe(debounceTimeWithFirst(this.debounceTimeMs), takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
void this.qp.set(clean(value));
this.search();
});
}
more() {
this.fetchPayoutsService.more();
}
search(options?: UpdateOptions) {
const value = clean(this.filtersForm.value);
this.fetchPayoutsService.load(
clean({
common_search_query_params: clean({
from_time:
value.dateRange?.start && getNoTimeZoneIsoString(value.dateRange?.start),
to_time:
value.dateRange?.end &&
getNoTimeZoneIsoString(endOfDay(value.dateRange?.end)),
party_id: value.partyId,
shop_ids: value.shops,
}),
payout_id: value.payoutId,
payout_status_types: value.payoutStatusTypes,
payout_type: value.payoutToolType,
}),
options,
);
}
reload(options?: UpdateOptions) {
this.fetchPayoutsService.reload(options);
}
create() {
this.dialogService.open(CreatePayoutDialogComponent);
}
}

View File

@ -1,70 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import {
DialogModule,
ActionsModule,
DateRangeFieldModule,
TableModule,
FiltersModule,
EnumKeysPipe,
} from '@vality/ng-core';
import { ThriftPipesModule } from '@vality/ng-thrift';
import {
PayoutToolFieldModule,
ShopFieldModule,
StatusModule,
PageLayoutModule,
} from '@cc/app/shared/components';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
import { CommonPipesModule } from '@cc/app/shared/pipes';
import { CancelPayoutDialogComponent } from './components/cancel-payout-dialog/cancel-payout-dialog.component';
import { CreatePayoutDialogComponent } from './components/create-payout-dialog/create-payout-dialog.component';
import { PayoutsRoutingModule } from './payouts-routing.module';
import { PayoutsComponent } from './payouts.component';
@NgModule({
declarations: [PayoutsComponent, CreatePayoutDialogComponent, CancelPayoutDialogComponent],
imports: [
CommonModule,
PayoutsRoutingModule,
MatButtonModule,
MatCardModule,
MatProgressBarModule,
MatFormFieldModule,
MatInputModule,
FormsModule,
MerchantFieldModule,
ReactiveFormsModule,
MatSelectModule,
MatDatepickerModule,
MatIconModule,
MatMenuModule,
CommonPipesModule,
ThriftPipesModule,
StatusModule,
MatDialogModule,
ShopFieldModule,
PayoutToolFieldModule,
DialogModule,
ActionsModule,
PageLayoutModule,
DateRangeFieldModule,
TableModule,
FiltersModule,
EnumKeysPipe,
],
})
export class PayoutsModule {}

View File

@ -1,5 +0,0 @@
import { Services, RoutingConfig } from '@cc/app/shared/services';
export const ROUTING_CONFIG: RoutingConfig = {
services: [Services.MerchantStatistics],
};

View File

@ -1,55 +0,0 @@
import { Injectable } from '@angular/core';
import { StatPayout, PayoutSearchQuery } from '@vality/magista-proto/magista';
import { FetchSuperclass, NotifyLogService, FetchResult, FetchOptions } from '@vality/ng-core';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Overwrite } from 'utility-types';
import { MerchantStatisticsService } from '@cc/app/api/magista';
export type SearchParams = Overwrite<
PayoutSearchQuery,
{
common_search_query_params: Omit<
PayoutSearchQuery['common_search_query_params'],
'continuation_token' | 'limit'
>;
}
>;
@Injectable()
export class FetchPayoutsService extends FetchSuperclass<StatPayout, SearchParams> {
constructor(
private merchantStatisticsService: MerchantStatisticsService,
private log: NotifyLogService,
) {
super();
}
protected fetch(
params: SearchParams,
options: FetchOptions,
): Observable<FetchResult<StatPayout>> {
return this.merchantStatisticsService
.SearchPayouts({
...params,
common_search_query_params: {
...params.common_search_query_params,
...(options.continuationToken
? { continuation_token: options.continuationToken }
: {}),
limit: options.size,
},
})
.pipe(
map(({ continuation_token, payouts }) => ({
result: payouts,
continuationToken: continuation_token,
})),
catchError((err) => {
this.log.error(err);
return of({ result: [] });
}),
);
}
}

View File

@ -1,51 +0,0 @@
import { Injectable, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PayoutID, PayoutStatus } from '@vality/magista-proto/magista';
import {
DialogResponseStatus,
DialogService,
ConfirmDialogComponent,
NotifyLogService,
} from '@vality/ng-core';
import { switchMap } from 'rxjs';
import { filter } from 'rxjs/operators';
import { PayoutManagementService } from '@cc/app/api/payout-manager';
import { CancelPayoutDialogComponent } from '../payouts/components/cancel-payout-dialog/cancel-payout-dialog.component';
@Injectable()
export class PayoutActionsService {
constructor(
private payoutManagementService: PayoutManagementService,
private dialogService: DialogService,
private log: NotifyLogService,
private destroyRef: DestroyRef,
) {}
canBeConfirmed(status: keyof PayoutStatus) {
return (['paid'] as (keyof PayoutStatus)[]).includes(status);
}
canBeCancelled(status: keyof PayoutStatus) {
return (['paid', 'confirmed', 'unpaid'] as (keyof PayoutStatus)[]).includes(status);
}
cancel(id: PayoutID) {
this.dialogService.open(CancelPayoutDialogComponent, { id });
}
confirm(id: PayoutID) {
this.dialogService
.open(ConfirmDialogComponent, { title: 'Confirm payout' })
.afterClosed()
.pipe(
filter(({ status }) => status === DialogResponseStatus.Success),
switchMap(() => this.payoutManagementService.ConfirmPayout(id)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe({
error: this.log.error,
});
}
}

View File

@ -1,4 +1,4 @@
import { Claim } from '@vality/domain-proto/claim_management';
import { Claim, PayoutToolModificationUnit } from '@vality/domain-proto/claim_management';
import { Party } from '@vality/domain-proto/domain';
import { isTypeWithAliases } from '@vality/ng-thrift';
import uniqBy from 'lodash-es/uniqBy';
@ -34,6 +34,20 @@ function createClaimOptions(
);
}
function createClaimPayoutToolOptions(
modificationUnits: PayoutToolModificationUnit[],
): MetadataFormExtensionOption[] {
return uniqBy(
modificationUnits.map((unit) => ({
label: 'From claim',
details: unit.modification,
value: unit.payout_tool_id,
color: 'primary',
})),
'value',
);
}
function mergeClaimAndPartyOptions(
claimOptions: MetadataFormExtensionOption[],
partyOptions: MetadataFormExtensionOption[],
@ -125,5 +139,22 @@ export function createPartyClaimDomainMetadataFormExtensions(
isIdentifier: true,
}),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'PayoutToolID', 'domain')),
extension: () =>
of({
options: createClaimPayoutToolOptions(
claim.changeset
.map(
(unit) =>
unit.modification.party_modification?.contract_modification
?.modification?.payout_tool_modification,
)
.filter(Boolean),
),
generate: () => of(short().generate()),
isIdentifier: true,
}),
},
];
}