mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 02:25:23 +00:00
[epic] Operations payments refactoring (#371)
- added accordion panel list in payments - added new quick filters(date, shops, invoices, binPan) - added new details view inlined in list - added new refunds view - added new hold detail view - implemented new error service api - added base dialog support
This commit is contained in:
parent
3d0f1734ae
commit
fb5adcc69d
@ -1,8 +1,6 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
process.env.CHROME_BIN = require('puppeteer').executablePath();
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
@ -11,6 +9,7 @@ module.exports = function (config) {
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-spec-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
],
|
||||
@ -22,13 +21,31 @@ module.exports = function (config) {
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true,
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
reporters: ['progress', 'kjhtml', 'spec'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: false,
|
||||
browsers: ['ChromeHeadless'],
|
||||
browsers: ['ChromeHeadless_no_sandbox'],
|
||||
browserNoActivityTimeout: 300000,
|
||||
browserDisconnectTimeout: 300000,
|
||||
captureTimeout: 300000,
|
||||
customLaunchers: {
|
||||
ChromeHeadless_no_sandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox', '--disable-setuid-sandbox', '--headless', '--disable-gpu'],
|
||||
},
|
||||
},
|
||||
singleRun: true,
|
||||
restartOnFileChange: false,
|
||||
specReporter: {
|
||||
maxLogLines: 5, // limit number of lines logged per test
|
||||
suppressErrorSummary: false, // do not print error summary
|
||||
suppressFailed: false, // do not print information about failed tests
|
||||
suppressPassed: false, // do not print information about passed tests
|
||||
suppressSkipped: true, // do not print information about skipped tests
|
||||
showSpecTiming: true, // print the time elapsed for each spec
|
||||
failFast: false, // test would finish with error when a first fail occurs.
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -27,8 +27,8 @@ module.exports = function (config) {
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadless_no_sandbox'],
|
||||
browserNoActivityTimeout: 300000,
|
||||
browserDisconnectTimeout: 300000,
|
||||
browserNoActivityTimeout: 30 * 60 * 1000,
|
||||
browserDisconnectTimeout: 30 * 60 * 1000,
|
||||
captureTimeout: 300000,
|
||||
customLaunchers: {
|
||||
ChromeHeadless_no_sandbox: {
|
||||
@ -36,7 +36,7 @@ module.exports = function (config) {
|
||||
flags: ['--no-sandbox', '--disable-setuid-sandbox', '--headless', '--disable-gpu'],
|
||||
},
|
||||
},
|
||||
singleRun: true,
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
specReporter: {
|
||||
maxLogLines: 5, // limit number of lines logged per test
|
||||
|
@ -6,7 +6,7 @@
|
||||
"stub": "ng serve --port 8000 --configuration=stub-keycloak",
|
||||
"build": "ng build --prod --extraWebpackConfig webpack.extra.js --progress=false",
|
||||
"test": "ng test",
|
||||
"test-ci": "ng test --watch=false",
|
||||
"test-ci": "ng run dashboard:test-ci",
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix",
|
||||
"e2e": "ng e2e",
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { UuidGeneratorModule } from '../../shared';
|
||||
import { IdGeneratorModule } from '../../shared';
|
||||
import { OrganizationsService } from './organizations.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [UuidGeneratorModule],
|
||||
imports: [IdGeneratorModule],
|
||||
providers: [OrganizationsService],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
RoleId,
|
||||
RolesService,
|
||||
} from '@dsh/api-codegen/organizations';
|
||||
import { UuidGeneratorService } from '@dsh/app/shared';
|
||||
import { IdGeneratorService } from '@dsh/app/shared';
|
||||
|
||||
import { WritableOrganization } from './types/writable-organization';
|
||||
|
||||
@ -19,7 +19,7 @@ export class OrganizationsService {
|
||||
private orgsService: OrgsService,
|
||||
private rolesService: RolesService,
|
||||
private membersService: MembersService,
|
||||
private uuidGeneratorService: UuidGeneratorService
|
||||
private uuidGeneratorService: IdGeneratorService
|
||||
) {}
|
||||
|
||||
listOrgMembership(limit?: number, continuationToken?: string) {
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { Organization } from '../../../api-codegen/organizations';
|
||||
import { Organization } from '@dsh/api-codegen/organizations';
|
||||
|
||||
export type WritableOrganization = Omit<Organization, 'id' | 'createdAt'>;
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { IdGeneratorModule } from '@dsh/app/shared/services';
|
||||
|
||||
import { RefundService } from './refund.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [IdGeneratorModule],
|
||||
providers: [RefundService],
|
||||
})
|
||||
export class RefundModule {}
|
||||
|
@ -2,14 +2,22 @@ import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { PaymentsService, Refund, RefundParams } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
|
||||
import { genXRequestID } from '../utils';
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefundService {
|
||||
constructor(private paymentsService: PaymentsService) {}
|
||||
constructor(private paymentsService: PaymentsService, private idsService: IdGeneratorService) {}
|
||||
|
||||
createRefund(invoiceID: string, paymentID: string, refundParams: RefundParams): Observable<Refund> {
|
||||
return this.paymentsService.createRefund(genXRequestID(), invoiceID, paymentID, refundParams);
|
||||
return this.paymentsService.createRefund(
|
||||
this.idsService.generateRequestID(),
|
||||
invoiceID,
|
||||
paymentID,
|
||||
refundParams
|
||||
);
|
||||
}
|
||||
|
||||
getRefunds(invoiceID: string, paymentID: string): Observable<Refund[]> {
|
||||
return this.paymentsService.getRefunds(this.idsService.generateRequestID(), invoiceID, paymentID);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import moment from 'moment';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { SearchService } from '@dsh/api-codegen/anapi';
|
||||
import { InlineResponse20010, PaymentSearchResult, SearchService } from '@dsh/api-codegen/anapi';
|
||||
|
||||
import { genXRequestID, toDateLike } from '../utils';
|
||||
import { Duration, PaymentsSearchParams } from './model';
|
||||
@ -17,7 +18,7 @@ export class PaymentSearchService {
|
||||
params: PaymentsSearchParams,
|
||||
limit: number,
|
||||
continuationToken?: string
|
||||
) {
|
||||
): Observable<InlineResponse20010> {
|
||||
return this.searchService.searchPayments(
|
||||
genXRequestID(),
|
||||
toDateLike(fromTime),
|
||||
@ -58,13 +59,13 @@ export class PaymentSearchService {
|
||||
params: PaymentsSearchParams,
|
||||
limit: number,
|
||||
continuationToken?: string
|
||||
) {
|
||||
): Observable<InlineResponse20010> {
|
||||
const from = moment().subtract(amount, unit).startOf('d').utc().format();
|
||||
const to = moment().endOf('d').utc().format();
|
||||
return this.searchPayments(from, to, params, limit, continuationToken);
|
||||
}
|
||||
|
||||
getPaymentByDuration(duration: Duration, invoiceID: string, paymentID: string) {
|
||||
getPaymentByDuration(duration: Duration, invoiceID: string, paymentID: string): Observable<PaymentSearchResult> {
|
||||
return this.searchPaymentsByDuration(
|
||||
duration,
|
||||
{
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { SenderModule as ApiSenderModule } from '../../api-codegen/sender';
|
||||
import { UuidGeneratorModule } from '../../shared/services';
|
||||
import { IdGeneratorModule } from '../../shared/services';
|
||||
import { MessagesService } from './services/messages/messages.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [ApiSenderModule, UuidGeneratorModule],
|
||||
imports: [ApiSenderModule, IdGeneratorModule],
|
||||
providers: [MessagesService],
|
||||
})
|
||||
export class SenderModule {}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
import { MessagesService as ApiMessagesService } from '../../../../api-codegen/sender';
|
||||
import { UuidGeneratorService } from '../../../../shared/services/uuid-generator/uuid-generator.service';
|
||||
|
||||
@Injectable()
|
||||
export class MessagesService {
|
||||
constructor(private messagesService: ApiMessagesService, private idGeneratorService: UuidGeneratorService) {}
|
||||
constructor(private messagesService: ApiMessagesService, private idGeneratorService: IdGeneratorService) {}
|
||||
|
||||
sendFeedbackEmailMsg(text: string) {
|
||||
return this.messagesService.sendFeedbackEmailMsg(this.idGeneratorService.generateUUID(), { text });
|
||||
|
@ -3,4 +3,4 @@ import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { findShopByID } from './find-shop-by-id';
|
||||
import { getShopName } from './get-shop-name';
|
||||
|
||||
export const toShopName = (s: Shop[], shopID: string): string | null => getShopName(findShopByID(s, shopID));
|
||||
export const getShopNameById = (s: Shop[], shopID: string): string | null => getShopName(findShopByID(s, shopID));
|
||||
|
@ -6,7 +6,7 @@ import { catchError, first, mapTo, shareReplay, switchMap, switchMapTo, takeLast
|
||||
|
||||
import { ApiShopsService, CAPIClaimsService, CAPIPartiesService, createTestShopClaimChangeset } from '@dsh/api';
|
||||
import { Claim } from '@dsh/api-codegen/capi';
|
||||
import { ErrorService } from '@dsh/app/shared';
|
||||
import { NotificationService } from '@dsh/app/shared';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable()
|
||||
@ -23,7 +23,7 @@ export class BootstrapService {
|
||||
private shopService: ApiShopsService,
|
||||
private capiClaimsService: CAPIClaimsService,
|
||||
private capiPartiesService: CAPIPartiesService,
|
||||
private errorService: ErrorService,
|
||||
private notificationService: NotificationService,
|
||||
// TODO: Wait access check
|
||||
// private organizationsService: OrganizationsService,
|
||||
// private userService: UserService,
|
||||
@ -43,8 +43,8 @@ export class BootstrapService {
|
||||
).pipe(
|
||||
takeLast(1),
|
||||
mapTo(true),
|
||||
catchError((err) => {
|
||||
this.errorService.error(err, this.transloco.translate('errors.bootstrapAppFailed'));
|
||||
catchError(() => {
|
||||
this.notificationService.error(this.transloco.translate('errors.bootstrapAppFailed'));
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import { anyString, instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { MessagesService } from '@dsh/api/sender';
|
||||
import { ErrorModule, ErrorService, NotificationService } from '@dsh/app/shared/services';
|
||||
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
|
||||
|
||||
import { FeedbackDialogComponent } from './feedback-dialog.component';
|
||||
|
||||
@ -27,7 +28,7 @@ describe('FeedbackDialogComponent', () => {
|
||||
mockNotificationService = mock(NotificationService);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MatDialogModule, ErrorModule, NoopAnimationsModule],
|
||||
imports: [getTranslocoModule(), MatDialogModule, ErrorModule, NoopAnimationsModule],
|
||||
declarations: [FeedbackDialogComponent],
|
||||
providers: [
|
||||
{
|
||||
@ -71,10 +72,11 @@ describe('FeedbackDialogComponent', () => {
|
||||
});
|
||||
|
||||
it("shouldn't send message", () => {
|
||||
when(mockMessagesService.sendFeedbackEmailMsg(anyString())).thenReturn(throwError('Test error'));
|
||||
const error = new Error('Test error');
|
||||
when(mockMessagesService.sendFeedbackEmailMsg(anyString())).thenReturn(throwError(error));
|
||||
component.send();
|
||||
verify(mockMessagesService.sendFeedbackEmailMsg('')).once();
|
||||
verify(mockErrorService.error('Test error')).once();
|
||||
verify(mockErrorService.error(error)).once();
|
||||
verify(mockMatDialogRef.close()).never();
|
||||
expect().nothing();
|
||||
});
|
||||
|
@ -21,6 +21,7 @@
|
||||
"input",
|
||||
"keyboard_arrow_down",
|
||||
"keyboard_arrow_up",
|
||||
"launch",
|
||||
"logo",
|
||||
"logo_white",
|
||||
"mastercard",
|
||||
|
@ -10,7 +10,6 @@ import { InvoiceDetailsService } from './invoice-details.service';
|
||||
@Component({
|
||||
templateUrl: 'invoice-details.component.html',
|
||||
styleUrls: ['invoice-details.component.scss'],
|
||||
providers: [InvoiceDetailsService],
|
||||
})
|
||||
export class InvoiceDetailsComponent implements OnInit {
|
||||
invoice$ = this.invoiceDetailsService.invoice$;
|
||||
|
@ -6,11 +6,11 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceSearchService, PaymentSearchService } from '@dsh/api/search';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../to-major';
|
||||
import { CreatePaymentLinkModule } from '../create-payment-link';
|
||||
import { ShopDetailsModule } from '../shop-details/shop-details.module';
|
||||
import { CartComponent } from './cart/cart.component';
|
||||
@ -19,6 +19,7 @@ import { DetailsComponent } from './details/details.component';
|
||||
import { StatusDetailsItemComponent } from './details/status-details-item';
|
||||
import { InvoiceDetailsRoutingModule } from './invoice-details-routing.module';
|
||||
import { InvoiceDetailsComponent } from './invoice-details.component';
|
||||
import { InvoiceDetailsService } from './invoice-details.service';
|
||||
import { CreatePaymentLinkDialogComponent, PaymentLinkComponent } from './payment-link';
|
||||
import { PaymentComponent } from './payments/payment/payment.component';
|
||||
import { PaymentsComponent } from './payments/payments.component';
|
||||
@ -50,6 +51,6 @@ import { PaymentsComponent } from './payments/payments.component';
|
||||
CreatePaymentLinkDialogComponent,
|
||||
],
|
||||
exports: [StatusDetailsItemComponent],
|
||||
providers: [InvoiceSearchService, PaymentSearchService],
|
||||
providers: [InvoiceSearchService, PaymentSearchService, InvoiceDetailsService],
|
||||
})
|
||||
export class InvoiceDetailsModule {}
|
||||
|
4
src/app/sections/partial-fetcher/consts.ts
Normal file
4
src/app/sections/partial-fetcher/consts.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const DEBOUNCE_FETCHER_ACTION_TIME = new InjectionToken<number>('debounceFetcherActionTime');
|
||||
export const DEFAULT_FETCHER_DEBOUNCE_ACTION_TIME = 300;
|
@ -1,3 +1,5 @@
|
||||
export * from './partial-fetcher';
|
||||
export * from './fetch-result';
|
||||
export * from './fetch-action';
|
||||
export * from './indicators-partial-fetcher.service';
|
||||
export * from './consts';
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { SEARCH_LIMIT } from '@dsh/app/sections/tokens';
|
||||
import { booleanDebounceTime, mapToTimestamp } from '@dsh/operators';
|
||||
|
||||
import { DEBOUNCE_FETCHER_ACTION_TIME } from './consts';
|
||||
import { PartialFetcher } from './partial-fetcher';
|
||||
|
||||
// TODO: remove this disable after making partial fetcher with injectable debounce time
|
||||
/* tslint:disable:no-unused-variable */
|
||||
@Injectable()
|
||||
export abstract class IndicatorsPartialFetcher<R, P> extends PartialFetcher<R, P> {
|
||||
isLoading$: Observable<boolean> = this.doAction$.pipe(booleanDebounceTime(), shareReplay(1));
|
||||
lastUpdated$: Observable<string> = this.searchResult$.pipe(mapToTimestamp, shareReplay(1));
|
||||
|
||||
constructor(
|
||||
@Inject(SEARCH_LIMIT)
|
||||
protected searchLimit: number,
|
||||
@Inject(DEBOUNCE_FETCHER_ACTION_TIME)
|
||||
protected debounceActionTime: number
|
||||
) {
|
||||
super(debounceActionTime);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { EMPTY, merge, Observable, of, Subject } from 'rxjs';
|
||||
import {
|
||||
debounceTime,
|
||||
@ -12,15 +13,16 @@ import {
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { progress, SHARE_REPLAY_CONF } from '../../custom-operators';
|
||||
import { progress, SHARE_REPLAY_CONF } from '@dsh/operators';
|
||||
|
||||
import { FetchAction } from './fetch-action';
|
||||
import { FetchFn } from './fetch-fn';
|
||||
import { FetchResult } from './fetch-result';
|
||||
import { scanAction, scanFetchResult } from './operators';
|
||||
|
||||
// TODO: make fetcher injectable
|
||||
@UntilDestroy()
|
||||
export abstract class PartialFetcher<R, P> {
|
||||
private action$ = new Subject<FetchAction<P>>();
|
||||
|
||||
readonly fetchResultChanges$: Observable<{ result: R[]; hasMore: boolean; continuationToken: string }>;
|
||||
|
||||
readonly searchResult$: Observable<R[]>;
|
||||
@ -29,13 +31,16 @@ export abstract class PartialFetcher<R, P> {
|
||||
readonly doSearchAction$: Observable<boolean>;
|
||||
readonly errors$: Observable<any>;
|
||||
|
||||
private action$ = new Subject<FetchAction<P>>();
|
||||
|
||||
// TODO: make a dependency for DI
|
||||
constructor(debounceActionTime: number = 300) {
|
||||
const actionWithParams$ = this.getActionWithParams(debounceActionTime);
|
||||
const fetchResult$ = this.getFetchResult(actionWithParams$);
|
||||
|
||||
this.fetchResultChanges$ = fetchResult$.pipe(
|
||||
map(({ result, continuationToken }) => ({
|
||||
result,
|
||||
result: result ?? [],
|
||||
continuationToken,
|
||||
hasMore: !!continuationToken,
|
||||
})),
|
||||
@ -69,7 +74,9 @@ export abstract class PartialFetcher<R, P> {
|
||||
this.doSearchAction$,
|
||||
this.errors$,
|
||||
this.fetchResultChanges$
|
||||
).subscribe();
|
||||
)
|
||||
.pipe(untilDestroyed(this))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
search(value: P) {
|
||||
|
@ -3,10 +3,10 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { CardModule, DetailsItemModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../to-major';
|
||||
import { StatusDetailsItemModule } from '../status-details-item';
|
||||
import { DetailsComponent } from './details.component';
|
||||
import { ErrorToMessagePipe } from './error-to-message.pipe';
|
||||
|
@ -5,10 +5,10 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceSearchService } from '@dsh/api/search';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { SpinnerModule } from '@dsh/components/indicators';
|
||||
import { CardModule, DetailsItemModule, LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../to-major';
|
||||
import { StatusDetailsItemModule } from '../status-details-item';
|
||||
import { InvoiceDetailsComponent } from './invoice-details.component';
|
||||
|
||||
|
@ -7,12 +7,12 @@ import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceModule } from '@dsh/api/invoice';
|
||||
import { SearchModule } from '@dsh/api/search';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { HumanizeDurationModule } from '../../humanize-duration';
|
||||
import { ToMajorModule } from '../../to-major';
|
||||
import { ShopDetailsModule } from '../shop-details/shop-details.module';
|
||||
import { DetailsModule } from './details';
|
||||
import { HoldDetailsModule } from './hold-details';
|
||||
|
@ -1 +1,2 @@
|
||||
// TODO: remove module when operations/payments epic migration would be finished
|
||||
export * from './create-refund.component';
|
||||
|
@ -12,11 +12,11 @@ import { AccountService } from '@dsh/api/account';
|
||||
import { RefundService } from '@dsh/api/refund';
|
||||
import { RefundSearchService } from '@dsh/api/search';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { FormControlsModule } from '@dsh/components/form-controls';
|
||||
import { DetailsItemModule, LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../to-major';
|
||||
import { StatusDetailsItemModule } from '../status-details-item';
|
||||
import { CreateRefundComponent } from './create-refund';
|
||||
import { RefundItemComponent } from './refund-item';
|
||||
|
@ -3,10 +3,10 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { SpinnerModule } from '@dsh/components/indicators';
|
||||
import { CardModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../../to-major';
|
||||
import { PercentDifferenceModule } from '../percent-difference';
|
||||
import { StatItemComponent } from './stat-item.component';
|
||||
|
||||
|
@ -4,8 +4,8 @@ import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { AnalyticsModule } from '@dsh/api/analytics';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
|
||||
import { ToMajorModule } from '../../../to-major';
|
||||
import { BalancesComponent } from './balances.component';
|
||||
|
||||
@NgModule({
|
||||
|
@ -7,7 +7,7 @@ import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
|
||||
import { Claim, Modification } from '@dsh/api-codegen/claim-management';
|
||||
import { ClaimsService } from '@dsh/api/claims';
|
||||
import { createTestContractCreationModification } from '@dsh/api/claims/claim-party-modification';
|
||||
import { UuidGeneratorService } from '@dsh/app/shared/services/uuid-generator/uuid-generator.service';
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
import { createTestContractPayoutToolModification } from '../../tests/create-test-contract-payout-tool-modification';
|
||||
import { createTestInternationalLegalEntityModification } from '../../tests/create-test-international-legal-entity-modification';
|
||||
@ -20,11 +20,11 @@ const TEST_UUID = 'test-uuid';
|
||||
describe('CreateInternationalShopEntityService', () => {
|
||||
let service: CreateInternationalShopEntityService;
|
||||
let mockClaimsService: ClaimsService;
|
||||
let mockUuidGeneratorService: UuidGeneratorService;
|
||||
let mockIdGeneratorService: IdGeneratorService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClaimsService = mock(ClaimsService);
|
||||
mockUuidGeneratorService = mock(UuidGeneratorService);
|
||||
mockIdGeneratorService = mock(IdGeneratorService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -36,8 +36,8 @@ describe('CreateInternationalShopEntityService', () => {
|
||||
useFactory: () => instance(mockClaimsService),
|
||||
},
|
||||
{
|
||||
provide: UuidGeneratorService,
|
||||
useFactory: () => instance(mockUuidGeneratorService),
|
||||
provide: IdGeneratorService,
|
||||
useFactory: () => instance(mockIdGeneratorService),
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -82,7 +82,7 @@ describe('CreateInternationalShopEntityService', () => {
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockUuidGeneratorService.generateUUID()).thenReturn(TEST_UUID);
|
||||
when(mockIdGeneratorService.generateUUID()).thenReturn(TEST_UUID);
|
||||
when(mockClaimsService.createClaim(deepEqual(modifications))).thenReturn(of(claim));
|
||||
when(mockClaimsService.requestReviewClaimByID(claim.id, claim.revision)).thenReturn(of(null));
|
||||
});
|
||||
|
@ -12,13 +12,13 @@ import {
|
||||
makeShopLocation,
|
||||
} from '@dsh/api/claims/claim-party-modification';
|
||||
import { createInternationalContractPayoutToolModification } from '@dsh/api/claims/claim-party-modification/claim-contract-modification/create-international-contract-payout-tool-modification';
|
||||
import { UuidGeneratorService } from '@dsh/app/shared/services/uuid-generator/uuid-generator.service';
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
import { InternationalShopEntityFormValue } from '../../types/international-shop-entity-form-value';
|
||||
|
||||
@Injectable()
|
||||
export class CreateInternationalShopEntityService {
|
||||
constructor(private claimsService: ClaimsService, private uuidGenerator: UuidGeneratorService) {}
|
||||
constructor(private claimsService: ClaimsService, private idGenerator: IdGeneratorService) {}
|
||||
|
||||
createShop(creationData: InternationalShopEntityFormValue) {
|
||||
return this.claimsService.createClaim(this.createClaimsModifications(creationData)).pipe(
|
||||
@ -39,10 +39,10 @@ export class CreateInternationalShopEntityService {
|
||||
payoutTool,
|
||||
correspondentPayoutTool = null,
|
||||
}: InternationalShopEntityFormValue): Modification[] {
|
||||
const contractorID = this.uuidGenerator.generateUUID();
|
||||
const contractID = this.uuidGenerator.generateUUID();
|
||||
const payoutToolID = this.uuidGenerator.generateUUID();
|
||||
const shopID = this.uuidGenerator.generateUUID();
|
||||
const contractorID = this.idGenerator.generateUUID();
|
||||
const contractID = this.idGenerator.generateUUID();
|
||||
const payoutToolID = this.idGenerator.generateUUID();
|
||||
const shopID = this.idGenerator.generateUUID();
|
||||
|
||||
return [
|
||||
createInternationalLegalEntityModification(contractorID, {
|
||||
|
@ -13,7 +13,7 @@ import { TranslocoModule } from '@ngneat/transloco';
|
||||
import { ClaimsModule } from '@dsh/api/claims';
|
||||
import { PayoutToolDetailsModule } from '@dsh/app/shared/components';
|
||||
import { AutocompleteVirtualScrollModule } from '@dsh/app/shared/components/selects/autocomplete-virtual-scroll';
|
||||
import { UuidGeneratorModule } from '@dsh/app/shared/services';
|
||||
import { IdGeneratorModule } from '@dsh/app/shared/services';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { FormatInputModule } from '@dsh/components/form-controls';
|
||||
import { DetailsItemModule } from '@dsh/components/layout';
|
||||
@ -46,7 +46,7 @@ import { CreateRussianShopEntityService } from './services/create-russian-shop-e
|
||||
AutocompleteVirtualScrollModule,
|
||||
MatDialogModule,
|
||||
ClaimsModule,
|
||||
UuidGeneratorModule,
|
||||
IdGeneratorModule,
|
||||
],
|
||||
declarations: [
|
||||
CreateRussianShopEntityComponent,
|
||||
|
@ -8,7 +8,7 @@ import { Contract } from '@dsh/api-codegen/capi';
|
||||
import { Claim, Modification } from '@dsh/api-codegen/claim-management';
|
||||
import { ClaimsService } from '@dsh/api/claims';
|
||||
import { createTestContractCreationModification } from '@dsh/api/claims/claim-party-modification';
|
||||
import { UuidGeneratorService } from '@dsh/app/shared/services/uuid-generator/uuid-generator.service';
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
import { createTestLegalEntityModification } from '../../tests/create-test-legal-entity-modification';
|
||||
import { createTestRussianContractPayoutToolModification } from '../../tests/create-test-russian-contract-payout-tool-modification';
|
||||
@ -21,11 +21,11 @@ const TEST_UUID = 'test-uuid';
|
||||
describe('CreateRussianShopEntityService', () => {
|
||||
let service: CreateRussianShopEntityService;
|
||||
let mockClaimsService: ClaimsService;
|
||||
let mockUuidGeneratorService: UuidGeneratorService;
|
||||
let mockIdGeneratorService: IdGeneratorService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClaimsService = mock(ClaimsService);
|
||||
mockUuidGeneratorService = mock(UuidGeneratorService);
|
||||
mockIdGeneratorService = mock(IdGeneratorService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -37,8 +37,8 @@ describe('CreateRussianShopEntityService', () => {
|
||||
useFactory: () => instance(mockClaimsService),
|
||||
},
|
||||
{
|
||||
provide: UuidGeneratorService,
|
||||
useFactory: () => instance(mockUuidGeneratorService),
|
||||
provide: IdGeneratorService,
|
||||
useFactory: () => instance(mockIdGeneratorService),
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -90,7 +90,7 @@ describe('CreateRussianShopEntityService', () => {
|
||||
let modifications: Modification[];
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockUuidGeneratorService.generateUUID()).thenReturn(TEST_UUID);
|
||||
when(mockIdGeneratorService.generateUUID()).thenReturn(TEST_UUID);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -13,13 +13,13 @@ import {
|
||||
createShopCreationModification,
|
||||
makeShopLocation,
|
||||
} from '@dsh/api/claims/claim-party-modification';
|
||||
import { UuidGeneratorService } from '@dsh/app/shared/services/uuid-generator/uuid-generator.service';
|
||||
import { IdGeneratorService } from '@dsh/app/shared/services/id-generator/id-generator.service';
|
||||
|
||||
import { RussianShopCreateData } from '../../types/russian-shop-create-data';
|
||||
|
||||
@Injectable()
|
||||
export class CreateRussianShopEntityService {
|
||||
constructor(private claimsService: ClaimsService, private uuidGenerator: UuidGeneratorService) {}
|
||||
constructor(private claimsService: ClaimsService, private idGenerator: IdGeneratorService) {}
|
||||
|
||||
createShop(creationData: RussianShopCreateData): Observable<Claim> {
|
||||
return this.claimsService.createClaim(this.createShopCreationModifications(creationData)).pipe(
|
||||
@ -37,11 +37,11 @@ export class CreateRussianShopEntityService {
|
||||
payoutToolID: shopPayoutToolID,
|
||||
bankAccount: { account, bankName, bankPostAccount, bankBik },
|
||||
}: RussianShopCreateData): PartyModification[] {
|
||||
const contractorID = this.uuidGenerator.generateUUID();
|
||||
const contractID = this.uuidGenerator.generateUUID();
|
||||
const shopID = this.uuidGenerator.generateUUID();
|
||||
const contractorID = this.idGenerator.generateUUID();
|
||||
const contractID = this.idGenerator.generateUUID();
|
||||
const shopID = this.idGenerator.generateUUID();
|
||||
|
||||
let payoutToolID = this.uuidGenerator.generateUUID();
|
||||
let payoutToolID = this.idGenerator.generateUUID();
|
||||
const payoutChangeset: PartyModification[] = [];
|
||||
|
||||
if (isNil(shopPayoutToolID)) {
|
||||
|
@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
|
||||
import { QueryFilterModule } from '@dsh/components/filters/query-filter';
|
||||
import { QueryFilterModule } from '@dsh/app/shared/components/filters/query-filter';
|
||||
|
||||
import { ShopQueryFilterComponent } from './shop-query-filter.component';
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { QueryFilterModule } from '@dsh/components/filters/query-filter';
|
||||
import { QueryFilterModule } from '@dsh/app/shared/components/filters/query-filter';
|
||||
|
||||
import { ShopQueryFilterComponent } from './shop-query-filter.component';
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToMajorModule } from '../../../../../../to-major';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
|
||||
import { generateMockBalance } from '../../tests/generate-mock-balance';
|
||||
import { generateMockShop } from '../../tests/generate-mock-shop';
|
||||
import { ShopBalanceComponent } from './shop-balance.component';
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ToMajorModule } from '../../../../../../to-major';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
|
||||
import { ShopBalanceComponent } from './shop-balance.component';
|
||||
|
||||
@NgModule({
|
||||
|
@ -3,13 +3,13 @@ import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
|
||||
import { SpinnerModule } from '@dsh/components/indicators';
|
||||
import { LastUpdatedModule } from '@dsh/components/indicators/last-updated/last-updated.module';
|
||||
import { AccordionModule, CardModule, ExpandPanelModule, RowModule } from '@dsh/components/layout';
|
||||
import { ShowMorePanelModule } from '@dsh/components/show-more-panel';
|
||||
|
||||
import { ToMajorModule } from '../../../../../to-major';
|
||||
import { ShopRowHeaderComponent } from './components/shop-row-header/shop-row-header.component';
|
||||
import { ShopRowComponent } from './components/shop-row/shop-row.component';
|
||||
import { ShopBalanceModule } from './shop-balance';
|
||||
@ -34,6 +34,6 @@ import { ShopsListComponent } from './shops-list.component';
|
||||
ExpandPanelModule,
|
||||
],
|
||||
declarations: [ShopsListComponent, ShopRowHeaderComponent, ShopRowComponent],
|
||||
exports: [ShopsListComponent],
|
||||
exports: [ShopsListComponent, ShopRowComponent],
|
||||
})
|
||||
export class ShopListModule {}
|
||||
|
@ -6,13 +6,13 @@ import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
import { of } from 'rxjs';
|
||||
import { instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
|
||||
import { SpinnerModule } from '@dsh/components/indicators';
|
||||
import { LastUpdatedModule } from '@dsh/components/indicators/last-updated/last-updated.module';
|
||||
import { AccordionModule, CardModule, ExpandPanelModule, RowModule } from '@dsh/components/layout';
|
||||
import { ShowMorePanelModule } from '@dsh/components/show-more-panel';
|
||||
|
||||
import { ToMajorModule } from '../../../../../to-major';
|
||||
import { generateMockShopsItemList } from '../tests/generate-mock-shops-item-list';
|
||||
import { ShopRowHeaderComponent } from './components/shop-row-header/shop-row-header.component';
|
||||
import { ShopRowComponent } from './components/shop-row/shop-row.component';
|
||||
|
@ -3,7 +3,8 @@
|
||||
{{ t('title') }}
|
||||
</div>
|
||||
<div *ngFor="let payment of payments$ | async as payments; index as i" fxLayout="column" fxLayoutGap="24px">
|
||||
<div class="dsh-subheading-2">{{ t('payment') }} #{{ payment.id }}</div>
|
||||
<!-- TODO: refactor payments display -->
|
||||
<div class="dsh-subheading-2">{{ t('payment') }} #{{ payment.invoiceID + '_' + payment.id }}</div>
|
||||
<dsh-payment-details [payment]="payment"></dsh-payment-details>
|
||||
<mat-divider *ngIf="i < payments.length - 1"></mat-divider>
|
||||
</div>
|
||||
|
@ -11,12 +11,11 @@ import {
|
||||
PaymentDetailsModule,
|
||||
RefundDetailsModule as ApiRefundDetailsModule,
|
||||
} from '@dsh/app/shared/components';
|
||||
import { ApiModelRefsModule } from '@dsh/app/shared/pipes';
|
||||
import { ApiModelRefsModule, ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../../../../to-major';
|
||||
import { CancelInvoiceModule } from './cancel-invoice';
|
||||
import { InvoiceActionsComponent } from './components/invoice-actions/invoice-actions.component';
|
||||
import { InvoiceCartLineComponent } from './components/invoice-cart-info/cart-info/invoice-cart-line.component';
|
||||
|
@ -6,11 +6,10 @@ import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceModule } from '@dsh/api/invoice';
|
||||
import { InvoiceDetailsModule as ApiInvoiceDetailsModule } from '@dsh/app/shared/components';
|
||||
import { ApiModelRefsModule } from '@dsh/app/shared/pipes';
|
||||
import { ApiModelRefsModule, ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { IndicatorsModule } from '@dsh/components/indicators';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
|
||||
import { ToMajorModule } from '../../../../../to-major';
|
||||
import { InvoiceRowHeaderComponent } from './components/invoice-row-header/invoice-row-header.component';
|
||||
import { InvoiceRowComponent } from './components/invoice-row/invoice-row.component';
|
||||
import { InvoiceDetailsModule } from './invoice-details';
|
||||
|
@ -15,6 +15,7 @@ import { TranslocoModule, TRANSLOCO_SCOPE } from '@ngneat/transloco';
|
||||
|
||||
import { InvoiceModule } from '@dsh/api/invoice';
|
||||
import { InvoiceDetailsModule } from '@dsh/app/shared/components';
|
||||
import { ToMajorModule } from '@dsh/app/shared/pipes';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
import { EmptySearchResultModule } from '@dsh/components/empty-search-result';
|
||||
import { FormControlsModule, RangeDatepickerModule } from '@dsh/components/form-controls';
|
||||
@ -25,7 +26,6 @@ import { ShowMorePanelModule } from '@dsh/components/show-more-panel';
|
||||
import { TableModule } from '@dsh/components/table';
|
||||
|
||||
import { LanguageModule } from '../../../../language';
|
||||
import { ToMajorModule } from '../../../../to-major';
|
||||
import { ShopSelectorModule } from '../../../shop-selector';
|
||||
import { CreateInvoiceModule } from './create-invoice';
|
||||
import { InvoicesListModule } from './invoices-list';
|
||||
|
@ -2,7 +2,7 @@ import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { Invoice, Shop } from '@dsh/api-codegen/anapi';
|
||||
import { toShopName } from '@dsh/api/shop/utils';
|
||||
import { getShopNameById } from '@dsh/api/shop/utils';
|
||||
|
||||
import { InvoicesTableData } from './table';
|
||||
|
||||
@ -15,7 +15,7 @@ const toInvoiceTableData = (
|
||||
status,
|
||||
createdAt: createdAt as any,
|
||||
invoiceID: id,
|
||||
shopName: toShopName(s, shopID),
|
||||
shopName: getShopNameById(s, shopID),
|
||||
product,
|
||||
});
|
||||
|
||||
|
@ -24,3 +24,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dsh-scroll-up [hideAfter]="200"></dsh-scroll-up>
|
||||
|
@ -5,12 +5,21 @@ import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { SearchModule } from '@dsh/api/search';
|
||||
import { LayoutModule } from '@dsh/components/layout';
|
||||
import { ScrollUpModule } from '@dsh/components/navigation';
|
||||
|
||||
import { OperationsRoutingModule } from './operations-routing.module';
|
||||
import { OperationsComponent } from './operations.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, OperationsRoutingModule, LayoutModule, FlexLayoutModule, SearchModule, TranslocoModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
OperationsRoutingModule,
|
||||
LayoutModule,
|
||||
FlexLayoutModule,
|
||||
SearchModule,
|
||||
TranslocoModule,
|
||||
ScrollUpModule,
|
||||
],
|
||||
declarations: [OperationsComponent],
|
||||
})
|
||||
export class OperationsModule {}
|
||||
|
@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const PAYMENTS_UPDATE_DELAY_TOKEN = new InjectionToken<number>('payments-update-delay-token');
|
||||
export const DEFAULT_PAYMENTS_UPDATE_DELAY = 300;
|
@ -0,0 +1 @@
|
||||
export * from './payments.module';
|
@ -1,26 +0,0 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { PaymentSearchResult, Shop } from '@dsh/api-codegen/capi';
|
||||
import { toShopName } from '@dsh/api/shop/utils';
|
||||
|
||||
import { PaymentsTableData } from './table';
|
||||
|
||||
const toPaymentTableData = (
|
||||
{ amount, status, statusChangedAt, invoiceID, shopID, id, currency }: PaymentSearchResult,
|
||||
s: Shop[]
|
||||
): PaymentsTableData | null => ({
|
||||
amount,
|
||||
status,
|
||||
currency,
|
||||
invoiceID,
|
||||
statusChangedAt: statusChangedAt as any,
|
||||
paymentID: id,
|
||||
shopName: toShopName(s, shopID),
|
||||
});
|
||||
|
||||
const paymentsToTableData = (searchResult: PaymentSearchResult[], s: Shop[]) =>
|
||||
searchResult.map((r) => toPaymentTableData(r, s));
|
||||
|
||||
export const mapToPaymentsTableData = (s: Observable<[PaymentSearchResult[], Shop[]]>) =>
|
||||
s.pipe(map(([searchResult, shops]) => paymentsToTableData(searchResult, shops)));
|
@ -0,0 +1,18 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { BaseDialogModule } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { ButtonModule } from '@dsh/components/buttons';
|
||||
|
||||
import { AdditionalFiltersService } from './additional-filters.service';
|
||||
import { DialogFiltersComponent } from './components/dialog-filters.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, BaseDialogModule, MatDividerModule, FlexLayoutModule, ButtonModule, TranslocoModule],
|
||||
declarations: [DialogFiltersComponent],
|
||||
providers: [AdditionalFiltersService],
|
||||
})
|
||||
export class AdditionalFiltersModule {}
|
@ -0,0 +1,13 @@
|
||||
// TODO: implement unit tests
|
||||
// describe('AdditionalFiltersService', () => {
|
||||
// let service: AdditionalFiltersService;
|
||||
//
|
||||
// beforeEach(() => {
|
||||
// TestBed.configureTestingModule({});
|
||||
// service = TestBed.inject(AdditionalFiltersService);
|
||||
// });
|
||||
//
|
||||
// it('should be created', () => {
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
// });
|
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { PaymentsAdditionalFilters } from '../types/payments-additional-filters';
|
||||
import { DialogFiltersComponent } from './components/dialog-filters.component';
|
||||
|
||||
@Injectable()
|
||||
export class AdditionalFiltersService {
|
||||
constructor(private dialog: MatDialog) {}
|
||||
|
||||
openFiltersDialog(data: PaymentsAdditionalFilters): Observable<PaymentsAdditionalFilters> {
|
||||
return this.dialog
|
||||
.open<DialogFiltersComponent, PaymentsAdditionalFilters>(DialogFiltersComponent, {
|
||||
panelClass: 'fill-bleed-dialog',
|
||||
width: '552px',
|
||||
minHeight: '400px',
|
||||
disableClose: true,
|
||||
data,
|
||||
})
|
||||
.afterClosed()
|
||||
.pipe(take(1));
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<ng-container *transloco="let t; read: 'filters'">
|
||||
<dsh-base-dialog [title]="t('additionalFilters')">
|
||||
<!-- TODO: add title close button -->
|
||||
<div>filters should be here</div>
|
||||
<div actions>
|
||||
<div fxLayout="row" fxLayoutAlign="space-between center">
|
||||
<button dsh-button (click)="clear()">
|
||||
{{ t('clearParams') }}
|
||||
</button>
|
||||
<button dsh-button color="accent" (click)="confirm()">
|
||||
{{ t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dsh-base-dialog>
|
||||
</ng-container>
|
@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// TODO: implement unit tests
|
||||
// describe('DialogFiltersComponent', () => {
|
||||
// let component: DialogFiltersComponent;
|
||||
// let fixture: ComponentFixture<DialogFiltersComponent>;
|
||||
//
|
||||
// beforeEach(async(() => {
|
||||
// TestBed.configureTestingModule({
|
||||
// declarations: [DialogFiltersComponent],
|
||||
// }).compileComponents();
|
||||
// }));
|
||||
//
|
||||
// beforeEach(() => {
|
||||
// fixture = TestBed.createComponent(DialogFiltersComponent);
|
||||
// component = fixture.componentInstance;
|
||||
// fixture.detectChanges();
|
||||
// });
|
||||
//
|
||||
// it('should create', () => {
|
||||
// expect(component).toBeTruthy();
|
||||
// });
|
||||
// });
|
@ -0,0 +1,25 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
import { PaymentsAdditionalFilters } from '../../types/payments-additional-filters';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-dialog-filters',
|
||||
templateUrl: 'dialog-filters.component.html',
|
||||
styleUrls: ['dialog-filters.component.scss'],
|
||||
})
|
||||
export class DialogFiltersComponent {
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: PaymentsAdditionalFilters,
|
||||
private dialogRef: MatDialogRef<DialogFiltersComponent, PaymentsAdditionalFilters>
|
||||
) {}
|
||||
|
||||
clear(): void {
|
||||
// TODO: fix logic. Should not close dialog. Only reset params
|
||||
this.dialogRef.close({});
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
this.dialogRef.close({ anyField: 'some value' });
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './additional-filters.module';
|
||||
export * from './additional-filters.service';
|
@ -0,0 +1,24 @@
|
||||
<ng-container *transloco="let t; scope: 'operations'; read: 'operations.payments.filter'">
|
||||
<dsh-filter
|
||||
[title]="titleValues ? t('binPan') + ' · ' + titleValues : t('binPan')"
|
||||
[active]="isActive"
|
||||
(opened)="popupOpened()"
|
||||
(closed)="popupClosed()"
|
||||
>
|
||||
<div class="card-bin-pan-filter-content">
|
||||
<dsh-filter-button-content>
|
||||
<form [formGroup]="form" fxLayout="column">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ t('first6') }}</mat-label>
|
||||
<dsh-format-input format="bin" formControlName="bin"></dsh-format-input>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ t('last4') }}</mat-label>
|
||||
<dsh-format-input format="lastDigits" formControlName="pan"></dsh-format-input>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</dsh-filter-button-content>
|
||||
<dsh-filter-button-actions (clear)="clear()" (save)="save()"></dsh-filter-button-actions>
|
||||
</div>
|
||||
</dsh-filter>
|
||||
</ng-container>
|
@ -0,0 +1,7 @@
|
||||
$content-size: 360px;
|
||||
|
||||
.card-bin-pan-filter {
|
||||
&-content {
|
||||
width: $content-size;
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { instance, mock, verify } from 'ts-mockito';
|
||||
|
||||
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
|
||||
import { FilterComponent, FilterModule } from '@dsh/components/filters/filter';
|
||||
import { FormatInputModule } from '@dsh/components/form-controls';
|
||||
import { ComponentChange } from '@dsh/type-utils';
|
||||
|
||||
import { CardBinPanFilterComponent } from './card-bin-pan-filter.component';
|
||||
import { CardBinPan } from './types/card-bin-pan';
|
||||
|
||||
describe('CardBinPanFilterComponent', () => {
|
||||
let component: CardBinPanFilterComponent;
|
||||
let fixture: ComponentFixture<CardBinPanFilterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
getTranslocoModule(),
|
||||
NoopAnimationsModule,
|
||||
FilterModule,
|
||||
MatFormFieldModule,
|
||||
FormatInputModule,
|
||||
ReactiveFormsModule,
|
||||
FlexLayoutModule,
|
||||
],
|
||||
declarations: [CardBinPanFilterComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CardBinPanFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('ngOnChanges', () => {
|
||||
function tickBinPanChanges(
|
||||
binPan: CardBinPan,
|
||||
change: Partial<ComponentChange<CardBinPanFilterComponent, 'binPan'>> = {}
|
||||
): void {
|
||||
component.binPan = binPan;
|
||||
component.ngOnChanges({
|
||||
binPan: {
|
||||
previousValue: undefined,
|
||||
currentValue: component.binPan,
|
||||
firstChange: false,
|
||||
isFirstChange(): boolean {
|
||||
return false;
|
||||
},
|
||||
...change,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tickBinPanChanges(
|
||||
{ bin: null, pan: null },
|
||||
{
|
||||
firstChange: true,
|
||||
isFirstChange(): boolean {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should update on any change', () => {
|
||||
expect(component.titleValues).toBe('');
|
||||
expect(component.form.value).toEqual({ bin: null, pan: null });
|
||||
expect(component.isActive).toBe(false);
|
||||
|
||||
tickBinPanChanges({ bin: '123456', pan: '1234' });
|
||||
|
||||
expect(component.titleValues).toBe('1234 56** **** 1234');
|
||||
expect(component.form.value).toEqual({ bin: '123456', pan: '1234' });
|
||||
expect(component.isActive).toBe(true);
|
||||
});
|
||||
|
||||
describe('update title', () => {
|
||||
it('should format title using values', () => {
|
||||
tickBinPanChanges({ bin: '111122', pan: '4444' });
|
||||
|
||||
expect(component.titleValues).toBe('1111 22** **** 4444');
|
||||
});
|
||||
|
||||
it('should format only bin', () => {
|
||||
tickBinPanChanges({ bin: '123456', pan: null });
|
||||
|
||||
expect(component.titleValues).toBe('1234 56** **** ****');
|
||||
});
|
||||
|
||||
it('should format only pan', () => {
|
||||
tickBinPanChanges({ bin: null, pan: '1234' });
|
||||
|
||||
expect(component.titleValues).toBe('**** **** **** 1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update isActive status', () => {
|
||||
it('should set isActive to true if any of values in binPan exist', () => {
|
||||
tickBinPanChanges({ bin: null, pan: '4444' });
|
||||
|
||||
expect(component.isActive).toBe(true);
|
||||
|
||||
tickBinPanChanges({ bin: '111122', pan: null });
|
||||
|
||||
expect(component.isActive).toBe(true);
|
||||
|
||||
tickBinPanChanges({ bin: '111122', pan: '4444' });
|
||||
|
||||
expect(component.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isActive to true if none of values in binPan exist', () => {
|
||||
tickBinPanChanges({ bin: null, pan: null });
|
||||
|
||||
expect(component.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update form', () => {
|
||||
it('should form using provided nullable binPan values', () => {
|
||||
tickBinPanChanges({ bin: null, pan: null });
|
||||
|
||||
expect(component.form.value).toEqual({ bin: null, pan: null });
|
||||
});
|
||||
|
||||
it('should form using provided only bin binPan values', () => {
|
||||
tickBinPanChanges({ bin: '123456', pan: null });
|
||||
|
||||
expect(component.form.value).toEqual({ bin: '123456', pan: null });
|
||||
});
|
||||
|
||||
it('should form using provided only pan binPan values', () => {
|
||||
tickBinPanChanges({ bin: null, pan: '1234' });
|
||||
|
||||
expect(component.form.value).toEqual({ bin: null, pan: '1234' });
|
||||
});
|
||||
|
||||
it('should form using provided fully binPan values', () => {
|
||||
tickBinPanChanges({ bin: '123456', pan: '1234' });
|
||||
|
||||
expect(component.form.value).toEqual({ bin: '123456', pan: '1234' });
|
||||
});
|
||||
|
||||
it('should form using provided undefined binPan value', () => {
|
||||
tickBinPanChanges(undefined);
|
||||
|
||||
expect(component.form.value).toEqual({ bin: null, pan: null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOpened', () => {
|
||||
it('should update form value', () => {
|
||||
expect(component.form.value).toEqual({ bin: null, pan: null });
|
||||
|
||||
component.binPan = { bin: '123456', pan: '1234' };
|
||||
component.popupOpened();
|
||||
|
||||
expect(component.form.value).toEqual({ bin: '123456', pan: '1234' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClosed', () => {
|
||||
it('should emit filter changed using form values', () => {
|
||||
const spyOnFilterChanged = spyOn(component.filterChanged, 'emit').and.callThrough();
|
||||
|
||||
component.form.setValue({ bin: '123456', pan: null });
|
||||
component.popupClosed();
|
||||
|
||||
expect(spyOnFilterChanged).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnFilterChanged).toHaveBeenCalledWith({ bin: '123456', pan: null });
|
||||
});
|
||||
|
||||
it('should update title and active status using form values', () => {
|
||||
component.form.setValue({ bin: null, pan: '1234' });
|
||||
component.popupClosed();
|
||||
|
||||
expect(component.titleValues).toBe('**** **** **** 1234');
|
||||
expect(component.isActive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSave', () => {
|
||||
it('should close filter component', () => {
|
||||
const mockFilterComponent = mock(FilterComponent);
|
||||
component.filter = instance(mockFilterComponent);
|
||||
|
||||
component.save();
|
||||
|
||||
verify(mockFilterComponent.close()).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClear', () => {
|
||||
it('should reset form data', () => {
|
||||
component.form.setValue({ bin: '123456', pan: '1234' });
|
||||
|
||||
component.clear();
|
||||
|
||||
expect(component.form.value).toEqual({ bin: null, pan: null });
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,99 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import isObject from 'lodash.isobject';
|
||||
|
||||
import { makeMaskedCardEnd, makeMaskedCardStart, splitCardNumber } from '@dsh/app/shared/utils/card-formatter';
|
||||
import { FilterComponent } from '@dsh/components/filters/filter';
|
||||
import { binValidator, lastDigitsValidator } from '@dsh/components/form-controls';
|
||||
import { ComponentChanges } from '@dsh/type-utils';
|
||||
|
||||
import { CardBinPan } from './types/card-bin-pan';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-card-bin-pan-filter',
|
||||
templateUrl: './card-bin-pan-filter.component.html',
|
||||
styleUrls: ['./card-bin-pan-filter.component.scss'],
|
||||
})
|
||||
export class CardBinPanFilterComponent implements OnChanges {
|
||||
@ViewChild(FilterComponent) filter: FilterComponent;
|
||||
|
||||
@Input() binPan: Partial<CardBinPan>;
|
||||
|
||||
@Output() filterChanged = new EventEmitter<Partial<CardBinPan>>();
|
||||
|
||||
form: FormGroup<CardBinPan> = this.formBuilder.group({
|
||||
bin: [null, binValidator],
|
||||
pan: [null, lastDigitsValidator],
|
||||
});
|
||||
|
||||
titleValues: string;
|
||||
isActive = false;
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
ngOnChanges(changes: ComponentChanges<CardBinPanFilterComponent>): void {
|
||||
if (isObject(changes.binPan)) {
|
||||
this.updateFilterForm(changes.binPan.currentValue);
|
||||
this.updateBadgePresentation();
|
||||
}
|
||||
}
|
||||
|
||||
popupOpened(): void {
|
||||
this.updateFilterForm(this.binPan);
|
||||
}
|
||||
|
||||
popupClosed(): void {
|
||||
this.saveFilterData();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.filter.close();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.clearForm();
|
||||
}
|
||||
|
||||
private saveFilterData(): void {
|
||||
this.updateBadgePresentation();
|
||||
this.filterChanged.emit(this.form.value);
|
||||
}
|
||||
|
||||
private updateFilterForm(binPan: Partial<CardBinPan> | undefined): void {
|
||||
const { bin = null, pan = null } = binPan ?? {};
|
||||
this.form.setValue({
|
||||
bin,
|
||||
pan,
|
||||
});
|
||||
}
|
||||
|
||||
private updateBadgePresentation(): void {
|
||||
this.updateTitleValues();
|
||||
this.updateActiveStatus();
|
||||
}
|
||||
|
||||
private updateActiveStatus(): void {
|
||||
const { bin, pan } = this.form.value;
|
||||
this.isActive = Boolean(bin) || Boolean(pan);
|
||||
}
|
||||
|
||||
private updateTitleValues(): void {
|
||||
const { bin, pan } = this.form.controls;
|
||||
const binString = bin.valid && Boolean(bin.value) ? bin.value : '';
|
||||
const panString = pan.valid && Boolean(pan.value) ? pan.value : '';
|
||||
const maskedBinPart = makeMaskedCardStart(binString, 12);
|
||||
const maskedPanPart = makeMaskedCardEnd(panString, 4);
|
||||
const filterValues = splitCardNumber(`${maskedBinPart}${maskedPanPart}`);
|
||||
|
||||
this.titleValues = Boolean(binString) || Boolean(panString) ? `${filterValues}` : '';
|
||||
}
|
||||
|
||||
private clearForm(): void {
|
||||
this.updateFilterForm({
|
||||
bin: null,
|
||||
pan: null,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { FilterModule } from '@dsh/components/filters/filter';
|
||||
import { FormatInputModule } from '@dsh/components/form-controls';
|
||||
|
||||
import { CardBinPanFilterComponent } from './card-bin-pan-filter.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FilterModule,
|
||||
MatFormFieldModule,
|
||||
FormatInputModule,
|
||||
ReactiveFormsModule,
|
||||
TranslocoModule,
|
||||
FlexLayoutModule,
|
||||
],
|
||||
declarations: [CardBinPanFilterComponent],
|
||||
exports: [CardBinPanFilterComponent],
|
||||
})
|
||||
export class CardBinPanFilterModule {}
|
@ -0,0 +1,2 @@
|
||||
export const BIN_LENGTH = 6;
|
||||
export const PAN_LENGTH = 4;
|
@ -0,0 +1,4 @@
|
||||
export * from './card-bin-pan-filter.module';
|
||||
export * from './card-bin-pan-filter.component';
|
||||
export * from './types/card-bin-pan';
|
||||
export { BIN_LENGTH, PAN_LENGTH } from './consts';
|
@ -0,0 +1,4 @@
|
||||
export interface CardBinPan {
|
||||
bin: string;
|
||||
pan: string;
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './payments-filters.module';
|
||||
export * from './payments-filters.component';
|
@ -0,0 +1,27 @@
|
||||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
|
||||
<ng-container *ngIf="filtersData$ | async as filters">
|
||||
<dsh-daterange-filter
|
||||
[selected]="filters.daterange"
|
||||
(selectedChange)="dateRangeChange($event)"
|
||||
></dsh-daterange-filter>
|
||||
<dsh-invoices-filter
|
||||
[selected]="filters.invoiceIDs"
|
||||
(selectedChange)="invoiceSelectionChange($event)"
|
||||
></dsh-invoices-filter>
|
||||
<dsh-filter-shops
|
||||
[shops]="shops$ | async"
|
||||
[selected]="selectedShops$ | async"
|
||||
(selectedChange)="shopSelectionChange($event)"
|
||||
></dsh-filter-shops>
|
||||
<dsh-card-bin-pan-filter
|
||||
[binPan]="filters.binPan"
|
||||
(filterChanged)="binPanChanged($event)"
|
||||
></dsh-card-bin-pan-filter>
|
||||
<!-- hidden till additional filters would be supported-->
|
||||
<!-- <ng-container *transloco="let t; read: 'filters'">-->
|
||||
<!-- <dsh-filter-button [active]="isAdditionalFilterApplied" (click)="openFiltersDialog()">-->
|
||||
<!-- {{ t('additionalFilters') }}-->
|
||||
<!-- </dsh-filter-button>-->
|
||||
<!-- </ng-container>-->
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,383 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import moment from 'moment';
|
||||
import { of } from 'rxjs';
|
||||
import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { getTranslocoModule } from '@dsh/app/shared/tests/get-transloco-module';
|
||||
|
||||
import { AdditionalFiltersService } from './additional-filters';
|
||||
import { CardBinPanFilterModule } from './card-bin-pan-filter';
|
||||
import { PaymentsFiltersComponent } from './payments-filters.component';
|
||||
import { PaymentsFiltersService } from './services/payments-filters/payments-filters.service';
|
||||
import { ShopsSelectionManagerService } from './services/shops-selection-manager/shops-selection-manager.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-daterange-filter',
|
||||
template: '',
|
||||
})
|
||||
class MockDaterangeFilterComponent {
|
||||
@Input() selected;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-invoices-filter',
|
||||
template: '',
|
||||
})
|
||||
class MockInvoicesFilterComponent {
|
||||
@Input() selected;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-filter-shops',
|
||||
template: '',
|
||||
})
|
||||
class MockFilterShopsComponent {
|
||||
@Input() selected;
|
||||
@Input() shops;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-filter-button',
|
||||
template: '',
|
||||
})
|
||||
class MockFilterButtonComponent {
|
||||
@Input() active;
|
||||
}
|
||||
|
||||
describe('PaymentsFiltersComponent', () => {
|
||||
let component: PaymentsFiltersComponent;
|
||||
let fixture: ComponentFixture<PaymentsFiltersComponent>;
|
||||
let mockShopsSelectionManagerService: ShopsSelectionManagerService;
|
||||
let mockPaymentsFiltersService: PaymentsFiltersService;
|
||||
let mockAdditionalFiltersService: AdditionalFiltersService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockShopsSelectionManagerService = mock(ShopsSelectionManagerService);
|
||||
mockPaymentsFiltersService = mock(PaymentsFiltersService);
|
||||
mockAdditionalFiltersService = mock(AdditionalFiltersService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockPaymentsFiltersService.filtersData$).thenReturn(
|
||||
of({
|
||||
daterange: {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
async function createComponent() {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, getTranslocoModule(), FlexLayoutModule, CardBinPanFilterModule],
|
||||
declarations: [
|
||||
MockDaterangeFilterComponent,
|
||||
MockInvoicesFilterComponent,
|
||||
MockFilterShopsComponent,
|
||||
MockFilterButtonComponent,
|
||||
PaymentsFiltersComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ShopsSelectionManagerService,
|
||||
useFactory: () => instance(mockShopsSelectionManagerService),
|
||||
},
|
||||
{
|
||||
provide: PaymentsFiltersService,
|
||||
useFactory: () => instance(mockPaymentsFiltersService),
|
||||
},
|
||||
{
|
||||
provide: AdditionalFiltersService,
|
||||
useFactory: () => instance(mockAdditionalFiltersService),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(PaymentsFiltersComponent);
|
||||
component = fixture.componentInstance;
|
||||
}
|
||||
|
||||
describe('creation', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should emit filters changed event on filters data change', async () => {
|
||||
const filtersData = {
|
||||
daterange: {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
},
|
||||
};
|
||||
|
||||
when(mockPaymentsFiltersService.filtersData$).thenReturn(of(filtersData));
|
||||
|
||||
await createComponent();
|
||||
|
||||
const spyOnFiltersChanged = spyOn(component.filtersChanged, 'emit').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(spyOnFiltersChanged).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnFiltersChanged).toHaveBeenCalledWith(filtersData);
|
||||
});
|
||||
|
||||
it('should update selected ids on filters data change', async () => {
|
||||
const filtersData = {
|
||||
daterange: {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
},
|
||||
shopIDs: ['id_one', 'id_two'],
|
||||
};
|
||||
|
||||
when(mockPaymentsFiltersService.filtersData$).thenReturn(of(filtersData));
|
||||
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
verify(mockShopsSelectionManagerService.setSelectedIds(deepEqual(['id_one', 'id_two']))).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnChanges', () => {
|
||||
it('should update tick realm changes', async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
component.ngOnChanges({
|
||||
realm: {
|
||||
previousValue: null,
|
||||
currentValue: PaymentInstitutionRealm.test,
|
||||
isFirstChange(): boolean {
|
||||
return true;
|
||||
},
|
||||
firstChange: true,
|
||||
},
|
||||
});
|
||||
|
||||
verify(mockShopsSelectionManagerService.setRealm(PaymentInstitutionRealm.test)).once();
|
||||
|
||||
component.ngOnChanges({
|
||||
realm: {
|
||||
previousValue: PaymentInstitutionRealm.test,
|
||||
currentValue: PaymentInstitutionRealm.live,
|
||||
isFirstChange(): boolean {
|
||||
return false;
|
||||
},
|
||||
firstChange: false,
|
||||
},
|
||||
});
|
||||
|
||||
verify(mockShopsSelectionManagerService.setRealm(PaymentInstitutionRealm.live)).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('openFiltersDialog', () => {
|
||||
beforeEach(() => {
|
||||
when(mockPaymentsFiltersService.filtersData$).thenReturn(
|
||||
of({
|
||||
daterange: {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
},
|
||||
additional: {
|
||||
myProperty: null,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should open dialog', async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
when(
|
||||
mockAdditionalFiltersService.openFiltersDialog(
|
||||
deepEqual({
|
||||
myProperty: null,
|
||||
})
|
||||
)
|
||||
).thenReturn(of());
|
||||
|
||||
component.openFiltersDialog();
|
||||
|
||||
verify(
|
||||
mockAdditionalFiltersService.openFiltersDialog(
|
||||
deepEqual({
|
||||
myProperty: null,
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
|
||||
it('should change isAdditionalFilterApplied using response from dialog', async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
|
||||
when(
|
||||
mockAdditionalFiltersService.openFiltersDialog(
|
||||
deepEqual({
|
||||
myProperty: null,
|
||||
})
|
||||
)
|
||||
).thenReturn(of({}));
|
||||
|
||||
component.openFiltersDialog();
|
||||
|
||||
expect(component.isAdditionalFilterApplied).toBe(false);
|
||||
|
||||
when(
|
||||
mockAdditionalFiltersService.openFiltersDialog(
|
||||
deepEqual({
|
||||
myProperty: null,
|
||||
})
|
||||
)
|
||||
).thenReturn(
|
||||
of({
|
||||
something: null,
|
||||
})
|
||||
);
|
||||
|
||||
component.openFiltersDialog();
|
||||
|
||||
expect(component.isAdditionalFilterApplied).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateRangeChange', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should tick filters changes', () => {
|
||||
const daterange = {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
};
|
||||
component.dateRangeChange(daterange);
|
||||
|
||||
verify(
|
||||
mockPaymentsFiltersService.changeFilters(
|
||||
deepEqual({
|
||||
daterange,
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invoiceSelectionChange', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should tick filters change', () => {
|
||||
component.invoiceSelectionChange(['invoice_id', 'another_invoice_id']);
|
||||
|
||||
verify(
|
||||
mockPaymentsFiltersService.changeFilters(
|
||||
deepEqual({
|
||||
invoiceIDs: ['invoice_id', 'another_invoice_id'],
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('shopSelectionChange', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should tick filters change', () => {
|
||||
const testList = new Array(4)
|
||||
.fill({
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
isBlocked: false,
|
||||
isSuspended: false,
|
||||
categoryID: 1,
|
||||
location: {
|
||||
locationType: 'type',
|
||||
},
|
||||
details: {
|
||||
name: 'my name',
|
||||
description: 'some description',
|
||||
},
|
||||
contractID: 'contractID',
|
||||
payoutToolID: 'payoutToolID',
|
||||
scheduleID: 1,
|
||||
account: {
|
||||
currency: 'USD',
|
||||
guaranteeID: 2,
|
||||
settlementID: 2,
|
||||
},
|
||||
})
|
||||
.map((el: Shop, i: number) => {
|
||||
return {
|
||||
...el,
|
||||
id: `test_id_${i}`,
|
||||
};
|
||||
});
|
||||
|
||||
component.shopSelectionChange(testList);
|
||||
|
||||
verify(
|
||||
mockPaymentsFiltersService.changeFilters(
|
||||
deepEqual({
|
||||
shopIDs: ['test_id_0', 'test_id_1', 'test_id_2', 'test_id_3'],
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
|
||||
describe('binPanChanged', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should tick filters change', () => {
|
||||
component.binPanChanged({
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
});
|
||||
|
||||
verify(
|
||||
mockPaymentsFiltersService.changeFilters(
|
||||
deepEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,110 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import isNil from 'lodash.isnil';
|
||||
import isObject from 'lodash.isobject';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
import { ComponentChange, ComponentChanges } from '@dsh/type-utils';
|
||||
|
||||
import { AdditionalFiltersService } from './additional-filters';
|
||||
import { CardBinPan } from './card-bin-pan-filter';
|
||||
import { PaymentsFiltersService } from './services/payments-filters/payments-filters.service';
|
||||
import { ShopsSelectionManagerService } from './services/shops-selection-manager/shops-selection-manager.service';
|
||||
import { PaymentsAdditionalFilters } from './types/payments-additional-filters';
|
||||
import { PaymentsFiltersData } from './types/payments-filters-data';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'dsh-payments-filters',
|
||||
templateUrl: 'payments-filters.component.html',
|
||||
})
|
||||
export class PaymentsFiltersComponent implements OnInit, OnChanges {
|
||||
@Input() realm: PaymentInstitutionRealm;
|
||||
|
||||
@Output() filtersChanged = new EventEmitter<PaymentsFiltersData>();
|
||||
|
||||
filtersData$: Observable<PaymentsFiltersData> = this.filtersHandler.filtersData$;
|
||||
|
||||
isAdditionalFilterApplied: boolean;
|
||||
|
||||
shops$: Observable<Shop[]> = this.shopService.shops$;
|
||||
selectedShops$: Observable<Shop[]> = this.shopService.selectedShops$;
|
||||
|
||||
constructor(
|
||||
private shopService: ShopsSelectionManagerService,
|
||||
private filtersHandler: PaymentsFiltersService,
|
||||
private additionalFilters: AdditionalFiltersService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filtersData$.pipe(untilDestroyed(this)).subscribe((filtersData: PaymentsFiltersData) => {
|
||||
this.filtersChanged.emit(filtersData);
|
||||
const { shopIDs = [] } = filtersData;
|
||||
this.shopService.setSelectedIds(shopIDs);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: ComponentChanges<PaymentsFiltersComponent>): void {
|
||||
if (isObject(changes.realm)) {
|
||||
this.updateRealm(changes.realm);
|
||||
}
|
||||
}
|
||||
|
||||
openFiltersDialog(): void {
|
||||
this.filtersData$
|
||||
.pipe(
|
||||
take(1),
|
||||
map((filtersData: PaymentsFiltersData) => filtersData.additional),
|
||||
switchMap((filters: PaymentsAdditionalFilters) => {
|
||||
return this.additionalFilters.openFiltersDialog(filters);
|
||||
}),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((filters: PaymentsAdditionalFilters) => {
|
||||
this.isAdditionalFilterApplied = !isEmpty(filters);
|
||||
});
|
||||
}
|
||||
|
||||
dateRangeChange(dateRange: Daterange): void {
|
||||
this.updateFilters({ daterange: dateRange });
|
||||
}
|
||||
|
||||
invoiceSelectionChange(invoiceIds: string[]): void {
|
||||
this.updateFilters({ invoiceIDs: invoiceIds });
|
||||
}
|
||||
|
||||
shopSelectionChange(selectedShops: Shop[]): void {
|
||||
this.updateFilters({
|
||||
shopIDs: selectedShops.map(({ id }: Shop) => id),
|
||||
});
|
||||
}
|
||||
|
||||
binPanChanged(binPan: Partial<CardBinPan>): void {
|
||||
this.updateFilters({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
...binPan,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private updateRealm(change: ComponentChange<PaymentsFiltersComponent, 'realm'>): void {
|
||||
const realm = change.currentValue;
|
||||
if (isNil(realm)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shopService.setRealm(realm);
|
||||
}
|
||||
|
||||
private updateFilters(change: Partial<PaymentsFiltersData>): void {
|
||||
this.filtersHandler.changeFilters({
|
||||
...change,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { FilterShopsModule, InvoicesFilterModule } from '@dsh/app/shared/components';
|
||||
import { DaterangeManagerModule } from '@dsh/app/shared/services/date-range-manager';
|
||||
import { DaterangeFilterModule } from '@dsh/components/filters/daterange-filter';
|
||||
import { FilterModule } from '@dsh/components/filters/filter';
|
||||
|
||||
import { AdditionalFiltersModule } from './additional-filters';
|
||||
import { CardBinPanFilterModule } from './card-bin-pan-filter';
|
||||
import { PaymentsFiltersComponent } from './payments-filters.component';
|
||||
import { PaymentsFiltersStoreService } from './services/payments-filters-store/payments-filters-store.service';
|
||||
import { PaymentsFiltersService } from './services/payments-filters/payments-filters.service';
|
||||
import { ShopsSelectionManagerService } from './services/shops-selection-manager/shops-selection-manager.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
AdditionalFiltersModule,
|
||||
TranslocoModule,
|
||||
FlexLayoutModule,
|
||||
FilterModule,
|
||||
DaterangeFilterModule,
|
||||
FilterShopsModule,
|
||||
InvoicesFilterModule,
|
||||
DaterangeManagerModule,
|
||||
CardBinPanFilterModule,
|
||||
],
|
||||
declarations: [PaymentsFiltersComponent],
|
||||
exports: [PaymentsFiltersComponent],
|
||||
providers: [PaymentsFiltersService, PaymentsFiltersStoreService, ShopsSelectionManagerService],
|
||||
})
|
||||
export class PaymentsFiltersModule {}
|
@ -0,0 +1,333 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { of } from 'rxjs';
|
||||
import { deepEqual, instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { DaterangeManagerService } from '@dsh/app/shared/services/date-range-manager';
|
||||
|
||||
import { PaymentsFiltersStoreService } from './payments-filters-store.service';
|
||||
|
||||
describe('PaymentsFiltersStoreService', () => {
|
||||
let service: PaymentsFiltersStoreService;
|
||||
let mockDaterangeManagerService: DaterangeManagerService;
|
||||
let mockRouter: Router;
|
||||
let mockActivatedRoute: ActivatedRoute;
|
||||
|
||||
const daterange = {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
};
|
||||
const formattedDaterange = {
|
||||
begin: daterange.begin.format(),
|
||||
end: daterange.end.format(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockDaterangeManagerService = mock(DaterangeManagerService);
|
||||
mockRouter = mock(Router);
|
||||
mockActivatedRoute = mock(ActivatedRoute);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockActivatedRoute.queryParams).thenReturn(of({}));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockDaterangeManagerService.serializeDateRange(deepEqual(daterange))).thenReturn(formattedDaterange);
|
||||
when(mockDaterangeManagerService.deserializeDateRange(deepEqual(formattedDaterange))).thenReturn(daterange);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
PaymentsFiltersStoreService,
|
||||
{
|
||||
provide: DaterangeManagerService,
|
||||
useFactory: () => instance(mockDaterangeManagerService),
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useFactory: () => instance(mockRouter),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useFactory: () => instance(mockActivatedRoute),
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(PaymentsFiltersStoreService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('mapToData', () => {
|
||||
it('should format daterange params', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
fromTime: daterange.begin.format(),
|
||||
toTime: daterange.end.format(),
|
||||
})
|
||||
).toEqual({
|
||||
daterange,
|
||||
});
|
||||
});
|
||||
|
||||
it('should format invoices and shops ids params', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
shopIDs: ['shop-id-1', 'shop-id-2'],
|
||||
invoiceIDs: ['invoice-id-1', 'invoice-id-2'],
|
||||
})
|
||||
).toEqual({
|
||||
shopIDs: ['shop-id-1', 'shop-id-2'],
|
||||
invoiceIDs: ['invoice-id-1', 'invoice-id-2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should format invoices and shops single id params', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
shopIDs: 'shop-id',
|
||||
invoiceIDs: 'invoice-id',
|
||||
})
|
||||
).toEqual({
|
||||
shopIDs: ['shop-id'],
|
||||
invoiceIDs: ['invoice-id'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse binPan fully', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '123456',
|
||||
last4: '1234',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: '1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create binPan object if there is no first6 and last4 exists', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
invoiceIDs: 'invoice-id',
|
||||
})
|
||||
).toEqual({
|
||||
invoiceIDs: ['invoice-id'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse bin if first6 has 6 numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '123456',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse pan if last4 has 4 numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
last4: '1234',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: '1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse bin if it has non-numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '12345a',
|
||||
last4: '1234',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: '1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse pan if it has non-numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '123456',
|
||||
last4: '123a',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse bin if it not 6 numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '12345',
|
||||
last4: '1234',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: '1234',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '1234567',
|
||||
last4: '1234',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: '1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse pan if it not 4 numeric symbols', () => {
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '123456',
|
||||
last4: '12345',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
service.mapToData({
|
||||
first6: '123456',
|
||||
last4: '123',
|
||||
})
|
||||
).toEqual({
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty values as params', () => {
|
||||
expect(service.mapToData({})).toEqual({});
|
||||
expect(service.mapToData({ shopIDs: '' })).toEqual({});
|
||||
expect(service.mapToData({ invoiceIDs: '' })).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapToParams', () => {
|
||||
it('should not set empty params', () => {
|
||||
expect(
|
||||
service.mapToParams({
|
||||
daterange,
|
||||
shopIDs: [],
|
||||
invoiceIDs: ['my-invoice-id'],
|
||||
additional: {
|
||||
mine: {},
|
||||
another: null,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
fromTime: formattedDaterange.begin,
|
||||
toTime: formattedDaterange.end,
|
||||
invoiceIDs: ['my-invoice-id'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform bin in first6', () => {
|
||||
expect(
|
||||
service.mapToParams({
|
||||
daterange,
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: null,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
fromTime: formattedDaterange.begin,
|
||||
toTime: formattedDaterange.end,
|
||||
first6: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform pan in last4', () => {
|
||||
expect(
|
||||
service.mapToParams({
|
||||
daterange,
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: '1234',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
fromTime: formattedDaterange.begin,
|
||||
toTime: formattedDaterange.end,
|
||||
last4: '1234',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform binPan data in first6 and last4', () => {
|
||||
expect(
|
||||
service.mapToParams({
|
||||
daterange,
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: '123456',
|
||||
pan: '1234',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
fromTime: formattedDaterange.begin,
|
||||
toTime: formattedDaterange.end,
|
||||
first6: '123456',
|
||||
last4: '1234',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove binPan from params if bin and pan empty', () => {
|
||||
expect(
|
||||
service.mapToParams({
|
||||
daterange,
|
||||
binPan: {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: null,
|
||||
pan: null,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
fromTime: formattedDaterange.begin,
|
||||
toTime: formattedDaterange.end,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,95 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import isNil from 'lodash.isnil';
|
||||
import isString from 'lodash.isstring';
|
||||
import pickBy from 'lodash.pickby';
|
||||
|
||||
import { QueryParamsStore } from '@dsh/app/shared/services';
|
||||
import { DaterangeManagerService } from '@dsh/app/shared/services/date-range-manager';
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
import { wrapValuesToArray } from '@dsh/utils';
|
||||
|
||||
import { BIN_LENGTH, PAN_LENGTH } from '../../card-bin-pan-filter';
|
||||
import { PaymentsFiltersData } from '../../types/payments-filters-data';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsFiltersStoreService extends QueryParamsStore<PaymentsFiltersData> {
|
||||
constructor(
|
||||
private daterangeManager: DaterangeManagerService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute
|
||||
) {
|
||||
super(router, route);
|
||||
}
|
||||
|
||||
mapToData(params: Params): Partial<PaymentsFiltersData> {
|
||||
const { fromTime, toTime, ...restParams } = params;
|
||||
|
||||
return this.removeUnusedFields({
|
||||
daterange: this.formatDaterange(fromTime, toTime),
|
||||
binPan: this.getBinPanParams(params),
|
||||
...this.getListParams(restParams),
|
||||
});
|
||||
}
|
||||
|
||||
mapToParams({ daterange, binPan, additional, ...restData }: PaymentsFiltersData): Params {
|
||||
const { begin: fromTime, end: toTime } = this.daterangeManager.serializeDateRange(daterange);
|
||||
const { bin = null, pan = null } = binPan ?? {};
|
||||
|
||||
return this.removeUnusedFields({
|
||||
fromTime,
|
||||
toTime,
|
||||
first6: bin,
|
||||
last4: pan,
|
||||
...additional,
|
||||
...restData,
|
||||
});
|
||||
}
|
||||
|
||||
private removeUnusedFields<T>(data: T): T | Partial<T> {
|
||||
return Object.entries(data).reduce((newData: T | Partial<T>, [key, value]: [string, any]) => {
|
||||
if (!isEmpty(value)) {
|
||||
newData[key] = value;
|
||||
}
|
||||
return newData;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private getBinPanParams({ first6, last4 }: Params): PaymentsFiltersData['binPan'] | null {
|
||||
const bin = Number(first6);
|
||||
const pan = Number(last4);
|
||||
const isValidBin = !isNil(first6) && !isNaN(bin) && first6.length === BIN_LENGTH;
|
||||
const isValidPan = !isNil(last4) && !isNaN(pan) && last4.length === PAN_LENGTH;
|
||||
|
||||
if (isValidBin || isValidPan) {
|
||||
return {
|
||||
paymentMethod: 'bankCard',
|
||||
bin: isValidBin ? first6 : null,
|
||||
pan: isValidPan ? last4 : null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getListParams(params: Params): Partial<PaymentsFiltersData> {
|
||||
const nonEmptyListParams = pickBy(
|
||||
params,
|
||||
(value: unknown, key: keyof PaymentsFiltersData) =>
|
||||
['shopIDs', 'invoiceIDs'].includes(key) && !isEmpty(value)
|
||||
);
|
||||
const stringListParams = pickBy(nonEmptyListParams, (value: unknown) => isString(value));
|
||||
|
||||
return {
|
||||
...nonEmptyListParams,
|
||||
...wrapValuesToArray(stringListParams),
|
||||
};
|
||||
}
|
||||
|
||||
private formatDaterange(fromTime: string | undefined, toTime: string | undefined): Daterange | null {
|
||||
return isNil(fromTime) || isNil(toTime)
|
||||
? null
|
||||
: this.daterangeManager.deserializeDateRange({ begin: fromTime, end: toTime });
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import moment from 'moment';
|
||||
import { of } from 'rxjs';
|
||||
import { deepEqual, instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { DaterangeManagerService } from '@dsh/app/shared/services/date-range-manager';
|
||||
|
||||
import { PaymentsFiltersStoreService } from '../payments-filters-store/payments-filters-store.service';
|
||||
import { PaymentsFiltersService } from './payments-filters.service';
|
||||
|
||||
describe('PaymentsFiltersService', () => {
|
||||
let service: PaymentsFiltersService;
|
||||
let mockDaterangeManagerService: DaterangeManagerService;
|
||||
let mockPaymentsFiltersStoreService: PaymentsFiltersStoreService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDaterangeManagerService = mock(DaterangeManagerService);
|
||||
mockPaymentsFiltersStoreService = mock(PaymentsFiltersStoreService);
|
||||
});
|
||||
|
||||
async function configureTestingModule() {
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
PaymentsFiltersService,
|
||||
{
|
||||
provide: DaterangeManagerService,
|
||||
useFactory: () => instance(mockDaterangeManagerService),
|
||||
},
|
||||
{
|
||||
provide: PaymentsFiltersStoreService,
|
||||
useFactory: () => instance(mockPaymentsFiltersStoreService),
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(PaymentsFiltersService);
|
||||
}
|
||||
|
||||
describe('creation', () => {
|
||||
it('should be created', async () => {
|
||||
const defaultDateRange = {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
};
|
||||
|
||||
when(mockDaterangeManagerService.defaultDaterange).thenReturn(defaultDateRange);
|
||||
when(mockPaymentsFiltersStoreService.data$).thenReturn(of({}));
|
||||
|
||||
await configureTestingModule();
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
const defaultDateRange = {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockDaterangeManagerService.defaultDaterange).thenReturn(defaultDateRange);
|
||||
});
|
||||
|
||||
it('should merge default daterange and query params data', async () => {
|
||||
when(mockPaymentsFiltersStoreService.data$).thenReturn(
|
||||
of({
|
||||
shopIDs: [],
|
||||
})
|
||||
);
|
||||
|
||||
const expected$ = cold('(a|)', {
|
||||
a: {
|
||||
daterange: defaultDateRange,
|
||||
shopIDs: [],
|
||||
},
|
||||
});
|
||||
|
||||
await configureTestingModule();
|
||||
|
||||
expect(service.filtersData$).toBeObservable(expected$);
|
||||
});
|
||||
|
||||
it('should rewrite default daterange with query params data', async () => {
|
||||
const storeDaterange = {
|
||||
begin: moment().startOf('m'),
|
||||
end: moment().endOf('m'),
|
||||
};
|
||||
|
||||
when(mockPaymentsFiltersStoreService.data$).thenReturn(
|
||||
of({
|
||||
daterange: storeDaterange,
|
||||
shopIDs: [],
|
||||
})
|
||||
);
|
||||
|
||||
const expected$ = cold('(a|)', {
|
||||
a: {
|
||||
daterange: storeDaterange,
|
||||
shopIDs: [],
|
||||
},
|
||||
});
|
||||
|
||||
await configureTestingModule();
|
||||
|
||||
expect(service.filtersData$).toBeObservable(expected$);
|
||||
});
|
||||
|
||||
it('should use default daterange if query params data has none', async () => {
|
||||
when(mockPaymentsFiltersStoreService.data$).thenReturn(
|
||||
of({
|
||||
shopIDs: ['shop-id'],
|
||||
invoiceIDs: ['invoice-id'],
|
||||
})
|
||||
);
|
||||
|
||||
const expected$ = cold('(a|)', {
|
||||
a: {
|
||||
daterange: defaultDateRange,
|
||||
shopIDs: ['shop-id'],
|
||||
invoiceIDs: ['invoice-id'],
|
||||
},
|
||||
});
|
||||
|
||||
await configureTestingModule();
|
||||
|
||||
expect(service.filtersData$).toBeObservable(expected$);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeFilters', () => {
|
||||
const defaultDateRange = {
|
||||
begin: moment(),
|
||||
end: moment(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
when(mockDaterangeManagerService.defaultDaterange).thenReturn(defaultDateRange);
|
||||
when(mockPaymentsFiltersStoreService.data$).thenReturn(
|
||||
of({
|
||||
shopIDs: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should update properties of existing filters data', async () => {
|
||||
await configureTestingModule();
|
||||
|
||||
service.changeFilters({
|
||||
shopIDs: ['mine'],
|
||||
});
|
||||
|
||||
verify(
|
||||
mockPaymentsFiltersStoreService.preserve(
|
||||
deepEqual({
|
||||
daterange: defaultDateRange,
|
||||
shopIDs: ['mine'],
|
||||
})
|
||||
)
|
||||
).once();
|
||||
expect().nothing();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
import { DaterangeManagerService } from '@dsh/app/shared/services/date-range-manager';
|
||||
|
||||
import { PaymentsFiltersData } from '../../types/payments-filters-data';
|
||||
import { PaymentsFiltersStoreService } from '../payments-filters-store/payments-filters-store.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable()
|
||||
export class PaymentsFiltersService {
|
||||
filtersData$: Observable<PaymentsFiltersData>;
|
||||
|
||||
private filtersChange$ = new ReplaySubject<Partial<PaymentsFiltersData>>(1);
|
||||
|
||||
constructor(
|
||||
private daterangeManager: DaterangeManagerService,
|
||||
private filtersParamsStore: PaymentsFiltersStoreService
|
||||
) {
|
||||
this.initFiltersData();
|
||||
this.initUpdatesData();
|
||||
}
|
||||
|
||||
changeFilters(dataChange: Partial<PaymentsFiltersData>): void {
|
||||
this.filtersChange$.next(dataChange);
|
||||
}
|
||||
|
||||
private initFiltersData(): void {
|
||||
this.filtersData$ = this.filtersParamsStore.data$.pipe(
|
||||
map((storeData: Partial<PaymentsFiltersData>) => {
|
||||
return {
|
||||
daterange: this.daterangeManager.defaultDaterange,
|
||||
...storeData,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private initUpdatesData(): void {
|
||||
this.filtersChange$
|
||||
.pipe(
|
||||
withLatestFrom(this.filtersData$),
|
||||
map(([dataChange, filtersData]: [Partial<PaymentsFiltersData>, PaymentsFiltersData]) => {
|
||||
return {
|
||||
...filtersData,
|
||||
...dataChange,
|
||||
};
|
||||
}),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((updatedData: PaymentsFiltersData) => {
|
||||
this.filtersParamsStore.preserve(updatedData);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
|
||||
import { ShopsSelectionManagerService } from './shops-selection-manager.service';
|
||||
|
||||
describe('ShopsSelectionManagerService', () => {
|
||||
let service: ShopsSelectionManagerService;
|
||||
let mockApiShopsService: ApiShopsService;
|
||||
|
||||
function createService() {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ShopsSelectionManagerService,
|
||||
{
|
||||
provide: ApiShopsService,
|
||||
useFactory: () => instance(mockApiShopsService),
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(ShopsSelectionManagerService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiShopsService = mock(ApiShopsService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const testList = new Array(12)
|
||||
.fill({
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
isBlocked: false,
|
||||
isSuspended: false,
|
||||
categoryID: 0,
|
||||
location: {
|
||||
locationType: 'type',
|
||||
},
|
||||
details: {
|
||||
name: 'my name',
|
||||
description: 'some description',
|
||||
},
|
||||
contractID: 'contractID',
|
||||
payoutToolID: 'payoutToolID',
|
||||
scheduleID: 1,
|
||||
account: {
|
||||
currency: 'USD',
|
||||
guaranteeID: 2,
|
||||
settlementID: 2,
|
||||
},
|
||||
})
|
||||
.map((el: Shop, i: number) => {
|
||||
const isOdd = i % 2 === 0;
|
||||
return {
|
||||
...el,
|
||||
id: isOdd ? `test_id_${i}` : `live_id_${i}`,
|
||||
categoryID: isOdd ? 1 : 2,
|
||||
};
|
||||
});
|
||||
when(mockApiShopsService.shops$).thenReturn(of(testList));
|
||||
});
|
||||
|
||||
describe('creation', () => {
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRealm', () => {
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
|
||||
it('should tick shops after realm was inited', () => {
|
||||
const beforeInit$ = hot('^', {});
|
||||
|
||||
expect(service.shops$.pipe(map((shops: Shop[]) => shops.map(({ id }) => id)))).toBeObservable(beforeInit$);
|
||||
|
||||
const afterInit$ = hot('a', {
|
||||
a: ['test_id_0', 'test_id_2', 'test_id_4', 'test_id_6', 'test_id_8', 'test_id_10'],
|
||||
});
|
||||
|
||||
service.setRealm(PaymentInstitutionRealm.test);
|
||||
|
||||
expect(service.shops$.pipe(map((shops: Shop[]) => shops.map(({ id }) => id)))).toBeObservable(afterInit$);
|
||||
});
|
||||
|
||||
it('should filter shops by realm even if they changed', () => {
|
||||
const testRealm$ = hot('a', {
|
||||
a: ['test_id_0', 'test_id_2', 'test_id_4', 'test_id_6', 'test_id_8', 'test_id_10'],
|
||||
});
|
||||
|
||||
service.setRealm(PaymentInstitutionRealm.test);
|
||||
|
||||
expect(service.shops$.pipe(map((shops: Shop[]) => shops.map(({ id }) => id)))).toBeObservable(testRealm$);
|
||||
|
||||
const liveRealm$ = hot('a', {
|
||||
a: ['live_id_1', 'live_id_3', 'live_id_5', 'live_id_7', 'live_id_9', 'live_id_11'],
|
||||
});
|
||||
|
||||
service.setRealm(PaymentInstitutionRealm.live);
|
||||
|
||||
expect(service.shops$.pipe(map((shops: Shop[]) => shops.map(({ id }) => id)))).toBeObservable(liveRealm$);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedIds', () => {
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
|
||||
it('should change selected list using selected ids', () => {
|
||||
const liveRealm$ = hot('a', {
|
||||
a: ['live_id_3', 'live_id_5'],
|
||||
});
|
||||
|
||||
service.setRealm(PaymentInstitutionRealm.live);
|
||||
service.setSelectedIds(['live_id_3', 'live_id_5']);
|
||||
|
||||
expect(service.selectedShops$.pipe(map((shops: Shop[]) => shops.map(({ id }) => id)))).toBeObservable(
|
||||
liveRealm$
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { Shop } from '@dsh/api-codegen/capi';
|
||||
import { PaymentInstitutionRealm } from '@dsh/api/model';
|
||||
import { ApiShopsService } from '@dsh/api/shop';
|
||||
|
||||
import { filterShopsByRealm } from '../../../../operators';
|
||||
|
||||
@UntilDestroy()
|
||||
@Injectable()
|
||||
export class ShopsSelectionManagerService {
|
||||
shops$: Observable<Shop[]>;
|
||||
selectedShops$: Observable<Shop[]>;
|
||||
|
||||
private realmChanges$ = new ReplaySubject<PaymentInstitutionRealm>(1);
|
||||
private selectedIds$ = new ReplaySubject<string[]>(1);
|
||||
|
||||
constructor(private shopService: ApiShopsService) {
|
||||
this.initShops();
|
||||
this.initShopsSelection();
|
||||
}
|
||||
|
||||
setRealm(realm: PaymentInstitutionRealm): void {
|
||||
this.realmChanges$.next(realm);
|
||||
}
|
||||
|
||||
setSelectedIds(selectedIds: string[]): void {
|
||||
this.selectedIds$.next(selectedIds);
|
||||
}
|
||||
|
||||
private initShops(): void {
|
||||
this.shops$ = this.realmChanges$.pipe(filterShopsByRealm(this.shopService.shops$));
|
||||
}
|
||||
|
||||
private initShopsSelection(): void {
|
||||
this.selectedShops$ = this.selectedIds$.pipe(
|
||||
map((selectedIds: string[]) => {
|
||||
return selectedIds.reduce((idsMap: Map<string, string>, id: string) => {
|
||||
idsMap.set(id, id);
|
||||
return idsMap;
|
||||
}, new Map<string, string>());
|
||||
}),
|
||||
mergeMap((selectedIds: Map<string, string>) => {
|
||||
return this.shops$.pipe(map((shops: Shop[]) => [selectedIds, shops]));
|
||||
}),
|
||||
map(([selectedIds, shops]: [Map<string, string>, Shop[]]) => {
|
||||
return shops.map((shop: Shop) => (selectedIds.has(shop.id) ? shop : null)).filter(Boolean);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { CardBinPan } from '../card-bin-pan-filter';
|
||||
|
||||
export type PaymentBinPan = {
|
||||
// can be expanded in the future
|
||||
paymentMethod: 'bankCard';
|
||||
} & Partial<CardBinPan>;
|
@ -0,0 +1,4 @@
|
||||
// TODO: add filters fields
|
||||
export interface PaymentsAdditionalFilters {
|
||||
[key: string]: any;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Daterange } from '@dsh/pipes/daterange';
|
||||
|
||||
import { PaymentBinPan } from './payment-bin-pan';
|
||||
import { PaymentsAdditionalFilters } from './payments-additional-filters';
|
||||
|
||||
export interface PaymentsFiltersData {
|
||||
daterange: Daterange;
|
||||
invoiceIDs?: string[];
|
||||
shopIDs?: string[];
|
||||
binPan?: PaymentBinPan;
|
||||
additional?: PaymentsAdditionalFilters;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<dsh-row
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.payments.table'"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
fxLayoutGap="24px"
|
||||
color="primary"
|
||||
>
|
||||
<dsh-row-header-label fxFlex>{{ t('amount') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex>{{ t('status') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex>{{ t('statusChanged') }}</dsh-row-header-label>
|
||||
<dsh-row-header-label fxFlex fxHide.lt-md> {{ t('shop') }} </dsh-row-header-label>
|
||||
</dsh-row>
|
@ -0,0 +1,70 @@
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
|
||||
import { RowModule } from '@dsh/components/layout';
|
||||
|
||||
import { PaymentsRowHeaderComponent } from './payments-row-header.component';
|
||||
|
||||
const translationConfig = {
|
||||
ru: {
|
||||
operations: {
|
||||
payments: {
|
||||
table: {
|
||||
amount: 'Сумма списания',
|
||||
status: 'Статус',
|
||||
statusChanged: 'Статус изменен',
|
||||
invoice: 'Инвойс',
|
||||
shop: 'Магазин',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('PaymentsRowHeaderComponent', () => {
|
||||
let fixture: ComponentFixture<PaymentsRowHeaderComponent>;
|
||||
let component: PaymentsRowHeaderComponent;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RowModule,
|
||||
TranslocoTestingModule.withLangs(translationConfig, {
|
||||
availableLangs: ['ru'],
|
||||
defaultLang: 'ru',
|
||||
}),
|
||||
],
|
||||
declarations: [PaymentsRowHeaderComponent],
|
||||
})
|
||||
.overrideComponent(PaymentsRowHeaderComponent, {
|
||||
set: {
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PaymentsRowHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render columns with names from translation config', () => {
|
||||
const columns = fixture.debugElement.queryAll(By.css('dsh-row dsh-row-header-label'));
|
||||
|
||||
expect(columns.length).toBe(4);
|
||||
expect(columns[0].nativeElement.textContent.trim()).toBe('Сумма списания');
|
||||
expect(columns[1].nativeElement.textContent.trim()).toBe('Статус');
|
||||
expect(columns[2].nativeElement.textContent.trim()).toBe('Статус изменен');
|
||||
expect(columns[3].nativeElement.textContent.trim()).toBe('Магазин');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-payments-row-header',
|
||||
templateUrl: 'payments-row-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PaymentsRowHeaderComponent {}
|
@ -0,0 +1,12 @@
|
||||
<dsh-row fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="24px">
|
||||
<dsh-row-label fxFlex>
|
||||
<div class="dsh-body-2">
|
||||
<dsh-balance [amount]="payment.amount" [currency]="payment.currency"></dsh-balance>
|
||||
</div>
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex>
|
||||
<dsh-payment-status [status]="payment.status"></dsh-payment-status>
|
||||
</dsh-row-label>
|
||||
<dsh-row-label fxFlex>{{ payment.statusChangedAt | date: 'dd MMMM yyyy, HH:mm' }}</dsh-row-label>
|
||||
<dsh-row-label fxFlex fxHide.lt-md>{{ payment.shopID | shopDetails }}</dsh-row-label>
|
||||
</dsh-row>
|
@ -0,0 +1,89 @@
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
import moment from 'moment';
|
||||
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/capi';
|
||||
import { BalanceModule } from '@dsh/app/shared/components/balance/balance.module';
|
||||
import { RowModule } from '@dsh/components/layout';
|
||||
|
||||
import { generateMockPayment } from '../../../tests/generate-mock-payment';
|
||||
import { MockShopDetailsPipe } from '../../../tests/mock-shop-details-pipe';
|
||||
import { PaymentStatusModule } from '../../payment-status';
|
||||
import { PaymentsRowComponent } from './payments-row.component';
|
||||
|
||||
describe('PaymentsRowComponent', () => {
|
||||
let fixture: ComponentFixture<PaymentsRowComponent>;
|
||||
let component: PaymentsRowComponent;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RowModule,
|
||||
PaymentStatusModule,
|
||||
BalanceModule,
|
||||
TranslocoTestingModule.withLangs(
|
||||
{
|
||||
ru: {
|
||||
paymentStatus: {
|
||||
pending: 'Запущен',
|
||||
processed: 'Обработан',
|
||||
captured: 'Подтвержден',
|
||||
cancelled: 'Отменен',
|
||||
refunded: 'Возвращен',
|
||||
failed: 'Неуспешен',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
availableLangs: ['ru'],
|
||||
defaultLang: 'ru',
|
||||
}
|
||||
),
|
||||
],
|
||||
declarations: [PaymentsRowComponent, MockShopDetailsPipe],
|
||||
})
|
||||
.overrideComponent(PaymentsRowComponent, {
|
||||
set: {
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PaymentsRowComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.payment = generateMockPayment();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should show balances component if shop was provided', () => {
|
||||
const date = moment();
|
||||
component.payment = generateMockPayment({
|
||||
amount: 20,
|
||||
currency: 'USD',
|
||||
status: PaymentSearchResult.StatusEnum.Pending,
|
||||
statusChangedAt: date.toDate(),
|
||||
invoiceID: 'id',
|
||||
id: 'id',
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const labels = fixture.debugElement.queryAll(By.css('dsh-row dsh-row-label'));
|
||||
|
||||
expect(labels.length).toBe(4);
|
||||
expect(labels[0].nativeElement.textContent.trim()).toBe(`$0.20`);
|
||||
expect(labels[1].nativeElement.textContent.trim()).toBe(`Запущен`);
|
||||
expect(labels[2].nativeElement.textContent.trim()).toBe(date.format('DD MMMM YYYY, HH:mm'));
|
||||
expect(labels[3].nativeElement.textContent.trim()).toBe(`shopID_name`);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/anapi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-payments-row',
|
||||
templateUrl: 'payments-row.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PaymentsRowComponent {
|
||||
@Input() payment: PaymentSearchResult;
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './payments-panels.module';
|
||||
export * from './payments-panels.component';
|
@ -0,0 +1,2 @@
|
||||
export * from './payment-detail-header.module';
|
||||
export * from './payment-detail-header.component';
|
@ -0,0 +1,8 @@
|
||||
<div
|
||||
*transloco="let t; scope: 'operations'; read: 'operations.payments.details'"
|
||||
fxLayout="row"
|
||||
fxLayoutAlign="space-between center"
|
||||
>
|
||||
<div>{{ t('name') + ' ' + '#' + id }}</div>
|
||||
<div>{{ changedDate | date: 'dd MMMM yyyy, HH:mm' }}</div>
|
||||
</div>
|
@ -0,0 +1,44 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
|
||||
import { PaymentDetailHeaderComponent } from './payment-detail-header.component';
|
||||
|
||||
describe('PaymentDetailHeaderComponent', () => {
|
||||
let component: PaymentDetailHeaderComponent;
|
||||
let fixture: ComponentFixture<PaymentDetailHeaderComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslocoTestingModule.withLangs(
|
||||
{
|
||||
ru: {
|
||||
operations: {
|
||||
payments: {
|
||||
details: {
|
||||
name: 'Платеж',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
availableLangs: ['ru'],
|
||||
defaultLang: 'ru',
|
||||
}
|
||||
),
|
||||
],
|
||||
declarations: [PaymentDetailHeaderComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PaymentDetailHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-payment-detail-header',
|
||||
templateUrl: 'payment-detail-header.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
})
|
||||
export class PaymentDetailHeaderComponent {
|
||||
@Input() id: string;
|
||||
@Input() changedDate: Date;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { PaymentDetailHeaderComponent } from './payment-detail-header.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, TranslocoModule, FlexLayoutModule],
|
||||
declarations: [PaymentDetailHeaderComponent],
|
||||
exports: [PaymentDetailHeaderComponent],
|
||||
})
|
||||
export class PaymentDetailHeaderModule {}
|
@ -0,0 +1 @@
|
||||
export * from './payment-status.module';
|
@ -0,0 +1,3 @@
|
||||
<dsh-status *transloco="let paymentStatus; read: 'paymentStatus'" [color]="status | paymentStatusColor">
|
||||
{{ paymentStatus(status) }}
|
||||
</dsh-status>
|
@ -0,0 +1,67 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslocoTestingModule } from '@ngneat/transloco';
|
||||
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/capi';
|
||||
import { StatusModule } from '@dsh/components/indicators';
|
||||
|
||||
import { PaymentStatusComponent } from './payment-status.component';
|
||||
import { PaymentStatusColorPipe } from './pipes/status-color/status-color.pipe';
|
||||
|
||||
describe('PaymentStatusComponent', () => {
|
||||
let component: PaymentStatusComponent;
|
||||
let fixture: ComponentFixture<PaymentStatusComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StatusModule,
|
||||
TranslocoTestingModule.withLangs(
|
||||
{
|
||||
ru: {
|
||||
paymentStatus: {
|
||||
pending: 'Запущен',
|
||||
processed: 'Обработан',
|
||||
captured: 'Подтвержден',
|
||||
cancelled: 'Отменен',
|
||||
refunded: 'Возвращен',
|
||||
failed: 'Неуспешен',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
availableLangs: ['ru'],
|
||||
defaultLang: 'ru',
|
||||
}
|
||||
),
|
||||
],
|
||||
declarations: [PaymentStatusComponent, PaymentStatusColorPipe],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PaymentStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render valid status name', () => {
|
||||
component.status = PaymentSearchResult.StatusEnum.Refunded;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('dsh-status')).nativeElement.textContent.trim()).toBe('Возвращен');
|
||||
|
||||
component.status = PaymentSearchResult.StatusEnum.Processed;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('dsh-status')).nativeElement.textContent.trim()).toBe('Обработан');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/capi';
|
||||
|
||||
@Component({
|
||||
selector: 'dsh-payment-status',
|
||||
templateUrl: './payment-status.component.html',
|
||||
})
|
||||
export class PaymentStatusComponent {
|
||||
@Input() status: PaymentSearchResult.StatusEnum;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { StatusModule } from '@dsh/components/indicators';
|
||||
|
||||
import { PaymentStatusComponent } from './payment-status.component';
|
||||
import { PaymentStatusColorPipe } from './pipes/status-color/status-color.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, StatusModule, TranslocoModule],
|
||||
declarations: [PaymentStatusComponent, PaymentStatusColorPipe],
|
||||
exports: [PaymentStatusComponent, PaymentStatusColorPipe],
|
||||
})
|
||||
export class PaymentStatusModule {}
|
@ -0,0 +1,38 @@
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/capi';
|
||||
|
||||
import { StatusColor } from '../../../../../../../../theme-manager';
|
||||
import { PaymentStatusColorPipe } from './status-color.pipe';
|
||||
|
||||
const statusEnum = PaymentSearchResult.StatusEnum;
|
||||
|
||||
describe('PaymentStatusColorPipe', () => {
|
||||
let pipe: PaymentStatusColorPipe;
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new PaymentStatusColorPipe();
|
||||
});
|
||||
|
||||
it('create an instance', () => {
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
it('should return "success" color for Captured or Processed statuses', () => {
|
||||
expect(pipe.transform(statusEnum.Captured)).toBe(StatusColor.success);
|
||||
expect(pipe.transform(statusEnum.Processed)).toBe(StatusColor.success);
|
||||
});
|
||||
|
||||
it('should return "warn" color for Failed or Cancelled statuses', () => {
|
||||
expect(pipe.transform(statusEnum.Failed)).toBe(StatusColor.warn);
|
||||
expect(pipe.transform(statusEnum.Cancelled)).toBe(StatusColor.warn);
|
||||
});
|
||||
|
||||
it('should return "pending" color for Pending status', () => {
|
||||
expect(pipe.transform(statusEnum.Pending)).toBe(StatusColor.pending);
|
||||
});
|
||||
|
||||
it('should return "neutral" color for Refunded status', () => {
|
||||
expect(pipe.transform(statusEnum.Refunded)).toBe(StatusColor.neutral);
|
||||
});
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
import { PaymentSearchResult } from '@dsh/api-codegen/capi/swagger-codegen';
|
||||
|
||||
import { StatusColor } from '../../../../theme-manager';
|
||||
import { StatusColor } from '../../../../../../../../theme-manager';
|
||||
|
||||
@Pipe({
|
||||
name: 'paymentStatusColor',
|
@ -0,0 +1,18 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslocoModule } from '@ngneat/transloco';
|
||||
|
||||
import { BaseDialogModule } from '@dsh/app/shared/components/dialog/base-dialog';
|
||||
import { MaxLengthInputModule } from '@dsh/app/shared/components/inputs/max-length-input/max-length-input.module';
|
||||
|
||||
import { CancelHoldService } from './cancel-hold.service';
|
||||
import { CancelHoldDialogComponent } from './components/cancel-hold-dialog/cancel-hold-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, BaseDialogModule, MaxLengthInputModule, ReactiveFormsModule, FlexModule, TranslocoModule],
|
||||
declarations: [CancelHoldDialogComponent],
|
||||
providers: [CancelHoldService],
|
||||
})
|
||||
export class CancelHoldModule {}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user