TD-364: Add chargebacks list, details, actions. Create claim. Fix searching payments (#122)

This commit is contained in:
Rinat Arsaev 2022-08-12 20:11:58 +03:00 committed by GitHub
parent 058b7147fd
commit ba22e86d20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 921 additions and 214 deletions

View File

@ -3,4 +3,7 @@ package-lock.json
node_modules
dist
src/assets/icons/
.angular
.angular
.github/settings.*
.github/workflows/basic-*

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="App Dev Server" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

5
.run/Debug.run.xml Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Debug" type="JavascriptDebugType" uri="http://localhost:4200">
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Libs Dev Server" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev-libs" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

7
.run/Start.run.xml Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start" type="CompoundRunConfigurationType">
<toRun name="App Dev Server" type="js.build_tools.npm" />
<toRun name="Libs Dev Server" type="js.build_tools.npm" />
<method v="2" />
</configuration>
</component>

View File

@ -14,7 +14,7 @@ export class BaseDialogService {
private dialog: MatDialog,
@Optional()
@Inject(DIALOG_CONFIG)
private dialogConfig: DialogConfig
private readonly dialogConfig: DialogConfig
) {
if (!dialogConfig) this.dialogConfig = DEFAULT_DIALOG_CONFIG;
}
@ -31,7 +31,7 @@ export class BaseDialogService {
: [data: D, configOrConfigName?: Omit<MatDialogConfig<D>, 'data'> | keyof DialogConfig]
): MatDialogRef<C, BaseDialogResponse<R, S>> {
let config: Partial<MatDialogConfig<D>>;
if (!configOrConfigName) config = this.dialogConfig.medium;
if (!configOrConfigName) config = {};
else if (typeof configOrConfigName === 'string')
config = this.dialogConfig[configOrConfigName];
else config = configOrConfigName;

View File

@ -16,5 +16,5 @@ export const BASE_CONFIG: ValuesType<DialogConfig> = {
export const DEFAULT_DIALOG_CONFIG: DialogConfig = {
small: { ...BASE_CONFIG, width: '360px' },
medium: BASE_CONFIG,
large: { ...BASE_CONFIG, width: '648px' },
large: { ...BASE_CONFIG, width: '800px' },
};

View File

@ -1,2 +1,2 @@
export * from './components';
export * from './utils/objects';
export * from './utils';

View File

@ -0,0 +1,45 @@
import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { ValuesType } from 'utility-types';
function isEmptyPrimitive(value: unknown): boolean {
return isNil(value) || value === '';
}
function isEmptyObjectOrPrimitive(value: unknown): boolean {
return isObject(value) ? isEmpty(value) : isEmptyPrimitive(value);
}
export function clean<T>(
value: T,
allowRootRemoval = false,
isNotDeep = false,
filterPredicate: (v: unknown, k?: PropertyKey) => boolean = (v) => !isEmptyObjectOrPrimitive(v)
): T | null {
if (!isObject(value)) return value;
if (allowRootRemoval && !filterPredicate(value as never)) return null;
let result: unknown;
const cleanChild = (v: unknown) =>
isNotDeep ? v : clean(v as never, allowRootRemoval, isNotDeep, filterPredicate);
if (Array.isArray(value))
result = (value as ValuesType<T>[])
.slice()
.map((v) => cleanChild(v))
.filter((v, idx) => filterPredicate(v, idx));
else
result = Object.fromEntries(
(Object.entries(value) as [keyof T, ValuesType<T>][])
.map(([k, v]) => [k, cleanChild(v)] as const)
.filter(([k, v]) => filterPredicate(v, k))
);
return allowRootRemoval && !filterPredicate(result) ? null : (result as never);
}
export function cleanPrimitiveProps<T extends object>(
obj: T,
allowRootRemoval = false,
isNotDeep = false
) {
return clean(obj, allowRootRemoval, isNotDeep, (v) => !isEmptyPrimitive(v));
}

View File

@ -0,0 +1 @@
export * from './clean';

View File

@ -1,26 +0,0 @@
import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { ValuesType } from 'utility-types';
function isEmptyValue(value: unknown): boolean {
return isNil(value) || value === '' || (typeof value === 'object' && isEmpty(value));
}
export function cleanObject<T extends object>(
obj: T,
requiredKeys: (keyof T)[] = [],
isNotDeep = false
): T {
if (!isObject(obj)) return obj;
if (Array.isArray(obj))
return obj
.slice()
.map((v: unknown) => (isObject(v) && !isNotDeep ? cleanObject(v) : v))
.filter((v) => !isEmptyValue(v)) as T;
return Object.fromEntries(
(Object.entries(obj) as [keyof T, ValuesType<T>][])
.map(([k, v]) => [k, isObject(v) && !isNotDeep ? cleanObject(v as object) : v] as const)
.filter(([k, v]) => requiredKeys.includes(k) || !isEmptyValue(v))
) as T;
}

View File

@ -1 +0,0 @@
export * from './clean-object';

View File

@ -12,5 +12,10 @@
"noUnusedParameters": true,
"noImplicitAny": true
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"strictTemplates": true
},
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

View File

@ -1 +1,2 @@
export * from './party-management.service';
export * from './invoicing.service';

View File

@ -0,0 +1,22 @@
import { Injectable, Injector } from '@angular/core';
import {
codegenClientConfig,
CodegenClient,
} from '@vality/domain-proto/lib/payment_processing-Invoicing';
import context from '@vality/domain-proto/lib/payment_processing/context';
import * as service from '@vality/domain-proto/lib/payment_processing/gen-nodejs/Invoicing';
import { createThriftApi } from '@cc/app/api/utils';
@Injectable({ providedIn: 'root' })
export class InvoicingService extends createThriftApi<CodegenClient>() {
constructor(injector: Injector) {
super(injector, {
service,
path: '/v1/processing/invoicing',
metadata: () => import('@vality/domain-proto/lib/metadata.json').then((m) => m.default),
context,
...codegenClientConfig,
});
}
}

View File

@ -47,7 +47,7 @@ import {
/**
* For use in specific locations (for example, questionary PDF document)
*/
moment.locale('en');
moment.locale('en-GB');
@NgModule({
declarations: [AppComponent],

View File

@ -2,10 +2,10 @@ import { Component, Injector, Input, OnChanges } from '@angular/core';
import { Validator } from '@angular/forms';
import { Claim } from '@vality/domain-proto/lib/claim_management';
import { Party } from '@vality/domain-proto/lib/domain';
import { from, Observable } from 'rxjs';
import { from, combineLatest, ReplaySubject, defer } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentChanges, MetadataFormExtension } from '@cc/app/shared';
import { ComponentChanges } from '@cc/app/shared';
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services/domain-metadata-form-extensions';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
@ -25,7 +25,14 @@ export class ModificationFormComponent
@Input() type: string;
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$: Observable<MetadataFormExtension[]>;
extensions$ = combineLatest([
defer(() => this.claimOrPartyChanged$).pipe(
map(() => createPartyClaimMetadataFormExtensions(this.party, this.claim))
),
this.domainMetadataFormExtensionsService.extensions$,
]).pipe(map((extensionGroups) => extensionGroups.flat()));
private claimOrPartyChanged$ = new ReplaySubject<void>(1);
constructor(
injector: Injector,
@ -37,12 +44,7 @@ export class ModificationFormComponent
ngOnChanges(changes: ComponentChanges<ModificationFormComponent>) {
super.ngOnChanges(changes);
if (changes.party || changes.claim) {
this.extensions$ = this.domainMetadataFormExtensionsService.extensions$.pipe(
map((e) => [
...createPartyClaimMetadataFormExtensions(this.party, this.claim),
...e,
])
);
this.claimOrPartyChanged$.next();
}
}
}

View File

@ -109,9 +109,5 @@ export function createPartyClaimMetadataFormExtensions(
isIdentifier: true,
}),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'ID', 'base')),
extension: () => of({ generate, isIdentifier: true }),
},
];
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { StatPayment } from '@vality/magista-proto';
import { cleanObject } from '@vality/ng-core';
import { cleanPrimitiveProps, clean } from '@vality/ng-core';
import { Observable, of, Subject } from 'rxjs';
import { mergeMap, shareReplay } from 'rxjs/operators';
@ -54,21 +54,21 @@ export class PaymentAdjustmentService {
terminalID,
} = params;
return this.merchantStatisticsService.SearchPayments(
cleanObject({
common_search_query_params: {
cleanPrimitiveProps({
common_search_query_params: clean({
from_time: fromTime,
to_time: toTime,
party_id: partyId,
shop_ids: [shopId],
continuation_token: continuationToken,
},
payment_params: {
}),
payment_params: clean({
from_payment_domain_revision: fromRevision,
to_payment_domain_revision: toRevision,
payment_provider_id: providerID,
payment_terminal_id: terminalID,
payment_status: status,
},
}),
invoice_ids: invoiceIds,
})
);

View File

@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
fromTime: [moment(), Validators.required],
toTime: [moment(), Validators.required],
invoiceIds: '',
partyId: ['', Validators.required],
partyId: '',
shopId: '',
fromRevision: [0, Validators.required],
toRevision: ['', Validators.required],

View File

@ -0,0 +1,15 @@
<cc-base-dialog title="Create chargeback">
<cc-metadata-form
[extensions]="extensions$ | async"
[formControl]="form"
[metadata]="metadata$ | async"
namespace="payment_processing"
type="InvoicePaymentChargebackParams"
></cc-metadata-form>
<cc-base-dialog-actions>
<button [disabled]="form.invalid" color="primary" mat-raised-button (click)="create()">
CREATE
</button>
</cc-base-dialog-actions>
</cc-base-dialog>

View File

@ -0,0 +1,58 @@
import { Component, Injector } from '@angular/core';
import { FormControl } from '@angular/forms';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { InvoicePaymentChargeback } from '@vality/domain-proto';
import { InvoicePaymentChargebackParams } from '@vality/domain-proto/lib/payment_processing';
import { BaseDialogSuperclass } from '@vality/ng-core';
import { from } from 'rxjs';
import uuid from 'uuid';
import { InvoicingService } from '@cc/app/api/payment-processing';
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services';
import { ErrorService } from '@cc/app/shared/services/error';
import { NotificationService } from '@cc/app/shared/services/notification';
@UntilDestroy()
@Component({
selector: 'cc-create-chargeback-dialog',
templateUrl: './create-chargeback-dialog.component.html',
})
export class CreateChargebackDialogComponent extends BaseDialogSuperclass<
CreateChargebackDialogComponent,
{ invoiceID: string; paymentID: string },
InvoicePaymentChargeback
> {
form = new FormControl<Partial<InvoicePaymentChargebackParams>>({ id: uuid() });
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
constructor(
injector: Injector,
private invoicingService: InvoicingService,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private errorService: ErrorService,
private notificationService: NotificationService
) {
super(injector);
}
create() {
this.invoicingService
.CreateChargeback(
this.dialogData.invoiceID,
this.dialogData.paymentID,
this.form.value as InvoicePaymentChargebackParams
)
.pipe(untilDestroyed(this))
.subscribe({
next: (res) => {
this.notificationService.success('Chargeback created');
this.closeWithSuccess(res);
},
error: (err) => {
this.errorService.error(err);
this.notificationService.error();
},
});
}
}

View File

@ -9,9 +9,10 @@
></cc-payment-main-info>
</mat-card-content>
</mat-card>
<h2 class="cc-headline">Refunds</h2>
<mat-card>
<mat-card-content>
<h2 class="cc-headline">Refunds</h2>
<cc-payment-refunds
[invoiceID]="payment.invoice_id"
[partyID]="payment.owner_id"
@ -19,6 +20,22 @@
></cc-payment-refunds>
</mat-card-content>
</mat-card>
<div fxLayout fxLayoutAlign="space-between">
<h2 class="cc-headline">Chargebacks</h2>
<cc-actions>
<button color="primary" mat-button (click)="createChargeback()">
CREATE CHARGEBACK
</button>
</cc-actions>
</div>
<mat-card>
<cc-chargebacks
[chargebacks]="chargebacks$ | async"
[invoiceId]="payment.invoice_id"
[paymentId]="payment.id"
></cc-chargebacks>
</mat-card>
</ng-container>
<div *ngIf="isLoading$ | async" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>

View File

@ -1,9 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { pluck } from 'rxjs/operators';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { BaseDialogService, BaseDialogResponseStatus } from '@vality/ng-core';
import { Subject, merge, defer } from 'rxjs';
import { pluck, shareReplay, switchMap, map } from 'rxjs/operators';
import { InvoicingService } from '../../api/payment-processing/invoicing.service';
import { CreateChargebackDialogComponent } from './create-chargeback-dialog/create-chargeback-dialog.component';
import { PaymentDetailsService } from './payment-details.service';
@UntilDestroy()
@Component({
templateUrl: 'payment-details.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
@ -15,8 +21,37 @@ export class PaymentDetailsComponent {
isLoading$ = this.paymentDetailsService.isLoading$;
shop$ = this.paymentDetailsService.shop$;
chargebacks$ = merge(
this.route.params,
defer(() => this.updateChargebacks$)
).pipe(
map(() => this.route.snapshot.params as Record<'invoiceID' | 'paymentID', string>),
switchMap(({ invoiceID, paymentID }) =>
this.invoicingService.GetPayment(invoiceID, paymentID)
),
map(({ chargebacks }) => chargebacks),
shareReplay({ refCount: true, bufferSize: 1 })
);
private updateChargebacks$ = new Subject<void>();
constructor(
private paymentDetailsService: PaymentDetailsService,
private route: ActivatedRoute
private route: ActivatedRoute,
private invoicingService: InvoicingService,
private baseDialogService: BaseDialogService
) {}
createChargeback() {
this.baseDialogService
.open(
CreateChargebackDialogComponent,
this.route.snapshot.params as Record<'invoiceID' | 'paymentID', string>
)
.afterClosed()
.pipe(untilDestroyed(this))
.subscribe(({ status }) => {
if (status === BaseDialogResponseStatus.Success) this.updateChargebacks$.next();
});
}
}

View File

@ -1,15 +1,19 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ActionsModule, BaseDialogModule } from '@vality/ng-core';
import { StatusModule } from '@cc/app/shared/components';
import { StatusModule, MetadataFormModule } from '@cc/app/shared/components';
import { DetailsItemModule } from '@cc/components/details-item';
import { HeadlineModule } from '@cc/components/headline';
import { ChargebacksComponent } from '../../shared/components/chargebacks/chargebacks.component';
import { CreateChargebackDialogComponent } from './create-chargeback-dialog/create-chargeback-dialog.component';
import { PaymentDetailsRoutingModule } from './payment-details-routing.module';
import { PaymentDetailsComponent } from './payment-details.component';
import { PaymentMainInfoModule } from './payment-main-info';
@ -31,7 +35,12 @@ import { PaymentRefundsModule } from './payment-refunds';
MatButtonModule,
MatDialogModule,
PaymentRefundsModule,
ChargebacksComponent,
ActionsModule,
BaseDialogModule,
MetadataFormModule,
ReactiveFormsModule,
],
declarations: [PaymentDetailsComponent],
declarations: [PaymentDetailsComponent, CreateChargebackDialogComponent],
})
export class PaymentDetailsModule {}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { cleanObject } from '@vality/ng-core';
import { cleanPrimitiveProps } from '@vality/ng-core';
import { combineLatest, of } from 'rxjs';
import { map, pluck, shareReplay, switchMap, tap } from 'rxjs/operators';
@ -20,7 +20,7 @@ export class PaymentDetailsService {
switchMap(({ partyID, invoiceID, paymentID }) =>
this.merchantStatisticsService
.SearchPayments(
cleanObject({
cleanPrimitiveProps({
common_search_query_params: {
from_time: new Date('2020-01-01').toISOString(), // TODO
to_time: new Date().toISOString(),

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { StatRefund, RefundSearchQuery } from '@vality/magista-proto';
import { cleanObject } from '@vality/ng-core';
import { cleanPrimitiveProps } from '@vality/ng-core';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { DeepPartial } from 'utility-types';
@ -28,7 +28,7 @@ export class FetchRefundsService extends PartialFetcher<
): Observable<FetchResult<StatRefund>> {
return this.merchantStatisticsService
.SearchRefunds(
cleanObject({
cleanPrimitiveProps({
...params,
common_search_query_params: Object.assign(
{

View File

@ -16,8 +16,7 @@
<ng-container matColumnDef="amount">
<th *matHeaderCellDef class="cc-caption" mat-header-cell>Amount</th>
<td *matCellDef="let refund" mat-cell>
{{ refund.amount | ccFormatAmount }}
{{ refund.currency_symbolic_code | ccCurrency }}
{{ refund.amount | amountCurrency: refund.currency_symbolic_code }}
</td>
</ng-container>

View File

@ -5,7 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { StatusModule } from '@cc/app/shared/components/status';
import { CommonPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
import { CommonPipesModule, ThriftPipesModule, AmountCurrencyPipe } from '@cc/app/shared/pipes';
import { RefundsTableComponent } from './refunds-table.component';
@ -18,6 +18,7 @@ import { RefundsTableComponent } from './refunds-table.component';
StatusModule,
ThriftPipesModule,
CommonPipesModule,
AmountCurrencyPipe,
],
declarations: [RefundsTableComponent],
exports: [RefundsTableComponent],

View File

@ -0,0 +1,13 @@
<cc-base-dialog title="Create claim">
<cc-merchant-field [formControl]="control"></cc-merchant-field>
<cc-base-dialog-actions>
<button
[disabled]="control.invalid || !!(progress$ | async)"
color="primary"
mat-raised-button
(click)="create()"
>
CREATE
</button>
</cc-base-dialog-actions>
</cc-base-dialog>

View File

@ -0,0 +1,52 @@
import { Component, Injector } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { BaseDialogSuperclass } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs';
import { ClaimManagementService } from '@cc/app/api/claim-management';
import { ErrorService } from '@cc/app/shared/services/error';
import { NotificationService } from '@cc/app/shared/services/notification';
import { progressTo } from '@cc/utils';
@UntilDestroy()
@Component({
selector: 'cc-create-claim-dialog',
templateUrl: './create-claim-dialog.component.html',
})
export class CreateClaimDialogComponent extends BaseDialogSuperclass<
CreateClaimDialogComponent,
{ partyId: string }
> {
control = new FormControl(this.dialogData.partyId, Validators.required);
progress$ = new BehaviorSubject(0);
constructor(
injector: Injector,
private claimService: ClaimManagementService,
private notificationService: NotificationService,
private errorService: ErrorService,
private router: Router
) {
super(injector);
}
create() {
this.claimService
.CreateClaim(this.dialogData.partyId, [])
.pipe(progressTo(this.progress$), untilDestroyed(this))
.subscribe({
next: (claim) => {
this.notificationService.success('Claim successfully created');
this.closeWithSuccess();
void this.router.navigate([
`party/${this.dialogData.partyId}/claim/${claim.id}`,
]);
},
error: (err) => {
this.errorService.error(err, 'An error occurred while claim creation');
},
});
}
}

View File

@ -1,5 +1,10 @@
<div class="search-claims-container" fxLayout="column" fxLayoutGap="32px">
<h1 class="cc-display-1">Claims</h1>
<div fxLayout fxLayoutAlign="space-between">
<h1 class="cc-display-1">Claims</h1>
<cc-actions>
<button color="primary" mat-raised-button (click)="create()">CREATE</button>
</cc-actions>
</div>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content>
@ -13,15 +18,12 @@
<cc-empty-search-result *ngIf="claims.length === 0"></cc-empty-search-result>
<mat-card *ngIf="claims.length > 0" fxLayout="column" fxLayoutGap="18px">
<cc-search-table [claims]="claims"></cc-search-table>
<button
<cc-show-more-button
*ngIf="hasMore$ | async"
[disabled]="doAction$ | async"
fxFlex="100"
mat-button
[inProgress]="doAction$ | async"
(click)="fetchMore()"
>
{{ (doAction$ | async) ? 'LOADING...' : 'SHOW MORE' }}
</button>
</cc-show-more-button>
</mat-card>
</ng-container>
</div>

View File

@ -1,8 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PartyID } from '@vality/domain-proto';
import { BaseDialogService, cleanPrimitiveProps, clean } from '@vality/ng-core';
import { ClaimSearchForm } from '@cc/app/shared/components';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { SearchClaimsService } from './search-claims.service';
@Component({
@ -11,10 +14,15 @@ import { SearchClaimsService } from './search-claims.service';
})
export class SearchClaimsComponent implements OnInit {
doAction$ = this.searchClaimService.doAction$;
claims$ = this.searchClaimService.claims$;
claims$ = this.searchClaimService.searchResult$;
hasMore$ = this.searchClaimService.hasMore$;
private selectedPartyId: PartyID;
constructor(private searchClaimService: SearchClaimsService, private snackBar: MatSnackBar) {}
constructor(
private searchClaimService: SearchClaimsService,
private snackBar: MatSnackBar,
private baseDialogService: BaseDialogService
) {}
ngOnInit(): void {
this.searchClaimService.errors$.subscribe((e) =>
@ -23,10 +31,17 @@ export class SearchClaimsComponent implements OnInit {
}
search(v: ClaimSearchForm): void {
this.searchClaimService.search(v);
this.selectedPartyId = v?.party_id;
this.searchClaimService.search(
cleanPrimitiveProps({ ...v, statuses: clean(v.statuses?.map((s) => ({ [s]: {} }))) })
);
}
fetchMore(): void {
this.searchClaimService.fetchMore();
}
create() {
this.baseDialogService.open(CreateClaimDialogComponent, { partyId: this.selectedPartyId });
}
}

View File

@ -14,12 +14,16 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { ActionsModule, BaseDialogModule } from '@vality/ng-core';
import { ClaimSearchFormModule } from '@cc/app/shared/components';
import { MerchantFieldModule } from '@cc/app/shared/components/merchant-field';
import { ApiModelPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { TableModule } from '@cc/components/table';
import { ClaimManagementService } from '../../thrift-services/damsel/claim-management.service';
import { CreateClaimDialogComponent } from './components/create-claim-dialog/create-claim-dialog.component';
import { SearchClaimsComponentRouting } from './search-claims-routing.module';
import { SearchClaimsComponent } from './search-claims.component';
import { SearchClaimsService } from './search-claims.service';
@ -48,8 +52,17 @@ import { SearchTableComponent } from './search-table/search-table.component';
EmptySearchResultModule,
ApiModelPipesModule,
ThriftPipesModule,
TableModule,
ActionsModule,
BaseDialogModule,
MerchantFieldModule,
],
declarations: [
SearchClaimsComponent,
SearchTableComponent,
ClaimMailPipePipe,
CreateClaimDialogComponent,
],
declarations: [SearchClaimsComponent, SearchTableComponent, ClaimMailPipePipe],
providers: [SearchClaimsService, ClaimManagementService],
})
export class SearchClaimsModule {}

View File

@ -1,28 +1,16 @@
import { Injectable } from '@angular/core';
import { PartyID } from '@vality/domain-proto';
import {
Claim,
ClaimID,
ClaimSearchQuery,
ClaimStatus,
} from '@vality/domain-proto/lib/claim_management';
import { Claim, ClaimSearchQuery } from '@vality/domain-proto/lib/claim_management';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ClaimManagementService } from '@cc/app/api/claim-management';
import { FetchResult, PartialFetcher } from '@cc/app/shared/services';
import { ClaimManagementService } from '../../thrift-services/damsel/claim-management.service';
type SearchClaimsParams = {
claim_id?: ClaimID;
statuses?: (keyof ClaimStatus)[];
party_id: PartyID;
};
@Injectable()
export class SearchClaimsService extends PartialFetcher<Claim, SearchClaimsParams> {
claims$: Observable<Claim[]> = this.searchResult$;
export class SearchClaimsService extends PartialFetcher<
Claim,
Omit<ClaimSearchQuery, 'continuation_token' | 'limit'>
> {
private readonly searchLimit = 10;
constructor(private claimManagementService: ClaimManagementService) {
@ -30,11 +18,11 @@ export class SearchClaimsService extends PartialFetcher<Claim, SearchClaimsParam
}
protected fetch(
params: SearchClaimsParams,
params: Omit<ClaimSearchQuery, 'continuation_token' | 'limit'>,
continuationToken: string
): Observable<FetchResult<Claim>> {
return this.claimManagementService
.searchClaims({
.SearchClaims({
...params,
continuation_token: continuationToken,
limit: this.searchLimit,

View File

@ -0,0 +1,31 @@
<cc-base-dialog title="Change chargeback">
<div gdColumn="1fr" gdGap="16px">
<mat-form-field style="width: 100%">
<mat-label>Action</mat-label>
<mat-select [formControl]="actionControl">
<mat-option *ngFor="let k of typeEnum | enumKeys" [value]="typeEnum[k]">{{
k
}}</mat-option>
</mat-select>
</mat-form-field>
<cc-metadata-form
[extensions]="extensions$ | async"
[formControl]="control"
[metadata]="metadata$ | async"
[type]="types[actionControl.value]"
namespace="payment_processing"
></cc-metadata-form>
</div>
<cc-base-dialog-actions>
<button
[disabled]="control.invalid || actionControl.invalid || !!(progress$ | async)"
color="primary"
mat-raised-button
(click)="confirm()"
>
{{ (actionControl.value | enumKey: typeEnum) || 'CHANGE' | uppercase }}
</button>
</cc-base-dialog-actions>
</cc-base-dialog>

View File

@ -0,0 +1,112 @@
import { CommonModule } from '@angular/common';
import { Component, Injector, OnInit } from '@angular/core';
import { GridModule } from '@angular/flex-layout';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { BaseDialogSuperclass, BaseDialogModule, BaseDialogService } from '@vality/ng-core';
import { from, BehaviorSubject, Observable } from 'rxjs';
import { InvoicingService } from '@cc/app/api/payment-processing';
import { MetadataFormModule, EnumKeysPipe, EnumKeyPipe } from '@cc/app/shared';
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services';
import { ErrorService } from '@cc/app/shared/services/error';
import { NotificationService } from '@cc/app/shared/services/notification';
import { progressTo } from '@cc/utils';
enum Action {
Accept,
Reject,
Reopen,
Cancel,
}
@UntilDestroy()
@Component({
standalone: true,
selector: 'cc-change-chargeback-status-dialog',
templateUrl: './change-chargeback-status-dialog.component.html',
imports: [
CommonModule,
BaseDialogModule,
MatButtonModule,
MetadataFormModule,
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
EnumKeysPipe,
GridModule,
EnumKeyPipe,
],
})
export class ChangeChargebackStatusDialogComponent
extends BaseDialogSuperclass<
ChangeChargebackStatusDialogComponent,
{ id: string; paymentId: string; invoiceId: string }
>
implements OnInit
{
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
control = new FormControl();
actionControl = new FormControl<Action>(null, Validators.required);
typeEnum = Action;
types = {
[Action.Accept]: 'InvoicePaymentChargebackAcceptParams',
[Action.Reject]: 'InvoicePaymentChargebackRejectParams',
[Action.Reopen]: 'InvoicePaymentChargebackReopenParams',
[Action.Cancel]: 'InvoicePaymentChargebackCancelParams',
};
progress$ = new BehaviorSubject(0);
constructor(
injector: Injector,
private invoicingService: InvoicingService,
private baseDialogService: BaseDialogService,
private notificationService: NotificationService,
private errorService: ErrorService,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService
) {
super(injector);
}
ngOnInit() {
this.actionControl.valueChanges.pipe(untilDestroyed(this)).subscribe(() => {
this.control.reset();
});
}
confirm() {
let action$: Observable<void>;
const args = [
this.dialogData.invoiceId,
this.dialogData.paymentId,
this.dialogData.id,
this.control.value,
] as const;
switch (this.actionControl.value) {
case Action.Accept:
action$ = this.invoicingService.AcceptChargeback(...args);
break;
case Action.Reject:
action$ = this.invoicingService.RejectChargeback(...args);
break;
case Action.Reopen:
action$ = this.invoicingService.ReopenChargeback(...args);
break;
case Action.Cancel:
action$ = this.invoicingService.CancelChargeback(...args);
break;
}
action$.pipe(progressTo(this.progress$), untilDestroyed(this)).subscribe({
next: () => {
this.notificationService.success();
},
error: (err) => {
this.errorService.error(err);
},
});
}
}

View File

@ -0,0 +1,49 @@
<table [dataSource]="chargebacks" mat-table style="min-width: 100%">
<ng-container [matColumnDef]="cols.def.id">
<th *matHeaderCellDef mat-header-cell>ID</th>
<td *matCellDef="let c" mat-cell>{{ c.chargeback.id }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.status">
<th *matHeaderCellDef mat-header-cell>Status</th>
<td *matCellDef="let c" mat-cell>{{ c.chargeback.status | ccUnionKey }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.created_at">
<th *matHeaderCellDef mat-header-cell>Created At</th>
<td *matCellDef="let c" mat-cell>
{{ c.chargeback.created_at | date: 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.body">
<th *matHeaderCellDef mat-header-cell>Body</th>
<td *matCellDef="let c" mat-cell>
{{
c.chargeback.body.amount | amountCurrency: c.chargeback.body.currency.symbolic_code
}}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.levy">
<th *matHeaderCellDef mat-header-cell>Levy</th>
<td *matCellDef="let c" mat-cell>
{{
c.chargeback.levy.amount | amountCurrency: c.chargeback.levy.currency.symbolic_code
}}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.stage">
<th *matHeaderCellDef mat-header-cell>Stage</th>
<td *matCellDef="let c" mat-cell>{{ c.chargeback.stage | ccUnionKey }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.actions">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let c" mat-cell style="width: 0">
<cc-menu-cell>
<button mat-menu-item (click)="showDetails(c)">Details</button>
<button mat-menu-item (click)="changeStatus(c.chargeback.id)">Change</button>
</cc-menu-cell>
</td>
</ng-container>
<cc-no-data-row></cc-no-data-row>
<tr *matHeaderRowDef="cols.list" mat-header-row></tr>
<tr *matRowDef="let row; columns: cols.list" mat-row></tr>
</table>

View File

@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table';
import { UntilDestroy } from '@ngneat/until-destroy';
import { InvoicePaymentChargeback } from '@vality/magista-proto/lib/payment_processing';
import { BaseDialogService } from '@vality/ng-core';
import { InvoicingService } from '@cc/app/api/payment-processing';
import { AmountCurrencyPipe, ThriftPipesModule } from '@cc/app/shared';
import { DetailsDialogComponent } from '@cc/app/shared/components/details-dialog/details-dialog.component';
import { TableModule, Columns } from '@cc/components/table';
import { ChangeChargebackStatusDialogComponent } from '../change-chargeback-status-dialog/change-chargeback-status-dialog.component';
@UntilDestroy()
@Component({
standalone: true,
selector: 'cc-chargebacks',
templateUrl: './chargebacks.component.html',
imports: [
MatTableModule,
TableModule,
ThriftPipesModule,
CommonModule,
AmountCurrencyPipe,
MatMenuModule,
],
})
export class ChargebacksComponent {
@Input() chargebacks: InvoicePaymentChargeback[];
@Input() paymentId: string;
@Input() invoiceId: string;
cols = new Columns('id', 'status', 'created_at', 'body', 'levy', 'stage', 'actions');
constructor(
private invoicingService: InvoicingService,
private baseDialogService: BaseDialogService
) {}
changeStatus(id: string) {
this.baseDialogService.open(ChangeChargebackStatusDialogComponent, {
paymentId: this.paymentId,
invoiceId: this.invoiceId,
id,
});
}
showDetails(chargeback: InvoicePaymentChargeback) {
this.baseDialogService.open(DetailsDialogComponent, {
title: 'Chargeback details',
json: chargeback,
});
}
}

View File

@ -9,15 +9,14 @@ import {
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ClaimStatus } from '@vality/domain-proto/lib/claim_management';
import { coerceBoolean } from 'coerce-property';
import { debounceTime, map, take } from 'rxjs/operators';
import { queryParamsToFormValue } from '@cc/app/shared/components/claim-search-form/query-params-to-form-value';
import { CLAIM_STATUSES } from '@cc/app/api/claim-management';
import { removeEmptyProperties } from '@cc/utils/remove-empty-properties';
import { ClaimSearchForm } from './claim-search-form';
import { formValueToSearchParams } from './form-value-to-search-params';
import { queryParamsToFormValue } from './query-params-to-form-value';
@UntilDestroy()
@Component({
@ -35,14 +34,7 @@ export class ClaimSearchFormComponent implements OnInit {
party_id: null,
});
claimStatuses: (keyof ClaimStatus)[] = [
'pending',
'review',
'accepted',
'denied',
'revoked',
'pending_acceptance',
];
claimStatuses = CLAIM_STATUSES;
constructor(private route: ActivatedRoute, private router: Router, private fb: FormBuilder) {}
@ -51,7 +43,7 @@ export class ClaimSearchFormComponent implements OnInit {
.pipe(debounceTime(600), map(removeEmptyProperties), untilDestroyed(this))
.subscribe((value) => {
void this.router.navigate([location.pathname], { queryParams: value });
this.valueChanges.emit(formValueToSearchParams(value));
this.valueChanges.emit(value as never);
});
this.route.queryParams
.pipe(

View File

@ -1,10 +1,8 @@
import { PartyID } from '@vality/domain-proto';
import { ClaimID } from '@vality/domain-proto/lib/claim_management';
import { ClaimStatus } from '@cc/app/api/claim-management';
import { ClaimID, ClaimStatus } from '@vality/domain-proto/lib/claim_management';
export interface ClaimSearchForm {
claim_id: ClaimID;
statuses: ClaimStatus[];
statuses: (keyof ClaimStatus)[];
party_id: PartyID;
}

View File

@ -1,14 +0,0 @@
import pick from 'lodash-es/pick';
import pickBy from 'lodash-es/pickBy';
import { isNumeric } from '@cc/utils/is-numeric';
import { mapValuesToNumber } from '@cc/utils/map-values-to-number';
import { mapValuesToThriftEnum } from '@cc/utils/map-values-to-thrift-enum';
import { ClaimSearchForm } from './claim-search-form';
export const formValueToSearchParams = (params: any): ClaimSearchForm => ({
...params,
...mapValuesToThriftEnum(pick(params, 'statuses')),
...mapValuesToNumber(pickBy(params, isNumeric)),
});

View File

@ -0,0 +1,3 @@
<cc-base-dialog [title]="dialogData.title || 'Details'" noActions>
<cc-json-viewer [json]="dialogData.json"></cc-json-viewer>
</cc-base-dialog>

View File

@ -0,0 +1,17 @@
import { Component } from '@angular/core';
import { BaseDialogSuperclass, BaseDialogModule, DEFAULT_DIALOG_CONFIG } from '@vality/ng-core';
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer';
@Component({
standalone: true,
selector: 'cc-details-dialog',
templateUrl: './details-dialog.component.html',
imports: [BaseDialogModule, JsonViewerModule],
})
export class DetailsDialogComponent extends BaseDialogSuperclass<
DetailsDialogComponent,
{ title?: string; json: unknown }
> {
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
}

View File

@ -3,15 +3,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { PartyID } from '@vality/domain-proto';
import { coerceBoolean } from 'coerce-property';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { catchError, debounceTime, filter, first, map, switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject, merge } from 'rxjs';
import { catchError, debounceTime, filter, map, switchMap, first, takeUntil } from 'rxjs/operators';
import { DeanonimusService } from '@cc/app/thrift-services/deanonimus';
import { Option } from '@cc/components/select-search-field';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { progressTo } from '@cc/utils/operators';
import { createControlProviders, ValidatedFormControlSuperclass } from '../../../../utils';
import { DeanonimusService } from '../../../thrift-services/deanonimus';
@UntilDestroy()
@Component({
selector: 'cc-merchant-field',
@ -38,8 +37,10 @@ export class MerchantFieldComponent
}
ngOnInit() {
this.control.valueChanges.pipe(first()).subscribe((v) => this.searchChange$.next(v));
this.searchChange$
merge(
this.searchChange$,
this.control.valueChanges.pipe(filter(Boolean), first(), takeUntil(this.searchChange$))
)
.pipe(
filter(Boolean),
debounceTime(600),

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { StatPayment } from '@vality/magista-proto';
import { cleanObject } from '@vality/ng-core';
import { cleanPrimitiveProps, clean } from '@vality/ng-core';
import * as moment from 'moment';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@ -14,8 +14,6 @@ const SEARCH_LIMIT = 10;
@Injectable()
export class FetchPaymentsService extends PartialFetcher<StatPayment, SearchFiltersParams> {
isLoading$ = this.doAction$;
constructor(private merchantStatisticsService: MerchantStatisticsService) {
super();
}
@ -47,36 +45,33 @@ export class FetchPaymentsService extends PartialFetcher<StatPayment, SearchFilt
} = params;
return this.merchantStatisticsService
.SearchPayments(
cleanObject(
{
common_search_query_params: {
from_time: moment(fromTime).utc().format(),
to_time: moment(toTime).utc().format(),
limit: SEARCH_LIMIT,
continuation_token: continuationToken,
party_id: partyID,
shop_ids: shopIDs,
},
invoice_ids: [invoiceID],
payment_params: {
payment_status: paymentStatus,
payment_tool: paymentMethod,
payment_email: payerEmail,
payment_first6: bin,
payment_system: { id: paymentSystem },
payment_last4: pan,
payment_provider_id: providerID,
payment_terminal_id: terminalID,
from_payment_domain_revision: domainRevisionFrom,
to_payment_domain_revision: domainRevisionTo,
payment_rrn: rrn,
payment_amount_from: paymentAmountFrom,
payment_amount_to: paymentAmountTo,
payment_token_provider: { id: tokenProvider },
},
},
['common_search_query_params', 'payment_params']
)
cleanPrimitiveProps({
common_search_query_params: clean({
from_time: moment(fromTime).utc().format(),
to_time: moment(toTime).utc().format(),
limit: SEARCH_LIMIT,
continuation_token: continuationToken,
party_id: partyID,
shop_ids: shopIDs,
}),
invoice_ids: clean([invoiceID], true),
payment_params: clean({
payment_status: paymentStatus,
payment_tool: paymentMethod,
payment_email: payerEmail,
payment_first6: bin,
payment_system: { id: paymentSystem },
payment_last4: pan,
payment_provider_id: providerID,
payment_terminal_id: terminalID,
from_payment_domain_revision: domainRevisionFrom,
to_payment_domain_revision: domainRevisionTo,
payment_rrn: rrn,
payment_amount_from: paymentAmountFrom,
payment_amount_to: paymentAmountTo,
payment_token_provider: { id: tokenProvider },
}),
})
)
.pipe(
map(({ payments, continuation_token }) => ({

View File

@ -14,16 +14,11 @@
(valueChanges)="searchParamsChanges($event)"
></cc-payments-other-search-filters>
</cc-actions>
<!-- TODO: Remove params (and params?.partyID validation) when backend starts working without merchant -->
<cc-empty-search-result
*ngIf="(!(isLoading$ | async) && (payments$ | async)?.length === 0) || !params?.partyID"
[label]="params?.partyID ? 'Payments not found' : 'Merchant input field is required'"
*ngIf="!(doAction$ | async) && !(payments$ | async)?.length"
label="Payments not found"
></cc-empty-search-result>
<mat-card
*ngIf="(payments$ | async)?.length > 0 && params?.partyID"
fxLayout="column"
fxLayoutGap="16px"
>
<mat-card *ngIf="(payments$ | async)?.length > 0" fxLayout="column" fxLayoutGap="16px">
<cc-payments-table
[payments]="payments$ | async"
(menuItemSelected$)="paymentMenuItemSelected($event)"

View File

@ -26,7 +26,6 @@ export class PaymentsSearcherComponent implements OnInit {
@Output() searchParamsChanged$: EventEmitter<SearchFiltersParams> = new EventEmitter();
@Output() paymentEventFired$: EventEmitter<PaymentMenuItemEvent> = new EventEmitter();
isLoading$ = this.fetchPaymentsService.isLoading$;
doAction$ = this.fetchPaymentsService.doAction$;
payments$ = this.fetchPaymentsService.searchResult$;
hasMore$ = this.fetchPaymentsService.hasMore$;
@ -41,10 +40,7 @@ export class PaymentsSearcherComponent implements OnInit {
.pipe(untilDestroyed(this))
.subscribe((params) => {
this.params = params;
// TODO: the partyID is optional, but the backend returns 500
if (params.partyID) {
this.fetchPaymentsService.search(params);
}
this.fetchPaymentsService.search(params);
this.searchParamsChanged$.emit(params);
});
}

View File

@ -0,0 +1,62 @@
import { formatCurrency, getCurrencySymbol } from '@angular/common';
import { Pipe, Inject, LOCALE_ID, DEFAULT_CURRENCY_CODE, PipeTransform } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import round from 'lodash-es/round';
import { ReplaySubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { DomainStoreService } from '../../thrift-services/damsel/domain-store.service';
@UntilDestroy()
@Pipe({
standalone: true,
pure: false,
name: 'amountCurrency',
})
export class AmountCurrencyPipe implements PipeTransform {
private params$ = new ReplaySubject<{
amount: number;
currencyCode: string;
format: 'short' | 'long';
}>(1);
private latestValue: string = '';
private isInit = false;
constructor(
@Inject(LOCALE_ID) private _locale: string,
@Inject(DEFAULT_CURRENCY_CODE) private _defaultCurrencyCode: string = 'USD',
private domainStoreService: DomainStoreService
) {}
init() {
this.isInit = true;
combineLatest([this.domainStoreService.getObjects('currency'), this.params$])
.pipe(
map(([currencies, { amount, currencyCode, format }]) => {
const exponent = currencies.find((c) => c.data.symbolic_code === currencyCode)
.data.exponent;
return formatCurrency(
round(amount / 10 ** exponent, exponent),
this._locale,
getCurrencySymbol(currencyCode, 'narrow', this._locale),
currencyCode,
format === 'short' ? '0.0-2' : undefined
);
}),
untilDestroyed(this)
)
.subscribe((value) => {
this.latestValue = value;
});
}
transform(
amount: number,
currencyCode: string = this._defaultCurrencyCode,
format: 'short' | 'long' = 'long'
) {
this.params$.next({ amount, currencyCode, format });
if (!this.isInit) this.init();
return this.latestValue;
}
}

View File

@ -0,0 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core';
import isNil from 'lodash-es/isNil';
import { ValuesType } from 'utility-types';
import { getEnumKey } from '@cc/utils';
@Pipe({
standalone: true,
name: 'enumKey',
})
export class EnumKeyPipe implements PipeTransform {
transform<E extends Record<PropertyKey, unknown>>(value: ValuesType<E>, enumObj: E): keyof E {
return isNil(value) ? '' : getEnumKey(enumObj, value);
}
}

View File

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import { getEnumKeys } from '@cc/utils';
@Pipe({
standalone: true,
name: 'enumKeys',
})
export class EnumKeysPipe implements PipeTransform {
transform<E extends Record<PropertyKey, unknown>>(value: E): (keyof E)[] {
return getEnumKeys(value);
}
}

View File

@ -2,3 +2,6 @@ export * from './thrift';
export * from './common';
export * from './api-model-types';
export * from './value-type-title';
export * from './amount-currency.pipe';
export * from './enum-keys.pipe';
export * from './enum-key.pipe';

View File

@ -1,13 +1,18 @@
import { Injectable } from '@angular/core';
import { DomainObject } from '@vality/domain-proto/lib/domain';
import { Field } from '@vality/thrift-ts';
import { from, Observable } from 'rxjs';
import { from, Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import uuid from 'uuid';
import { ThriftAstMetadata } from '@cc/app/api/utils';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { MetadataFormData, MetadataFormExtension } from '../../components/metadata-form';
import {
MetadataFormData,
MetadataFormExtension,
isTypeWithAliases,
} from '../../components/metadata-form';
import { createDomainObjectExtension } from './utils/create-domain-object-extension';
import {
defaultDomainObjectToOption,
@ -24,7 +29,13 @@ export class DomainMetadataFormExtensionsService {
(m) => m.default as never as ThriftAstMetadata[]
)
).pipe(
map((metadata) => this.createDomainObjectsOptions(metadata)),
map((metadata) => [
...this.createDomainObjectsOptions(metadata),
{
determinant: (data) => of(isTypeWithAliases(data, 'ID', 'base')),
extension: () => of({ generate: () => of(uuid()), isIdentifier: true }),
},
]),
shareReplay(1)
);

View File

@ -17,10 +17,10 @@ import { switchMap } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../services/thrift/thrift-service';
@Injectable()
/**
* @deprecated use api/ClaimManagement service
*/
@Injectable()
export class ClaimManagementService extends ThriftService {
constructor(zone: NgZone, keycloakTokenInfoService: KeycloakTokenInfoService) {
super(zone, keycloakTokenInfoService, '/v1/cm', ClaimManagement);

View File

@ -15,4 +15,7 @@ import { RoutingRulesModule } from './routing-rules';
ClaimManagementService,
],
})
/**
* @deprecated
*/
export class DamselModule {}

View File

@ -24,6 +24,9 @@ import { ThriftService } from '../services/thrift/thrift-service';
import { createDamselInstance, damselInstanceToObject } from './utils/create-damsel-instance';
@Injectable()
/**
* @deprecated
*/
export class PaymentProcessingService extends ThriftService {
constructor(zone: NgZone, keycloakTokenInfoService: KeycloakTokenInfoService) {
super(zone, keycloakTokenInfoService, '/v1/processing/invoicing', Invoicing);

View File

@ -1,6 +1,8 @@
<cc-base-dialog [title]="title || 'Confirm this action'" noContent>
<cc-base-dialog [title]="dialogData?.['title'] ?? 'Confirm this action'" noContent>
<cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button color="primary" mat-raised-button (click)="confirm()">CONFIRM</button>
<button color="primary" mat-raised-button (click)="confirm()">
{{ dialogData?.['confirmLabel'] || 'CONFIRM' }}
</button>
</cc-base-dialog-actions>
</cc-base-dialog>

View File

@ -8,12 +8,8 @@ import { BaseDialogResponseStatus, BaseDialogSuperclass } from '@vality/ng-core'
})
export class ConfirmActionDialogComponent extends BaseDialogSuperclass<
ConfirmActionDialogComponent,
{ title?: string } | void
{ title?: string; confirmLabel?: string } | void
> {
get title() {
return typeof this.dialogData === 'object' ? this.dialogData.title : '';
}
cancel() {
this.dialogRef.close({ status: BaseDialogResponseStatus.Cancelled });
}

View File

@ -42,9 +42,12 @@
</ng-template>
</mat-select>
<button *ngIf="selected$ | async" mat-icon-button matSuffix (click)="clear($event)">
<ng-container *ngIf="svgIcon; else defaultIcon">
<mat-icon [svgIcon]="svgIcon"></mat-icon>
</ng-container>
<ng-template #defaultIcon><mat-icon>close</mat-icon></ng-template>
<mat-icon *ngIf="!options?.length; else clearIcon">sync</mat-icon>
<ng-template #clearIcon>
<ng-container *ngIf="svgIcon; else defaultIcon">
<mat-icon [svgIcon]="svgIcon"></mat-icon>
</ng-container>
<ng-template #defaultIcon><mat-icon>close</mat-icon></ng-template>
</ng-template>
</button>
</mat-form-field>

View File

@ -46,10 +46,10 @@ export class SelectSearchFieldComponent<Value>
@Output() searchChange = new EventEmitter<string>();
selectSearchControl = new FormControl<string>('');
filteredOptions$: Observable<Option<Value>[]> = combineLatest(
filteredOptions$: Observable<Option<Value>[]> = combineLatest([
getFormValueChanges(this.selectSearchControl),
defer(() => this.options$)
).pipe(map(([value, options]) => filterOptions(options, value)));
defer(() => this.options$),
]).pipe(map(([value, options]) => filterOptions(options, value)));
selected$ = new BehaviorSubject<Value>(null);
cachedOption: Option<Value> = null;

View File

@ -1,2 +1,3 @@
export * from './table.module';
export * from './select-column/select-column.component';
export * from './types/columns';

View File

@ -0,0 +1,6 @@
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu>
<ng-content></ng-content>
</mat-menu>

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'cc-menu-cell',
templateUrl: './menu-cell.component.html',
})
export class MenuCellComponent {}

View File

@ -0,0 +1,5 @@
<tr *matNoDataRow class="mat-row">
<td class="mat-cell cc-headline cc-secondary-text" colspan="9999" style="text-align: center">
No data
</td>
</tr>

View File

@ -0,0 +1,27 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, ViewChild, Optional, OnInit, OnDestroy } from '@angular/core';
import { MatTable, MatNoDataRow } from '@angular/material/table';
@Component({
selector: 'cc-no-data-row',
templateUrl: './no-data-row.component.html',
})
export class NoDataRowComponent<T> implements OnInit, OnDestroy {
@ViewChild(MatNoDataRow, { static: true }) matNoDataRow!: MatNoDataRow;
selection = new SelectionModel(true, []);
constructor(@Optional() public table: MatTable<T>) {}
ngOnInit(): void {
if (this.table) {
this.table.setNoDataRow(this.matNoDataRow);
}
}
ngOnDestroy(): void {
if (this.table) {
this.table.setNoDataRow(null);
}
}
}

View File

@ -3,9 +3,14 @@ import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MenuCellComponent } from '@cc/components/table/menu-cell/menu-cell.component';
import { NoDataRowComponent } from './no-data-row/no-data-row.component';
import { SelectColumnComponent } from './select-column/select-column.component';
import { ShowMoreButtonComponent } from './show-more-button/show-more-button.component';
@ -17,8 +22,20 @@ import { ShowMoreButtonComponent } from './show-more-button/show-more-button.com
MatButtonModule,
FlexModule,
MatSortModule,
MatMenuModule,
MatIconModule,
],
declarations: [
SelectColumnComponent,
ShowMoreButtonComponent,
MenuCellComponent,
NoDataRowComponent,
],
exports: [
SelectColumnComponent,
ShowMoreButtonComponent,
MenuCellComponent,
NoDataRowComponent,
],
declarations: [SelectColumnComponent, ShowMoreButtonComponent],
exports: [SelectColumnComponent, ShowMoreButtonComponent],
})
export class TableModule {}

View File

@ -0,0 +1,9 @@
export class Columns<T extends readonly string[]> {
list: T;
def: { [N in T[number]]: N };
constructor(...list: T) {
this.list = list;
this.def = Object.fromEntries(list.map((k) => [k, k])) as never;
}
}

View File

@ -6,6 +6,13 @@ export function getEnumKeyValues<E extends Record<PropertyKey, unknown>>(srcEnum
.map(([value, key]) => ({ key, value })) as { key: keyof E; value: ValuesType<E> }[];
}
export function getEnumKey<E extends Record<PropertyKey, unknown>>(
srcEnum: E,
value: ValuesType<E>
): keyof E {
return getEnumKeyValues(srcEnum).find((e) => e.value === String(value)).key;
}
export function getEnumKeys<E extends Record<PropertyKey, unknown>>(srcEnum: E): (keyof E)[] {
return Object.values(srcEnum).filter((v) => typeof v === 'string') as string[];
}

View File

@ -1,9 +1,6 @@
export * from './to-optional';
export * from './get-union-key';
export * from './remove-empty-properties';
export * from './map-values-to-thrift-enum';
export * from './map-values-to-number';
export * from './is-numeric';
export * from './wrap-values-to-array';
export * from './get-or';
export * from './skip-null-values';

View File

@ -1,2 +0,0 @@
export const isNumeric = (x): boolean =>
(typeof x === 'number' || typeof x === 'string') && !isNaN(Number(x));

View File

@ -1,4 +0,0 @@
import toNumber from 'lodash-es/toNumber';
export const mapValuesToNumber = (obj: any): any =>
Object.entries(obj).reduce((acc, [k, v]) => ({ ...acc, [k]: toNumber(v) }), {});

View File

@ -1,6 +0,0 @@
// Thrift enum ex: [{ enumVal: {} }, ...]
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return
const toThriftEnum = (arr: string[]) => arr.reduce((acc, cv) => [...acc, { [cv]: {} }], []);
export const mapValuesToThriftEnum = (obj: any): any =>
Object.entries(obj).reduce((acc, [k, v]) => ({ ...acc, [k]: toThriftEnum(v as string[]) }), {});