IMP-219: Allow/deny terminal button (#351)

This commit is contained in:
Rinat Arsaev 2024-04-17 17:05:54 +09:00 committed by GitHub
parent e590bb8ca5
commit 0c85ee9a32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 207 additions and 185 deletions

View File

@ -35,22 +35,17 @@ export class AddPartyRoutingRuleDialogComponent extends DialogSuperclass<
add() {
const { shopID, walletID, name, description } = this.form.value;
(this.dialogData.type === RoutingRulesType.Payment
? this.routingRulesService.addShopRuleset({
name,
description,
partyRulesetRefID: this.dialogData.refID,
partyID: this.dialogData.partyID,
shopID,
})
: this.routingRulesService.addWalletRuleset({
name,
description,
partyRulesetRefID: this.dialogData.refID,
partyID: this.dialogData.partyID,
walletID,
})
)
this.routingRulesService
.addRuleset({
name,
description,
partyRulesetRefID: this.dialogData.refID,
partyID: this.dialogData.partyID,
definition:
this.dialogData.type === RoutingRulesType.Payment
? { shop_is: shopID }
: { wallet_is: walletID },
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => this.dialogRef.close({ status: DialogResponseStatus.Success }),

View File

@ -1,10 +0,0 @@
<v-dialog title="Change priorities">
<cc-thrift-viewer
[compared]="dialogData.object"
[value]="dialogData.prevObject"
></cc-thrift-viewer>
<v-dialog-actions>
<button color="primary" mat-raised-button (click)="closeWithSuccess()">Update</button>
</v-dialog-actions>
</v-dialog>

View File

@ -1,22 +0,0 @@
import { Component } from '@angular/core';
import { DialogSuperclass, DEFAULT_DIALOG_CONFIG } from '@vality/ng-core';
@Component({
selector: 'cc-change-candidates-priorities-dialog',
templateUrl: './change-candidates-priorities-dialog.component.html',
styles: [],
})
export class ChangeCandidatesPrioritiesDialogComponent<T> extends DialogSuperclass<
ChangeCandidatesPrioritiesDialogComponent<T>,
{
prevObject: T;
object: T;
},
{ object: T }
> {
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
closeWithSuccess() {
super.closeWithSuccess({ object: this.dialogData.object });
}
}

View File

@ -1,5 +1,5 @@
<cc-page-layout
[id]="(shopRuleset$ | async)?.ref?.id"
[id]="(ruleset$ | async)?.ref?.id"
[title]="((routingRulesType$ | async) === 'payment' ? 'Shop' : 'Wallet') + ' Routing Rules'"
[upLink]="[
'/party/' +
@ -12,7 +12,7 @@
(idLinkClick)="openRefId()"
>
<cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="addShopRule()">Add</button>
<button color="primary" mat-raised-button (click)="addRule()">Add</button>
</cc-page-layout-actions>
<v-table
[columns]="columns"

View File

@ -2,6 +2,7 @@ import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { RoutingCandidate } from '@vality/domain-proto/domain';
import { Predicate } from '@vality/domain-proto/internal/domain';
import {
DialogResponseStatus,
DialogService,
@ -20,30 +21,43 @@ import { RoutingRulesType } from '@cc/app/sections/routing-rules/types/routing-r
import {
DomainThriftFormDialogComponent,
DomainObjectCardComponent,
UpdateThriftDialogComponent,
} from '@cc/app/shared/components/thrift-api-crud';
import { objectToJSON } from '../../../../utils';
import { objectToJSON, getUnionKey } from '../../../../utils';
import { createPredicateColumn } from '../../../shared';
import { CandidateCardComponent } from '../../../shared/components/candidate-card/candidate-card.component';
import { SidenavInfoService } from '../../../shared/components/sidenav-info';
import { createTerminalColumn } from '../../../shared/utils/table/create-terminal-column';
import { RoutingRulesService } from '../services/routing-rules';
import { ChangeCandidatesPrioritiesDialogComponent } from './components/change-candidates-priorities-dialog/change-candidates-priorities-dialog.component';
import { RoutingRulesetService } from './routing-ruleset.service';
function togglePredicate(predicate: Predicate): { toggled: Predicate; prevAllowed: boolean } {
const predicates: Predicate[] =
getUnionKey(predicate) === 'all_of' ? Array.from(predicate.all_of) : [predicate];
const idx = predicates.findIndex((a) => getUnionKey(a) === 'constant');
const prevAllowed = idx !== -1 ? predicates[idx].constant : true;
if (idx !== -1) {
predicates.splice(idx, 1);
}
predicates.unshift({ constant: !prevAllowed });
return {
toggled: { all_of: new Set(predicates) },
prevAllowed,
};
}
@Component({
templateUrl: 'routing-ruleset.component.html',
providers: [RoutingRulesetService],
})
export class RoutingRulesetComponent {
shopRuleset$ = this.routingRulesetService.shopRuleset$;
ruleset$ = this.routingRulesetService.ruleset$;
partyID$ = this.routingRulesetService.partyID$;
partyRulesetRefID$ = this.routingRulesetService.partyRulesetRefID$;
routingRulesType$ = this.route.params.pipe(map((p) => p.type)) as Observable<RoutingRulesType>;
candidates$ = this.routingRulesetService.shopRuleset$.pipe(
map((r) => r.data.decisions.candidates),
);
candidates$ = this.routingRulesetService.ruleset$.pipe(map((r) => r.data.decisions.candidates));
isLoading$ = this.domainStoreService.isLoading$;
columns: Column<RoutingCandidate>[] = [
{ field: 'priority', sortable: true },
@ -53,7 +67,7 @@ export class RoutingRulesetComponent {
sortable: true,
formatter: (d) => this.getCandidateIdx(d).pipe(map((idx) => `#${idx + 1}`)),
click: (d) => {
combineLatest([this.getCandidateIdx(d), this.routingRulesetService.shopRuleset$])
combineLatest([this.getCandidateIdx(d), this.routingRulesetService.ruleset$])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([idx, ruleset]) => {
this.sidenavInfoService.toggle(CandidateCardComponent, {
@ -77,7 +91,15 @@ export class RoutingRulesetComponent {
}),
),
),
createPredicateColumn('allowed', (d) => d.allowed),
createPredicateColumn('allowed', (d) => d.allowed, {
click: (d) => {
this.getCandidateIdx(d)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((idx) => {
void this.toggleAllow(idx);
});
},
}),
{ field: 'weight', sortable: true },
{
field: 'pin',
@ -91,7 +113,7 @@ export class RoutingRulesetComponent {
this.getCandidateIdx(d)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((idx) => {
this.editShopRule(idx);
this.editRule(idx);
});
},
},
@ -101,7 +123,17 @@ export class RoutingRulesetComponent {
this.getCandidateIdx(d)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((idx) => {
void this.duplicateShopRule(idx);
void this.duplicateRule(idx);
});
},
},
{
label: (d) => (togglePredicate(d.allowed).prevAllowed ? 'Deny' : 'Allow'),
click: (d) => {
this.getCandidateIdx(d)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((idx) => {
void this.toggleAllow(idx);
});
},
},
@ -111,7 +143,7 @@ export class RoutingRulesetComponent {
this.getCandidateIdx(d)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((idx) => {
void this.removeShopRule(idx);
void this.removeRule(idx);
});
},
},
@ -129,7 +161,7 @@ export class RoutingRulesetComponent {
private destroyRef: DestroyRef,
) {}
addShopRule() {
addRule() {
this.routingRulesetService.refID$
.pipe(
first(),
@ -137,9 +169,9 @@ export class RoutingRulesetComponent {
this.dialog
.open(DomainThriftFormDialogComponent<RoutingCandidate>, {
type: 'RoutingCandidate',
title: 'Add shop routing candidate',
title: 'Add routing candidate',
object: { allowed: { all_of: new Set([{ constant: true }]) } },
action: (params) => this.routingRulesService.addShopRule(refId, params),
action: (params) => this.routingRulesService.addRule(refId, params),
})
.afterClosed(),
),
@ -158,20 +190,20 @@ export class RoutingRulesetComponent {
});
}
editShopRule(idx: number) {
editRule(idx: number) {
this.routingRulesetService.refID$
.pipe(
first(),
switchMap((refId) => this.routingRulesService.getShopCandidate(refId, idx)),
switchMap((refId) => this.routingRulesService.getCandidate(refId, idx)),
withLatestFrom(this.routingRulesetService.refID$),
switchMap(([shopCandidate, refId]) =>
switchMap(([candidate, refId]) =>
this.dialog
.open(DomainThriftFormDialogComponent<RoutingCandidate>, {
type: 'RoutingCandidate',
title: `Edit shop routing candidate #${idx + 1}`,
object: shopCandidate,
title: `Edit routing candidate #${idx + 1}`,
object: candidate,
action: (params) =>
this.routingRulesService.updateShopRule(refId, idx, params),
this.routingRulesService.updateRule(refId, idx, params),
})
.afterClosed(),
),
@ -190,20 +222,20 @@ export class RoutingRulesetComponent {
});
}
duplicateShopRule(idx: number) {
duplicateRule(idx: number) {
this.routingRulesetService.refID$
.pipe(
first(),
switchMap((refId) => this.routingRulesService.getShopCandidate(refId, idx)),
switchMap((refId) => this.routingRulesService.getCandidate(refId, idx)),
withLatestFrom(this.routingRulesetService.refID$),
switchMap(([shopCandidate, refId]) =>
switchMap(([candidate, refId]) =>
this.dialog
.open(DomainThriftFormDialogComponent<RoutingCandidate>, {
type: 'RoutingCandidate',
title: 'Add shop routing candidate',
object: shopCandidate,
title: 'Add routing candidate',
object: candidate,
actionType: 'create',
action: (params) => this.routingRulesService.addShopRule(refId, params),
action: (params) => this.routingRulesService.addRule(refId, params),
})
.afterClosed(),
),
@ -222,8 +254,39 @@ export class RoutingRulesetComponent {
});
}
removeShopRule(idx: number) {
this.routingRulesetService.removeShopRule(idx);
toggleAllow(idx: number) {
this.routingRulesetService.refID$
.pipe(
first(),
switchMap((refId) => this.routingRulesService.getCandidate(refId, idx)),
withLatestFrom(this.routingRulesetService.refID$),
switchMap(([candidate, refId]) => {
const newAllowed = togglePredicate(candidate.allowed).toggled;
return this.dialog
.open(UpdateThriftDialogComponent, {
title: 'Toggle allowed',
prevObject: candidate.allowed,
object: newAllowed,
action: () =>
this.routingRulesService.updateRule(refId, idx, {
...candidate,
allowed: newAllowed,
}),
})
.afterClosed();
}),
)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((res) => {
if (res.status === DialogResponseStatus.Success) {
this.domainStoreService.forceReload();
this.log.successOperation('update', 'Allowed');
}
});
}
removeRule(idx: number) {
this.routingRulesetService.removeRule(idx);
}
getCandidateIdx(candidate: RoutingCandidate) {
@ -244,7 +307,7 @@ export class RoutingRulesetComponent {
first(),
switchMap((candidates) =>
this.dialog
.open(ChangeCandidatesPrioritiesDialogComponent, {
.open(UpdateThriftDialogComponent, {
object: candidates.map((c) => ({
...c,
priority: resPriorities[e.currentData.findIndex((d) => d === c)],
@ -254,22 +317,22 @@ export class RoutingRulesetComponent {
.afterClosed()
.pipe(filter((res) => res.status === DialogResponseStatus.Success)),
),
withLatestFrom(this.routingRulesetService.shopRuleset$),
withLatestFrom(this.routingRulesetService.ruleset$),
switchMap(
([
{
data: { object: candidates },
},
shopRuleset,
ruleset,
]) => {
const newShopRuleset = cloneDeep(shopRuleset);
newShopRuleset.data.decisions.candidates = candidates as RoutingCandidate[];
const newRuleset = cloneDeep(ruleset);
newRuleset.data.decisions.candidates = candidates as RoutingCandidate[];
return this.domainStoreService.commit({
ops: [
{
update: {
old_object: { routing_rules: shopRuleset },
new_object: { routing_rules: newShopRuleset },
old_object: { routing_rules: ruleset },
new_object: { routing_rules: newRuleset },
},
},
],
@ -289,7 +352,7 @@ export class RoutingRulesetComponent {
}
openRefId() {
this.shopRuleset$.pipe(take(1), filter(Boolean)).subscribe(({ ref }) => {
this.ruleset$.pipe(take(1), filter(Boolean)).subscribe(({ ref }) => {
this.sidenavInfoService.toggle(DomainObjectCardComponent, {
ref: { routing_rules: { id: Number(ref.id) } },
});

View File

@ -22,7 +22,6 @@ import { DomainThriftViewerComponent } from '@cc/app/shared/components/thrift-ap
import { PageLayoutModule } from '../../../shared';
import { ThriftViewerModule } from '../../../shared/components/thrift-viewer';
import { ChangeCandidatesPrioritiesDialogComponent } from './components/change-candidates-priorities-dialog/change-candidates-priorities-dialog.component';
import { RoutingRulesetRoutingModule } from './routing-ruleset-routing.module';
import { RoutingRulesetComponent } from './routing-ruleset.component';
@ -51,6 +50,6 @@ import { RoutingRulesetComponent } from './routing-ruleset.component';
DialogModule,
PageLayoutModule,
],
declarations: [RoutingRulesetComponent, ChangeCandidatesPrioritiesDialogComponent],
declarations: [RoutingRulesetComponent],
})
export class RoutingRulesetModule {}

View File

@ -7,11 +7,9 @@ import {
DialogResponseStatus,
NotifyLogService,
} from '@vality/ng-core';
import { combineLatest, Observable, filter } from 'rxjs';
import { Observable, filter } from 'rxjs';
import { map, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { RoutingRulesService as RoutingRulesDamselService } from '../services/routing-rules';
@Injectable()
@ -30,36 +28,20 @@ export class RoutingRulesetService {
map((p) => +p),
shareReplay(1),
);
shopRuleset$ = this.refID$.pipe(
ruleset$ = this.refID$.pipe(
switchMap((refID) => this.routingRulesService.getRuleset(refID)),
shareReplay(1),
);
private party$ = this.partyID$.pipe(
switchMap((partyID) => this.partyManagementService.Get(partyID)),
shareReplay(1),
);
// eslint-disable-next-line @typescript-eslint/member-ordering
shop$ = combineLatest([this.party$, this.shopRuleset$]).pipe(
map(([{ shops }, ruleset]) =>
shops.get(
ruleset?.data?.decisions?.delegates?.find(
(d) => d?.allowed?.condition?.party?.definition?.shop_is,
)?.allowed?.condition?.party?.definition?.shop_is,
),
),
shareReplay(1),
);
constructor(
private routingRulesService: RoutingRulesDamselService,
private route: ActivatedRoute,
private partyManagementService: PartyManagementService,
private log: NotifyLogService,
private dialog: DialogService,
private destroyRef: DestroyRef,
) {}
removeShopRule(candidateIdx: number) {
removeRule(candidateIdx: number) {
this.dialog
.open(ConfirmDialogComponent)
.afterClosed()

View File

@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { RoutingCandidate, RoutingDelegate, RoutingRulesObject } from '@vality/domain-proto/domain';
import { Version } from '@vality/domain-proto/domain_config';
import { PartyConditionDefinition } from '@vality/domain-proto/internal/domain';
import cloneDeep from 'lodash-es/cloneDeep';
import { combineLatest, concat, Observable } from 'rxjs';
import { map, pluck, shareReplay, switchMap, take } from 'rxjs/operators';
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { createNextId } from '@cc/utils/create-next-id';
@ -88,15 +89,15 @@ export class RoutingRulesService {
);
}
addShopRuleset({
addRuleset({
name,
shopID,
definition,
partyID,
partyRulesetRefID,
description,
}: {
name: string;
shopID: string;
definition: PartyConditionDefinition;
partyID: string;
partyRulesetRefID: number;
description?: string;
@ -104,7 +105,7 @@ export class RoutingRulesService {
return combineLatest([this.getRuleset(partyRulesetRefID), this.nextRefID$]).pipe(
take(1),
switchMap(([partyRuleset, id]) => {
const shopRuleset: RoutingRulesObject = {
const ruleset: RoutingRulesObject = {
ref: { id },
data: {
name,
@ -120,9 +121,7 @@ export class RoutingRulesService {
condition: {
party: {
id: partyID,
definition: {
shop_is: shopID,
},
definition,
},
},
},
@ -131,7 +130,7 @@ export class RoutingRulesService {
ops: [
{
insert: {
object: { routing_rules: shopRuleset },
object: { routing_rules: ruleset },
},
},
{
@ -146,65 +145,7 @@ export class RoutingRulesService {
);
}
addWalletRuleset({
name,
walletID,
partyID,
partyRulesetRefID,
description,
}: {
name: string;
walletID: string;
partyID: string;
partyRulesetRefID: number;
description?: string;
}): Observable<Version> {
return combineLatest([this.getRuleset(partyRulesetRefID), this.nextRefID$]).pipe(
take(1),
switchMap(([partyRuleset, id]) => {
const walletRuleset: RoutingRulesObject = {
ref: { id },
data: {
name,
description,
decisions: {
candidates: [],
},
},
};
const newPartyRuleset = this.cloneRulesetAndPushDelegate(partyRuleset, {
ruleset: { id },
allowed: {
condition: {
party: {
id: partyID,
definition: {
wallet_is: walletID,
},
},
},
},
});
return this.domainStoreService.commit({
ops: [
{
insert: {
object: { routing_rules: walletRuleset },
},
},
{
update: {
old_object: { routing_rules: partyRuleset },
new_object: { routing_rules: newPartyRuleset },
},
},
],
});
}),
);
}
addShopRule(refID: number, params: RoutingCandidate): Observable<Version> {
addRule(refID: number, params: RoutingCandidate): Observable<Version> {
return this.getRuleset(refID).pipe(
take(1),
switchMap((ruleset) => {
@ -223,22 +164,22 @@ export class RoutingRulesService {
);
}
updateShopRule(
updateRule(
refID: number,
candidateIdx: number,
shopCandidate: RoutingCandidate,
candidate: RoutingCandidate,
): Observable<Version> {
return this.getRuleset(refID).pipe(
take(1),
switchMap((ruleset) => {
const newShopRuleset = cloneDeep(ruleset);
newShopRuleset.data.decisions.candidates.splice(candidateIdx, 1, shopCandidate);
const newRuleset = cloneDeep(ruleset);
newRuleset.data.decisions.candidates.splice(candidateIdx, 1, candidate);
return this.domainStoreService.commit({
ops: [
{
update: {
old_object: { routing_rules: ruleset },
new_object: { routing_rules: newShopRuleset },
new_object: { routing_rules: newRuleset },
},
},
],
@ -295,10 +236,10 @@ export class RoutingRulesService {
);
}
getShopCandidate(refID: number, candidateIdx: number) {
getCandidate(refID: number, candidateIdx: number) {
return this.getRuleset(refID).pipe(
take(1),
map((shopRuleset) => cloneDeep(shopRuleset.data.decisions.candidates.at(candidateIdx))),
map((ruleset) => cloneDeep(ruleset.data.decisions.candidates.at(candidateIdx))),
);
}
@ -480,7 +421,6 @@ export class RoutingRulesService {
}),
);
}),
pluck('1'),
);
}

View File

@ -1,2 +1,3 @@
export * from './domain';
export * from './magista';
export * from './update-thrift-dialog';

View File

@ -0,0 +1 @@
export * from './update-thrift-dialog.component';

View File

@ -0,0 +1,17 @@
<v-dialog [progress]="!!(progress$ | async)" [title]="dialogData?.title || 'View changes'">
<cc-thrift-viewer
[compared]="dialogData.object"
[value]="dialogData.prevObject"
></cc-thrift-viewer>
<v-dialog-actions>
<button
[disabled]="!!(progress$ | async)"
color="primary"
mat-raised-button
(click)="closeWithSuccess()"
>
Update
</button>
</v-dialog-actions>
</v-dialog>

View File

@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButton } from '@angular/material/button';
import {
DialogSuperclass,
DEFAULT_DIALOG_CONFIG,
DialogModule,
NotifyLogService,
progressTo,
} from '@vality/ng-core';
import { Observable, BehaviorSubject } from 'rxjs';
import { ThriftViewerModule } from '../../thrift-viewer';
@Component({
standalone: true,
templateUrl: './update-thrift-dialog.component.html',
imports: [CommonModule, DialogModule, ThriftViewerModule, MatButton],
})
export class UpdateThriftDialogComponent<T> extends DialogSuperclass<
UpdateThriftDialogComponent<T>,
{
title?: string;
prevObject: T;
object: T;
action?: () => Observable<unknown>;
},
{ object: T }
> {
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
progress$ = new BehaviorSubject(0);
private destroyRef = inject(DestroyRef);
private log = inject(NotifyLogService);
closeWithSuccess() {
if (this.dialogData.action) {
this.dialogData
.action()
.pipe(progressTo(this.progress$), takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
super.closeWithSuccess({ object: this.dialogData.object });
},
error: (err) => {
this.log.error(err);
this.closeWithError();
},
});
} else {
super.closeWithSuccess({ object: this.dialogData.object });
}
}
}