IMP-237: Show shops tariffs decision-value (#360)

This commit is contained in:
Rinat Arsaev 2024-05-24 14:35:45 +05:00 committed by GitHub
parent 1650516ab6
commit 45cf33deb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 255 additions and 34 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"]
"words": ["submain", "papaparse", "msgpack", "termsets"]
}

8
package-lock.json generated
View File

@ -26,7 +26,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "17.2.1-pr-62-e7ae3c9.0",
"@vality/ng-core": "17.2.1-pr-62-bd071a2.0",
"@vality/ng-thrift": "17.0.1-pr-5-2ce0f11.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
@ -6462,9 +6462,9 @@
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
},
"node_modules/@vality/ng-core": {
"version": "17.2.1-pr-62-e7ae3c9.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-62-e7ae3c9.0.tgz",
"integrity": "sha512-7x3TctfL6SAwmucbWo78i8Qk/ytC9qn4R2PXLxux1/6aHOaI+JB02Cv9zdTfXrKLQiPbzF/rAW/lbC8PtXVduQ==",
"version": "17.2.1-pr-62-bd071a2.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-62-bd071a2.0.tgz",
"integrity": "sha512-/zLUC/ogUZbIQEuciYDibCBMA3USixuPtm2aWtp7E+ppNJtpd56Wu4py/BTCrDUMjlt0kDog8aqqin1snqGscA==",
"dependencies": {
"@angular/material-date-fns-adapter": "^17.2.0",
"@ng-matero/extensions": "^17.1.0",

View File

@ -34,7 +34,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "17.2.1-pr-62-e7ae3c9.0",
"@vality/ng-core": "17.2.1-pr-62-bd071a2.0",
"@vality/ng-thrift": "17.0.1-pr-5-2ce0f11.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",

View File

@ -1,4 +1,4 @@
import { Component, isDevMode } from '@angular/core';
import { Component } from '@angular/core';
import { Link } from '@vality/ng-core';
import { KeycloakService } from 'keycloak-angular';
import sortBy from 'lodash-es/sortBy';
@ -81,15 +81,11 @@ export class AppComponent {
url: '/sources',
services: SOURCES_ROUTING_CONFIG.services,
},
...(isDevMode()
? [
{
label: 'Tariffs',
url: '/tariffs',
services: TARIFFS_ROUTING_CONFIG.services,
},
]
: []),
{
label: 'Tariffs',
url: '/tariffs',
services: TARIFFS_ROUTING_CONFIG.services,
},
],
[
{

View File

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

View File

@ -0,0 +1,38 @@
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

@ -17,6 +17,10 @@
</v-filters>
<v-table
[cellTemplate]="{
decision: arrayColumnTemplate,
value: arrayColumnTemplate
}"
[columns]="columns"
[data]="tariffs$ | async"
[hasMore]="hasMore$ | async"
@ -24,4 +28,11 @@
(more)="more()"
(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>
</cc-page-layout>

View File

@ -2,7 +2,11 @@ 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 } from '@vality/domain-proto/internal/domain';
import {
TermSetHierarchyRef,
type CashFlowSelector,
type CashFlowPosting,
} from '@vality/domain-proto/internal/domain';
import {
CommonSearchQueryParams,
ShopSearchQuery,
@ -23,7 +27,12 @@ import {
TableModule,
UpdateOptions,
} from '@vality/ng-core';
import { getUnionKey } from '@vality/ng-thrift';
import { map, shareReplay } from 'rxjs/operators';
import {
DomainObjectCardComponent,
getDomainObjectDetails,
} from 'src/app/shared/components/thrift-api-crud';
import { Overwrite } from 'utility-types';
import {
@ -32,9 +41,12 @@ 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';
import { SidenavInfoService } from '@cc/app/shared/components/sidenav-info';
import { DEBOUNCE_TIME_MS } from '@cc/app/tokens';
import { ShopsTariffsService } from './shops-tariffs.service';
@ -45,6 +57,66 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
{ term_sets_ids?: TermSetHierarchyRef['id'][] }
>;
function getViewedCashFlowSelectors(d: ShopTermSet) {
return (
d.current_term_set.data.term_sets
?.map?.((t) => t?.terms?.payments?.fees)
?.filter?.(Boolean) ?? []
);
}
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,
@ -76,7 +148,7 @@ export class ShopsTariffsComponent implements OnInit {
hasMore$ = this.shopsTariffsService.hasMore$;
isLoading$ = this.shopsTariffsService.isLoading$;
columns: Column<ShopTermSet>[] = [
createShopColumn<ShopTermSet>('shop_id', (d) => d.owner_id),
createShopColumn<ShopTermSet>('shop_id', (d) => d.owner_id, undefined, { pinned: 'left' }),
createPartyColumn<ShopTermSet>('owner_id'),
createContractColumn<ShopTermSet>(
(d) => d.contract_id,
@ -86,9 +158,20 @@ export class ShopsTariffsComponent implements OnInit {
{ field: 'currency' },
{
field: 'current_term_set',
formatter: (d) => d.current_term_set?.data?.name,
description: (d) => d.current_term_set?.data?.description,
tooltip: (d) => d.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: 'decision',
formatter: (d) => getInlineDecisions(getViewedCashFlowSelectors(d)).map((v) => v.if),
},
{
field: 'value',
formatter: (d) => getInlineDecisions(getViewedCashFlowSelectors(d)).map((v) => v.value),
},
{
field: 'term_set_history',
@ -109,6 +192,7 @@ export class ShopsTariffsComponent implements OnInit {
private qp: QueryParamsService<Params>,
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
private dr: DestroyRef,
private sidenavInfoService: SidenavInfoService,
) {}
ngOnInit() {

View File

@ -0,0 +1,3 @@
<cc-card [title]="data()?.data?.name">
<cc-cash-flows-selector-table [data]="feesData()"></cc-cash-flows-selector-table>
</cc-card>

View File

@ -0,0 +1,23 @@
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,22 @@
import { CashVolume } from '@vality/domain-proto/internal/domain';
import { formatCurrency } from '@vality/ng-core';
import { getUnionKey, getUnionValue } from '@vality/ng-thrift';
import { formatRational } from './format-rational';
export function formatCashVolume(d: CashVolume) {
switch (getUnionKey(d)) {
case 'fixed':
return formatCurrency(d?.fixed?.cash?.amount, d?.fixed?.cash?.currency?.symbolic_code);
case 'share':
return (
formatRational(d?.share?.parts) +
(d?.share?.of === 2 ? ' of surplus' : '') +
(d?.share?.rounding_method === 1 ? ' (round .5+)' : '')
);
case 'product':
return `${getUnionKey(d.product).slice(0, -3)}(${Array.from(getUnionValue(d.product))
.map((c) => formatCashVolume(c))
.join(', ')})`;
}
}

View File

@ -1,5 +1,6 @@
import { Predicate } from '@vality/domain-proto/domain';
import { getUnionKey, getUnionValue } from '@vality/ng-thrift';
import { formatCurrency, inlineJson } from '@vality/ng-core';
import { getUnionKey, getUnionValue, toJson } from '@vality/ng-thrift';
import startCase from 'lodash-es/startCase';
export function formatPredicate(predicate: Predicate, level = 0) {
@ -18,24 +19,57 @@ export function formatPredicate(predicate: Predicate, level = 0) {
if (predicatesSet.size <= 1) {
return formatPredicate(predicatesSet.keys().next().value, level + 1);
}
return `${startCase(getUnionKey(predicate))} ${
predicatesSet.size <= 3
? `(${Array.from(predicatesSet)
.map((p) => formatPredicate(p, level + 1))
.join(', ')})`
: predicatesSet.size
}`;
const res = Array.from(predicatesSet)
.map((p) => formatPredicate(p, level + 1))
.join(type === 'all_of' ? ' & ' : ' OR ');
return level === 0 ? `(${res})` : res;
}
case 'condition': {
const condition = predicate.condition;
switch (getUnionKey(condition)) {
case 'currency_is':
return `currency: ${condition.currency_is.symbolic_code}`;
case 'bin_data':
return `bin_data: ${inlineJson(toJson(condition.bin_data), Infinity)}`;
case 'category_is':
return `category: #${condition.category_is.id}`;
case 'cost_in':
return `cost: ${
getUnionKey(condition.cost_in.upper) === 'inclusive' ? '[' : '('
}${formatCurrency(
getUnionValue(condition.cost_in.upper)?.amount,
getUnionValue(condition.cost_in.upper)?.currency?.symbolic_code,
)}, ${formatCurrency(
getUnionValue(condition.cost_in.lower)?.amount,
getUnionValue(condition.cost_in.lower)?.currency?.symbolic_code,
)}${getUnionKey(condition.cost_in.lower) === 'inclusive' ? ']' : ')'}`;
case 'cost_is_multiple_of':
return `cost_is_multiple: ${formatCurrency(
condition.cost_is_multiple_of.amount,
condition.cost_is_multiple_of.currency.symbolic_code,
)}`;
case 'identification_level_is':
return `identification_level: ${condition.identification_level_is}`; // TODO: fix enum value
case 'p2p_tool':
return `p2p_tool: ${inlineJson(toJson(condition.p2p_tool), Infinity)}`;
case 'party':
return `party: ${inlineJson(toJson(condition.party), Infinity)}`;
case 'payment_tool':
return `payment_tool: ${inlineJson(toJson(condition.payment_tool), Infinity)}`;
case 'payout_method_is':
return `payout_method: #${condition.payout_method_is.id}`; // TODO: fix enum value
case 'shop_location_is':
return `shop_url: ${condition.shop_location_is.url}`;
}
return '';
}
case 'condition':
return startCase(getUnionKey(getUnionValue(predicate) as Predicate['condition']));
case 'criterion':
return `${startCase(getUnionKey(predicate))} #${predicate.criterion.id}`;
case 'is_not': {
if (getUnionKey(getUnionValue(predicate) as Predicate) !== 'is_not') {
return `${getUnionKey(predicate)} ${formatPredicate(predicate.is_not, level + 1)}`;
return `NOT ${formatPredicate(predicate.is_not, level + 1)}`;
}
break;
return '';
}
}
return startCase(getUnionKey(predicate));
}

View File

@ -0,0 +1,6 @@
import { Rational } from '@vality/domain-proto/internal/base';
import round from 'lodash-es/round';
export function formatRational(value: Rational) {
return `${round((value.p / value.q) * 100, 4)}%`;
}

View File

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