TD-373: Load invoices and refunds in payments (#83)

This commit is contained in:
Rinat Arsaev 2022-08-16 17:56:18 +03:00 committed by GitHub
parent f0a1714760
commit 418b418cad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 68 additions and 46 deletions

View File

@ -1,11 +1,9 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Invoice, PaymentSearchResult } from '@vality/swag-anapi-v2'; import { Invoice, PaymentSearchResult } from '@vality/swag-anapi-v2';
import isEmpty from 'lodash-es/isEmpty'; import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ComponentChange, ComponentChanges } from '@dsh/type-utils'; import { ComponentChanges } from '@dsh/type-utils';
import { PaymentIds } from '../../types'; import { PaymentIds } from '../../types';
import { InvoiceDetailsService } from './services/invoice-details/invoice-details.service'; import { InvoiceDetailsService } from './services/invoice-details/invoice-details.service';
@ -15,10 +13,11 @@ import { isPaymentFlowHold } from './types/is-payment-flow-hold';
selector: 'dsh-payment-details', selector: 'dsh-payment-details',
templateUrl: 'payment-details.component.html', templateUrl: 'payment-details.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [InvoiceDetailsService],
}) })
export class PaymentDetailsComponent implements OnChanges { export class PaymentDetailsComponent implements OnChanges {
@Input() payment: PaymentSearchResult; @Input() payment: PaymentSearchResult;
@Output() refreshPayment: EventEmitter<PaymentIds> = new EventEmitter(); @Output() refreshPayment = new EventEmitter<PaymentIds>();
get isHoldShown(): boolean { get isHoldShown(): boolean {
if (isPaymentFlowHold(this.payment.flow)) { if (isPaymentFlowHold(this.payment.flow)) {
@ -31,9 +30,9 @@ export class PaymentDetailsComponent implements OnChanges {
constructor(private invoiceDetails: InvoiceDetailsService) {} constructor(private invoiceDetails: InvoiceDetailsService) {}
ngOnChanges(changes: ComponentChanges<PaymentDetailsComponent>): void { ngOnChanges({ payment }: ComponentChanges<PaymentDetailsComponent>): void {
if (isObject(changes.payment)) { if (payment && payment.currentValue) {
this.changeInvoiceID(changes.payment); this.changeInvoiceID(payment.currentValue);
} }
} }
@ -41,10 +40,7 @@ export class PaymentDetailsComponent implements OnChanges {
this.refreshPayment.emit(ids); this.refreshPayment.emit(ids);
} }
private changeInvoiceID({ currentValue: payment }: ComponentChange<PaymentDetailsComponent, 'payment'>): void { private changeInvoiceID(payment: PaymentSearchResult): void {
if (isNil(payment)) {
return;
}
this.invoiceDetails.setInvoiceID(payment.invoiceID); this.invoiceDetails.setInvoiceID(payment.invoiceID);
} }
} }

View File

@ -1,21 +1,23 @@
<ng-container *transloco="let t; scope: 'payment-section'; read: 'paymentSection.paymentDetails.invoiceDetails'"> <ng-container *transloco="let t; scope: 'payment-section'; read: 'paymentSection.paymentDetails.invoiceDetails'">
<div *ngIf="invoice; else loading" fxLayout="column" fxLayoutGap="24px"> <div fxLayout="column" fxLayoutGap="24px">
<div class="payment-invoice-info-title mat-title" fxLayout fxLayoutAlign="space-between center"> <div class="payment-invoice-info-title mat-title" fxLayout fxLayoutAlign="space-between center">
<div> <div>
{{ t('title') }} {{ t('title') }}
<span dshSecondaryTitle>#{{ invoice.id }}</span> <span *ngIf="invoice">#{{ invoice.id }}</span>
</div> </div>
</div> </div>
<dsh-invoice-details [invoice]="invoice"></dsh-invoice-details> <ng-container *ngIf="!invoice; else details">
<div *ngIf="!isLoading; else loading" class="dsh-body-1">
{{ t('notFound') }}
</div>
<ng-template #loading>
<div *transloco="let c; scope: 'components'; read: 'components.shared'">
<div class="dsh-body-1">{{ c('loading') }}</div>
</div>
</ng-template>
</ng-container>
<ng-template #details>
<dsh-invoice-details [invoice]="invoice"></dsh-invoice-details>
</ng-template>
</div> </div>
<ng-template #loading>
<div class="payment-invoice-info-title mat-title" fxLayout fxLayoutAlign="space-between center">
<div>
{{ t('title') }}
</div>
</div>
<div *transloco="let c; scope: 'components'; read: 'components.shared'">
<div class="dsh-body-1">{{ c('loading') }}</div>
</div>
</ng-template>
</ng-container> </ng-container>

View File

@ -10,4 +10,8 @@ import { Invoice } from '@vality/swag-payments';
// TODO: implement dump component for this + shared one for operations // TODO: implement dump component for this + shared one for operations
export class PaymentInvoiceInfoComponent { export class PaymentInvoiceInfoComponent {
@Input() invoice: Invoice; @Input() invoice: Invoice;
get isLoading() {
return this.invoice === undefined;
}
} }

View File

@ -11,7 +11,6 @@ import { PaymentDetailsComponent } from './payment-details.component';
import { PaymentInvoiceInfoModule } from './payment-invoice-info'; import { PaymentInvoiceInfoModule } from './payment-invoice-info';
import { PaymentMainInfoModule } from './payment-main-info'; import { PaymentMainInfoModule } from './payment-main-info';
import { RefundsModule } from './refunds'; import { RefundsModule } from './refunds';
import { InvoiceDetailsService } from './services/invoice-details/invoice-details.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -27,6 +26,5 @@ import { InvoiceDetailsService } from './services/invoice-details/invoice-detail
], ],
declarations: [PaymentDetailsComponent], declarations: [PaymentDetailsComponent],
exports: [PaymentDetailsComponent], exports: [PaymentDetailsComponent],
providers: [InvoiceDetailsService],
}) })
export class PaymentsDetailsModule {} export class PaymentsDetailsModule {}

View File

@ -7,7 +7,7 @@
<div fxLayout fxLayoutGap="16px" fxLayoutAlign="space-between"> <div fxLayout fxLayoutGap="16px" fxLayoutAlign="space-between">
<div> <div>
{{ t('header') }} {{ t('header') }}
<span dshSecondaryTitle>#{{ item.id }}</span> <span>#{{ item.id }}</span>
</div> </div>
</div> </div>
<dsh-refund-details [refund]="item"></dsh-refund-details> <dsh-refund-details [refund]="item"></dsh-refund-details>

View File

@ -1,13 +1,15 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { SearchRefundsRequestParams, RefundSearchResult } from '@vality/swag-anapi-v2'; import { SearchRefundsRequestParams, RefundSearchResult } from '@vality/swag-anapi-v2';
import moment from 'moment'; import moment from 'moment';
import { Observable } from 'rxjs'; import { Observable, switchMap } from 'rxjs';
import { shareReplay } from 'rxjs/operators'; import { shareReplay, first } from 'rxjs/operators';
import { SearchService } from '@dsh/api/anapi'; import { SearchService } from '@dsh/api/anapi';
import { SEARCH_LIMIT } from '@dsh/app/sections/tokens'; import { SEARCH_LIMIT } from '@dsh/app/sections/tokens';
import { DEBOUNCE_FETCHER_ACTION_TIME, PartialFetcher } from '@dsh/app/shared'; import { DEBOUNCE_FETCHER_ACTION_TIME, PartialFetcher } from '@dsh/app/shared';
import { PaymentInstitutionRealmService } from '../../../../../../../services';
@Injectable() @Injectable()
export class FetchRefundsService extends PartialFetcher< export class FetchRefundsService extends PartialFetcher<
RefundSearchResult, RefundSearchResult,
@ -20,7 +22,8 @@ export class FetchRefundsService extends PartialFetcher<
@Inject(SEARCH_LIMIT) @Inject(SEARCH_LIMIT)
private searchLimit: number, private searchLimit: number,
@Inject(DEBOUNCE_FETCHER_ACTION_TIME) @Inject(DEBOUNCE_FETCHER_ACTION_TIME)
debounceActionTime: number debounceActionTime: number,
private paymentInstitutionRealmService: PaymentInstitutionRealmService
) { ) {
super(debounceActionTime); super(debounceActionTime);
} }
@ -29,13 +32,19 @@ export class FetchRefundsService extends PartialFetcher<
{ invoiceID, paymentID }: Pick<SearchRefundsRequestParams, 'invoiceID' | 'paymentID'>, { invoiceID, paymentID }: Pick<SearchRefundsRequestParams, 'invoiceID' | 'paymentID'>,
continuationToken: string continuationToken: string
) { ) {
return this.searchService.searchRefunds({ return this.paymentInstitutionRealmService.realm$.pipe(
fromTime: moment().subtract(3, 'y').startOf('d').utc().format(), first(),
toTime: moment().endOf('d').utc().format(), switchMap((paymentInstitutionRealm) =>
invoiceID, this.searchService.searchRefunds({
paymentID, fromTime: moment().subtract(3, 'y').startOf('d').utc().format(),
limit: this.searchLimit, toTime: moment().endOf('d').utc().format(),
continuationToken, invoiceID,
}); paymentID,
limit: this.searchLimit,
continuationToken,
paymentInstitutionRealm,
})
)
);
} }
} }

View File

@ -3,11 +3,13 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Invoice } from '@vality/swag-anapi-v2'; import { Invoice } from '@vality/swag-anapi-v2';
import moment from 'moment'; import moment from 'moment';
import { Observable, ReplaySubject } from 'rxjs'; import { Observable, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, switchMap, tap, pluck } from 'rxjs/operators'; import { distinctUntilChanged, switchMap, tap, map, withLatestFrom } from 'rxjs/operators';
import { SearchService } from '@dsh/api/anapi'; import { SearchService } from '@dsh/api/anapi';
import { ErrorService } from '@dsh/app/shared/services'; import { ErrorService } from '@dsh/app/shared/services';
import { PaymentInstitutionRealmService } from '../../../../../../services';
@UntilDestroy() @UntilDestroy()
@Injectable() @Injectable()
export class InvoiceDetailsService { export class InvoiceDetailsService {
@ -18,7 +20,11 @@ export class InvoiceDetailsService {
private invoiceData$ = new ReplaySubject<Invoice | null>(1); private invoiceData$ = new ReplaySubject<Invoice | null>(1);
private innerErrors$ = new ReplaySubject<Error>(1); private innerErrors$ = new ReplaySubject<Error>(1);
constructor(private searchService: SearchService, private errorService: ErrorService) { constructor(
private searchService: SearchService,
private errorService: ErrorService,
private paymentInstitutionRealmService: PaymentInstitutionRealmService
) {
this.invoice$ = this.invoiceData$.asObservable(); this.invoice$ = this.invoiceData$.asObservable();
this.error$ = this.innerErrors$.asObservable(); this.error$ = this.innerErrors$.asObservable();
@ -36,15 +42,17 @@ export class InvoiceDetailsService {
tap(() => { tap(() => {
this.resetInvoiceData(); this.resetInvoiceData();
}), }),
switchMap((invoiceID: string) => { withLatestFrom(this.paymentInstitutionRealmService.realm$),
switchMap(([invoiceID, paymentInstitutionRealm]) => {
return this.searchService.searchInvoices({ return this.searchService.searchInvoices({
invoiceID, invoiceID,
fromTime: moment().subtract(3, 'y').startOf('d').utc().format(), fromTime: moment().subtract(3, 'y').startOf('d').utc().format(),
toTime: moment().endOf('d').utc().format(), toTime: moment().endOf('d').utc().format(),
limit: 1, limit: 1,
paymentInstitutionRealm,
}); });
}), }),
pluck('result', 0), map(({ result }) => result?.[0] ?? null),
untilDestroyed(this) untilDestroyed(this)
) )
.subscribe({ .subscribe({

View File

@ -5,7 +5,8 @@ import { Observable } from 'rxjs';
import { QueryParamsService } from '@dsh/app/shared/services'; import { QueryParamsService } from '@dsh/app/shared/services';
import { PaymentInstitutionRealmService, RealmMixService } from '../../services'; import { RealmMixService } from '../../services';
import { PaymentInstitutionRealmService } from '../../services/payment-institution-realm.service';
import { Filters } from './payments-filters'; import { Filters } from './payments-filters';
import { PaymentsExpandedIdManager, FetchPaymentsService } from './services'; import { PaymentsExpandedIdManager, FetchPaymentsService } from './services';
import { PaymentSearchFormValue } from './types'; import { PaymentSearchFormValue } from './types';
@ -14,7 +15,7 @@ import { PaymentSearchFormValue } from './types';
@Component({ @Component({
selector: 'dsh-payments', selector: 'dsh-payments',
templateUrl: 'payments.component.html', templateUrl: 'payments.component.html',
providers: [FetchPaymentsService, PaymentsExpandedIdManager, RealmMixService], providers: [FetchPaymentsService, PaymentsExpandedIdManager, RealmMixService, PaymentInstitutionRealmService],
}) })
export class PaymentsComponent implements OnInit { export class PaymentsComponent implements OnInit {
realm$ = this.paymentInstitutionRealmService.realm$; realm$ = this.paymentInstitutionRealmService.realm$;

View File

@ -5,7 +5,7 @@
> >
<div fxLayout fxLayoutGap="16px" fxLayoutAlign="space-between"> <div fxLayout fxLayoutGap="16px" fxLayoutAlign="space-between">
<div class="mat-title"> <div class="mat-title">
{{ t('title') }} <span dshSecondaryTitle>#{{ (invoice$ | async)?.id }}</span> {{ t('title') }} <span>#{{ (invoice$ | async)?.id }}</span>
</div> </div>
</div> </div>
<ng-container *ngIf="isLoading$ | async; else afterLoading"> <ng-container *ngIf="isLoading$ | async; else afterLoading">

View File

@ -4,12 +4,15 @@ import { PaymentInstitution } from '@vality/swag-payments';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { shareReplayRefCount } from '@dsh/operators';
import RealmEnum = PaymentInstitution.RealmEnum; import RealmEnum = PaymentInstitution.RealmEnum;
@Injectable() @Injectable()
export class PaymentInstitutionRealmService { export class PaymentInstitutionRealmService {
realm$: Observable<RealmEnum | undefined> = this.route.params.pipe( realm$: Observable<RealmEnum | undefined> = this.route.params.pipe(
map(() => this.route.snapshot.params.realm as RealmEnum) map(({ realm }) => realm as RealmEnum),
shareReplayRefCount()
); );
constructor(private route: ActivatedRoute) {} constructor(private route: ActivatedRoute) {}

View File

@ -212,6 +212,7 @@
"title": "Платеж совершен с удержанием денежных средств" "title": "Платеж совершен с удержанием денежных средств"
}, },
"invoiceDetails": { "invoiceDetails": {
"notFound": "Инвойс не найден",
"title": "Инвойс" "title": "Инвойс"
}, },
"payerDetails": { "payerDetails": {