mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
TD-935: New table with virtual scroll: Payments & Terms (#375)
This commit is contained in:
parent
043feb63bf
commit
dc3855d791
33
package-lock.json
generated
33
package-lock.json
generated
@ -26,8 +26,8 @@
|
||||
"@vality/fistful-proto": "2.0.1-88e69a5.0",
|
||||
"@vality/machinegun-proto": "1.0.0",
|
||||
"@vality/magista-proto": "2.0.2-28d11b9.0",
|
||||
"@vality/ng-core": "18.1.0",
|
||||
"@vality/ng-thrift": "18.0.1-pr-12-d099f93.0",
|
||||
"@vality/ng-core": "18.2.1-pr-66-23525f5.0",
|
||||
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
|
||||
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
|
||||
"@vality/repairer-proto": "2.0.2-07b73e9.0",
|
||||
"@vality/scrooge-proto": "0.1.1-9ce7fc6.0",
|
||||
@ -6615,9 +6615,9 @@
|
||||
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
|
||||
},
|
||||
"node_modules/@vality/ng-core": {
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-18.1.0.tgz",
|
||||
"integrity": "sha512-N0bOmfezQxKnZO9peMhMxlmMrH0A3dGrNNpzYgLoxO8XVIxoVSc9QpaXsb7YwR3M3pwmCWDjqxYXVhGVjVBrFA==",
|
||||
"version": "18.2.1-pr-66-23525f5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-18.2.1-pr-66-23525f5.0.tgz",
|
||||
"integrity": "sha512-Y4kMHbWgjMuDvk+ev7ZP7MY3QZFd1vaHy3ehGBAzNidzCQVC1eYkc3x6ta6VL8quRfrHzUGwphC50Pxgodxsaw==",
|
||||
"dependencies": {
|
||||
"@angular/material-date-fns-adapter": "^18.0.5",
|
||||
"@ng-matero/extensions": "^18.0.3",
|
||||
@ -6627,6 +6627,7 @@
|
||||
"@s-libs/rxjs-core": "^18.0.0",
|
||||
"dinero.js": "^2.0.0-alpha.14",
|
||||
"fuse.js": "^7.0.0",
|
||||
"ng-table-virtual-scroll": "^1.6.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@ -6643,9 +6644,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vality/ng-thrift": {
|
||||
"version": "18.0.1-pr-12-d099f93.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-thrift/-/ng-thrift-18.0.1-pr-12-d099f93.0.tgz",
|
||||
"integrity": "sha512-TjcZZWiVytQIOmuTGDKeZozWlhyjn04Npi4YXiGXG64tA0qtQgyWawl1OPUXTbX19tJMg7ib7tgZ/CIXrZrAWw==",
|
||||
"version": "18.0.1-pr-13-bdb6d51.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/ng-thrift/-/ng-thrift-18.0.1-pr-13-bdb6d51.0.tgz",
|
||||
"integrity": "sha512-maXeenYBoPcjbvKZfR2/VkPRGxzxedOit0HXAqvK+h1RUiozmfkjC/YtXzxsiIAarPk36QQpES2xTmCvVb/WoQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0",
|
||||
"yaml": "^2.4.5"
|
||||
@ -6656,7 +6657,7 @@
|
||||
"@angular/core": "^18.0.0",
|
||||
"@angular/material": "^18.0.0",
|
||||
"@types/lodash-es": "^4.0.0",
|
||||
"@vality/ng-core": "^18.0.0 || ^18.0.1-pr",
|
||||
"@vality/ng-core": "*",
|
||||
"@vality/thrift-ts": "^2.4.1-8ad5123.0",
|
||||
"lodash-es": "^4.0.0",
|
||||
"rxjs": "^7.0.0",
|
||||
@ -14090,6 +14091,20 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ng-table-virtual-scroll": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ng-table-virtual-scroll/-/ng-table-virtual-scroll-1.6.1.tgz",
|
||||
"integrity": "sha512-HXcRoPPHBBHU47HPsdegGoLKbu0UYnrIVVHLwwdtje1AduEKNY2ZCUK/T4nuX3biImSrKoydUa7/vbFtmYx86Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=15.0.0",
|
||||
"@angular/common": ">=15.0.0",
|
||||
"@angular/core": ">=15.0.0",
|
||||
"@angular/material": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-color": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
|
||||
|
@ -34,8 +34,8 @@
|
||||
"@vality/fistful-proto": "2.0.1-88e69a5.0",
|
||||
"@vality/machinegun-proto": "1.0.0",
|
||||
"@vality/magista-proto": "2.0.2-28d11b9.0",
|
||||
"@vality/ng-core": "18.1.0",
|
||||
"@vality/ng-thrift": "18.0.1-pr-12-d099f93.0",
|
||||
"@vality/ng-core": "18.2.1-pr-66-23525f5.0",
|
||||
"@vality/ng-thrift": "18.0.1-pr-13-bdb6d51.0",
|
||||
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
|
||||
"@vality/repairer-proto": "2.0.2-07b73e9.0",
|
||||
"@vality/scrooge-proto": "0.1.1-9ce7fc6.0",
|
||||
|
@ -1,4 +1,4 @@
|
||||
<v-table
|
||||
<v-table2
|
||||
[columns]="columns"
|
||||
[data]="data"
|
||||
[hasMore]="hasMore"
|
||||
@ -12,4 +12,4 @@
|
||||
<v-table-actions>
|
||||
<ng-content></ng-content>
|
||||
</v-table-actions>
|
||||
</v-table>
|
||||
</v-table2>
|
||||
|
@ -1,20 +1,24 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { InvoicePaymentStatus } from '@vality/domain-proto/domain';
|
||||
import { StatPayment } from '@vality/magista-proto/magista';
|
||||
import { Column, TagColumn, LoadOptions, createOperationColumn } from '@vality/ng-core';
|
||||
import { LoadOptions, Column2, createMenuColumn } from '@vality/ng-core';
|
||||
import { getUnionKey } from '@vality/ng-thrift';
|
||||
import startCase from 'lodash-es/startCase';
|
||||
|
||||
import { AmountCurrencyService } from '@cc/app/shared/services';
|
||||
|
||||
import { createFailureColumn, createPartyColumn, createShopColumn } from '../../../../shared';
|
||||
import { createProviderColumn } from '../../../../shared/utils/table/create-provider-column';
|
||||
import { createTerminalColumn } from '../../../../shared/utils/table/create-terminal-column';
|
||||
import { createFailureColumn2 } from '../../../../shared';
|
||||
import {
|
||||
createPartyColumn,
|
||||
createShopColumn,
|
||||
createDomainObjectColumn,
|
||||
createCurrencyColumn,
|
||||
} from '../../../../shared/utils/table2';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-payments-table',
|
||||
templateUrl: './payments-table.component.html',
|
||||
styles: `:host { height: 100%; }`,
|
||||
})
|
||||
export class PaymentsTableComponent {
|
||||
@Input() data!: StatPayment[];
|
||||
@ -26,64 +30,58 @@ export class PaymentsTableComponent {
|
||||
@Output() update = new EventEmitter<LoadOptions>();
|
||||
@Output() more = new EventEmitter<void>();
|
||||
|
||||
columns: Column<StatPayment>[] = [
|
||||
{ field: 'id', click: (d) => this.toDetails(d), pinned: 'left' },
|
||||
{ field: 'invoice_id', pinned: 'left' },
|
||||
{
|
||||
columns: Column2<StatPayment>[] = [
|
||||
{ field: 'id', cell: (d) => ({ click: () => this.toDetails(d) }), sticky: 'start' },
|
||||
{ field: 'invoice_id', sticky: 'start' },
|
||||
createCurrencyColumn((d) => ({ amount: d.amount, code: d.currency_symbolic_code }), {
|
||||
field: 'amount',
|
||||
type: 'currency',
|
||||
formatter: (data) =>
|
||||
this.amountCurrencyService.toMajor(data.amount, data.currency_symbolic_code),
|
||||
typeParameters: {
|
||||
currencyCode: 'currency_symbolic_code',
|
||||
},
|
||||
},
|
||||
{
|
||||
}),
|
||||
createCurrencyColumn((d) => ({ amount: d.fee, code: d.currency_symbolic_code }), {
|
||||
field: 'fee',
|
||||
type: 'currency',
|
||||
formatter: (data) =>
|
||||
this.amountCurrencyService.toMajor(data.fee, data.currency_symbolic_code),
|
||||
typeParameters: {
|
||||
currencyCode: 'currency_symbolic_code',
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
field: 'status',
|
||||
type: 'tag',
|
||||
formatter: (data) => getUnionKey(data.status),
|
||||
typeParameters: {
|
||||
label: (data) => startCase(getUnionKey(data.status)),
|
||||
tags: {
|
||||
captured: { color: 'success' },
|
||||
refunded: { color: 'success' },
|
||||
charged_back: { color: 'success' },
|
||||
pending: { color: 'pending' },
|
||||
processed: { color: 'pending' },
|
||||
failed: { color: 'warn' },
|
||||
cancelled: { color: 'neutral' },
|
||||
cell: (d) => ({
|
||||
value: startCase(getUnionKey(d.status)),
|
||||
color: (
|
||||
{
|
||||
captured: 'success',
|
||||
refunded: 'success',
|
||||
charged_back: 'success',
|
||||
pending: 'pending',
|
||||
processed: 'pending',
|
||||
failed: 'warn',
|
||||
cancelled: 'neutral',
|
||||
} as const
|
||||
)[getUnionKey(d.status)],
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as TagColumn<StatPayment, keyof InvoicePaymentStatus>,
|
||||
{ field: 'created_at', type: 'datetime' },
|
||||
createPartyColumn('owner_id'),
|
||||
createShopColumn('shop_id', (d) => d.owner_id),
|
||||
'domain_revision',
|
||||
createTerminalColumn((d) => d.terminal_id.id),
|
||||
createProviderColumn((d) => d.provider_id.id),
|
||||
'external_id',
|
||||
createFailureColumn<StatPayment>(
|
||||
(d) => d.status?.failed?.failure?.failure,
|
||||
(d) =>
|
||||
{ field: 'created_at', cell: { type: 'datetime' } },
|
||||
createPartyColumn((d) => ({ id: d.owner_id })),
|
||||
createShopColumn((d) => ({ partyId: d.owner_id, shopId: d.shop_id })),
|
||||
{ field: 'domain_revision' },
|
||||
createDomainObjectColumn((d) => ({ ref: { terminal: d.terminal_id } }), {
|
||||
header: 'Terminal',
|
||||
}),
|
||||
createDomainObjectColumn((d) => ({ ref: { provider: d.provider_id } }), {
|
||||
header: 'Provider',
|
||||
}),
|
||||
{ field: 'external_id' },
|
||||
createFailureColumn2((d) => ({
|
||||
failure: d.status?.failed?.failure?.failure,
|
||||
noFailureMessage:
|
||||
getUnionKey(d.status?.failed?.failure) === 'failure'
|
||||
? ''
|
||||
: startCase(getUnionKey(d.status?.failed?.failure)),
|
||||
),
|
||||
createOperationColumn<StatPayment>([
|
||||
})),
|
||||
createMenuColumn((d) => ({
|
||||
items: [
|
||||
{
|
||||
label: 'Details',
|
||||
click: (data) => this.toDetails(data),
|
||||
click: () => this.toDetails(d),
|
||||
},
|
||||
]),
|
||||
],
|
||||
})),
|
||||
];
|
||||
|
||||
constructor(
|
||||
|
@ -1,4 +1,4 @@
|
||||
<cc-page-layout title="Payments">
|
||||
<cc-page-layout fullHeight title="Payments">
|
||||
<cc-page-layout-actions>
|
||||
<v-more-filters-button [filters]="filters"></v-more-filters-button>
|
||||
</cc-page-layout-actions>
|
||||
|
@ -18,11 +18,10 @@
|
||||
|
||||
<v-table2
|
||||
[columns]="columns"
|
||||
[data]="tariffs$ | async"
|
||||
[hasMore]="hasMore$ | async"
|
||||
[maxSize]="250"
|
||||
[progress]="isLoading$ | async"
|
||||
infinityScroll
|
||||
[treeData]="tariffs$ | async"
|
||||
(more)="more()"
|
||||
(update)="update($event)"
|
||||
></v-table2>
|
||||
|
@ -40,10 +40,15 @@ import {
|
||||
createContractColumn,
|
||||
createDomainObjectColumn,
|
||||
} from '../../../../shared/utils/table2';
|
||||
import { getInlineDecisions2, InlineDecision2 } from '../../utils/get-inline-decisions';
|
||||
import { ShopsTermSetHistoryCardComponent } from '../shops-term-set-history-card';
|
||||
|
||||
import { ShopsTariffsService } from './shops-tariffs.service';
|
||||
import { createShopFeesColumn } from './utils/create-shop-fees-column';
|
||||
import {
|
||||
isShopTermSetDecision,
|
||||
SHOP_FEES_COLUMNS,
|
||||
getShopCashFlowSelectors,
|
||||
} from './utils/shop-fees-columns';
|
||||
|
||||
type Params = Pick<CommonSearchQueryParams, 'currencies'> &
|
||||
Overwrite<
|
||||
@ -80,10 +85,24 @@ export class ShopsTariffsComponent implements OnInit {
|
||||
term_sets_ids: null,
|
||||
}),
|
||||
);
|
||||
tariffs$ = this.shopsTariffsService.result$;
|
||||
tariffs$ = this.shopsTariffsService.result$.pipe(
|
||||
map((terms) =>
|
||||
terms.map((t) => ({
|
||||
value: t,
|
||||
children: getInlineDecisions2(getShopCashFlowSelectors(t.current_term_set)).filter(
|
||||
(v) =>
|
||||
isShopTermSetDecision(v, {
|
||||
partyId: t.owner_id,
|
||||
shopId: t.shop_id,
|
||||
currency: t.currency,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
),
|
||||
);
|
||||
hasMore$ = this.shopsTariffsService.hasMore$;
|
||||
isLoading$ = this.shopsTariffsService.isLoading$;
|
||||
columns: Column2<ShopTermSet>[] = [
|
||||
columns: Column2<ShopTermSet, InlineDecision2>[] = [
|
||||
createShopColumn(
|
||||
(d) => ({
|
||||
shopId: d.shop_id,
|
||||
@ -101,12 +120,7 @@ export class ShopsTariffsComponent implements OnInit {
|
||||
createDomainObjectColumn((d) => ({ ref: { term_set_hierarchy: d.current_term_set.ref } }), {
|
||||
header: 'Term Set',
|
||||
}),
|
||||
...createShopFeesColumn<ShopTermSet>(
|
||||
(d) => d?.current_term_set,
|
||||
(d) => d.owner_id,
|
||||
(d) => d.shop_id,
|
||||
(d) => d.currency,
|
||||
),
|
||||
...SHOP_FEES_COLUMNS,
|
||||
{
|
||||
field: 'term_set_history',
|
||||
cell: (d) => ({
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Column2 } from '@vality/ng-core';
|
||||
|
||||
import type {
|
||||
TermSetHierarchyObject,
|
||||
CashFlowPosting,
|
||||
} from '@vality/dominator-proto/internal/proto/domain';
|
||||
|
||||
import { createFeesColumns } from '../../../utils/create-fees-columns';
|
||||
import {
|
||||
getInlineDecisions,
|
||||
type InlineCashFlowSelector,
|
||||
} from '../../../utils/get-inline-decisions';
|
||||
|
||||
export function getViewedCashFlowSelectors(d: TermSetHierarchyObject) {
|
||||
return d?.data?.term_sets?.map?.((t) => t?.terms?.payments?.fees)?.filter?.(Boolean) ?? [];
|
||||
}
|
||||
|
||||
export function createShopFeesColumn<T extends object>(
|
||||
fn: (d: T) => TermSetHierarchyObject = (d) => d as never,
|
||||
getPartyId: (d: T) => string,
|
||||
getShopId: (d: T) => string,
|
||||
getCurrency: (d: T) => string,
|
||||
): Column2<T>[] {
|
||||
const filterRreserve = (v: CashFlowPosting) =>
|
||||
v?.source?.merchant === 0 && v?.destination?.merchant === 1;
|
||||
const filterDecisions = (d: T) => (v: InlineCashFlowSelector) =>
|
||||
(!v?.if?.condition?.party?.definition?.shop_is ||
|
||||
(v?.if?.condition?.party?.id === getPartyId(d) &&
|
||||
v?.if?.condition?.party?.definition?.shop_is === getShopId(d))) &&
|
||||
(!getCurrency(d) ||
|
||||
!v?.if?.condition?.currency_is?.symbolic_code ||
|
||||
v?.if?.condition?.currency_is?.symbolic_code === getCurrency(d));
|
||||
const cols = createFeesColumns<T>(
|
||||
(d) => getViewedCashFlowSelectors(fn(d)),
|
||||
(v) => v?.source?.merchant === 0 && v?.destination?.system === 0,
|
||||
(v) => !filterRreserve(v),
|
||||
filterDecisions,
|
||||
);
|
||||
return [
|
||||
...cols.slice(0, -1),
|
||||
{
|
||||
field: 'rreserve',
|
||||
header: 'RReserve',
|
||||
cell: (d) => ({
|
||||
value: getInlineDecisions(getViewedCashFlowSelectors(fn(d)), filterRreserve)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => v.value),
|
||||
}),
|
||||
},
|
||||
cols.at(-1),
|
||||
];
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
import {
|
||||
CashFlowPosting,
|
||||
Predicate,
|
||||
PartyID,
|
||||
ShopID,
|
||||
TermSetHierarchyObject,
|
||||
} from '@vality/domain-proto/internal/domain';
|
||||
import { Column2 } from '@vality/ng-core';
|
||||
|
||||
import { getCashVolumeParts, formatCashVolumes } from '../../../../../shared';
|
||||
import { InlineDecision2, formatLevelPredicate } from '../../../utils/get-inline-decisions';
|
||||
import { isOneHundredPercentCashFlowPosting } from '../../../utils/is-one-hundred-percent-cash-flow-posting';
|
||||
import { isThatCurrency } from '../../../utils/is-that-currency';
|
||||
|
||||
export function getShopCashFlowSelectors(d: TermSetHierarchyObject) {
|
||||
return d?.data?.term_sets?.map?.((t) => t?.terms?.payments?.fees)?.filter?.(Boolean) ?? [];
|
||||
}
|
||||
|
||||
export function isShopFee(v: CashFlowPosting) {
|
||||
return v?.source?.merchant === 0 && v?.destination?.system === 0;
|
||||
}
|
||||
|
||||
export function isShopRreserve(v: CashFlowPosting) {
|
||||
return v?.source?.merchant === 0 && v?.destination?.merchant === 1;
|
||||
}
|
||||
|
||||
export function isThatShopParty(predicate: Predicate, partyId: PartyID, shopId: ShopID) {
|
||||
return (
|
||||
predicate?.condition?.party?.id === partyId &&
|
||||
predicate?.condition?.party?.definition?.shop_is === shopId
|
||||
);
|
||||
}
|
||||
|
||||
export function isShopTermSetDecision(
|
||||
v: InlineDecision2,
|
||||
params: { partyId: PartyID; shopId: ShopID; currency: string },
|
||||
) {
|
||||
return (
|
||||
(!v?.if?.condition?.party?.definition?.shop_is ||
|
||||
isThatShopParty(v?.if, params.partyId, params.shopId)) &&
|
||||
(!v?.if?.condition?.currency_is?.symbolic_code || isThatCurrency(v?.if, params.currency))
|
||||
);
|
||||
}
|
||||
|
||||
export const SHOP_FEES_COLUMNS = [
|
||||
{
|
||||
field: 'condition',
|
||||
child: (d) => ({ value: formatLevelPredicate(d) }),
|
||||
},
|
||||
{
|
||||
field: 'feeShare',
|
||||
header: 'Fee, %',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isShopFee).map((v) => v.volume))?.share,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeFixed',
|
||||
header: 'Fee, fix',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isShopFee).map((v) => v.volume))?.fixed,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeMin',
|
||||
header: 'Fee, min',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isShopFee).map((v) => v.volume))?.max,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeMax',
|
||||
header: 'Fee, max',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isShopFee).map((v) => v.volume))?.min,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'rreserve',
|
||||
header: 'RReserve',
|
||||
child: (d) => ({
|
||||
value: formatCashVolumes(d.value.filter(isShopRreserve).map((v) => v.volume)),
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'other',
|
||||
child: (d) => ({
|
||||
value: formatCashVolumes(
|
||||
d.value
|
||||
.filter(
|
||||
(v) =>
|
||||
!isShopFee(v) &&
|
||||
!isShopRreserve(v) &&
|
||||
!isOneHundredPercentCashFlowPosting(v),
|
||||
)
|
||||
.map((v) => v.volume),
|
||||
),
|
||||
}),
|
||||
},
|
||||
] satisfies Column2<object, InlineDecision2>[];
|
@ -1,2 +1 @@
|
||||
export * from './shops-term-set-history-card.component';
|
||||
export * from '../../utils/create-fees-columns';
|
||||
|
@ -1,3 +1,3 @@
|
||||
<cc-card [title]="'Term Sets History'">
|
||||
<v-table2 [columns]="columns" [data]="historyData()" infinityScroll></v-table2>
|
||||
<v-table2 [columns]="columns" [treeData]="historyData()"></v-table2>
|
||||
</cc-card>
|
||||
|
@ -7,7 +7,12 @@ import type { TermSetHistory, ShopTermSet } from '@vality/dominator-proto/intern
|
||||
|
||||
import { SidenavInfoModule } from '../../../../shared/components/sidenav-info';
|
||||
import { getDomainObjectDetails } from '../../../../shared/components/thrift-api-crud';
|
||||
import { createShopFeesColumn } from '../shops-tariffs/utils/create-shop-fees-column';
|
||||
import { getInlineDecisions2 } from '../../utils/get-inline-decisions';
|
||||
import {
|
||||
getShopCashFlowSelectors,
|
||||
isShopTermSetDecision,
|
||||
SHOP_FEES_COLUMNS,
|
||||
} from '../shops-tariffs/utils/shop-fees-columns';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-shops-term-set-history-card',
|
||||
@ -18,7 +23,18 @@ import { createShopFeesColumn } from '../shops-tariffs/utils/create-shop-fees-co
|
||||
})
|
||||
export class ShopsTermSetHistoryCardComponent {
|
||||
data = input<ShopTermSet>();
|
||||
historyData = computed(() => this.data()?.term_set_history?.reverse?.());
|
||||
historyData = computed(() =>
|
||||
(this.data()?.term_set_history?.reverse?.() || []).map((t) => ({
|
||||
value: t,
|
||||
children: getInlineDecisions2(getShopCashFlowSelectors(t.term_set)).filter((v) =>
|
||||
isShopTermSetDecision(v, {
|
||||
partyId: this.data().owner_id,
|
||||
shopId: this.data().shop_id,
|
||||
currency: this.data().currency,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
columns: Column2<TermSetHistory>[] = [
|
||||
{ field: 'applied_at', cell: { type: 'datetime' } },
|
||||
@ -30,11 +46,6 @@ export class ShopsTermSetHistoryCardComponent {
|
||||
?.description,
|
||||
}),
|
||||
},
|
||||
...createShopFeesColumn<TermSetHistory>(
|
||||
(d) => d.term_set,
|
||||
() => this.data().owner_id,
|
||||
() => this.data().shop_id,
|
||||
() => this.data().currency,
|
||||
),
|
||||
...SHOP_FEES_COLUMNS,
|
||||
];
|
||||
}
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './terminals-term-set-history-card.component';
|
||||
export * from '../../utils/create-fees-columns';
|
||||
|
@ -1,30 +0,0 @@
|
||||
import type { InlineCashFlowSelector } from '../../../utils/get-inline-decisions';
|
||||
import type { TermSetHierarchyObject } from '@vality/dominator-proto/internal/proto/domain';
|
||||
|
||||
import { createFeesColumns } from '../../../utils/create-fees-columns';
|
||||
|
||||
export function getViewedCashFlowSelectors(d: TermSetHierarchyObject) {
|
||||
return (
|
||||
d.data.term_sets
|
||||
?.map?.((t) => t?.terms?.wallets?.withdrawals?.cash_flow)
|
||||
?.filter?.(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export function createWalletFeesColumn<T extends object = TermSetHierarchyObject>(
|
||||
fn: (d: T) => TermSetHierarchyObject = (d) => d as never,
|
||||
getWalletId: (d: T) => string,
|
||||
getCurrency: (d: T) => string,
|
||||
) {
|
||||
return createFeesColumns<T>(
|
||||
(d) => getViewedCashFlowSelectors(fn(d)),
|
||||
(v) => v?.source?.wallet === 1 && v?.destination?.system === 0,
|
||||
undefined,
|
||||
(d: T) => (v: InlineCashFlowSelector) =>
|
||||
(!v?.if?.condition?.party?.definition?.wallet_is ||
|
||||
v?.if?.condition?.party?.definition?.wallet_is === getWalletId(d)) &&
|
||||
(!getCurrency(d) ||
|
||||
!v?.if?.condition?.currency_is?.symbolic_code ||
|
||||
v?.if?.condition?.currency_is?.symbolic_code === getCurrency(d)),
|
||||
);
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
import {
|
||||
CashFlowPosting,
|
||||
PartyID,
|
||||
Predicate,
|
||||
WalletID,
|
||||
} from '@vality/domain-proto/internal/domain';
|
||||
import { Column2 } from '@vality/ng-core';
|
||||
|
||||
import type { TermSetHierarchyObject } from '@vality/dominator-proto/internal/proto/domain';
|
||||
|
||||
import { getCashVolumeParts, formatCashVolumes } from '../../../../../shared';
|
||||
import { InlineDecision2, formatLevelPredicate } from '../../../utils/get-inline-decisions';
|
||||
import { isOneHundredPercentCashFlowPosting } from '../../../utils/is-one-hundred-percent-cash-flow-posting';
|
||||
import { isThatCurrency } from '../../../utils/is-that-currency';
|
||||
|
||||
export function getWalletCashFlowSelectors(d: TermSetHierarchyObject) {
|
||||
return (
|
||||
d.data.term_sets
|
||||
?.map?.((t) => t?.terms?.wallets?.withdrawals?.cash_flow)
|
||||
?.filter?.(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export function isWalletFee(v: CashFlowPosting) {
|
||||
return v?.source?.wallet === 1 && v?.destination?.system === 0;
|
||||
}
|
||||
|
||||
export function isThatWalletParty(predicate: Predicate, partyId: PartyID, walletId: WalletID) {
|
||||
return (
|
||||
predicate?.condition?.party?.id === partyId &&
|
||||
predicate?.condition?.party?.definition?.wallet_is === walletId
|
||||
);
|
||||
}
|
||||
|
||||
export function isWalletTermSetDecision(
|
||||
v: InlineDecision2,
|
||||
params: { partyId: PartyID; walletId: WalletID; currency: string },
|
||||
) {
|
||||
return (
|
||||
(!v?.if?.condition?.party?.definition?.wallet_is ||
|
||||
isThatWalletParty(v?.if, params.partyId, params.walletId)) &&
|
||||
(!v?.if?.condition?.currency_is?.symbolic_code || isThatCurrency(v?.if, params.currency))
|
||||
);
|
||||
}
|
||||
|
||||
export const WALLET_FEES_COLUMNS = [
|
||||
{
|
||||
field: 'condition',
|
||||
child: (d) => ({ value: formatLevelPredicate(d) }),
|
||||
},
|
||||
{
|
||||
field: 'feeShare',
|
||||
header: 'Fee, %',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isWalletFee).map((v) => v.volume))?.share,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeFixed',
|
||||
header: 'Fee, fix',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isWalletFee).map((v) => v.volume))?.fixed,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeMin',
|
||||
header: 'Fee, min',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isWalletFee).map((v) => v.volume))?.max,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'feeMax',
|
||||
header: 'Fee, max',
|
||||
child: (d) => ({
|
||||
value: getCashVolumeParts(d.value.filter(isWalletFee).map((v) => v.volume))?.min,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'other',
|
||||
child: (d) => ({
|
||||
value: formatCashVolumes(
|
||||
d.value
|
||||
.filter((v) => !isWalletFee(v) && !isOneHundredPercentCashFlowPosting(v))
|
||||
.map((v) => v.volume),
|
||||
),
|
||||
}),
|
||||
},
|
||||
] satisfies Column2<object, InlineDecision2>[];
|
@ -15,11 +15,10 @@
|
||||
|
||||
<v-table2
|
||||
[columns]="columns"
|
||||
[data]="tariffs$ | async"
|
||||
[hasMore]="hasMore$ | async"
|
||||
[maxSize]="250"
|
||||
[progress]="isLoading$ | async"
|
||||
infinityScroll
|
||||
[treeData]="tariffs$ | async"
|
||||
(more)="more()"
|
||||
(update)="update($event)"
|
||||
></v-table2>
|
||||
|
@ -42,9 +42,14 @@ import {
|
||||
} from '@cc/app/shared/utils/table2';
|
||||
import { DEBOUNCE_TIME_MS } from '@cc/app/tokens';
|
||||
|
||||
import { InlineDecision2, getInlineDecisions2 } from '../../utils/get-inline-decisions';
|
||||
import { WalletsTermSetHistoryCardComponent } from '../wallets-term-set-history-card';
|
||||
|
||||
import { createWalletFeesColumn } from './utils/create-wallet-fees-column';
|
||||
import {
|
||||
WALLET_FEES_COLUMNS,
|
||||
getWalletCashFlowSelectors,
|
||||
isWalletTermSetDecision,
|
||||
} from './utils/wallet-fees-columns';
|
||||
import { WalletsTariffsService } from './wallets-tariffs.service';
|
||||
|
||||
type Params = Pick<CommonSearchQueryParams, 'currencies'> &
|
||||
@ -83,10 +88,25 @@ export class WalletsTariffsComponent implements OnInit {
|
||||
identity_ids: null,
|
||||
}),
|
||||
);
|
||||
tariffs$ = this.walletsTariffsService.result$;
|
||||
tariffs$ = this.walletsTariffsService.result$.pipe(
|
||||
map((terms) =>
|
||||
terms.map((t) => ({
|
||||
value: t,
|
||||
children: getInlineDecisions2(
|
||||
getWalletCashFlowSelectors(t.current_term_set),
|
||||
).filter((v) =>
|
||||
isWalletTermSetDecision(v, {
|
||||
partyId: t.owner_id,
|
||||
walletId: t.wallet_id,
|
||||
currency: t.currency,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
),
|
||||
);
|
||||
hasMore$ = this.walletsTariffsService.hasMore$;
|
||||
isLoading$ = this.walletsTariffsService.isLoading$;
|
||||
columns: Column2<WalletTermSet>[] = [
|
||||
columns: Column2<WalletTermSet, InlineDecision2>[] = [
|
||||
createWalletColumn((d) => ({ id: d.wallet_id, name: d.wallet_name, partyId: d.owner_id }), {
|
||||
sticky: 'start',
|
||||
}),
|
||||
@ -97,21 +117,13 @@ export class WalletsTariffsComponent implements OnInit {
|
||||
createDomainObjectColumn((d) => ({ ref: { term_set_hierarchy: d.current_term_set.ref } }), {
|
||||
header: 'Term Set',
|
||||
}),
|
||||
...createWalletFeesColumn<WalletTermSet>(
|
||||
(d) => d.current_term_set,
|
||||
(d) => d.wallet_id,
|
||||
(d) => d.currency,
|
||||
),
|
||||
...WALLET_FEES_COLUMNS,
|
||||
{
|
||||
field: 'term_set_history',
|
||||
cell: (d) => ({
|
||||
value: d.term_set_history?.length || '',
|
||||
click: () =>
|
||||
this.sidenavInfoService.open(WalletsTermSetHistoryCardComponent, {
|
||||
data: d?.term_set_history?.reverse(),
|
||||
walletId: d?.wallet_id,
|
||||
currency: d?.currency,
|
||||
}),
|
||||
this.sidenavInfoService.open(WalletsTermSetHistoryCardComponent, { data: d }),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './wallets-term-set-history-card.component';
|
||||
export * from '../../utils/create-fees-columns';
|
||||
|
@ -1,3 +1,3 @@
|
||||
<cc-card [title]="'Term Sets History'">
|
||||
<v-table2 [columns]="columns" [data]="data()" infinityScroll></v-table2>
|
||||
<v-table2 [columns]="columns" [treeData]="historyData()"></v-table2>
|
||||
</cc-card>
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Component, input, computed } from '@angular/core';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TableModule, VSelectPipe, Column2 } from '@vality/ng-core';
|
||||
|
||||
import type { TermSetHistory } from '@vality/dominator-proto/internal/dominator';
|
||||
import type { TermSetHistory, WalletTermSet } from '@vality/dominator-proto/internal/dominator';
|
||||
|
||||
import { SidenavInfoModule } from '../../../../shared/components/sidenav-info';
|
||||
import { getDomainObjectDetails } from '../../../../shared/components/thrift-api-crud';
|
||||
import { createWalletFeesColumn } from '../wallets-tariffs/utils/create-wallet-fees-column';
|
||||
import { getInlineDecisions2 } from '../../utils/get-inline-decisions';
|
||||
import {
|
||||
WALLET_FEES_COLUMNS,
|
||||
isWalletTermSetDecision,
|
||||
getWalletCashFlowSelectors,
|
||||
} from '../wallets-tariffs/utils/wallet-fees-columns';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-wallets-term-set-history-card',
|
||||
@ -17,9 +22,20 @@ import { createWalletFeesColumn } from '../wallets-tariffs/utils/create-wallet-f
|
||||
styles: ``,
|
||||
})
|
||||
export class WalletsTermSetHistoryCardComponent {
|
||||
data = input<TermSetHistory[]>();
|
||||
walletId = input<string>();
|
||||
currency = input<string>();
|
||||
data = input<WalletTermSet>();
|
||||
historyData = computed(() =>
|
||||
(this.data()?.term_set_history?.reverse?.() || []).map((t) => ({
|
||||
value: t,
|
||||
children: getInlineDecisions2(getWalletCashFlowSelectors(t.term_set)).filter((v) =>
|
||||
isWalletTermSetDecision(v, {
|
||||
partyId: this.data().owner_id,
|
||||
walletId: this.data().wallet_id,
|
||||
currency: this.data().currency,
|
||||
}),
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
columns: Column2<TermSetHistory>[] = [
|
||||
{ field: 'applied_at', cell: { type: 'datetime' } },
|
||||
{
|
||||
@ -30,10 +46,6 @@ export class WalletsTermSetHistoryCardComponent {
|
||||
?.description,
|
||||
}),
|
||||
},
|
||||
...createWalletFeesColumn<TermSetHistory>(
|
||||
(d) => d.term_set,
|
||||
() => this.walletId(),
|
||||
() => this.currency(),
|
||||
),
|
||||
...WALLET_FEES_COLUMNS,
|
||||
];
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import type {
|
||||
CashFlowSelector,
|
||||
CashFlowPosting,
|
||||
} from '@vality/dominator-proto/internal/proto/domain';
|
||||
import type { Column2 } from '@vality/ng-core';
|
||||
|
||||
import {
|
||||
getInlineDecisions,
|
||||
formatLevelPredicate,
|
||||
type InlineCashFlowSelector,
|
||||
} from './get-inline-decisions';
|
||||
|
||||
export function createFeesColumns<T extends object>(
|
||||
getFees: (d: T) => CashFlowSelector[],
|
||||
filterFee: (v: CashFlowPosting) => boolean,
|
||||
filterOther: (v: CashFlowPosting) => boolean = () => true,
|
||||
filterDecisions: (d: T) => (v: InlineCashFlowSelector) => boolean = () => () => true,
|
||||
): Column2<T>[] {
|
||||
const filterOtherFn: (v: CashFlowPosting) => boolean = (v) =>
|
||||
!filterFee(v) &&
|
||||
filterOther(v) &&
|
||||
!(v?.volume?.share?.parts?.p === 1 && v?.volume?.share?.parts?.q === 1);
|
||||
return [
|
||||
{
|
||||
field: 'condition',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterFee)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: formatLevelPredicate(v) })),
|
||||
},
|
||||
{
|
||||
field: 'feeShare',
|
||||
header: 'Fee, %',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterFee)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: v.parts?.share })),
|
||||
},
|
||||
{
|
||||
field: 'feeFixed',
|
||||
header: 'Fee, fix',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterFee)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: v.parts?.fixed })),
|
||||
},
|
||||
{
|
||||
field: 'feeMin',
|
||||
header: 'Fee, min',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterFee)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: v.parts?.max })),
|
||||
},
|
||||
{
|
||||
field: 'feeMax',
|
||||
header: 'Fee, max',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterFee)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: v.parts?.min })),
|
||||
},
|
||||
{
|
||||
field: 'other',
|
||||
cell: (d) =>
|
||||
getInlineDecisions(getFees(d), filterOtherFn)
|
||||
.filter(filterDecisions(d))
|
||||
.map((v) => ({ value: v.value, tooltip: v.description })),
|
||||
},
|
||||
];
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { CashFlow } from '@vality/domain-proto/internal/domain';
|
||||
import { getUnionKey } from '@vality/ng-thrift';
|
||||
|
||||
import type {
|
||||
@ -63,7 +64,7 @@ function formatCashFlowAccount(acc: CashFlowAccount) {
|
||||
);
|
||||
}
|
||||
|
||||
export function formatLevelPredicate(v: InlineCashFlowSelector) {
|
||||
export function formatLevelPredicate(v: { level: number; if?: Predicate }) {
|
||||
return `${'\xa0'.repeat(Math.max(v.level - 1, 0))}${v.level > 0 ? '↳' : ''} ${formatPredicate(
|
||||
v.if,
|
||||
)}`;
|
||||
@ -118,3 +119,38 @@ export function getInlineDecisions(
|
||||
return acc;
|
||||
}, [] as InlineCashFlowSelector[]);
|
||||
}
|
||||
|
||||
export interface InlineDecision2 {
|
||||
value: CashFlow;
|
||||
level: number;
|
||||
if?: Predicate;
|
||||
}
|
||||
|
||||
export function getInlineDecisions2(d: CashFlowSelector[], level = 0) {
|
||||
return d.reduce<InlineDecision2[]>((acc, c) => {
|
||||
if (c.value) {
|
||||
acc.push({
|
||||
value: c.value.sort((a, b) => compareCashVolumes(a.volume, b.volume)),
|
||||
level,
|
||||
});
|
||||
}
|
||||
if (c.decisions?.length) {
|
||||
acc.push(
|
||||
...c.decisions.flatMap((d) => {
|
||||
const thenInlineDecisions = getInlineDecisions2([d.then_], level + 1);
|
||||
if (d.if_) {
|
||||
const ifInlineDecision = {
|
||||
if: d.if_,
|
||||
level,
|
||||
};
|
||||
return thenInlineDecisions.length > 1
|
||||
? [{ ...ifInlineDecision, value: [] }, ...thenInlineDecisions]
|
||||
: [{ ...thenInlineDecisions[0], ...ifInlineDecision }];
|
||||
}
|
||||
return thenInlineDecisions;
|
||||
}),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { CashFlowPosting } from '@vality/domain-proto/domain';
|
||||
|
||||
export function isOneHundredPercentCashFlowPosting(v: CashFlowPosting) {
|
||||
return v?.volume?.share?.parts?.p === 1 && v?.volume?.share?.parts?.q === 1;
|
||||
}
|
5
src/app/sections/tariffs/utils/is-that-currency.ts
Normal file
5
src/app/sections/tariffs/utils/is-that-currency.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Predicate } from '@vality/domain-proto/internal/domain';
|
||||
|
||||
export function isThatCurrency(predicate: Predicate, currency: string) {
|
||||
return predicate?.condition?.currency_is?.symbolic_code === currency;
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import { Failure } from '@vality/domain-proto/domain';
|
||||
import { PossiblyAsync, ColumnObject, getPossiblyAsyncObservable } from '@vality/ng-core';
|
||||
import {
|
||||
PossiblyAsync,
|
||||
ColumnObject,
|
||||
getPossiblyAsyncObservable,
|
||||
createColumn,
|
||||
} from '@vality/ng-core';
|
||||
import { of } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
|
||||
@ -45,3 +50,12 @@ export function createFailureColumn<T extends object>(
|
||||
...params,
|
||||
} as ColumnObject<T>;
|
||||
}
|
||||
|
||||
export const createFailureColumn2 = createColumn(
|
||||
({ failure, noFailureMessage }: { failure: Failure; noFailureMessage: string }) => ({
|
||||
value: noFailureMessage || getFailureMessageTree(failure, false, 2),
|
||||
description: failure?.reason || '',
|
||||
tooltip: failure?.sub?.sub ? JSON.stringify(failure.sub.sub, null, 2) : '',
|
||||
}),
|
||||
{ header: 'Failure' },
|
||||
);
|
||||
|
@ -14,5 +14,5 @@ export const createContractColumn = createColumn(
|
||||
},
|
||||
};
|
||||
},
|
||||
'Contract',
|
||||
{ header: 'Contract' },
|
||||
);
|
||||
|
23
src/app/shared/utils/table2/create-currency-column.ts
Normal file
23
src/app/shared/utils/table2/create-currency-column.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { createColumn } from '@vality/ng-core';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import { of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { AmountCurrencyService } from '../../services';
|
||||
|
||||
export const createCurrencyColumn = createColumn(
|
||||
({ amount, code }: { amount: number; code: string }) => {
|
||||
const amountCurrencyService = inject(AmountCurrencyService);
|
||||
return (isNil(amount) ? of(undefined) : amountCurrencyService.toMajor(amount, code)).pipe(
|
||||
map((value) => ({
|
||||
value,
|
||||
type: 'currency',
|
||||
params: {
|
||||
code,
|
||||
major: true,
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
);
|
@ -22,4 +22,4 @@ export const createDomainObjectColumn = createColumn(({ ref }: { ref: Reference
|
||||
},
|
||||
})),
|
||||
);
|
||||
}, 'Object');
|
||||
});
|
||||
|
@ -24,5 +24,5 @@ export const createPartyColumn = createColumn(
|
||||
})),
|
||||
);
|
||||
},
|
||||
'Party',
|
||||
{ header: 'Party' },
|
||||
);
|
||||
|
@ -26,5 +26,5 @@ export const createShopColumn = createColumn(
|
||||
})),
|
||||
);
|
||||
},
|
||||
'Shop',
|
||||
{ header: 'Shop' },
|
||||
);
|
||||
|
@ -20,5 +20,5 @@ export const createWalletColumn = createColumn(
|
||||
})),
|
||||
);
|
||||
},
|
||||
'Wallet',
|
||||
{ header: 'Wallet' },
|
||||
);
|
||||
|
@ -3,3 +3,4 @@ export * from './create-party-column';
|
||||
export * from './create-wallet-column';
|
||||
export * from './create-contract-column';
|
||||
export * from './create-domain-object-column';
|
||||
export * from './create-currency-column';
|
||||
|
Loading…
Reference in New Issue
Block a user