FE-642: Claim management. (#2)

This commit is contained in:
Ildar Galeev 2018-08-21 16:15:51 +03:00 committed by GitHub
parent e056314a15
commit 8a8eb6ea9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
205 changed files with 7168 additions and 2491 deletions

5
Jenkinsfile vendored
View File

@ -15,9 +15,7 @@ build('control-center', 'docker-host') {
pipeDefault() {
runStage('init') {
withGithubSshCredentials {
withWsCache("node_modules") {
sh 'make wc_init'
}
sh 'make wc_init'
}
}
runStage('build') {
@ -40,4 +38,3 @@ build('control-center', 'docker-host') {
}
}
}

View File

@ -38,7 +38,7 @@ submodules: $(SUBTARGETS)
init:
npm install
build: src/gen-nodejs src/gen-json
build: lint src/gen-nodejs src/gen-json
npm run build
clean:
@ -50,3 +50,6 @@ src/gen-nodejs: node_modules/damsel/proto/domain_config.thrift
src/gen-json: node_modules/damsel/proto/domain_config.thrift
thrift -r -gen json -o ./src/assets ./node_modules/damsel/proto/domain_config.thrift
lint:
npm run lint

View File

@ -4,6 +4,6 @@
make wc_shell
thrift -r -gen js:node,runtime_package=woody_js/src/client/gen -o ./src/app/domain ./node_modules/damsel/proto/domain_config.thrift
thrift -r -gen js:node,runtime_package=woody_js/dist/thrift -o ./src/app/domain ./node_modules/damsel/proto/domain_config.thrift
thrift -r -gen json -o ./src/assets ./node_modules/damsel/proto/domain_config.thrift

5654
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@
"version": "0.0.0",
"scripts": {
"ng": "./node_modules/.bin/ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"start": "ng serve --proxy-config proxy.conf.json --port 7000",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
@ -16,18 +16,24 @@
"@angular/common": "^6.1.0",
"@angular/compiler": "^6.1.0",
"@angular/core": "^6.1.0",
"@angular/flex-layout": "^6.0.0-beta.17",
"@angular/forms": "^6.1.0",
"@angular/http": "^6.1.0",
"@angular/material": "^6.4.2",
"@angular/platform-browser": "^6.1.0",
"@angular/platform-browser-dynamic": "^6.1.0",
"@angular/router": "^6.1.0",
"angular2-prettyjson": "3.0.1",
"core-js": "^2.5.4",
"damsel": "git+ssh://git@github.com/rbkmoney/damsel.git#1510cd7caa1e4338429a3a0783289d36e140472a",
"hammerjs": "^2.0.8",
"keycloak-angular": "3.0.2",
"keycloak-js": "3.4.0",
"lodash-es": "^4.17.10",
"moment": "^2.22.2",
"rxjs": "^6.0.0",
"woody_js": "git+ssh://git@github.com/rbkmoney/woody_js.git#e6e3eaeebc3933315abb6af4a099cb698424f830",
"uuid": "^3.3.2",
"woody_js": "git+ssh://git@github.com/rbkmoney/woody_js.git#dfe1ec26573de991f59952aa64b409280fec6c58",
"zone.js": "~0.8.26"
},
"devDependencies": {
@ -37,7 +43,9 @@
"@angular/language-service": "^6.1.0",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/lodash-es": "^4.17.1",
"@types/node": "~8.9.4",
"@types/uuid": "^3.4.3",
"codelyzer": "~4.2.1",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
@ -46,7 +54,7 @@
"karma-coverage-istanbul-reporter": "~2.0.0",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.3.0",
"protractor": "^5.4.0",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~2.7.2"

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot([
{
path: '',
redirectTo: '/claims',
pathMatch: 'full'
}
])
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}

View File

@ -1,3 +1,7 @@
.spacer {
flex: 1 1 auto;
}
}
.active {
background: #f5f5f5;
}

View File

@ -4,14 +4,14 @@
</button>
<span>Control Center</span>
<span class="spacer"></span>
<span>Lexa Svotin</span>
<span>{{username}}</span>
<button mat-icon-button [matMenuTriggerFor]="userMenu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu #userMenu="matMenu">
<button mat-menu-item>Logout</button>
<button mat-menu-item (click)="logout()">Logout</button>
</mat-menu>
<mat-sidenav-container>
@ -22,12 +22,14 @@
fixedInViewport="true"
fixedTopGap="64">
<mat-nav-list>
<mat-list-item *ngFor="let item of menuItems">
{{item}}
<mat-list-item *ngFor="let item of menuItems"
[routerLink]="item.route"
[routerLinkActive]="['active']">
{{item.name}}
</mat-list-item>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content role="main">
<!--<router-outlet></router-outlet>-->
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@ -1,14 +1,35 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
@Component({
selector: 'app-root',
selector: 'cc-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
menuItems: string[] = [
'Domain config',
'Выплаты',
'Заявки'
];
export class AppComponent implements OnInit {
username: string;
menuItems: { name: string, route: string }[] = [];
constructor(private keycloakService: KeycloakService) {}
ngOnInit() {
this.username = this.keycloakService.getUsername();
this.menuItems = this.getMenuItems();
}
logout() {
this.keycloakService.logout();
}
private getMenuItems() {
const menuItems = [
// {name: 'Domain config', route: '/domain', activateRole: 'dmt:checkout'},
{name: 'Payouts', route: '/payouts', activateRole: 'payout:read'},
{name: 'Claims', route: '/claims', activateRole: 'claim:get'}
];
const roles = this.keycloakService.getUserRoles();
return menuItems.filter((item) => roles.includes(item.activateRole));
}
}

View File

@ -11,6 +11,10 @@ import {
} from '@angular/material';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { ClaimsModule } from './claims/claims.module';
import { AppRoutingModule } from './app-routing.module';
import { ClaimModule } from './claim/claim.module';
@NgModule({
declarations: [
@ -19,15 +23,18 @@ import { AppComponent } from './app.component';
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatMenuModule,
MatSidenavModule,
MatListModule
MatListModule,
ClaimsModule,
ClaimModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}
export class AppModule {}

View File

@ -0,0 +1,12 @@
<div class="mat-dialog-title">Confirm claim accepting</div>
<mat-dialog-actions fxLayout="column" fxLayoutGap="10px">
<div>
<button mat-button
[disabled]="isLoading"
color="primary"
(click)="accept()">CONFIRM
</button>
<button [disabled]="isLoading" mat-button [mat-dialog-close]="false">CANCEL</button>
</div>
<mat-progress-bar *ngIf="isLoading" mode="indeterminate"></mat-progress-bar>
</mat-dialog-actions>

View File

@ -0,0 +1,30 @@
import { Component } from '@angular/core';
import { MatDialogRef, MatSnackBar } from '@angular/material';
import { ClaimService } from '../claim.service';
@Component({
templateUrl: 'accept-claim.component.html'
})
export class AcceptClaimComponent {
isLoading = false;
constructor(
private dialogRef: MatDialogRef<AcceptClaimComponent>,
private claimService: ClaimService,
private snackBar: MatSnackBar
) {}
accept() {
this.isLoading = true;
this.claimService.acceptClaim().subscribe(() => {
this.isLoading = false;
this.dialogRef.close();
this.snackBar.open('Claim accepted', 'OK', {duration: 3000});
}, () => {
this.isLoading = false;
this.snackBar.open('An error occurred while claim accepting', 'OK');
});
}
}

View File

@ -0,0 +1,12 @@
import { ContractModificationName, ShopModificationName } from '../model';
export enum ActionType {
contractAction = 'contractAction',
shopAction = 'shopAction',
domainAction = 'domainAction'
}
export interface ClaimAction {
type: ActionType;
name?: ContractModificationName | ShopModificationName;
}

View File

@ -0,0 +1,13 @@
<mat-nav-list>
<a mat-list-item *ngFor="let contractAction of contractActions" (click)="select(contractAction)">
{{contractAction.name | ccContainerName:"ContractModification"}}
</a>
<mat-divider></mat-divider>
<a mat-list-item *ngFor="let shopAction of shopActions" (click)="select(shopAction)">
{{shopAction.name | ccContainerName:"ShopModification"}}
</a>
<mat-divider></mat-divider>
<a mat-list-item *ngFor="let domainAction of domainActions" (click)="select(domainAction)">
Domain config modification: Add terminal
</a>
</mat-nav-list>

View File

@ -0,0 +1,62 @@
import { Component } from '@angular/core';
import { MatBottomSheetRef, MatDialog } from '@angular/material';
import { ShopModificationName, ContractModificationName } from '../model';
import { ActionType, ClaimAction } from './claim-action';
import { CreateChangeComponent } from '../create-change/create-change.component';
@Component({
templateUrl: 'claim-actions.component.html'
})
export class ClaimActionsComponent {
constructor(private bottomSheetRef: MatBottomSheetRef,
private dialog: MatDialog) {
}
contractActions: ClaimAction[] = [
{
type: ActionType.contractAction,
name: ContractModificationName.legalAgreementBinding
},
{
type: ActionType.contractAction,
name: ContractModificationName.reportPreferencesModification
},
{
type: ActionType.contractAction,
name: ContractModificationName.adjustmentModification
},
];
shopActions: ClaimAction[] = [
{
type: ActionType.shopAction,
name: ShopModificationName.categoryModification
},
{
type: ActionType.shopAction,
name: ShopModificationName.shopAccountCreation
},
{
type: ActionType.shopAction,
name: ShopModificationName.payoutScheduleModification
}
];
domainActions: ClaimAction[] = [
{
type: ActionType.domainAction
}
];
select(action: ClaimAction) {
this.bottomSheetRef.dismiss();
const config = {
data: action,
width: '720px',
disableClose: true
};
this.dialog.open<CreateChangeComponent, ClaimAction>(CreateChangeComponent, config);
}
}

View File

@ -0,0 +1,21 @@
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable()
export class ClaimAuthGuardService extends KeycloakAuthGuard {
constructor(protected router: Router, protected keycloakAngular: KeycloakService) {
super(router, keycloakAngular);
}
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Promise((resolve) => {
if (!this.roles || this.roles.length === 0) {
resolve(false);
}
const granted = this.roles.includes('claim:get');
resolve(granted);
});
}
}

View File

@ -0,0 +1,34 @@
<div fxLayout="row" fxLayoutGap="10px" fxLayout.sm="column" fxLayout.xs="column">
<div fxFlex="50" fxLayout="column" fxLayoutGap="10px">
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Claim ID:</label>
<div>{{claimInfoContainer?.claimId}}</div>
</div>
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Status:</label>
<div>{{claimInfoContainer?.status}}</div>
</div>
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Revision:</label>
<div>{{claimInfoContainer?.revision}}</div>
</div>
<div *ngIf="claimInfoContainer?.reason" fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Reason:</label>
<div>{{claimInfoContainer?.reason}}</div>
</div>
</div>
<div fxFlex="50" fxLayout="column" fxLayoutGap="10px">
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Party ID:</label>
<div>{{claimInfoContainer?.partyId}}</div>
</div>
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Created At:</label>
<div>{{claimInfoContainer?.createdAt | date: "dd.MM.yyyy HH:mm:ss"}}</div>
</div>
<div fxLayout="row" fxLayout.xs="column">
<label fxFlex="20">Updated At:</label>
<div>{{claimInfoContainer?.updatedAt | date: "dd.MM.yyyy HH:mm:ss"}}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
import { Component, Input } from '@angular/core';
import { ClaimInfoContainer } from '../../model';
@Component({
selector: 'cc-claim-info-details',
templateUrl: 'claim-info-details.component.html',
styleUrls: ['./claim-info-details.component.css']
})
export class ClaimInfoDetailsComponent {
@Input()
claimInfoContainer: ClaimInfoContainer;
}

View File

@ -0,0 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { MatBottomSheet, MatDialog } from '@angular/material';
import { ClaimService } from '../claim.service';
import { ClaimInfoContainer } from '../model';
import { ClaimActionsComponent } from '../claim-actions/claim-actions.component';
import { AcceptClaimComponent } from '../accept-claim/accept-claim.component';
import { DenyClaimComponent } from '../deny-claim/deny-claim.component';
@Component({
selector: 'cc-claim-info',
templateUrl: 'claim.info.component.html'
})
export class ClaimInfoComponent implements OnInit {
claimInfoContainer: ClaimInfoContainer;
constructor(private claimService: ClaimService,
private bottomSheet: MatBottomSheet,
private dialog: MatDialog) {
}
ngOnInit() {
this.claimService.claimInfoContainer$.subscribe((container) => {
this.claimInfoContainer = container;
});
}
openClaimActions() {
this.bottomSheet.open(ClaimActionsComponent);
}
accept() {
this.dialog.open(AcceptClaimComponent, {
disableClose: true
});
}
deny() {
this.dialog.open(DenyClaimComponent, {
disableClose: true,
width: '30vw'
});
}
}

View File

@ -0,0 +1,28 @@
<mat-card *ngIf="claimInfoContainer">
<mat-card-subtitle>
Claim details
</mat-card-subtitle>
<mat-card-content>
<cc-claim-info-details [claimInfoContainer]="claimInfoContainer"></cc-claim-info-details>
</mat-card-content>
<mat-card-actions fxLayout="row" fxLayoutAlign="space-between stretch">
<div fxLayout="row" fxLayout.xs="column">
<button mat-button
[disabled]="claimInfoContainer?.status !== 'pending'"
(click)="openClaimActions()">CREATE CHANGE
</button>
</div>
<div fxLayout="row" fxLayout.xs="column">
<button mat-button
color="primary"
[disabled]="claimInfoContainer?.status !== 'pending'"
(click)="accept()">ACCEPT CLAIM
</button>
<button mat-button
color="warn"
[disabled]="claimInfoContainer?.status !== 'pending'"
(click)="deny()">DENY CLAIM
</button>
</div>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ClaimComponent } from './claim.component';
import { ClaimAuthGuardService } from './claim-auth-guard.service';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'claims/:partyId/:claimId',
component: ClaimComponent,
canActivate: [ClaimAuthGuardService]
}
])
],
exports: [
RouterModule
],
providers: [
ClaimAuthGuardService
]
})
export class ClaimRoutingModule {}

View File

@ -0,0 +1,6 @@
<div class="container">
<div class="card-container" fxLayout="column" fxLayoutGap="20px">
<cc-claim-info></cc-claim-info>
<cc-party-modifications></cc-party-modifications>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MatSnackBar } from '@angular/material';
import { switchMap } from 'rxjs/internal/operators';
import { ClaimService } from './claim.service';
@Component({
templateUrl: 'claim.component.html',
styleUrls: ['../shared/container.css']
})
export class ClaimComponent {
constructor(private route: ActivatedRoute,
private claimService: ClaimService,
private snackBar: MatSnackBar) {
this.route.params.pipe(switchMap((params) => {
const {partyId, claimId} = params;
return this.claimService.resolveClaimInfo(partyId, claimId);
})).subscribe(null, (error) => {
console.error(error);
this.snackBar.open('An error occurred while claim resolving', 'OK');
});
}
}

View File

@ -0,0 +1,109 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
MatBottomSheetModule,
MatButtonModule,
MatCardModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatListModule,
MatNativeDateModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatSelectModule,
MatSnackBarModule,
MatTabsModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { PrettyJsonModule } from 'angular2-prettyjson';
import { PapiModule } from '../papi/papi.module';
import { DomainModule } from '../domain/domain.module';
import { ClaimRoutingModule } from './claim-routing.module';
import { ClaimComponent } from './claim.component';
import { ClaimInfoComponent } from './claim-info/claim-info.component';
import { ClaimInfoDetailsComponent } from './claim-info/claim-info-details/claim-info-details.component';
import { PartyModificationsComponent } from './party-modifications/party-modifications.component';
import { PartyModificationContainerComponent } from './party-modification-container/party-modification-container.component';
import { ClaimActionsComponent } from './claim-actions/claim-actions.component';
import { CreateChangeComponent } from './create-change/create-change.component';
import { CreateLegalAgreementComponent } from './create-change/create-legal-agreement/create-legal-agreement.component';
import { CreateCategoryRefComponent } from './create-change/create-category-ref/create-category-ref.component';
import { ClaimService } from './claim.service';
import { CreateCurrencyRefComponent } from './create-change/create-currency-ref/create-currency-ref.component';
import { CreateContractTemplateComponent } from './create-change/create-contract-template/create-contract-template.component';
import { AcceptClaimComponent } from './accept-claim/accept-claim.component';
import { DenyClaimComponent } from './deny-claim/deny-claim.component';
import { CreateBusinessScheduleRefComponent } from './create-change/create-business-schedule-ref/create-business-schedule-ref.component';
import {
CreateServiceAcceptanceActPreferencesComponent
} from './create-change/create-service-acceptance-act-preferences/create-service-acceptance-act-preferences.component';
import { CreateTerminalObjectComponent } from './create-change/create-terminal-object/create-terminal-object.component';
import { SharedModule } from '../shared/shared.module';
import { ContainerNamePipe } from './container-name.pipe';
@NgModule({
imports: [
PapiModule,
DomainModule,
CommonModule,
SharedModule,
ReactiveFormsModule,
ClaimRoutingModule,
FlexLayoutModule,
MatCardModule,
MatButtonModule,
MatTabsModule,
MatBottomSheetModule,
MatListModule,
MatIconModule,
MatDialogModule,
MatInputModule,
MatDatepickerModule,
MatNativeDateModule,
MatDividerModule,
MatSelectModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatRadioModule,
PrettyJsonModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
ClaimComponent,
ClaimInfoComponent,
ClaimInfoDetailsComponent,
PartyModificationsComponent,
PartyModificationContainerComponent,
ClaimActionsComponent,
CreateChangeComponent,
CreateLegalAgreementComponent,
CreateCategoryRefComponent,
CreateCurrencyRefComponent,
CreateContractTemplateComponent,
CreateBusinessScheduleRefComponent,
CreateServiceAcceptanceActPreferencesComponent,
AcceptClaimComponent,
DenyClaimComponent,
CreateTerminalObjectComponent,
ContainerNamePipe
],
entryComponents: [
ClaimActionsComponent,
CreateChangeComponent,
AcceptClaimComponent,
DenyClaimComponent
],
providers: [
ClaimService
]
})
export class ClaimModule {
}

View File

@ -0,0 +1,188 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { delay, map, repeatWhen, switchMap, takeWhile, tap } from 'rxjs/internal/operators';
import isEqual from 'lodash-es/isEqual';
import get from 'lodash-es/get';
import toNumber from 'lodash-es/toNumber';
import { ClaimService as ClaimPapi } from '../papi/claim.service';
import { ClaimInfo, PartyModificationUnit } from '../papi/model';
import { ShopModification, ContractModification, PartyModification } from '../damsel';
import {
ClaimInfoContainer,
DomainModificationInfo,
PartyModificationContainerType
} from './model';
import { PartyModificationContainerConverter } from './party-modification-container-converter';
@Injectable()
export class ClaimService {
claimInfoContainer$: Subject<ClaimInfoContainer> = new Subject();
domainModificationInfo$: Subject<DomainModificationInfo> = new BehaviorSubject(null);
private claimInfoContainer: ClaimInfoContainer;
constructor(private papiClaimService: ClaimPapi) {
}
resolveClaimInfo(partyID: string, claimID: string): Observable<void> {
return this.papiClaimService.getClaim(partyID, toNumber(claimID))
.pipe(
tap((claimInfo) => {
this.claimInfoContainer = this.toClaimInfoContainer(claimInfo);
const domainModificationInfo = this.toDomainModificationInfo(claimInfo, this.claimInfoContainer.extractedIds.shopId);
this.domainModificationInfo$.next(domainModificationInfo);
this.claimInfoContainer$.next(this.claimInfoContainer);
}),
map(() => null)
);
}
createChange(type: PartyModificationContainerType, modification: ShopModification | ContractModification): Observable<void> {
const {partyId, claimId} = this.claimInfoContainer;
const unit = this.toModificationUnit(type, modification);
return this.papiClaimService.getClaim(partyId, claimId)
.pipe(
switchMap((claimInfo) =>
this.papiClaimService
.updateClaim(partyId, claimId, claimInfo.revision, unit)
.pipe(map(() => claimInfo.revision))),
switchMap((revision) =>
this.pollClaimChange(revision))
);
}
acceptClaim(): Observable<void> {
const {claimId, partyId} = this.claimInfoContainer;
return this.papiClaimService.getClaim(partyId, claimId)
.pipe(
switchMap((claimInfo) =>
this.papiClaimService
.acceptClaim({partyId, claimId, revision: claimInfo.revision})
.pipe(map(() => claimInfo.revision))),
switchMap((revision) =>
this.pollClaimChange(revision))
);
}
denyClaim(reason: string): Observable<void> {
const {claimId, partyId} = this.claimInfoContainer;
return this.papiClaimService.getClaim(partyId, claimId)
.pipe(
switchMap((claimInfo) =>
this.papiClaimService
.denyClaim({claimId, partyId, revision: claimInfo.revision, reason})
.pipe(map(() => claimInfo.revision))),
switchMap((revision) =>
this.pollClaimChange(revision))
);
}
private toModificationUnit(type: PartyModificationContainerType, modification: ShopModification | ContractModification): PartyModificationUnit {
const result = {
modifications: []
};
let unit;
const {contractId, shopId} = this.claimInfoContainer.extractedIds;
switch (type) {
case PartyModificationContainerType.ContractModification:
unit = {
contractModification: {
id: contractId,
modification
}
};
break;
case PartyModificationContainerType.ShopModification:
unit = {
shopModification: {
id: shopId,
modification
}
};
break;
}
result.modifications.push(unit);
return result;
}
private toClaimInfoContainer(claimInfo: ClaimInfo): ClaimInfoContainer {
const modifications = claimInfo.modifications.modifications;
const {claimId, partyId, revision, status, reason, createdAt, updatedAt} = claimInfo;
const partyModificationUnits = PartyModificationContainerConverter.convert(modifications);
const extractedIds = this.extractIds(modifications);
return {
claimId,
partyId,
revision,
status,
reason,
createdAt,
updatedAt,
extractedIds,
partyModificationUnits
};
}
private toDomainModificationInfo(claimInfo: ClaimInfo, shopId: string): DomainModificationInfo {
const modifications = claimInfo.modifications.modifications;
return {
shopUrl: this.findShopUrl(modifications),
shopId,
partyId: claimInfo.partyId
};
}
private findShopUrl(modifications: PartyModification[]): string {
const found = modifications.find((item) =>
!!(item.shopModification && item.shopModification.modification.creation));
return get(found, 'shopModification.modification.creation.location.url');
}
private extractIds(modifications: PartyModification[]): { shopId: string, contractId: string } {
return modifications.reduce((prev, current) => {
if (!prev.shopId && current.shopModification) {
const shopId = current.shopModification.id;
return {...prev, shopId};
} else if (!prev.contractId && current.contractModification) {
const contractId = current.contractModification.id;
return {...prev, contractId};
} else {
return prev;
}
}, {shopId: null, contractId: null});
}
private pollClaimChange(revision: string, delayMs = 2000, retryCount = 15): Observable<void> {
const container = this.claimInfoContainer;
const {partyId, claimId, status} = container;
const currentPair = {status, revision};
let newPair = {};
return Observable.create((observer) => {
this.papiClaimService.getClaim(partyId, claimId)
.pipe(
repeatWhen((notifications) => {
return notifications.pipe(
delay(delayMs),
takeWhile((value, retries) =>
isEqual(newPair, currentPair) && retries <= retryCount)
);
})
)
.subscribe((claimInfo) => {
newPair = {
status: claimInfo.status,
revision: claimInfo.revision
};
if (!isEqual(newPair, currentPair)) {
this.claimInfoContainer = this.toClaimInfoContainer(claimInfo);
this.claimInfoContainer$.next(this.claimInfoContainer);
observer.next();
observer.complete();
}
});
});
}
}

View File

@ -0,0 +1,71 @@
import { Pipe, PipeTransform } from '@angular/core';
import {
PartyModificationContainerType,
ShopModificationName,
ContractModificationName
} from './model';
@Pipe({
name: 'ccContainerName'
})
export class ContainerNamePipe implements PipeTransform {
transform(value: any, ...args: any[]): any {
if (args.length < 1) {
return value;
}
const type = args[0] as PartyModificationContainerType;
switch (type) {
case PartyModificationContainerType.ShopModification:
return this.transformShopModification(value);
case PartyModificationContainerType.ContractModification:
return this.transformContractModificationName(value);
default:
return value;
}
}
private transformShopModification(value: ShopModificationName): string {
switch (value) {
case ShopModificationName.creation:
return 'Shop creation';
case ShopModificationName.categoryModification:
return 'Shop category modification';
case ShopModificationName.detailsModification:
return 'Shop details modification';
case ShopModificationName.contractModification:
return 'Shop contract modification';
case ShopModificationName.payoutToolModification:
return 'Shop payout tool modification';
case ShopModificationName.locationModification:
return 'Shop location modification';
case ShopModificationName.shopAccountCreation:
return 'Shop account creation';
case ShopModificationName.payoutScheduleModification:
return 'Shop payout schedule modification';
default:
return value;
}
}
private transformContractModificationName(value: ContractModificationName): string {
switch (value) {
case ContractModificationName.creation:
return 'Contract creation';
case ContractModificationName.termination:
return 'Contract termination';
case ContractModificationName.adjustmentModification:
return 'Contract adjustment modification';
case ContractModificationName.payoutToolModification:
return 'Contract payout tool modification';
case ContractModificationName.legalAgreementBinding:
return 'Contract legal agreement binding';
case ContractModificationName.reportPreferencesModification:
return 'Contract report preferences modification';
default:
return value;
}
}
}

View File

@ -0,0 +1,14 @@
<form fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
[formGroup]="form">
<mat-form-field fxFlex>
<mat-select placeholder="Select payout schedule"
formControlName="id">
<mat-option *ngFor="let schedule of payoutSchedules$ | async" [value]="schedule.ref.id" required>
{{schedule.ref.id}} {{schedule.data.name}}
</mat-option>
</mat-select>
</mat-form-field>
</form>

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { CreateBusinessScheduleRefService } from './create-business-schedule-ref.service';
import { BusinessScheduleObject } from '../../../damsel/domain';
import { DomainTypedManager } from '../../../domain/domain-typed-manager';
@Component({
selector: 'cc-create-business-schedule-ref',
templateUrl: 'create-business-schedule-ref.component.html'
})
export class CreateBusinessScheduleRefComponent implements OnInit {
form: FormGroup;
payoutSchedules$: Observable<BusinessScheduleObject[]>;
constructor(
private createBusinessScheduleRefService: CreateBusinessScheduleRefService,
private domainManager: DomainTypedManager) {}
ngOnInit() {
this.form = this.createBusinessScheduleRefService.form;
this.payoutSchedules$ = this.domainManager.getBusinessScheduleObjects();
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CreateChangeItem } from '../create-change-item';
import { ShopModification } from '../../../damsel/payment-processing';
@Injectable()
export class CreateBusinessScheduleRefService implements CreateChangeItem {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
}
getValue(): ShopModification {
return {
payoutScheduleModification: {
schedule: this.form.value
}
};
}
isValid(): boolean {
return this.form.valid;
}
private prepareForm(): FormGroup {
return this.fb.group({
id: ['', Validators.required]
});
}
}

View File

@ -0,0 +1,14 @@
<form fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
[formGroup]="form">
<mat-form-field fxFlex>
<mat-select placeholder="Select category"
formControlName="id">
<mat-option *ngFor="let category of categories$ | async" [value]="category.id" required>
{{category.id}} {{category.name}}
</mat-option>
</mat-select>
</mat-form-field>
</form>

View File

@ -0,0 +1,31 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/internal/operators';
import sortBy from 'lodash-es/sortBy';
import { Category } from '../../../papi/model/category';
import { CreateCategoryRefService } from './create-category-ref.service';
import { CategoryService } from '../../../papi/category.service';
@Component({
selector: 'cc-create-category-ref',
templateUrl: 'create-category-ref.component.html'
})
export class CreateCategoryRefComponent implements OnInit {
categories$: Observable<Category[]>;
form: FormGroup;
constructor(
private createCategoryService: CreateCategoryRefService,
private categoryService: CategoryService) {}
ngOnInit() {
this.form = this.createCategoryService.form;
this.categories$ = this.categoryService
.getCategories()
.pipe(map((categories) => sortBy(categories, 'id')));
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CreateChangeItem } from '../create-change-item';
import { ShopModification } from '../../../damsel';
import { CategoryService } from '../../../papi/category.service';
@Injectable()
export class CreateCategoryRefService implements CreateChangeItem {
form: FormGroup;
constructor(private categoryService: CategoryService,
private fb: FormBuilder) {
this.form = this.prepareForm();
}
getValue(): ShopModification {
return {
categoryModification: this.form.value
};
}
isValid(): boolean {
return this.form.valid;
}
private prepareForm(): FormGroup {
return this.fb.group({
id: ['', Validators.required]
});
}
}

View File

@ -0,0 +1,8 @@
import { ContractModification, ShopModification } from '../../damsel';
import { CreateTerminalParams } from '../../domain/domain-typed-manager';
export interface CreateChangeItem {
getValue(): ContractModification | ShopModification | CreateTerminalParams;
isValid(): boolean;
}

View File

@ -0,0 +1,41 @@
<div class="mat-dialog-title">{{claimAction.name | ccContainerName:getContainerType(claimAction.type)}}</div>
<mat-dialog-content>
<div *ngIf="claimAction.type === ActionType.contractAction">
<cc-create-legal-agreement
*ngIf="claimAction.name === ContractModificationName.legalAgreementBinding">
</cc-create-legal-agreement>
<cc-create-contract-template
*ngIf="claimAction.name === ContractModificationName.adjustmentModification">
</cc-create-contract-template>
<cc-create-service-acceptance-act-preferences
*ngIf="claimAction.name === ContractModificationName.reportPreferencesModification">
</cc-create-service-acceptance-act-preferences>
</div>
<div *ngIf="claimAction.type === ActionType.shopAction">
<cc-create-category-ref
*ngIf="claimAction.name === ShopModificationName.categoryModification">
</cc-create-category-ref>
<cc-create-currency-ref
*ngIf="claimAction.name === ShopModificationName.shopAccountCreation">
</cc-create-currency-ref>
<cc-create-business-schedule-ref
*ngIf="claimAction.name === ShopModificationName.payoutScheduleModification">
</cc-create-business-schedule-ref>
</div>
<div *ngIf="claimAction.type === ActionType.domainAction">
<cc-create-terminal-object
[domainModificationInfo]="(domainModificationInfo$ | async)">
</cc-create-terminal-object>
</div>
</mat-dialog-content>
<mat-dialog-actions fxLayout="column" fxLayoutGap="10px">
<div>
<button mat-button
[disabled]="!isFormValid() || isLoading"
color="primary"
(click)="create()">CREATE
</button>
<button [disabled]="isLoading" mat-button [mat-dialog-close]="false">CANCEL</button>
</div>
<mat-progress-bar *ngIf="isLoading" mode="indeterminate"></mat-progress-bar>
</mat-dialog-actions>

View File

@ -0,0 +1,85 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material';
import { Observable } from 'rxjs';
import { ActionType, ClaimAction } from '../claim-actions/claim-action';
import { CreateChangeService } from './create-change.service';
import { CreateLegalAgreementService } from './create-legal-agreement/create-legal-agreement.service';
import { CreateCategoryRefService } from './create-category-ref/create-category-ref.service';
import { CreateCurrencyRefService } from './create-currency-ref/create-currency-ref.service';
import { CreateContractTemplateService } from './create-contract-template/create-contract-template.service';
import { CreateBusinessScheduleRefService } from './create-business-schedule-ref/create-business-schedule-ref.service';
import { CreateServiceAcceptanceActPreferencesService } from './create-service-acceptance-act-preferences/create-service-acceptance-act-preferences.service';
import { CreateTerminalObjectService } from './create-terminal-object/create-terminal-object.service';
import {
ContractModificationName,
DomainModificationInfo,
ShopModificationName,
PartyModificationContainerType
} from '../model';
@Component({
templateUrl: 'create-change.component.html',
providers: [
CreateChangeService,
CreateLegalAgreementService,
CreateCategoryRefService,
CreateCurrencyRefService,
CreateContractTemplateService,
CreateBusinessScheduleRefService,
CreateServiceAcceptanceActPreferencesService,
CreateTerminalObjectService
]
})
export class CreateChangeComponent implements OnInit {
ActionType = ActionType;
ContractModificationName = ContractModificationName;
ShopModificationName = ShopModificationName;
isLoading = false;
domainModificationInfo$: Observable<DomainModificationInfo>;
constructor(
private dialogRef: MatDialogRef<CreateChangeComponent>,
@Inject(MAT_DIALOG_DATA) public claimAction: ClaimAction,
private snackBar: MatSnackBar,
private createChangeService: CreateChangeService) {
}
ngOnInit() {
this.domainModificationInfo$ = this.createChangeService.domainModificationInfo$;
}
create() {
const {name} = this.claimAction;
this.isLoading = true;
this.createChangeService.createChange(this.claimAction).subscribe(() => {
this.isLoading = false;
this.dialogRef.close();
this.snackBar.open(`${name} created`, 'OK', {duration: 3000});
}, (error) => {
console.error(error);
this.isLoading = false;
this.snackBar.open(`An error occurred while creating ${name}`, 'OK');
});
}
isFormValid() {
return this.createChangeService.isFormValid(this.claimAction);
}
getContainerType(type: ActionType): string {
switch (type) {
case ActionType.shopAction:
return PartyModificationContainerType.ShopModification;
case ActionType.contractAction:
return PartyModificationContainerType.ContractModification;
case ActionType.domainAction:
return 'Domain modification';
}
}
}

View File

@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/internal/operators';
import { CreateLegalAgreementService } from './create-legal-agreement/create-legal-agreement.service';
import { CreateCategoryRefService } from './create-category-ref/create-category-ref.service';
import { ActionType, ClaimAction } from '../claim-actions/claim-action';
import { CreateCurrencyRefService } from './create-currency-ref/create-currency-ref.service';
import { CreateContractTemplateService } from './create-contract-template/create-contract-template.service';
import { CreateBusinessScheduleRefService } from './create-business-schedule-ref/create-business-schedule-ref.service';
import { CreateServiceAcceptanceActPreferencesService } from './create-service-acceptance-act-preferences/create-service-acceptance-act-preferences.service';
import { CreateTerminalObjectService } from './create-terminal-object/create-terminal-object.service';
import { CreateTerminalParams, DomainTypedManager } from '../../domain/domain-typed-manager';
import {
ContractModificationName,
DomainModificationInfo,
PartyModificationContainerType,
ShopModificationName
} from '../model';
import { CreateChangeItem } from './create-change-item';
import { ClaimService } from '../claim.service';
import { ContractModification, ShopModification } from '../../damsel';
@Injectable()
export class CreateChangeService {
domainModificationInfo$: Observable<DomainModificationInfo>;
constructor(private createLegalAgreementService: CreateLegalAgreementService,
private createCategoryRefService: CreateCategoryRefService,
private createCurrencyRefService: CreateCurrencyRefService,
private createContractTemplateService: CreateContractTemplateService,
private createBusinessScheduleRefService: CreateBusinessScheduleRefService,
private createServiceAcceptanceActPreferencesService: CreateServiceAcceptanceActPreferencesService,
private createTerminalObjectService: CreateTerminalObjectService,
private claimService: ClaimService,
private domainTypedManager: DomainTypedManager) {
this.domainModificationInfo$ = this.claimService.domainModificationInfo$;
}
createChange(claimAction: ClaimAction): Observable<void> {
const instance = this.getCreateServiceInstance(claimAction);
const value = instance.getValue();
switch (claimAction.type) {
case ActionType.shopAction:
case ActionType.contractAction:
const partyType = this.toPartyModificationType(claimAction.type);
return this.claimService
.createChange(partyType, value as ShopModification | ContractModification);
case ActionType.domainAction:
return this.domainTypedManager
.createTerminal(value as CreateTerminalParams)
.pipe(map(() => {}));
}
}
isFormValid(claimAction: ClaimAction): boolean {
const instance = this.getCreateServiceInstance(claimAction);
return instance.isValid();
}
private getCreateServiceInstance(action: ClaimAction): CreateChangeItem {
switch (action.type) {
case ActionType.contractAction:
return this.getContractServiceInstance(action);
case ActionType.shopAction:
return this.getShopServiceInstance(action);
case ActionType.domainAction:
return this.createTerminalObjectService;
}
}
private getContractServiceInstance(action: ClaimAction): CreateChangeItem {
switch (action.name) {
case ContractModificationName.legalAgreementBinding:
return this.createLegalAgreementService;
case ContractModificationName.adjustmentModification:
return this.createContractTemplateService;
case ContractModificationName.reportPreferencesModification:
return this.createServiceAcceptanceActPreferencesService;
}
}
private getShopServiceInstance(action: ClaimAction): CreateChangeItem {
switch (action.name) {
case ShopModificationName.categoryModification:
return this.createCategoryRefService;
case ShopModificationName.shopAccountCreation:
return this.createCurrencyRefService;
case ShopModificationName.payoutScheduleModification:
return this.createBusinessScheduleRefService;
}
}
private toPartyModificationType(type: ActionType): PartyModificationContainerType {
switch (type) {
case ActionType.contractAction:
return PartyModificationContainerType.ContractModification;
case ActionType.shopAction:
return PartyModificationContainerType.ShopModification;
}
}
}

View File

@ -0,0 +1,14 @@
<form fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
[formGroup]="form">
<mat-form-field fxFlex>
<mat-select placeholder="Select contract template"
formControlName="id">
<mat-option *ngFor="let contract of contracts$ | async" [value]="contract.id" required>
{{contract.id}} {{contract.name}}
</mat-option>
</mat-select>
</mat-form-field>
</form>

View File

@ -0,0 +1,31 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/internal/operators';
import sortBy from 'lodash-es/sortBy';
import { CreateContractTemplateService } from './create-contract-template.service';
import { ContractTemplate } from '../../../papi/model';
import { ContractService } from '../../../papi/contract.service';
@Component({
selector: 'cc-create-contract-template',
templateUrl: 'create-contract-template.component.html'
})
export class CreateContractTemplateComponent implements OnInit {
contracts$: Observable<ContractTemplate[]>;
form: FormGroup;
constructor(
private createContractTemplateService: CreateContractTemplateService,
private contractService: ContractService) {}
ngOnInit() {
this.form = this.createContractTemplateService.form;
this.contracts$ = this.contractService
.getContractTemplates()
.pipe(map((contracts) => sortBy(contracts, 'id')));
}
}

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import * as uuid from 'uuid/v4';
import { CreateChangeItem } from '../create-change-item';
import { ContractModification } from '../../../damsel/payment-processing';
@Injectable()
export class CreateContractTemplateService implements CreateChangeItem {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
}
getValue(): ContractModification {
return {
adjustmentModification: {
adjustmentId: uuid(),
modification: {
creation: {
template: this.form.value
}
}
}
};
}
isValid(): boolean {
return this.form.valid;
}
private prepareForm(): FormGroup {
return this.fb.group({
id: ['', Validators.required]
});
}
}

View File

@ -0,0 +1,12 @@
<form fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
[formGroup]="form">
<mat-form-field fxFlex>
<input matInput
placeholder="Symbolic Code"
formControlName="symbolicCode"
required>
</mat-form-field>
</form>

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CreateCurrencyRefService } from './create-currency-ref.service';
@Component({
selector: 'cc-create-currency-ref',
templateUrl: 'create-currency-ref.component.html'
})
export class CreateCurrencyRefComponent implements OnInit {
form: FormGroup;
constructor(private createCurrencyRefService: CreateCurrencyRefService) {
}
ngOnInit() {
this.form = this.createCurrencyRefService.form;
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CreateChangeItem } from '../create-change-item';
import { ShopModification } from '../../../damsel/payment-processing';
@Injectable()
export class CreateCurrencyRefService implements CreateChangeItem {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
}
getValue(): ShopModification {
return {
shopAccountCreation: {
currency: this.form.value
}
};
}
isValid(): boolean {
return this.form.valid;
}
private prepareForm(): FormGroup {
return this.fb.group({
symbolicCode: ['RUB', Validators.required]
});
}
}

View File

@ -0,0 +1,21 @@
<form fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="20px"
[formGroup]="form">
<mat-form-field fxFlex="50">
<input matInput
placeholder="Legal agreement ID"
formControlName="legalAgreementId"
required>
</mat-form-field>
<mat-form-field fxFlex="50">
<input matInput
[matDatepicker]="signedAtDatepicker"
placeholder="Signed at"
formControlName="signedAt"
required>
<mat-datepicker-toggle matSuffix [for]="signedAtDatepicker"></mat-datepicker-toggle>
<mat-datepicker #signedAtDatepicker></mat-datepicker>
</mat-form-field>
</form>

View File

@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CreateLegalAgreementService } from './create-legal-agreement.service';
@Component({
selector: 'cc-create-legal-agreement',
templateUrl: './create-legal-agreement.component.html'
})
export class CreateLegalAgreementComponent implements OnInit {
form: FormGroup;
constructor(private createLegalAgreementService: CreateLegalAgreementService) {
}
ngOnInit() {
this.form = this.createLegalAgreementService.form;
}
}

View File

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import * as moment from 'moment';
import { CreateChangeItem } from '../create-change-item';
import { ContractModification } from '../../../damsel';
@Injectable()
export class CreateLegalAgreementService implements CreateChangeItem {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
}
getValue(): ContractModification {
const {legalAgreementId, signedAt} = this.form.value;
return {
legalAgreementBinding: {
legalAgreementId,
signedAt: moment(signedAt).utc().format()
}
};
}
isValid(): boolean {
return this.form.valid;
}
private prepareForm() {
return this.fb.group({
legalAgreementId: ['', Validators.required],
signedAt: ['', Validators.required]
});
}
}

View File

@ -0,0 +1,66 @@
<form [formGroup]="form" fxLayout="column">
<mat-form-field fxFlex>
<input matInput
placeholder="Schedule ID"
formControlName="scheduleID"
type="number"
required>
</mat-form-field>
<div fxFlex>
<div fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column">
<mat-form-field fxFlex>
<input matInput
placeholder="Full Name"
formControlName="fullName"
required>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
placeholder="Position"
formControlName="position"
required>
</mat-form-field>
</div>
</div>
<mat-form-field fxFlex>
<mat-select placeholder="Document type" formControlName="documentType">
<mat-option *ngFor="let documentType of documentTypes" [value]="documentType">
{{documentType}}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout="column" *ngIf="form.value.documentType === 'PowerOfAttorney'" formGroupName="signer">
<mat-form-field fxFlex>
<input matInput
placeholder="Legal Agreement ID"
formControlName="legalAgreementId"
required>
</mat-form-field>
<div fxFlex>
<div fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column">
<mat-form-field fxFlex>
<input matInput
[matDatepicker]="signedAtPicker"
placeholder="Signed At"
formControlName="signedAt"
required>
<mat-datepicker-toggle matSuffix [for]="signedAtPicker"></mat-datepicker-toggle>
<mat-datepicker #signedAtPicker></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
[matDatepicker]="validUntilPicker"
placeholder="Valid Until"
formControlName="validUntil"
required>
<mat-datepicker-toggle matSuffix [for]="validUntilPicker"></mat-datepicker-toggle>
<mat-datepicker #validUntilPicker></mat-datepicker>
</mat-form-field>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CreateServiceAcceptanceActPreferencesService } from './create-service-acceptance-act-preferences.service';
import { RepresentativeDocumentType } from '../../../damsel/domain/representative-document';
@Component({
selector: 'cc-create-service-acceptance-act-preferences',
templateUrl: 'create-service-acceptance-act-preferences.component.html'
})
export class CreateServiceAcceptanceActPreferencesComponent {
form: FormGroup;
documentTypes = [
RepresentativeDocumentType.PowerOfAttorney,
RepresentativeDocumentType.ArticlesOfAssociation
];
constructor(private createServiceAcceptanceActPreferencesService: CreateServiceAcceptanceActPreferencesService) {
this.form = this.createServiceAcceptanceActPreferencesService.form;
}
}

View File

@ -0,0 +1,85 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import toNumber from 'lodash-es/toNumber';
import * as moment from 'moment';
import { CreateChangeItem } from '../create-change-item';
import { ContractModification } from '../../../damsel/payment-processing';
import { RepresentativeDocument, RepresentativeDocumentType } from '../../../damsel/domain/representative-document';
@Injectable()
export class CreateServiceAcceptanceActPreferencesService implements CreateChangeItem {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
this.form.controls.documentType.valueChanges.subscribe((documentType) => {
switch (documentType) {
case RepresentativeDocumentType.PowerOfAttorney:
this.form.setControl('signer', this.getPowerOfAttorneyGroup());
break;
case RepresentativeDocumentType.ArticlesOfAssociation:
this.form.removeControl('signer');
break;
}
});
}
getValue(): ContractModification {
const {scheduleID, fullName, position, documentType, signer} = this.form.value;
return {
reportPreferencesModification: {
serviceAcceptanceActPreferences: {
schedule: {
id: toNumber(scheduleID)
},
signer: {
fullName,
position,
document: this.getDocument(documentType, signer)
}
}
}
};
}
isValid(): boolean {
return this.form.valid;
}
private getDocument(type: RepresentativeDocumentType, signerValue: any): RepresentativeDocument {
switch (type) {
case RepresentativeDocumentType.PowerOfAttorney:
const {legalAgreementId, validUntil, signedAt} = signerValue;
return {
powerOfAttorney: {
legalAgreementId,
validUntil: moment(validUntil).utc().format(),
signedAt: moment(signedAt).utc().format()
}
};
case RepresentativeDocumentType.ArticlesOfAssociation:
return {
articlesOfAssociation: {}
};
}
}
private prepareForm(): FormGroup {
return this.fb.group({
scheduleID: ['', Validators.required],
fullName: ['', Validators.required],
position: ['', Validators.required],
documentType: ['', Validators.required]
});
}
private getPowerOfAttorneyGroup(): FormGroup {
return this.fb.group({
legalAgreementId: ['', Validators.required],
signedAt: ['', Validators.required],
validUntil: ['', Validators.required]
});
}
}

View File

@ -0,0 +1,77 @@
<form *ngIf="form" [formGroup]="form" fxLayout="column" fxLayoutGap="20px">
<div fxLayout>
<mat-form-field fxFlex>
<mat-select placeholder="Provider name" formControlName="providerID" required>
<mat-option *ngFor="let provider of providerObjects$ | async" [value]="provider.ref.id">
{{provider.ref.id}} {{provider.data.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.sm="column" fxLayout.xs="column" fxLayoutGap="10px">
<label fxFlex="25">Bank options template:</label>
<mat-radio-group fxFlex="70" fxLayout fxLayoutAlign="space-between center"
formControlName="bankOptionsTemplate">
<mat-radio-button *ngFor="let template of optionTemplates"
fxFlex
[value]="template"
(click)="setBankOptionsTemplate(template)">{{template}}
</mat-radio-button>
</mat-radio-group>
</div>
<div fxLayout="row" fxLayout.sm="column" fxLayout.xs="column">
<mat-form-field fxFlex>
<input matInput
placeholder="Terminal name"
formControlName="terminalName"
required>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
placeholder="Terminal description"
formControlName="terminalDescription"
required>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.sm="column" fxLayout.xs="column" fxLayoutGap="10px">
<label fxFlex="25">Risk coverage:</label>
<mat-radio-group fxFlex="50" fxLayout fxLayoutAlign="space-between center"
formControlName="riskCoverage">
<mat-radio-button *ngFor="let coverage of riskCoverages"
fxFlex
[value]="coverage.value">{{coverage.name}}
</mat-radio-button>
</mat-radio-group>
</div>
<div fxLayout="column" fxLayoutGap="10px">
<div>Options:</div>
<div formArrayName="options">
<div fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
*ngFor="let option of form.controls.options.controls; let i=index"
[formGroupName]="i">
<mat-form-field fxFlex="30">
<input matInput
placeholder="Key"
formControlName="key"
required>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
placeholder="Value"
formControlName="value"
required>
</mat-form-field>
<button mat-icon-button color="basic" fxFlex="40px" (click)="removeOption(i)">
<mat-icon>clear</mat-icon>
</button>
</div>
</div>
<div fxLayout fxLayoutAlign="end center">
<button mat-icon-button color="basic" fxFlex="40px" (click)="addOption()">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
</form>

View File

@ -0,0 +1,54 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { CreateTerminalObjectService } from './create-terminal-object.service';
import { DomainTypedManager } from '../../../domain/domain-typed-manager';
import { ProviderObject } from '../../../damsel/domain';
import { DomainModificationInfo } from '../../model';
@Component({
selector: 'cc-create-terminal-object',
templateUrl: 'create-terminal-object.component.html'
})
export class CreateTerminalObjectComponent implements OnInit, OnChanges {
@Input()
domainModificationInfo: DomainModificationInfo;
form: FormGroup;
providerObjects$: Observable<ProviderObject[]>;
optionTemplates: string[];
riskCoverages: Array<{ name: string, value: number }>;
constructor(private createTerminalObjectService: CreateTerminalObjectService,
private domainTypedManager: DomainTypedManager) {
}
ngOnInit() {
this.providerObjects$ = this.domainTypedManager.getProviderObjects();
this.optionTemplates = this.createTerminalObjectService.optionTemplates;
this.riskCoverages = this.createTerminalObjectService.riskCoverages;
}
ngOnChanges() {
if (this.domainModificationInfo) {
this.form = this.createTerminalObjectService.initForm(this.domainModificationInfo);
}
}
setBankOptionsTemplate(selectedOption: string) {
this.createTerminalObjectService.setBankOptionsTemplate(selectedOption);
}
addOption() {
this.createTerminalObjectService.addOption();
}
removeOption(index: number) {
this.createTerminalObjectService.removeOption(index);
}
}

View File

@ -0,0 +1,107 @@
import { Injectable } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CreateChangeItem } from '../create-change-item';
import { CreateTerminalParams, TerminalOption } from '../../../domain/domain-typed-manager';
import { getOptions, prepareTerminalName } from './form-default-values';
import { DomainModificationInfo } from '../../model';
const toFormArray = (fb: FormBuilder, options: TerminalOption[]): FormArray =>
fb.array(options.map((option) => fb.group(option)));
@Injectable()
export class CreateTerminalObjectService implements CreateChangeItem {
optionTemplates: string[] = ['VTB', 'TCS', 'RIET', 'SNGB', 'none'];
riskCoverages = [
{
name: 'low',
value: 0
}, {
name: 'high',
value: 100
}, {
name: 'fatal',
value: 9999
}
];
form: FormGroup;
private domainModificationInfo: DomainModificationInfo;
constructor(private fb: FormBuilder) {
}
getValue(): CreateTerminalParams {
const {partyId, shopId} = this.domainModificationInfo;
return this.toCreateTerminalParams(partyId, shopId);
}
isValid(): boolean {
return this.form ? this.form.valid : false;
}
addOption() {
(this.form.controls.options as FormArray).push(this.getOption());
}
removeOption(index: number) {
const options = this.form.controls.options as FormArray;
if (options.length > 1) {
options.removeAt(index);
}
}
initForm(info: DomainModificationInfo): FormGroup {
this.domainModificationInfo = info;
this.form = this.prepareForm(info);
return this.form;
}
setBankOptionsTemplate(option: string) {
this.form.setControl('options', toFormArray(this.fb, getOptions(option, this.domainModificationInfo)));
this.form.patchValue({
terminalName: prepareTerminalName(option, this.domainModificationInfo.shopUrl)
});
}
private prepareForm(param: DomainModificationInfo): FormGroup {
const defaultOption = 'VTB';
return this.fb.group({
providerID: ['', Validators.required],
terminalName: [prepareTerminalName(defaultOption, param.shopUrl), Validators.required],
terminalDescription: 'No',
riskCoverage: [100, Validators.required],
bankOptionsTemplate: [defaultOption],
options: toFormArray(this.fb, getOptions(defaultOption, param))
});
}
private getOption(): FormGroup {
return this.fb.group({
key: '',
value: ''
});
}
private toCreateTerminalParams(partyID: string, shopID: string): CreateTerminalParams {
const {
providerID,
terminalName,
terminalDescription,
riskCoverage,
options
} = this.form.value;
return {
providerID,
terminalName,
terminalDescription,
riskCoverage,
options,
partyID,
shopID
};
}
}

View File

@ -0,0 +1,9 @@
export const getHost = (shopUrl: string) => {
let host;
try {
host = new URL(shopUrl).host;
} catch (ex) {
console.warn(ex);
}
return host;
};

View File

@ -0,0 +1,27 @@
import { getVtbTemplateOptions } from './get-vtb-template-options';
import { getTcsTemplateOptions } from './get-tcs-template-options';
import { getRietTemplateOptions } from './get-riet-template-options';
import { getSngbTemplateOptions } from './get-sngb-template-options';
import { TerminalOption } from '../../../../domain/domain-typed-manager';
import { DomainModificationInfo } from '../../../model';
export const getOptions = (option: string, param: DomainModificationInfo): TerminalOption[] => {
const {shopUrl, partyId} = param;
let options = [{key: '', value: ''}];
switch (option) {
case 'VTB':
options = getVtbTemplateOptions(shopUrl, partyId);
break;
case 'TCS':
options = getTcsTemplateOptions(shopUrl);
break;
case 'RIET':
options = getRietTemplateOptions();
break;
case 'SNGB':
options = getSngbTemplateOptions();
break;
}
return options;
};

View File

@ -0,0 +1,21 @@
import { TerminalOption } from '../../../../domain/domain-typed-manager';
export const getRietTemplateOptions = (): TerminalOption[] =>
([
{
key: 'custom_return_url',
value: ''
},
{
key: 'routing',
value: ''
},
{
key: 'routing_recurrent',
value: ''
},
{
key: 'terminal_id',
value: ''
}
]);

View File

@ -0,0 +1,14 @@
export const getSngbTemplateOptions = () => ([
{
key: 'mpiEnable',
value: 'true'
},
{
key: 'terminal_id',
value: ''
},
{
key: 'tran_portal_password',
value: ''
}
]);

View File

@ -0,0 +1,34 @@
import { getHost } from './get-host';
import { TerminalOption } from '../../../../domain/domain-typed-manager';
export const getTcsTemplateOptions = (shopUrl: string): TerminalOption[] =>
([
{
key: 'terminalIdNon3ds',
value: ''
},
{
key: 'terminalId3ds',
value: ''
},
{
key: 'submerchantId',
value: ''
},
{
key: 'mpiEnable',
value: 'true'
},
{
key: 'merchantId',
value: ''
},
{
key: 'merchantName',
value: getHost(shopUrl)
},
{
key: 'merchantUrl',
value: shopUrl
}
]);

View File

@ -0,0 +1,47 @@
import last from 'lodash-es/last';
import { getHost } from './get-host';
import { TerminalOption } from '../../../../domain/domain-typed-manager';
const partyIDToSmid = (partyID: string) => last(partyID.split('-'));
const prepareMerchantName = (shopUrl: string) => `${getHost(shopUrl)}@RBKmoney`;
export const getVtbTemplateOptions = (shopUrl: string, partyID: string): TerminalOption[] =>
([
{
key: 'PFSNAME',
value: ''
},
{
key: 'SMADDRESS',
value: ''
},
{
key: 'SMCITY',
value: ''
},
{
key: 'SMID',
value: partyIDToSmid(partyID)
},
{
key: 'SMMCC',
value: ''
},
{
key: 'SMPOSTCODE',
value: ''
},
{
key: 'merchant_id',
value: ''
},
{
key: 'merchant_name',
value: prepareMerchantName(shopUrl)
},
{
key: 'term_id',
value: ''
}
]);

View File

@ -0,0 +1,2 @@
export * from './get-options';
export * from './prepare-terminal-name';

View File

@ -0,0 +1,3 @@
import { getHost } from './get-host';
export const prepareTerminalName = (bankOption: string, shopUrl: string) => `${bankOption} / ${getHost(shopUrl)}`;

View File

@ -0,0 +1,23 @@
<div class="mat-dialog-title">Confirm claim denying</div>
<mat-dialog-content>
<form fxLayout="row">
<mat-form-field fxFlex class="example-full-width">
<textarea matInput
placeholder="Reason"
[(ngModel)]="reason"
name="reason">
</textarea>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions fxLayout="column" fxLayoutGap="10px">
<div>
<button mat-button
[disabled]="isLoading"
color="primary"
(click)="deny()">CONFIRM
</button>
<button [disabled]="isLoading" mat-button [mat-dialog-close]="false">CANCEL</button>
</div>
<mat-progress-bar *ngIf="isLoading" mode="indeterminate"></mat-progress-bar>
</mat-dialog-actions>

View File

@ -0,0 +1,33 @@
import { Component } from '@angular/core';
import { MatDialogRef, MatSnackBar } from '@angular/material';
import { ClaimService } from '../claim.service';
@Component({
templateUrl: 'deny-claim.component.html'
})
export class DenyClaimComponent {
isLoading = false;
reason = '';
constructor(
private dialogRef: MatDialogRef<DenyClaimComponent>,
private claimService: ClaimService,
private snackBar: MatSnackBar
) {
}
deny() {
this.isLoading = true;
this.claimService.denyClaim(this.reason).subscribe(() => {
this.isLoading = false;
this.dialogRef.close();
this.snackBar.open('Claim denied', 'OK', {duration: 3000});
}, () => {
this.isLoading = false;
this.snackBar.open('An error occurred while claim denying', 'OK');
});
}
}

View File

@ -0,0 +1,16 @@
import { PartyModificationUnit } from './party-modification-unit';
export class ClaimInfoContainer {
status: string;
partyId: string;
claimId: number;
revision: string;
createdAt: string;
updatedAt: string;
reason: string;
partyModificationUnits: PartyModificationUnit[];
extractedIds: {
shopId: string,
contractId: string
};
}

View File

@ -0,0 +1,9 @@
export enum ContractModificationName {
creation = 'creation',
termination = 'termination',
adjustmentModification = 'adjustmentModification',
payoutToolModification = 'payoutToolModification',
legalAgreementBinding = 'legalAgreementBinding',
reportPreferencesModification = 'reportPreferencesModification',
unknown = 'unknown'
}

View File

@ -0,0 +1,5 @@
export class DomainModificationInfo {
shopUrl: string;
shopId: string;
partyId: string;
}

View File

@ -0,0 +1,8 @@
export * from './claim-info-container';
export * from './contract-modification-name';
export * from './party-modification-container';
export * from './party-modification-unit-type';
export * from './party-modification-unit';
export * from './shop-modification-name';
export * from './party-modification-container-type';
export * from './domain-modification-info';

View File

@ -0,0 +1,4 @@
export enum PartyModificationContainerType {
ContractModification = 'ContractModification',
ShopModification = 'ShopModification'
}

View File

@ -0,0 +1,10 @@
import { ContractModificationName } from './contract-modification-name';
import { ShopModificationName } from './shop-modification-name';
import { ShopModificationUnit, ContractModificationUnit } from '../../damsel';
import { PartyModificationContainerType } from './party-modification-container-type';
export class PartyModificationContainer {
type: PartyModificationContainerType;
name: ContractModificationName | ShopModificationName;
modifications: ContractModificationUnit[] | ShopModificationUnit[];
}

View File

@ -0,0 +1,5 @@
export enum PartyModificationUnitType {
ShopModification = 'ShopModification',
ContractModification = 'ContractModification',
unknown = 'unknown'
}

View File

@ -0,0 +1,7 @@
import { PartyModificationUnitType } from './party-modification-unit-type';
import { PartyModificationContainer } from './party-modification-container';
export class PartyModificationUnit {
type: PartyModificationUnitType;
containers: PartyModificationContainer[];
}

View File

@ -0,0 +1,11 @@
export enum ShopModificationName {
creation = 'creation',
categoryModification = 'categoryModification',
detailsModification = 'detailsModification',
contractModification = 'contractModification',
payoutToolModification = 'payoutToolModification',
locationModification = 'locationModification',
shopAccountCreation = 'shopAccountCreation',
payoutScheduleModification = 'payoutScheduleModification',
unknown = 'unknown'
}

View File

@ -0,0 +1,136 @@
import groupBy from 'lodash-es/groupBy';
import reduce from 'lodash-es/reduce';
import map from 'lodash-es/map';
import {
PartyModification,
ShopModificationUnit,
ContractModificationUnit
} from '../damsel';
import {
PartyModificationUnitType,
PartyModificationContainer,
PartyModificationUnit,
ContractModificationName,
ShopModificationName,
PartyModificationContainerType
} from './model';
export class PartyModificationContainerConverter {
static convert(modificationUnits: PartyModification[]): PartyModificationUnit[] {
const grouped = groupBy(modificationUnits, (item: PartyModification) => {
const {shopModification, contractModification} = item;
if (shopModification) {
return PartyModificationUnitType.ShopModification;
}
if (contractModification) {
return PartyModificationUnitType.ContractModification;
}
return PartyModificationUnitType.unknown;
});
return reduce(grouped, (result, group, type) => {
switch (type) {
case PartyModificationUnitType.ContractModification:
result.push({type, ...this.resolveContractContainers(group)});
break;
case PartyModificationUnitType.ShopModification:
result.push({type, ...this.resolveShopContainers(group)});
break;
}
return result;
}, []);
}
private static resolveContractContainers(contracts: PartyModification[]) {
const modifications = contracts.map((modification) => modification.contractModification);
return {containers: this.toContractContainers(modifications)};
}
private static resolveShopContainers(shops: PartyModification[]) {
const modifications = shops.map((modification) => modification.shopModification);
return {containers: this.toShopContainer(modifications)};
}
private static toContractContainers(modification: ContractModificationUnit[]): PartyModificationContainer[] {
const grouped = groupBy(modification, (item: ContractModificationUnit) => {
const {
creation,
termination,
adjustmentModification,
payoutToolModification,
legalAgreementBinding,
reportPreferencesModification,
} = item.modification;
if (creation) {
return ContractModificationName.creation;
}
if (termination) {
return ContractModificationName.termination;
}
if (adjustmentModification) {
return ContractModificationName.adjustmentModification;
}
if (payoutToolModification) {
return ContractModificationName.payoutToolModification;
}
if (legalAgreementBinding) {
return ContractModificationName.legalAgreementBinding;
}
if (reportPreferencesModification) {
return ContractModificationName.reportPreferencesModification;
}
return ContractModificationName.unknown;
});
return map(grouped, (modifications, name: ContractModificationName) => ({
type: PartyModificationContainerType.ContractModification,
name,
modifications
}));
}
private static toShopContainer(modification: ShopModificationUnit[]): PartyModificationContainer[] {
const grouped = groupBy(modification, (item: ShopModificationUnit) => {
const {
creation,
categoryModification,
detailsModification,
contractModification,
payoutToolModification,
locationModification,
shopAccountCreation,
payoutScheduleModification
} = item.modification;
if (creation) {
return ShopModificationName.creation;
}
if (categoryModification) {
return ShopModificationName.categoryModification;
}
if (detailsModification) {
return ShopModificationName.detailsModification;
}
if (contractModification) {
return ShopModificationName.contractModification;
}
if (payoutToolModification) {
return ShopModificationName.payoutToolModification;
}
if (locationModification) {
return ShopModificationName.locationModification;
}
if (shopAccountCreation) {
return ShopModificationName.shopAccountCreation;
}
if (payoutScheduleModification) {
return ShopModificationName.payoutScheduleModification;
}
return ShopModificationName.unknown;
});
return map(grouped, (modifications, name: ShopModificationName) => ({
type: PartyModificationContainerType.ShopModification,
name,
modifications
}));
}
}

View File

@ -0,0 +1,10 @@
<mat-card>
<mat-card-subtitle>{{container.name | ccContainerName:container.type }}</mat-card-subtitle>
<mat-card-content>
<mat-tab-group>
<mat-tab *ngFor="let unit of modifications" label="Modification unit">
<pre [innerHtml]="unit | ccThriftEncode | prettyjson:2"></pre>
</mat-tab>
</mat-tab-group>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,48 @@
import { Component, Input, OnInit } from '@angular/core';
import { PartyModificationContainer } from '../model';
import { ContractModificationUnit, ShopModificationUnit } from '../../damsel';
@Component({
selector: 'cc-party-modification-container',
templateUrl: 'party-modification-container.component.html',
styles: [`
:host /deep/ .string {
color: #008000;
font-weight: bold;
}
:host /deep/ .number {
color: #0000FF;
font-weight: bold;
}
:host /deep/ .boolean {
color: #000080;
font-weight: bold;
}
:host /deep/ .null {
color: magenta;
font-weight: bold;
}
:host /deep/ .key {
color: #660E7A;
font-weight: bold;
}
`]
})
export class PartyModificationContainerComponent implements OnInit {
@Input()
container: PartyModificationContainer;
modifications: ContractModificationUnit[] | ShopModificationUnit[];
ngOnInit() {
this.modifications = this.container.modifications
.slice()
.reverse();
}
}

View File

@ -0,0 +1,34 @@
<div fxLayout="row" fxLayout.md="column" fxLayout.sm="column" fxLayout.xs="column" fxLayoutGap="20px">
<div *ngIf="shopUnit" fxFlex="50" fxLayout="column" fxLayoutGap="20px">
<mat-card>
<mat-card-subtitle fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="10px"
fxLayoutAlign="space-between stretch">
<div>Shop modifications</div>
<div>Shop ID: {{extractedIds.shopId}}</div>
</mat-card-subtitle>
</mat-card>
<cc-party-modification-container
*ngFor="let container of shopUnit.containers"
[container]="container">
</cc-party-modification-container>
</div>
<div *ngIf="contractUnit" fxFlex="50" fxLayout="column" fxLayoutGap="20px">
<mat-card>
<mat-card-subtitle fxLayout="row"
fxLayout.sm="column"
fxLayout.xs="column"
fxLayoutGap="10px"
fxLayoutAlign="space-between stretch">
<div>Contract modifications</div>
<div>Contract ID: {{extractedIds.contractId}}</div>
</mat-card-subtitle>
</mat-card>
<cc-party-modification-container
*ngFor="let container of contractUnit.containers"
[container]="container">
</cc-party-modification-container>
</div>
</div>

View File

@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { ClaimService } from '../claim.service';
import { PartyModificationUnit, PartyModificationUnitType } from '../model';
@Component({
selector: 'cc-party-modifications',
templateUrl: 'party-modifications.component.html'
})
export class PartyModificationsComponent implements OnInit {
shopUnit: PartyModificationUnit;
contractUnit: PartyModificationUnit;
extractedIds: {
shopId: string;
contractId: string;
};
constructor(private claimService: ClaimService,
private snackBar: MatSnackBar) {
}
ngOnInit() {
this.claimService.claimInfoContainer$.subscribe((container) => {
if (!container) {
return;
}
this.extractedIds = container.extractedIds;
const units = container.partyModificationUnits;
for (const unit of units) {
switch (unit.type) {
case PartyModificationUnitType.ShopModification:
this.shopUnit = unit;
break;
case PartyModificationUnitType.ContractModification:
this.contractUnit = unit;
break;
case PartyModificationUnitType.unknown:
this.snackBar.open('Detected unknown party modification unit', 'OK');
break;
}
}
});
}
}

View File

@ -0,0 +1,21 @@
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable()
export class ClaimsAuthGuardService extends KeycloakAuthGuard {
constructor(protected router: Router, protected keycloakAngular: KeycloakService) {
super(router, keycloakAngular);
}
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Promise((resolve) => {
if (!this.roles || this.roles.length === 0) {
resolve(false);
}
const granted = this.roles.includes('claim:get');
resolve(granted);
});
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ClaimsComponent } from './claims.component';
import { ClaimsAuthGuardService } from './claims-auth-guard.service';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'claims',
component: ClaimsComponent,
canActivate: [ClaimsAuthGuardService]
}
])
],
exports: [
RouterModule
],
providers: [
ClaimsAuthGuardService
]
})
export class ClaimsRoutingModule {}

View File

@ -0,0 +1,7 @@
table {
width: 100%;
}
.action-cell {
width: 10px;
}

View File

@ -0,0 +1,36 @@
<table mat-table [dataSource]="claims" class="mat-elevation-z2">
<ng-container matColumnDef="partyID">
<th mat-header-cell *matHeaderCellDef>Party ID</th>
<td mat-cell *matCellDef="let claim">{{ claim.partyId }}</td>
</ng-container>
<ng-container matColumnDef="claimID">
<th fxHide.sm fxHide.xs mat-header-cell *matHeaderCellDef>Claim ID</th>
<td fxHide.sm fxHide.xs mat-cell *matCellDef="let claim">{{ claim.claimId }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let claim">{{ claim.status }}</td>
</ng-container>
<ng-container matColumnDef="revision">
<th fxHide.sm fxHide.xs mat-header-cell *matHeaderCellDef>Revision</th>
<td fxHide.sm fxHide.xs mat-cell *matCellDef="let claim">{{ claim.revision }}</td>
</ng-container>
<ng-container matColumnDef="createdAt">
<th fxHide.xs mat-header-cell *matHeaderCellDef>Created at</th>
<td fxHide.xs mat-cell *matCellDef="let claim">{{ claim.createdAt | date: "dd.MM.yyyy HH:mm:ss" }}</td>
</ng-container>
<ng-container matColumnDef="updatedAt">
<th fxHide.sm fxHide.xs mat-header-cell *matHeaderCellDef>Updated at</th>
<td fxHide.sm fxHide.xs mat-cell *matCellDef="let claim">{{ claim.updatedAt | date: "dd.MM.yyyy HH:mm:ss" }}</td>
</ng-container>
<ng-container matColumnDef="claimDetailButton">
<th mat-header-cell *matHeaderCellDef class="action-cell"></th>
<td mat-cell *matCellDef="let claim;" class="action-cell">
<button mat-icon-button (click)="navigateToClaim(claim)">
<mat-icon>more_vert</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let claim; columns: displayedColumns;"></tr>
</table>

View File

@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { ClaimInfo } from '../../papi/model';
@Component({
selector: 'cc-claims-table',
templateUrl: 'claims-table.component.html',
styleUrls: ['./claims-table.component.css']
})
export class ClaimsTableComponent {
@Input()
claims: ClaimInfo[];
displayedColumns = [
'partyID',
'claimID',
'status',
'revision',
'createdAt',
'updatedAt',
'claimDetailButton'
];
constructor(private router: Router) {}
navigateToClaim(claim: ClaimInfo) {
this.router.navigate([`/claims/${claim.partyId}/${claim.claimId}`]);
}
}

View File

@ -0,0 +1,16 @@
<div class="container">
<div class="card-container" fxLayout="column" fxLayoutGap="20px">
<mat-card>
<mat-card-subtitle>
Search claims
</mat-card-subtitle>
<mat-card-content>
<cc-search-form (valueChanges)="search($event)"></cc-search-form>
</mat-card-content>
<mat-card-footer *ngIf="isLoading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<cc-claims-table [claims]="claims"></cc-claims-table>
</div>
</div>

View File

@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { HttpErrorResponse } from '@angular/common/http';
import { ClaimService } from '../papi/claim.service';
import { ClaimSearchParams } from '../papi/params';
import { ClaimInfo } from '../papi/model';
@Component({
templateUrl: 'claims.component.html',
styleUrls: ['../shared/container.css'],
})
export class ClaimsComponent implements OnInit {
isLoading = false;
claims: ClaimInfo[];
constructor(private claimService: ClaimService,
private snackBar: MatSnackBar) {
}
ngOnInit() {
this.search({claimStatus: 'pending'});
}
search(params: ClaimSearchParams) {
this.isLoading = true;
this.claimService.getClaims(params).subscribe((claims) => {
this.isLoading = false;
this.claims = claims.reverse();
}, (error: HttpErrorResponse) => {
this.isLoading = false;
this.snackBar.open(`${error.status}: ${error.message}`, 'OK');
});
}
}

View File

@ -0,0 +1,51 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatProgressBarModule,
MatSelectModule,
MatSnackBarModule,
MatTableModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { CdkTableModule } from '@angular/cdk/table';
import { ClaimsComponent } from './claims.component';
import { ClaimsRoutingModule } from './claims-routing.module';
import { PapiModule } from '../papi/papi.module';
import { SearchFormComponent } from './search-form/search-form.component';
import { ClaimsTableComponent } from './claims-table/claims-table.component';
@NgModule({
imports: [
CommonModule,
ClaimsRoutingModule,
FlexLayoutModule,
PapiModule,
ReactiveFormsModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatProgressBarModule,
MatSnackBarModule,
MatTableModule,
MatButtonModule,
MatIconModule,
MatMenuModule,
CdkTableModule
],
declarations: [
ClaimsComponent,
SearchFormComponent,
ClaimsTableComponent
]
})
export class ClaimsModule {
}

View File

@ -0,0 +1,11 @@
<form fxLayout="row" fxLayout.xs="column" fxLayoutGap="20px" [formGroup]="form">
<mat-form-field fxFlex="15" fxFlex.sm="30">
<mat-select placeholder="Claim status" formControlName="claimStatus">
<mat-option [value]="null">any</mat-option>
<mat-option *ngFor="let status of claimStatuses" [value]="status">{{status}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="30" fxFlex.sm="70">
<input matInput placeholder="Party ID" formControlName="partyId">
</mat-form-field>
</form>

View File

@ -0,0 +1,33 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { SearchFormService } from './search-form.service';
import { ClaimSearchParams } from '../../papi/params';
import { debounceTime } from 'rxjs/internal/operators';
@Component({
selector: 'cc-search-form',
templateUrl: 'search-form.component.html',
providers: [SearchFormService]
})
export class SearchFormComponent implements OnInit {
@Output()
valueChanges: EventEmitter<ClaimSearchParams> = new EventEmitter();
form: FormGroup;
claimStatuses: string[];
constructor(private searchFormService: SearchFormService) {}
ngOnInit() {
const {claimStatuses, form, formValueToSearchParams} = this.searchFormService;
this.claimStatuses = claimStatuses;
this.form = form;
this.form.valueChanges
.pipe(debounceTime(300))
.subscribe((value) =>
this.valueChanges.emit(formValueToSearchParams(value)));
}
}

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import values from 'lodash-es/values';
import mapValues from 'lodash-es/mapValues';
import isString from 'lodash-es/isString';
import { ClaimStatus } from '../../papi/model/claim-statuses';
import { ClaimSearchParams } from '../../papi/params';
@Injectable()
export class SearchFormService {
form: FormGroup;
claimStatuses: string[];
constructor(private fb: FormBuilder) {
this.form = this.prepareForm();
this.claimStatuses = values(ClaimStatus);
}
formValueToSearchParams(formValue): ClaimSearchParams {
return mapValues(formValue, (value) => {
let result = value;
if (value === '') {
result = null;
} else if (isString(value)) {
result = value.trim();
}
return result;
});
}
private prepareForm(): FormGroup {
return this.fb.group({
claimStatus: 'pending',
partyId: ''
});
}
}

View File

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface AppConfig {
papiEndpoint: string;
}
@Injectable()
export class ConfigService {
config: AppConfig;
constructor(private http: HttpClient) {}
load() {
return new Promise((resolve) => {
this.http
.get<AppConfig>('assets/appConfig.json')
.subscribe((config) => {
this.config = config;
resolve();
});
});
}
}

View File

@ -0,0 +1,39 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { ConfigService } from './config.service';
const initializer = (keycloak: KeycloakService, configService: ConfigService) =>
() => Promise.all([
configService.load(),
keycloak.init({
config: '/assets/authConfig.json',
initOptions: {
onLoad: 'login-required',
checkLoginIframe: true
},
enableBearerInterceptor: true,
bearerExcludedUrls: [
'/assets'
],
bearerPrefix: 'Bearer'
})
]);
@NgModule({
imports: [
CommonModule,
KeycloakAngularModule
],
providers: [
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: initializer,
multi: true,
deps: [KeycloakService, ConfigService]
}
]
})
export class CoreModule {}

View File

@ -0,0 +1,5 @@
import { Operation } from './operation';
export class Commit {
ops: Operation[];
}

View File

@ -0,0 +1,2 @@
// tslint:disable-next-line:interface-over-type-literal
export type Head = {};

View File

@ -0,0 +1,9 @@
export * from './snapshot';
export * from './version';
export * from './commit';
export * from './insert-op';
export * from './operation';
export * from './remove-op';
export * from './update-op';
export * from './head';
export * from './reference';

View File

@ -0,0 +1,5 @@
import { DomainObject } from '../domain';
export class InsertOp {
object: DomainObject;
}

View File

@ -0,0 +1,9 @@
import { InsertOp } from './insert-op';
import { UpdateOp } from './update-op';
import { RemoveOp } from './remove-op';
export class Operation {
insert?: InsertOp;
update?: UpdateOp;
remove?: RemoveOp;
}

View File

@ -0,0 +1,7 @@
import { Version } from './version';
import { Head } from './head';
export class Reference {
version?: Version;
head?: Head;
}

View File

@ -0,0 +1,5 @@
import { DomainObject } from '../domain';
export class RemoveOp {
object: DomainObject;
}

View File

@ -0,0 +1,7 @@
import { Domain } from '../domain';
import { Version } from './version';
export class Snapshot {
version: Version;
domain: Domain;
}

View File

@ -0,0 +1,6 @@
import { DomainObject } from '../domain';
export class UpdateOp {
oldObject: DomainObject;
newObject: DomainObject;
}

View File

@ -0,0 +1 @@
export type Version = number;

View File

@ -0,0 +1,6 @@
import { AbstractDomainRef } from './abstract-domain-ref';
export class AbstractDomainObject {
ref: AbstractDomainRef;
data: any;
}

Some files were not shown because too many files have changed in this diff Show More