IMP-232: Wallets terms (#361)

This commit is contained in:
Rinat Arsaev 2024-05-28 14:14:40 +05:00 committed by GitHub
parent 45cf33deb8
commit a40b2f0a05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 605 additions and 154 deletions

View File

@ -2,5 +2,5 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"import": "./node_modules/@vality/cspell-config/cspell.config.js",
"words": ["submain", "papaparse", "msgpack", "termsets"]
"words": ["submain", "papaparse", "msgpack", "termsets", "rreserve"]
}

View File

@ -82,7 +82,7 @@ export class AppComponent {
services: SOURCES_ROUTING_CONFIG.services,
},
{
label: 'Tariffs',
label: 'Terms',
url: '/tariffs',
services: TARIFFS_ROUTING_CONFIG.services,
},

View File

@ -1 +0,0 @@
<v-table [columns]="columns" [data]="data()"></v-table>

View File

@ -1,38 +0,0 @@
import { Component, input } from '@angular/core';
import { CashFlowSelector } from '@vality/domain-proto/internal/domain';
import { Column, TableModule } from '@vality/ng-core';
import { getUnionKey } from '@vality/ng-thrift';
import { formatCashFlowDecisions } from '@cc/app/sections/tariffs/components/cash-flows-selector-table/format-cash-flow.decisions';
import { formatCashVolume } from '@cc/app/shared/utils/table/format-cash-volume';
@Component({
selector: 'cc-cash-flows-selector-table',
standalone: true,
imports: [TableModule],
templateUrl: './cash-flows-selector-table.component.html',
styles: ``,
})
export class CashFlowsSelectorTableComponent {
data = input<CashFlowSelector[]>();
columns: Column<CashFlowSelector>[] = [
{
field: 'decisions',
formatter: (d) => formatCashFlowDecisions(d?.decisions),
},
{
field: 'value',
formatter: (d) =>
d?.value
?.filter(
(c) =>
getUnionKey(c?.source) === 'merchant' &&
getUnionKey(c?.destination) === 'system',
)
?.sort()
?.map((c) => formatCashVolume(c.volume))
.join(' + '),
},
];
}

View File

@ -1,4 +1,4 @@
<cc-page-layout title="Shops tariffs">
<cc-page-layout title="Shops terms">
<cc-page-layout-actions>
<v-more-filters-button [filters]="filters"></v-more-filters-button>
</cc-page-layout-actions>
@ -18,8 +18,10 @@
<v-table
[cellTemplate]="{
decision: arrayColumnTemplate,
value: arrayColumnTemplate
condition: arrayColumnTemplate,
fee: arrayColumnTemplate,
rreserve: arrayColumnTemplate,
other: arrayColumnTemplate
}"
[columns]="columns"
[data]="tariffs$ | async"
@ -29,10 +31,16 @@
(update)="update($event)"
></v-table>
<ng-template #arrayColumnTemplate let-value="value">
{{ rowData?.length }}
<div *ngFor="let item of value" style="white-space: nowrap">
{{ item }}
</div>
<ng-template #arrayColumnTemplate let-colDef="colDef" let-rowData="rowData" let-value="value">
<ng-container *ngIf="(rowData | vSelect: colDef.tooltip : '' : [colDef]) || ' ' as tooltip">
<div
*ngFor="let item of value; let index = index"
[matTooltip]="tooltip[index]"
matTooltipPosition="right"
style="white-space: nowrap; cursor: default"
>
{{ item }}
</div>
</ng-container>
</ng-template>
</cc-page-layout>

View File

@ -2,11 +2,8 @@ import { CommonModule } from '@angular/common';
import { Component, DestroyRef, Inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms';
import {
TermSetHierarchyRef,
type CashFlowSelector,
type CashFlowPosting,
} from '@vality/domain-proto/internal/domain';
import { MatTooltip } from '@angular/material/tooltip';
import { TermSetHierarchyRef } from '@vality/domain-proto/internal/domain';
import {
CommonSearchQueryParams,
ShopSearchQuery,
@ -26,9 +23,10 @@ import {
QueryParamsService,
TableModule,
UpdateOptions,
VSelectPipe,
} from '@vality/ng-core';
import { getUnionKey } from '@vality/ng-thrift';
import { map, shareReplay } from 'rxjs/operators';
import { getInlineDecisions } from 'src/app/sections/tariffs/utils/get-inline-decisions';
import {
DomainObjectCardComponent,
getDomainObjectDetails,
@ -41,8 +39,6 @@ import {
createShopColumn,
PageLayoutModule,
ShopFieldModule,
formatCashVolume,
formatPredicate,
} from '@cc/app/shared';
import { CurrencyFieldComponent } from '@cc/app/shared/components/currency-field';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
@ -65,58 +61,6 @@ function getViewedCashFlowSelectors(d: ShopTermSet) {
);
}
interface InlineCashFlowSelector {
if?: string;
value?: string;
parent?: InlineCashFlowSelector;
level: number;
}
function getInlineDecisions(
d: CashFlowSelector[],
filterValue: (v: CashFlowPosting) => boolean = (v) =>
getUnionKey(v?.source) === 'merchant' && getUnionKey(v?.destination) === 'system',
level = 0,
): InlineCashFlowSelector[] {
return d.reduce((acc, c) => {
if (c.value) {
acc.push({
value: c.value
.filter(filterValue)
.map((v) => formatCashVolume(v.volume))
.join(' + '),
level,
});
}
if (c.decisions?.length) {
acc.push(
...c.decisions
.map((d) => {
const thenInlineDecisions = getInlineDecisions(
[d.then_],
filterValue,
level + 1,
);
if (d.if_) {
const ifInlineDecision = {
if: `${' '.repeat(level)}${
formatPredicate(d.if_) || (level > 0 ? '↳' : '')
}`,
level,
};
return thenInlineDecisions.length > 1
? [ifInlineDecision, ...thenInlineDecisions]
: [{ ...ifInlineDecision, value: thenInlineDecisions[0].value }];
}
return thenInlineDecisions;
})
.flat(),
);
}
return acc;
}, [] as InlineCashFlowSelector[]);
}
@Component({
selector: 'cc-shops-tariffs',
standalone: true,
@ -131,6 +75,8 @@ function getInlineDecisions(
ShopFieldModule,
ListFieldModule,
CurrencyFieldComponent,
VSelectPipe,
MatTooltip,
],
templateUrl: './shops-tariffs.component.html',
})
@ -148,7 +94,15 @@ export class ShopsTariffsComponent implements OnInit {
hasMore$ = this.shopsTariffsService.hasMore$;
isLoading$ = this.shopsTariffsService.isLoading$;
columns: Column<ShopTermSet>[] = [
createShopColumn<ShopTermSet>('shop_id', (d) => d.owner_id, undefined, { pinned: 'left' }),
createShopColumn<ShopTermSet>(
'shop_id',
(d) => d.owner_id,
undefined,
(d) => d.shop_name,
{
pinned: 'left',
},
),
createPartyColumn<ShopTermSet>('owner_id'),
createContractColumn<ShopTermSet>(
(d) => d.contract_id,
@ -166,12 +120,46 @@ export class ShopsTariffsComponent implements OnInit {
}),
},
{
field: 'decision',
field: 'condition',
formatter: (d) => getInlineDecisions(getViewedCashFlowSelectors(d)).map((v) => v.if),
},
{
field: 'value',
formatter: (d) => getInlineDecisions(getViewedCashFlowSelectors(d)).map((v) => v.value),
field: 'fee',
formatter: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) => v?.source?.merchant === 0 && v?.destination?.system === 0,
).map((v) => v.value),
},
{
field: 'rreserve',
header: 'RReserve',
formatter: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) => v?.source?.merchant === 0 && v?.destination?.merchant === 1,
).map((v) => v.value),
},
{
field: 'other',
formatter: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) =>
!(
(v?.source?.merchant === 0 && v?.destination?.system === 0) ||
(v?.source?.merchant === 0 && v?.destination?.merchant === 1)
),
).map((v) => v.value),
tooltip: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) =>
!(
(v?.source?.merchant === 0 && v?.destination?.system === 0) ||
(v?.source?.merchant === 0 && v?.destination?.merchant === 1)
),
).map((v) => v.description),
},
{
field: 'term_set_history',

View File

@ -1,23 +0,0 @@
import { Component, computed, input } from '@angular/core';
import { TermSetHierarchyObject } from '@vality/domain-proto/internal/domain';
import { CardComponent } from '@cc/app/shared/components/sidenav-info/components/card/card.component';
import { CashFlowsSelectorTableComponent } from '../cash-flows-selector-table/cash-flows-selector-table.component';
@Component({
selector: 'cc-termsets-card',
standalone: true,
imports: [CardComponent, CashFlowsSelectorTableComponent],
templateUrl: './termsets-card.component.html',
styles: ``,
})
export class TermsetsCardComponent {
data = input<TermSetHierarchyObject>();
feesData = computed(
() =>
this.data()
?.data?.term_sets?.map?.((t) => t?.terms?.payments?.fees)
?.filter?.(Boolean),
);
}

View File

@ -0,0 +1,42 @@
<cc-page-layout title="Wallets terms">
<cc-page-layout-actions>
<v-more-filters-button [filters]="filters"></v-more-filters-button>
</cc-page-layout-actions>
<v-filters #filters [active]="active$ | async" (clear)="filtersForm.reset()">
<ng-template [formGroup]="filtersForm">
<cc-merchant-field formControlName="party_id"></cc-merchant-field>
<cc-wallet-field formControlName="wallet_ids" multiple></cc-wallet-field>
<v-list-field formControlName="identity_ids" label="Identity IDs"></v-list-field>
<v-list-field formControlName="currencies" label="Currencies"></v-list-field>
<v-list-field formControlName="term_sets_names" label="Term sets names"></v-list-field>
<v-list-field formControlName="term_sets_ids" label="Term sets IDs"></v-list-field>
</ng-template>
</v-filters>
<v-table
[cellTemplate]="{
condition: arrayColumnTemplate,
fee: arrayColumnTemplate,
other: arrayColumnTemplate
}"
[columns]="columns"
[data]="tariffs$ | async"
[hasMore]="hasMore$ | async"
[progress]="isLoading$ | async"
(more)="more()"
(update)="update($event)"
></v-table>
<ng-template #arrayColumnTemplate let-colDef="colDef" let-rowData="rowData" let-value="value">
<ng-container *ngIf="(rowData | vSelect: colDef.tooltip : '' : [colDef]) || ' ' as tooltip">
<div
*ngFor="let item of value; let index = index"
[matTooltip]="tooltip[index]"
matTooltipPosition="right"
style="white-space: nowrap; cursor: default"
>
{{ item }}
</div>
</ng-container>
</ng-template>
</cc-page-layout>

View File

@ -0,0 +1,215 @@
import { CommonModule } from '@angular/common';
import { Component, DestroyRef, Inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NonNullableFormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip';
import {
TermSetHierarchyRef,
type IdentityProviderRef,
} from '@vality/domain-proto/internal/domain';
import {
CommonSearchQueryParams,
type WalletTermSet,
type WalletSearchQuery,
} from '@vality/dominator-proto/internal/dominator';
import {
clean,
Column,
countChanged,
createControls,
debounceTimeWithFirst,
FiltersModule,
getValueChanges,
InputFieldModule,
ListFieldModule,
LoadOptions,
QueryParamsService,
TableModule,
UpdateOptions,
VSelectPipe,
} from '@vality/ng-core';
import { map, shareReplay } from 'rxjs/operators';
import { WalletsTariffsService } from 'src/app/sections/tariffs/components/wallets-tariffs/wallets-tariffs.service';
import { getInlineDecisions } from 'src/app/sections/tariffs/utils/get-inline-decisions';
import {
DomainObjectCardComponent,
getDomainObjectDetails,
} from 'src/app/shared/components/thrift-api-crud';
import { Overwrite } from 'utility-types';
import {
createContractColumn,
createPartyColumn,
PageLayoutModule,
WalletFieldModule,
createWalletColumn,
formatCashVolume,
} from '@cc/app/shared';
import { CurrencyFieldComponent } from '@cc/app/shared/components/currency-field';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
import { SidenavInfoService } from '@cc/app/shared/components/sidenav-info';
import { DEBOUNCE_TIME_MS } from '@cc/app/tokens';
type Params = Pick<CommonSearchQueryParams, 'currencies'> &
Overwrite<
Omit<WalletSearchQuery, 'common_search_query_params'>,
{ term_sets_ids?: TermSetHierarchyRef['id'][]; identity_ids?: IdentityProviderRef['id'][] }
>;
function getViewedCashFlowSelectors(d: WalletTermSet) {
return (
d.current_term_set.data.term_sets
?.map?.((t) => t?.terms?.wallets?.withdrawals?.cash_flow)
?.filter?.(Boolean) ?? []
);
}
@Component({
selector: 'cc-wallets-tariffs',
standalone: true,
imports: [
CommonModule,
PageLayoutModule,
TableModule,
InputFieldModule,
FiltersModule,
ReactiveFormsModule,
MerchantFieldModule,
ListFieldModule,
CurrencyFieldComponent,
WalletFieldModule,
MatTooltip,
VSelectPipe,
],
templateUrl: './wallets-tariffs.component.html',
})
export class WalletsTariffsComponent implements OnInit {
filtersForm = this.fb.group(
createControls<Params>({
currencies: null,
party_id: null,
wallet_ids: null,
term_sets_names: null,
term_sets_ids: null,
identity_ids: null,
}),
);
tariffs$ = this.walletsTariffsService.result$;
hasMore$ = this.walletsTariffsService.hasMore$;
isLoading$ = this.walletsTariffsService.isLoading$;
columns: Column<WalletTermSet>[] = [
createWalletColumn<WalletTermSet>(
'wallet_id',
(d) => d.owner_id,
undefined,
(d) => d.wallet_name,
{
pinned: 'left',
},
),
createPartyColumn<WalletTermSet>('owner_id'),
createContractColumn<WalletTermSet>(
(d) => d.contract_id,
(d) => d.owner_id,
(d) => d.wallet_id,
),
{ field: 'currency' },
{
field: 'current_term_set',
formatter: (d) =>
getDomainObjectDetails({ term_set_hierarchy: d.current_term_set })?.label,
click: (d) =>
this.sidenavInfoService.open(DomainObjectCardComponent, {
ref: { term_set_hierarchy: d?.current_term_set?.ref },
}),
},
{
field: 'condition',
formatter: (d) => getInlineDecisions(getViewedCashFlowSelectors(d)).map((v) => v.if),
},
{
field: 'fee',
formatter: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) => v?.source?.wallet === 1 && v?.destination?.system === 0,
).map((v) => v.value),
},
{
field: 'other',
formatter: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) =>
!(
(v?.source?.wallet === 1 && v?.destination?.system === 0) ||
(v?.source?.wallet === 1 &&
v?.destination?.wallet === 3 &&
formatCashVolume(v?.volume) === '100%')
),
).map((v) => v.value),
tooltip: (d) =>
getInlineDecisions(
getViewedCashFlowSelectors(d),
(v) =>
!(
(v?.source?.wallet === 1 && v?.destination?.system === 0) ||
(v?.source?.wallet === 1 &&
v?.destination?.wallet === 3 &&
formatCashVolume(v?.volume) === '100%')
),
).map((v) => v.description),
},
{
field: 'term_set_history',
formatter: (d) => d.term_set_history?.length,
tooltip: (d) => d.term_set_history,
},
];
active$ = getValueChanges(this.filtersForm).pipe(
map((filters) => countChanged(this.initFiltersValue, filters)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private initFiltersValue = this.filtersForm.value;
constructor(
private walletsTariffsService: WalletsTariffsService,
private fb: NonNullableFormBuilder,
private qp: QueryParamsService<Params>,
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
private dr: DestroyRef,
private sidenavInfoService: SidenavInfoService,
) {}
ngOnInit() {
this.filtersForm.patchValue(this.qp.params);
getValueChanges(this.filtersForm)
.pipe(debounceTimeWithFirst(this.debounceTimeMs), takeUntilDestroyed(this.dr))
.subscribe((filters) => {
void this.qp.set(filters);
this.load(filters);
});
}
load(params: Params, options?: LoadOptions) {
const { currencies, term_sets_ids, identity_ids, ...otherParams } = params;
this.walletsTariffsService.load(
clean({
common_search_query_params: { currencies },
term_sets_ids: term_sets_ids?.map((id) => ({ id })),
identity_ids: identity_ids?.map((id) => ({ id })),
...otherParams,
}),
options,
);
}
update(options?: UpdateOptions) {
this.walletsTariffsService.reload(options);
}
more() {
this.walletsTariffsService.more();
}
}

View File

@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import {
type WalletSearchQuery,
type WalletTermSet,
} from '@vality/dominator-proto/internal/dominator';
import {
FetchOptions,
FetchSuperclass,
handleError,
NotifyLogService,
clean,
} from '@vality/ng-core';
import { map } from 'rxjs/operators';
import { DominatorService } from '@cc/app/api/dominator';
@Injectable({
providedIn: 'root',
})
export class WalletsTariffsService extends FetchSuperclass<WalletTermSet, WalletSearchQuery> {
constructor(
private dominatorService: DominatorService,
private log: NotifyLogService,
) {
super();
}
protected fetch(params: WalletSearchQuery, options: FetchOptions<string>) {
return this.dominatorService
.SearchWalletTermSets({
...params,
common_search_query_params: clean({
continuation_token: options.continuationToken,
limit: options.size,
currencies: params?.common_search_query_params?.currencies,
}),
})
.pipe(
map(({ terms, continuation_token }) => ({
result: terms,
continuationToken: continuation_token,
})),
handleError(this.log.error),
);
}
}

View File

@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../shared/services';
import { ShopsTariffsComponent } from './components/shops-tariffs/shops-tariffs.component';
import { WalletsTariffsComponent } from './components/wallets-tariffs/wallets-tariffs.component';
import { ROUTING_CONFIG } from './routing-config';
import { TariffsComponent } from './tariffs.component';
@ -20,6 +21,10 @@ import { TariffsComponent } from './tariffs.component';
path: 'shops',
component: ShopsTariffsComponent,
},
{
path: 'wallets',
component: WalletsTariffsComponent,
},
{
path: '',
redirectTo: 'shops',

View File

@ -18,6 +18,10 @@ export class TariffsComponent {
label: 'Shops',
url: 'shops',
},
{
label: 'Wallets',
url: 'wallets',
},
];
constructor(public sidenavInfoService: SidenavInfoService) {}

View File

@ -0,0 +1,107 @@
import { getUnionKey } from '@vality/ng-thrift';
import type {
CashFlowPosting,
CashFlowSelector,
CashFlowAccount,
} from '@vality/dominator-proto/internal/proto/domain';
import { formatPredicate, formatCashVolumes, compareCashVolumes } from '@cc/app/shared';
export interface InlineCashFlowSelector {
if?: string;
value?: string;
parent?: InlineCashFlowSelector;
description?: string;
level: number;
}
// TODO: use enums
function formatCashFlowAccount(acc: CashFlowAccount) {
return (
getUnionKey(acc) +
':' +
(() => {
switch (getUnionKey(acc)) {
case 'system':
return {
0: 'settlement',
1: 'subagent',
}[acc.system];
case 'merchant':
return {
0: 'settlement',
1: 'guarantee',
2: 'payout',
}[acc.merchant];
case 'wallet':
return {
0: 'sender_source',
1: 'sender_settlement',
2: 'receiver_settlement',
3: 'receiver_destination',
}[acc.wallet];
case 'external':
return {
0: 'income',
1: 'outcome',
}[acc.external];
case 'provider':
return {
0: 'settlement',
}[acc.provider];
}
})()
);
}
export function getInlineDecisions(
d: CashFlowSelector[],
filterValue: (v: CashFlowPosting) => boolean = Boolean,
level = 0,
): InlineCashFlowSelector[] {
return d.reduce((acc, c) => {
if (c.value) {
acc.push({
value: formatCashVolumes(c.value.filter(filterValue).map((v) => v.volume)),
level,
description: c.value
.filter(filterValue)
.sort((a, b) => compareCashVolumes(a.volume, b.volume))
.map(
(v) =>
`${formatCashFlowAccount(v.source)}${formatCashFlowAccount(
v.destination,
)}` + (v.details ? ` (${v.details})` : ''),
)
.join(', '),
});
}
if (c.decisions?.length) {
acc.push(
...c.decisions
.map((d) => {
const thenInlineDecisions = getInlineDecisions(
[d.then_],
filterValue,
level + 1,
);
if (d.if_) {
const ifInlineDecision = {
if: `${' '.repeat(level)}${
formatPredicate(d.if_) || (level > 0 ? '↳' : '')
}`,
level,
};
return thenInlineDecisions.length > 1
? [ifInlineDecision, ...thenInlineDecisions]
: [{ ...thenInlineDecisions[0], ...ifInlineDecision }];
}
return thenInlineDecisions;
})
.flat(),
);
}
return acc;
}, [] as InlineCashFlowSelector[]);
}

View File

@ -0,0 +1,22 @@
import { Component, computed, input } from '@angular/core';
import { TermSetHierarchyObject } from '@vality/domain-proto/internal/domain';
import { CardComponent } from '../sidenav-info/components/card/card.component';
@Component({
selector: 'cc-termsets-card',
standalone: true,
imports: [CardComponent],
templateUrl: './termsets-history-card.component.html',
styles: ``,
})
export class TermsetsHistoryCardComponent {
data = input<TermSetHierarchyObject[]>();
feesData = computed(
() =>
this.data()?.map?.(
(d) =>
d?.data?.term_sets?.map?.((t) => t?.terms?.payments?.fees)?.filter?.(Boolean),
),
);
}

View File

@ -13,10 +13,10 @@ import {
NotifyLogService,
FormControlSuperclass,
createControlProviders,
getValueChanges,
debounceTimeWithFirst,
progressTo,
} from '@vality/ng-core';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject, merge } from 'rxjs';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject, concat, forkJoin } from 'rxjs';
import { catchError, map, switchMap, tap, distinctUntilChanged } from 'rxjs/operators';
import { DeanonimusService } from '@cc/app/api/deanonimus';
@ -28,7 +28,10 @@ import { DEBOUNCE_TIME_MS } from '../../../tokens';
templateUrl: 'wallet-field.component.html',
providers: createControlProviders(() => WalletFieldComponent),
})
export class WalletFieldComponent extends FormControlSuperclass<WalletID> implements AfterViewInit {
export class WalletFieldComponent
extends FormControlSuperclass<WalletID | WalletID[]>
implements AfterViewInit
{
@Input() label: string;
@Input({ transform: booleanAttribute }) required: boolean;
@Input() size?: string;
@ -38,7 +41,7 @@ export class WalletFieldComponent extends FormControlSuperclass<WalletID> implem
options$ = new ReplaySubject<Option<WalletID>[]>(1);
searchChange$ = new Subject<string>();
progress$ = new BehaviorSubject(false);
progress$ = new BehaviorSubject(0);
private debounceTimeMs = inject(DEBOUNCE_TIME_MS);
@ -51,20 +54,27 @@ export class WalletFieldComponent extends FormControlSuperclass<WalletID> implem
}
ngAfterViewInit() {
merge(getValueChanges(this.control), this.searchChange$)
.pipe(
concat(
of(this.control.value).pipe(
switchMap((term) =>
forkJoin(
(Array.isArray(term) ? term : [term ?? '']).map((t) => this.findOption(t)),
),
),
map((o) => o.filter(Boolean)),
),
this.searchChange$.pipe(
distinctUntilChanged(),
tap(() => {
this.options$.next([]);
this.progress$.next(true);
}),
debounceTimeWithFirst(this.debounceTimeMs),
switchMap((term) => this.searchOptions(term)),
takeUntilDestroyed(this.destroyRef),
)
),
)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((options) => {
this.options$.next(options);
this.progress$.next(false);
});
}
@ -84,6 +94,13 @@ export class WalletFieldComponent extends FormControlSuperclass<WalletID> implem
this.log.error(err, 'Search error');
return of([]);
}),
progressTo(this.progress$),
);
}
private findOption(id: WalletID) {
return this.searchOptions(id).pipe(
map((options) => (options?.length ? options.find((o) => o.value === id) : null)),
);
}
}

View File

@ -12,18 +12,15 @@ export function createShopColumn<T extends object>(
field: ColumnObject<T>['field'],
selectPartyId: (d: T) => PossiblyAsync<string>,
selectShopId?: (d: T) => PossiblyAsync<string>,
selectShopName?: (d: T) => PossiblyAsync<string>,
params: Partial<ColumnObject<T>> = {},
): ColumnObject<T> {
if (!selectShopId) {
selectShopId = (d) => get(d, field);
}
const partiesStoreService = inject(PartiesStoreService);
const sidenavInfoService = inject(SidenavInfoService);
return {
field,
header: 'Shop',
description: (d) => selectShopId(d),
formatter: (d) =>
if (!selectShopName) {
const partiesStoreService = inject(PartiesStoreService);
selectShopName = (d) =>
getPossiblyAsyncObservable(selectPartyId(d)).pipe(
switchMap((partyId) =>
combineLatest([
@ -32,7 +29,14 @@ export function createShopColumn<T extends object>(
]),
),
map(([party, shopId]) => party.shops.get(shopId).details.name),
),
);
}
const sidenavInfoService = inject(SidenavInfoService);
return {
field,
header: 'Shop',
description: (d) => getPossiblyAsyncObservable(selectShopId(d)),
formatter: (d) => getPossiblyAsyncObservable(selectShopName(d)),
click: (d) => {
combineLatest([
getPossiblyAsyncObservable(selectPartyId(d)),

View File

@ -0,0 +1,39 @@
import { inject } from '@angular/core';
import { PossiblyAsync, ColumnObject, getPossiblyAsyncObservable } from '@vality/ng-core';
import get from 'lodash-es/get';
import { combineLatest } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { PartiesStoreService } from '../../../api/payment-processing';
export function createWalletColumn<T extends object>(
field: ColumnObject<T>['field'],
selectPartyId: (d: T) => PossiblyAsync<string>,
selectWalletId?: (d: T) => PossiblyAsync<string>,
selectWalletName?: (d: T) => PossiblyAsync<string>,
params: Partial<ColumnObject<T>> = {},
): ColumnObject<T> {
if (!selectWalletId) {
selectWalletId = (d) => get(d, field);
}
if (!selectWalletName) {
const partiesStoreService = inject(PartiesStoreService);
selectWalletName = (d) =>
getPossiblyAsyncObservable(selectPartyId(d)).pipe(
switchMap((partyId) =>
combineLatest([
partiesStoreService.get(partyId),
getPossiblyAsyncObservable(selectWalletId(d)),
]),
),
map(([party, walletId]) => party.wallets.get(walletId)?.name),
);
}
return {
field,
header: 'Wallet',
description: (d) => getPossiblyAsyncObservable(selectWalletId(d)),
formatter: (d) => getPossiblyAsyncObservable(selectWalletName(d)),
...params,
} as ColumnObject<T>;
}

View File

@ -4,6 +4,20 @@ import { getUnionKey, getUnionValue } from '@vality/ng-thrift';
import { formatRational } from './format-rational';
const CASH_VOLUME_PRIORITY: Record<keyof CashVolume, number> = {
fixed: 0,
share: 1,
product: 2,
};
export function compareCashVolumes(a: CashVolume, b: CashVolume) {
return CASH_VOLUME_PRIORITY[getUnionKey(a)] - CASH_VOLUME_PRIORITY[getUnionKey(b)];
}
export function formatCashVolumes(c: CashVolume[]) {
return c.sort(compareCashVolumes).map(formatCashVolume).join(' + ');
}
export function formatCashVolume(d: CashVolume) {
switch (getUnionKey(d)) {
case 'fixed':
@ -16,6 +30,7 @@ export function formatCashVolume(d: CashVolume) {
);
case 'product':
return `${getUnionKey(d.product).slice(0, -3)}(${Array.from(getUnionValue(d.product))
.sort(compareCashVolumes)
.map((c) => formatCashVolume(c))
.join(', ')})`;
}

View File

@ -7,3 +7,4 @@ export * from './create-contract-column';
export * from './format-cash-volume';
export * from './format-rational';
export * from './format-predicate';
export * from './create-wallet-column';