Shops table update (#316)

* implemented basic logic of new shop tables

* fixed karma

* refactored implementation, removed unused code

* added tests

* changed tests configs

* fixed linter

* fixed review issues

* fixed ci

* test ci

* fixed startWith generics, added todos

* fixed review issues

* fixed review issues

* fixed review issues

* fixed review issues

* fixed reload bug

* added tslint rules, fixed review issues, added some tests

* fixed build errors

Co-authored-by: egrigorieva <e.grigoreva@rbkmoney.com>
This commit is contained in:
Jenny 2020-11-20 18:49:03 +03:00 committed by GitHub
parent 06904e97b2
commit 5861fe2c39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 2467 additions and 602 deletions

View File

@ -26,7 +26,7 @@ module.exports = function (config) {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
browsers: ['ChromeHeadless'],
singleRun: false,
restartOnFileChange: true,
});

281
package-lock.json generated
View File

@ -3246,6 +3246,16 @@
"source-map": "^0.6.1"
}
},
"@types/yauzl": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
"integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
"dev": true,
"optional": true,
"requires": {
"@types/node": "*"
}
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -4051,6 +4061,40 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true
},
"bl": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
"integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@ -5756,6 +5800,12 @@
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
"dev": true
},
"devtools-protocol": {
"version": "0.0.818844",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz",
"integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==",
"dev": true
},
"dezalgo": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
@ -6873,6 +6923,12 @@
"readable-stream": "^2.0.0"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@ -9197,6 +9253,15 @@
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"jasmine-marbles": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/jasmine-marbles/-/jasmine-marbles-0.6.0.tgz",
"integrity": "sha512-1uzgjEesEeCb+r+v46qn5x326TiGqk5SUZa+A3O+XnMCjG/pGcUOhL9Xsg5L7gLC6RFHyWGTkB5fei4rcvIOiQ==",
"dev": true,
"requires": {
"lodash": "^4.5.0"
}
},
"jasmine-spec-reporter": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
@ -10414,6 +10479,12 @@
"minimist": "^1.2.5"
}
},
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
@ -10593,6 +10664,12 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"dev": true
},
"node-fetch-npm": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz",
@ -12574,6 +12651,12 @@
"ipaddr.js": "1.9.0"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -12645,6 +12728,144 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"puppeteer": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.5.0.tgz",
"integrity": "sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"devtools-protocol": "0.0.818844",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.6.1",
"pkg-dir": "^4.2.0",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
},
"dependencies": {
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"dev": true
},
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"requires": {
"@types/yauzl": "^2.9.1",
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"https-proxy-agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"dev": true,
"requires": {
"agent-base": "5",
"debug": "4"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"requires": {
"find-up": "^4.0.0"
}
},
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"ws": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
"dev": true
}
}
},
"q": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
@ -14778,6 +14999,44 @@
}
}
},
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"tar-stream": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz",
"integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"terser": {
"version": "4.6.10",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.10.tgz",
@ -15193,6 +15452,28 @@
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true
},
"unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"through": "^2.3.8"
},
"dependencies": {
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
}
}
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@ -102,9 +102,10 @@
"codelyzer": "^5.1.2",
"del": "^5.1.0",
"jasmine-core": "~3.5.0",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "~2.0.5",
"karma-jasmine": "~3.1.1",
"karma-jasmine-html-reporter": "^1.4.2",
@ -113,6 +114,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"protractor": "~5.4.2",
"puppeteer": "^5.5.0",
"ts-node": "~8.8.1",
"tslint": "~6.1.0",
"tslint-config-prettier": "^1.18.0",

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AnalyticsService as APIAnalyticsService, SplitUnit } from '../../api-codegen/anapi';
import { AnalyticsService as APIAnalyticsService, InlineResponse200, SplitUnit } from '../../api-codegen/anapi';
import { PaymentInstitutionRealm } from '../model';
import { genXRequestID, toDateLike } from '../utils';
@ -215,4 +216,13 @@ export class AnalyticsService {
params.paymentInstitutionRealm
);
}
getGroupBalances(params: { shopIDs?: string[]; excludeShopIDs?: string[] }): Observable<InlineResponse200> {
return this.analyticsService.getCurrentBalancesGroupByShop(
genXRequestID(),
undefined,
params.shopIDs,
params.excludeShopIDs
);
}
}

View File

@ -8,11 +8,11 @@ import { SHARE_REPLAY_CONF } from '../../custom-operators';
import { genXRequestID } from '../utils';
@Injectable()
export class ShopService {
export class ApiShopsService {
private reloadShops$ = new Subject<void>();
shops$: Observable<Shop[]> = this.reloadShops$.pipe(
startWith(undefined as Shop[]),
startWith<void, null>(null),
switchMapTo(this.shopsService.getShops(genXRequestID())),
shareReplay(SHARE_REPLAY_CONF)
);

View File

@ -1,3 +1,3 @@
export * from './shop.service';
export * from './api-shops.service';
export * from './shop.module';
export * from './operators';

View File

@ -1,10 +1,10 @@
import { NgModule } from '@angular/core';
import { CAPIModule } from '../capi';
import { ShopService } from './shop.service';
import { ApiShopsService } from './api-shops.service';
@NgModule({
imports: [CAPIModule],
providers: [ShopService],
providers: [ApiShopsService],
})
export class ShopModule {}

View File

@ -5,7 +5,7 @@ import { TestShopService } from './test-shop.service';
@Component({
selector: 'dsh-root',
templateUrl: './app.component.html',
templateUrl: 'app.component.html',
providers: [TestShopService],
})
export class AppComponent implements OnInit {

View File

@ -6,7 +6,7 @@ import { ThemeManager, ThemeName } from '../../theme-manager';
@Component({
selector: 'dsh-actionbar',
templateUrl: './actionbar.component.html',
templateUrl: 'actionbar.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActionbarComponent {

View File

@ -4,7 +4,7 @@ import { BrandType } from './brand-type';
@Component({
selector: 'dsh-brand',
templateUrl: './brand.component.html',
templateUrl: 'brand.component.html',
styleUrls: ['./brand.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Event, Router } from '@angular/router';
import { combineLatest } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, startWith } from 'rxjs/operators';
@ -22,7 +22,7 @@ export enum LinkId {
@Injectable()
export class ToolbarLinksService {
private url$ = this.router.events.pipe(
startWith(null),
startWith<Event, null>(null),
map(() => this.router.url),
distinctUntilChanged(),
shareReplay(1)

View File

@ -6,7 +6,7 @@ import { LinkId, ToolbarLinksService } from './toolbar-links.service';
@Component({
selector: 'dsh-toolbar',
templateUrl: './toolbar.component.html',
templateUrl: 'toolbar.component.html',
styleUrls: ['./toolbar.component.scss'],
providers: [ToolbarLinksService],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -47,7 +47,8 @@ export class CreateInvoiceTemplateService {
form = this.createForm();
summary$ = this.cartForm.valueChanges.pipe(
startWith(this.cartForm.value),
// TODO: add form types
startWith<any, any>(this.cartForm.value),
map((v) => v.reduce((sum, c) => sum + c.price * c.quantity, 0)),
shareReplay(1)
);
@ -123,7 +124,7 @@ export class CreateInvoiceTemplateService {
private subscribeFormChanges() {
const templateType$ = this.form.controls.templateType.valueChanges.pipe(
startWith(this.form.value.templateType),
startWith<TemplateType, TemplateType>(this.form.value.templateType),
shareReplay(SHARE_REPLAY_CONF)
);
const costType$ = this.form.controls.costType.valueChanges.pipe(startWith(this.form.value.costType));

View File

@ -19,7 +19,8 @@ export class CreateInvoiceService {
form = this.createForm();
totalAmount$ = this.form.controls.cart.valueChanges.pipe(
startWith(this.form.controls.cart.value),
// TODO: add form types
startWith<any, any>(this.form.controls.cart.value),
map((v) => v.map(({ price, quantity }) => price * quantity).reduce((sum, s) => (sum += s), 0)),
shareReplay(SHARE_REPLAY_CONF)
);

View File

@ -6,7 +6,7 @@ import { TranslocoService } from '@ngneat/transloco';
import { of, ReplaySubject } from 'rxjs';
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { ShopService } from '../../../api';
import { ApiShopsService } from '../../../api';
import { BankContent } from '../../../api-codegen/aggr-proxy';
import { BankAccount } from '../../../api-codegen/capi';
import { filterShopsByRealm } from '../../payment-section/operations/operators';
@ -58,7 +58,7 @@ export class CreateShopRussianLegalEntityComponent {
constructor(
private fb: FormBuilder,
private createShopRussianLegalEntityService: CreateShopRussianLegalEntityService,
private shopService: ShopService,
private shopService: ApiShopsService,
private transloco: TranslocoService,
private snackBar: MatSnackBar,
private router: Router

View File

@ -4,11 +4,11 @@ import { map, pluck, switchMap, switchMapTo } from 'rxjs/operators';
import uuid from 'uuid';
import {
ApiShopsService,
ClaimsService,
createDocumentModificationUnit,
PayoutsService,
QuestionaryService,
ShopService,
} from '../../../api';
import {
BankAccount,
@ -22,7 +22,7 @@ import {
export class CreateShopRussianLegalEntityService {
constructor(
private claimsService: ClaimsService,
private shopService: ShopService,
private shopService: ApiShopsService,
private payoutsService: PayoutsService,
private questionaryService: QuestionaryService
) {}

View File

@ -9,7 +9,7 @@ export interface StatusViewInfo {
@Component({
selector: 'dsh-details-status-item',
templateUrl: './status-details-item.component.html',
templateUrl: 'status-details-item.component.html',
})
export class StatusDetailsItemComponent {
@Input() color: Color;

View File

@ -4,7 +4,7 @@ import { TranslocoService } from '@ngneat/transloco';
import { combineLatest, Observable } from 'rxjs';
import { pluck } from 'rxjs/operators';
import { ClaimsService, ShopService } from '../../../../api';
import { ApiShopsService, ClaimsService } from '../../../../api';
import { ClaimStatus } from '../../../../api/claims';
import { booleanDelay, takeError } from '../../../../custom-operators';
import { ActionBtnContent, TestEnvBtnContent } from './content-config';
@ -18,7 +18,7 @@ export class PaymentsService {
isLoading$: Observable<boolean>;
constructor(
private shopService: ShopService,
private shopService: ApiShopsService,
private claimService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService

View File

@ -26,7 +26,8 @@ export class AuthorityConfirmingDocumentComponent {
switchMap((form) =>
form
? form.valueChanges.pipe(
startWith(form.value),
// TODO: add form types
startWith<any, any>(form.value),
map((v) => v.type === this.customType)
)
: of(false)

View File

@ -3,7 +3,7 @@ import { Router } from '@angular/router';
@Component({
selector: 'dsh-page-not-found',
templateUrl: './page-not-found.component.html',
templateUrl: 'page-not-found.component.html',
})
export class PageNotFoundComponent {
constructor(private router: Router) {}

View File

@ -7,7 +7,7 @@ import { getPaymentStatusInfo } from '../../get-payment-status-info';
@Component({
selector: 'dsh-details',
templateUrl: './details.component.html',
templateUrl: 'details.component.html',
})
export class DetailsComponent implements OnChanges {
@Input() payment: PaymentSearchResult;

View File

@ -15,7 +15,7 @@ export interface CancelHoldData {
@Component({
selector: 'dsh-cancel-hold',
templateUrl: './cancel-hold.component.html',
templateUrl: 'cancel-hold.component.html',
providers: [PaymentService],
})
export class CancelHoldComponent {

View File

@ -21,7 +21,7 @@ export interface ConfirmHoldData {
@Component({
selector: 'dsh-confirm-hold',
templateUrl: './confirm-hold.component.html',
templateUrl: 'confirm-hold.component.html',
providers: [PaymentService],
})
export class ConfirmHoldComponent {

View File

@ -11,7 +11,7 @@ const onHoldExpirationEnum = PaymentFlowHold.OnHoldExpirationEnum;
@Component({
selector: 'dsh-hold-details',
templateUrl: './hold-details.component.html',
templateUrl: 'hold-details.component.html',
})
export class HoldDetailsComponent {
@Input() payment: PaymentSearchResult;

View File

@ -9,7 +9,7 @@ import { InvoiceDetailsService } from './invoice-details.service';
@Component({
selector: 'dsh-invoice-details',
templateUrl: './invoice-details.component.html',
templateUrl: 'invoice-details.component.html',
styleUrls: ['./invoice-details.component.scss'],
providers: [InvoiceDetailsService],
})

View File

@ -2,6 +2,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'dsh-make-recurrent',
templateUrl: './make-recurrent.component.html',
templateUrl: 'make-recurrent.component.html',
})
export class MakeRecurrentComponent {}

View File

@ -4,7 +4,7 @@ import { CustomerPayer } from '../../../../api-codegen/capi/swagger-codegen';
@Component({
selector: 'dsh-customer-payer',
templateUrl: './customer-payer.component.html',
templateUrl: 'customer-payer.component.html',
})
export class CustomerPayerComponent {
@Input() customerPayer: CustomerPayer;

View File

@ -11,7 +11,7 @@ export enum PayerType {
@Component({
selector: 'dsh-payer-details',
templateUrl: './payer-details.component.html',
templateUrl: 'payer-details.component.html',
})
export class PayerDetailsComponent {
@Input() payer: Payer;

View File

@ -5,7 +5,7 @@ import { LAYOUT_GAP } from '../../../constants';
@Component({
selector: 'dsh-payment-resource-payer',
templateUrl: './payment-resource-payer.component.html',
templateUrl: 'payment-resource-payer.component.html',
})
export class PaymentResourcePayerComponent {
@Input() paymentResourcePayer: PaymentResourcePayer;

View File

@ -7,7 +7,7 @@ import { PayerType } from './payer-details';
import { ReceivePaymentService } from './receive-payment.service';
@Component({
templateUrl: './payment-details.component.html',
templateUrl: 'payment-details.component.html',
styleUrls: ['./payment-details.component.scss'],
providers: [ReceivePaymentService],
})

View File

@ -4,7 +4,7 @@ import { BankCardDetails, PaymentToolDetailsBankCard } from '../../../../api-cod
@Component({
selector: 'dsh-bank-card',
templateUrl: './bank-card.component.html',
templateUrl: 'bank-card.component.html',
})
export class BankCardComponent {
@Input() bankCard: BankCardDetails;

View File

@ -9,7 +9,7 @@ import DigitalWalletDetailsTypeEnum = PaymentToolDetailsDigitalWallet.DigitalWal
@Component({
selector: 'dsh-digital-wallet',
templateUrl: './digital-wallet.component.html',
templateUrl: 'digital-wallet.component.html',
})
export class DigitalWalletComponent implements OnChanges {
@Input() digitalWallet: DigitalWalletDetails;

View File

@ -4,7 +4,7 @@ import { PaymentTerminalDetails } from '../../../../api-codegen/capi/swagger-cod
@Component({
selector: 'dsh-payment-terminal',
templateUrl: './payment-terminal.component.html',
templateUrl: 'payment-terminal.component.html',
})
export class PaymentTerminalComponent {
@Input() paymentTerminal: PaymentTerminalDetails;

View File

@ -5,7 +5,7 @@ import { PaymentToolDetails } from '../../../api-codegen/capi';
@Component({
selector: 'dsh-payment-tool',
templateUrl: './payment-tool.component.html',
templateUrl: 'payment-tool.component.html',
})
export class PaymentToolComponent {
@Input() paymentToolDetails: PaymentToolDetails;

View File

@ -5,7 +5,7 @@ import { LAYOUT_GAP } from '../../constants';
@Component({
selector: 'dsh-recurrent-details',
templateUrl: './recurrent-details.component.html',
templateUrl: 'recurrent-details.component.html',
})
export class RecurrentDetailsComponent {
@Input() recurrentPayer: RecurrentPayer;

View File

@ -23,7 +23,7 @@ export interface CreateRefundData {
@Component({
selector: 'dsh-create-refund',
templateUrl: './create-refund.component.html',
templateUrl: 'create-refund.component.html',
providers: [CreateRefundService],
})
export class CreateRefundComponent implements OnInit {

View File

@ -5,13 +5,13 @@ import { switchMap } from 'rxjs/operators';
import { Account, Refund, RefundParams, Shop } from '../../../../api-codegen/capi/swagger-codegen';
import { AccountService } from '../../../../api/account';
import { RefundService } from '../../../../api/refund';
import { ShopService } from '../../../../api/shop';
import { ApiShopsService } from '../../../../api/shop';
@Injectable()
export class CreateRefundService {
constructor(
private refundService: RefundService,
private shopService: ShopService,
private shopService: ApiShopsService,
private accountService: AccountService
) {}

View File

@ -6,7 +6,7 @@ import { LAYOUT_GAP } from '../../../constants';
@Component({
selector: 'dsh-refund-item',
templateUrl: './refund-item.component.html',
templateUrl: 'refund-item.component.html',
})
export class RefundItemComponent implements OnChanges {
@Input() refund: RefundSearchResult;

View File

@ -11,7 +11,7 @@ const PaymentStatuses = PaymentSearchResult.StatusEnum;
@Component({
selector: 'dsh-refunds',
templateUrl: './refunds.component.html',
templateUrl: 'refunds.component.html',
styleUrls: ['./refunds.component.scss'],
providers: [RefundsService],
})

View File

@ -15,7 +15,7 @@ import { DetailsItemModule, LayoutModule } from '@dsh/components/layout';
import { AccountService } from '../../../api/account';
import { RefundService } from '../../../api/refund';
import { RefundSearchService } from '../../../api/search';
import { ShopService } from '../../../api/shop';
import { ApiShopsService } from '../../../api/shop';
import { ToMajorModule } from '../../../to-major';
import { StatusDetailsItemModule } from '../status-details-item';
import { CreateRefundComponent } from './create-refund';
@ -41,7 +41,7 @@ import { RefundsComponent } from './refunds.component';
],
declarations: [CreateRefundComponent, RefundsComponent, RefundItemComponent],
exports: [RefundsComponent],
providers: [RefundSearchService, RefundService, ShopService, AccountService],
providers: [RefundSearchService, RefundService, ApiShopsService, AccountService],
entryComponents: [CreateRefundComponent],
})
export class RefundsModule {}

View File

@ -9,7 +9,7 @@ export interface StatusViewInfo {
@Component({
selector: 'dsh-details-status-item',
templateUrl: './status-details-item.component.html',
templateUrl: 'status-details-item.component.html',
})
export class StatusDetailsItemComponent {
@Input() color: Color;

View File

@ -14,7 +14,7 @@ import { distinctUntilChanged, map, scan, shareReplay, switchMap, take } from 'r
import { Daterange } from '@dsh/pipes/daterange';
import { Shop } from '../../../../api-codegen/capi';
import { ShopService } from '../../../../api/shop';
import { ApiShopsService } from '../../../../api/shop';
import { filterShopsByRealm, removeEmptyProperties } from '../../operations/operators';
import { searchFilterParamsToDaterange } from '../../reports/reports-search-filters/search-filter-params-to-daterange';
import { SearchParams } from '../search-params';
@ -66,7 +66,7 @@ export class AnalyticsSearchFiltersComponent implements OnChanges {
shareReplay(1)
);
constructor(private shopService: ShopService) {
constructor(private shopService: ApiShopsService) {
this.selectedCurrency$.subscribe((currency) => {
this.searchParams$.next({ currency });
this.selectedShopIDs$.next([]);

View File

@ -2,10 +2,10 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { FilterShopsModule } from '@dsh/app/shared/*';
import { FilterShopsModule } from '@dsh/app/shared/components';
import { CurrencyFilterModule } from '@dsh/app/shared/components/filters/currency-filter';
import { DaterangeFilterModule } from '@dsh/components/filters/daterange-filter';
import { CurrencyFilterModule } from '../../../../shared/components/filters/currency-filter';
import { AnalyticsSearchFiltersComponent } from './analytics-search-filters.component';
@NgModule({

View File

@ -7,7 +7,7 @@ import { AveragePaymentService } from './average-payment.service';
@Component({
selector: 'dsh-average-payment',
templateUrl: './average-payment.component.html',
templateUrl: 'average-payment.component.html',
providers: [AveragePaymentService],
})
export class AveragePaymentComponent implements OnChanges {

View File

@ -6,7 +6,7 @@ import { ChartData } from '../utils';
@Component({
selector: 'dsh-bar-chart-item',
templateUrl: './bar-chart-item.component.html',
templateUrl: 'bar-chart-item.component.html',
})
export class BarChartItemComponent implements OnChanges {
@Input() spinnerType: SpinnerType;

View File

@ -6,7 +6,7 @@ import { DistributionChartData } from '../utils';
@Component({
selector: 'dsh-donut-chart-item',
templateUrl: './donut-chart-item.component.html',
templateUrl: 'donut-chart-item.component.html',
styleUrls: ['donut-chart-item.component.scss'],
})
export class DonutChartItemComponent implements OnChanges {

View File

@ -8,7 +8,7 @@ import { PaymentSplitAmountService } from './payment-split-amount.service';
@Component({
selector: 'dsh-payment-split-amount',
templateUrl: './payment-split-amount.component.html',
templateUrl: 'payment-split-amount.component.html',
providers: [PaymentSplitAmountService],
})
export class PaymentSplitAmountComponent implements OnChanges {

View File

@ -8,7 +8,7 @@ import { PaymentSplitCountService } from './payment-split-count.service';
@Component({
selector: 'dsh-payment-split-count',
templateUrl: './payment-split-count.component.html',
templateUrl: 'payment-split-count.component.html',
providers: [PaymentSplitCountService],
})
export class PaymentSplitCountComponent implements OnChanges {

View File

@ -7,7 +7,7 @@ import { PaymentsAmountService } from './payments-amount.service';
@Component({
selector: 'dsh-payments-amount',
templateUrl: './payments-amount.component.html',
templateUrl: 'payments-amount.component.html',
providers: [PaymentsAmountService],
})
export class PaymentsAmountComponent implements OnChanges {

View File

@ -7,7 +7,7 @@ import { PaymentsCountService } from './payments-count.service';
@Component({
selector: 'dsh-payments-count',
templateUrl: './payments-count.component.html',
templateUrl: 'payments-count.component.html',
providers: [PaymentsCountService],
})
export class PaymentsCountComponent implements OnChanges {

View File

@ -8,7 +8,7 @@ import { PaymentsErrorDistributionService } from './payments-error-distribution.
@Component({
selector: 'dsh-payments-error-distribution',
templateUrl: './payments-error-distribution.component.html',
templateUrl: 'payments-error-distribution.component.html',
providers: [PaymentsErrorDistributionService],
encapsulation: ViewEncapsulation.Emulated,
})

View File

@ -8,7 +8,7 @@ import { PaymentsToolDistributionService } from './payments-tool-distribution.se
@Component({
selector: 'dsh-payments-tool-distribution',
templateUrl: './payments-tool-distribution.component.html',
templateUrl: 'payments-tool-distribution.component.html',
providers: [PaymentsToolDistributionService],
})
export class PaymentsToolDistributionComponent implements OnChanges {

View File

@ -2,7 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'dsh-percent-difference',
templateUrl: './percent-difference.component.html',
templateUrl: 'percent-difference.component.html',
})
export class PercentDifferenceComponent implements OnChanges {
@Input() current: number;

View File

@ -7,7 +7,7 @@ import { RefundsAmountService } from './refunds-amount.service';
@Component({
selector: 'dsh-refunds-amount',
templateUrl: './refunds-amount.component.html',
templateUrl: 'refunds-amount.component.html',
providers: [RefundsAmountService],
})
export class RefundsAmountComponent implements OnChanges {

View File

@ -7,7 +7,7 @@ import { StatData } from '../utils';
@Component({
selector: 'dsh-stat-item',
templateUrl: './stat-item.component.html',
templateUrl: 'stat-item.component.html',
styleUrls: ['./stat-item.component.scss'],
})
export class StatItemComponent implements OnChanges {

View File

@ -3,7 +3,7 @@ import { FormBuilder } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { pluck, shareReplay } from 'rxjs/operators';
import { ShopService } from '../../../../../api';
import { ApiShopsService } from '../../../../../api';
import { SHARE_REPLAY_CONF } from '../../../../../custom-operators';
import { filterShopsByRealm } from '../../../operations/operators';
@ -17,5 +17,5 @@ export class CreateInvoiceOrInvoiceTemplateService {
shareReplay(SHARE_REPLAY_CONF)
);
constructor(private fb: FormBuilder, private route: ActivatedRoute, private shopService: ShopService) {}
constructor(private fb: FormBuilder, private route: ActivatedRoute, private shopService: ApiShopsService) {}
}

View File

@ -1 +0,0 @@
export * from './create-shop-dialog.component';

View File

@ -0,0 +1,21 @@
import { isNil } from '@ngneat/transloco';
import { Dict } from '../../../../../../../type-utils';
import { Shop as ApiShop } from '../../../../../../api-codegen/capi/swagger-codegen';
import { ShopBalance } from '../../types/shop-balance';
import { ShopItem } from '../../types/shop-item';
export function combineShopItem(shops: ApiShop[], balances: ShopBalance[]): ShopItem[] {
const balancesMap = balances.reduce((acc: Dict<ShopBalance>, el: ShopBalance) => {
acc[el.id] = el;
return acc;
}, {});
return shops.map((shop: ApiShop) => {
const balance = balancesMap[shop.id];
return {
...shop,
balance: isNil(balance) ? null : balance.data,
} as ShopItem;
});
}

View File

@ -0,0 +1,485 @@
import { TestBed } from '@angular/core/testing';
import { cold } from 'jasmine-marbles';
import { Observable, of, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { InlineResponse200, ShopLocation } from '../../../../../../api-codegen/anapi/swagger-codegen';
import { Shop } from '../../../../../../api-codegen/capi/swagger-codegen';
import { AnalyticsService } from '../../../../../../api/analytics';
import { PaymentInstitutionRealm } from '../../../../../../api/model';
import { ApiShopsService } from '../../../../../../api/shop';
import { ShopBalanceModule } from '../../shops-list/shop-balance';
import { ShopsBalanceService } from '../shops-balance/shops-balance.service';
import { FetchShopsService } from './fetch-shops.service';
class MockApiShopsService {
shops$: Observable<Shop[]>;
private innerShops$ = new ReplaySubject<Shop[]>(1);
private mockShops: Shop[];
constructor() {
this.shops$ = this.innerShops$.asObservable();
}
reloadShops(): void {
this.innerShops$.next(this.mockShops);
}
setMockShops(shops: Shop[]): void {
this.mockShops = shops;
}
}
class MockAnalyticsService {
private innerResponse: InlineResponse200 = {
result: [],
};
getGroupBalances(): Observable<InlineResponse200> {
console.log('getGroupBalances');
return of(this.innerResponse);
}
setMockBalancesResponse(response: InlineResponse200): void {
this.innerResponse = response;
}
}
describe('FetchShopsService', () => {
let service: FetchShopsService;
let apiShopsService: MockApiShopsService;
let analyticsService: MockAnalyticsService;
let balancesService: ShopBalanceModule;
beforeEach(() => {
apiShopsService = new MockApiShopsService();
analyticsService = new MockAnalyticsService();
});
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
FetchShopsService,
ShopsBalanceService,
{
provide: ApiShopsService,
useValue: apiShopsService,
},
{
provide: AnalyticsService,
useValue: analyticsService,
},
],
});
});
beforeEach(() => {
service = TestBed.inject(FetchShopsService);
balancesService = TestBed.inject(ShopsBalanceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('initRealm', () => {
it('should init realm and init allShops$ work', () => {
apiShopsService.setMockShops([
{
id: 'mock1',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock2',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock3',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 3,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
]);
const expectedShops$ = cold('a', {
a: ['mock1', 'mock2'],
});
apiShopsService.reloadShops();
service.initRealm(PaymentInstitutionRealm.test);
expect(
service.allShops$.pipe(
map((list) => {
return list.map(({ id }) => id);
})
)
).toBeObservable(expectedShops$);
});
});
describe('initOffsetIndex', () => {
it('should init shop$ working', () => {
apiShopsService.setMockShops([
{
id: 'mock1',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock2',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock3',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock4',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock5',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock6',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock7',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
]);
const expectedShops$ = cold('a', {
a: ['mock1', 'mock2', 'mock3', 'mock4', 'mock5'],
});
apiShopsService.reloadShops();
service.initRealm(PaymentInstitutionRealm.test);
service.initOffsetIndex(3);
expect(
service.loadedShops$.pipe(
map((list) => {
return list.map(({ id }) => id);
})
)
).toBeObservable(expectedShops$);
});
});
describe('refreshData', () => {
it('should call apiShopService reload method', () => {
const spyOnApiShopService = spyOn(apiShopsService, 'reloadShops').and.callThrough();
service.refreshData();
expect(spyOnApiShopService).toHaveBeenCalledTimes(1);
});
it('should update allShops list', () => {
apiShopsService.setMockShops([
{
id: 'mock6',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
]);
service.initRealm(PaymentInstitutionRealm.test);
apiShopsService.reloadShops();
apiShopsService.setMockShops([
{
id: 'mock6',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
{
id: 'mock16',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
},
]);
service.refreshData();
const expectedShops$ = cold('a', {
a: ['mock6', 'mock16'],
});
expect(
service.allShops$.pipe(
map((list) => {
return list.map(({ id }) => id);
})
)
).toBeObservable(expectedShops$);
});
it('should update loading value', () => {
apiShopsService.setMockShops([]);
apiShopsService.reloadShops();
service.initRealm(PaymentInstitutionRealm.test);
service.initOffsetIndex(-1);
service.refreshData();
expect(service.isLoading$).toBeObservable(
cold('a', {
a: true,
})
);
});
});
describe('showMore', () => {
it('should update loading value', () => {
apiShopsService.setMockShops([]);
apiShopsService.reloadShops();
service.initRealm(PaymentInstitutionRealm.test);
service.initOffsetIndex(-1);
service.showMore();
expect(service.isLoading$).toBeObservable(
cold('a', {
a: true,
})
);
});
});
});

View File

@ -0,0 +1,124 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs';
import {
distinctUntilChanged,
map,
mapTo,
pluck,
scan,
shareReplay,
switchMap,
tap,
withLatestFrom,
} from 'rxjs/operators';
import { Shop as ApiShop } from '../../../../../../api-codegen/capi/swagger-codegen';
import { PaymentInstitutionRealm } from '../../../../../../api/model';
import { ApiShopsService } from '../../../../../../api/shop';
import { SHARE_REPLAY_CONF } from '../../../../../../custom-operators';
import { filterShopsByRealm, mapToTimestamp } from '../../../../operations/operators';
import { ShopBalance } from '../../types/shop-balance';
import { ShopItem } from '../../types/shop-item';
import { ShopsBalanceService } from '../shops-balance/shops-balance.service';
import { combineShopItem } from './combine-shop-item';
const LIST_OFFSET = 5;
@Injectable()
export class FetchShopsService {
allShops$: Observable<ApiShop[]>;
loadedShops$: Observable<ShopItem[]>;
lastUpdated$: Observable<string>;
isLoading$: Observable<boolean>;
hasMore$: Observable<boolean>;
private selectedIndex$ = new ReplaySubject<number>(1);
private listOffset$: Observable<number>;
private realmData$ = new ReplaySubject<PaymentInstitutionRealm>(1);
private showMore$ = new ReplaySubject<void>(1);
private loader$ = new BehaviorSubject<boolean>(true);
constructor(private apiShopsService: ApiShopsService, private shopsBalance: ShopsBalanceService) {
this.initAllShopsFetching();
this.initOffsetObservable();
this.initShownShopsObservable();
this.initIndicators();
}
initRealm(realm: PaymentInstitutionRealm): void {
this.realmData$.next(realm);
}
initOffsetIndex(offsetIndex: number): void {
this.selectedIndex$.next(offsetIndex);
this.showMore$.next();
}
refreshData(): void {
this.startLoading();
this.apiShopsService.reloadShops();
}
showMore(): void {
this.startLoading();
this.showMore$.next();
}
protected startLoading(): void {
this.loader$.next(true);
}
protected stopLoading(): void {
this.loader$.next(false);
}
protected updateShopsBalances(shops: ApiShop[]): void {
const shopIds: string[] = shops.map(({ id }: ApiShop) => id);
this.shopsBalance.setShopIds(shopIds);
}
private initAllShopsFetching(): void {
this.allShops$ = this.realmData$.pipe(
filterShopsByRealm(this.apiShopsService.shops$),
shareReplay(SHARE_REPLAY_CONF)
);
}
private initOffsetObservable(): void {
this.listOffset$ = this.showMore$.pipe(
mapTo(LIST_OFFSET),
withLatestFrom(this.selectedIndex$),
map(([curOffset]: [number, number]) => curOffset),
scan((offset: number, limit: number) => offset + limit, 0),
shareReplay(SHARE_REPLAY_CONF)
);
}
private initShownShopsObservable(): void {
this.loadedShops$ = combineLatest([this.allShops$, this.listOffset$]).pipe(
map(([shops, showedCount]: [ShopItem[], number]) => shops.slice(0, showedCount)),
tap((shops: ApiShop[]) => {
this.updateShopsBalances(shops);
}),
switchMap((shops: ApiShop[]) => {
return this.shopsBalance.balances$.pipe(
distinctUntilChanged(),
map((balances: ShopBalance[]) => combineShopItem(shops, balances))
);
}),
tap(() => this.stopLoading()),
shareReplay(SHARE_REPLAY_CONF)
);
}
private initIndicators(): void {
this.lastUpdated$ = this.allShops$.pipe(mapToTimestamp, shareReplay(1));
this.isLoading$ = this.loader$.asObservable();
this.hasMore$ = combineLatest([this.allShops$.pipe(pluck('length')), this.listOffset$]).pipe(
map(([count, showedCount]: [number, number]) => count > showedCount),
shareReplay(SHARE_REPLAY_CONF)
);
}
}

View File

@ -0,0 +1,201 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { cold } from 'jasmine-marbles';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { AnalyticsService as APIAnalyticsService } from '../../../../../../api-codegen/anapi';
import { InlineResponse200 } from '../../../../../../api-codegen/anapi/swagger-codegen';
import { AnalyticsService } from '../../../../../../api/analytics';
import { ShopsBalanceService } from './shops-balance.service';
@Injectable()
class MockAnalyticsService extends AnalyticsService {
set mockGroupBalances(value: InlineResponse200) {
this._mockGroupBalances = value;
}
private _mockGroupBalances: InlineResponse200;
getGroupBalances(): Observable<InlineResponse200> {
return of(this._mockGroupBalances);
}
}
describe('ShopsBalanceService', () => {
let service: ShopsBalanceService;
let analyticsService: MockAnalyticsService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ShopsBalanceService,
{
provide: AnalyticsService,
useClass: MockAnalyticsService,
},
{
provide: APIAnalyticsService,
useValue: null, // not used
},
],
});
service = TestBed.inject(ShopsBalanceService);
analyticsService = TestBed.inject(AnalyticsService) as MockAnalyticsService;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('balances$', () => {
it('should return balances list from AnalyticsService', () => {
const expected$ = cold('a', {
a: [
{
id: 'first-shop',
data: {
amount: 20,
currency: 'USD',
},
},
{
id: 'second-shop',
data: {
amount: 4,
currency: 'USD',
},
},
{
id: 'third-shop',
data: {
amount: 2,
currency: 'USD',
},
},
],
});
analyticsService.mockGroupBalances = {
result: [
{
groupBySHopResults: [
{
id: 'first-shop',
amountResults: [
{
amount: 20,
currency: 'USD',
},
],
},
{
id: 'second-shop',
amountResults: [
{
amount: 4,
currency: 'USD',
},
],
},
{
id: 'third-shop',
amountResults: [
{
amount: 2,
currency: 'USD',
},
],
},
],
},
],
};
expect(service.balances$).toBeObservable(expected$);
});
it('should return list with nullable data if api has no data for some shops', () => {
const expected$ = cold('a', {
a: [
{
id: 'first-shop',
data: null,
},
{
id: 'second-shop',
data: null,
},
{
id: 'third-shop',
data: {
amount: 2,
currency: 'USD',
},
},
],
});
analyticsService.mockGroupBalances = {
result: [
{
groupBySHopResults: [
{
id: 'first-shop',
amountResults: [],
},
{
id: 'second-shop',
},
{
id: 'third-shop',
amountResults: [
{
amount: 2,
currency: 'USD',
},
],
},
],
},
],
};
expect(service.balances$).toBeObservable(expected$);
});
it('should return empty balances list if api has no data', () => {
const expected$ = cold('a', {
a: [],
});
analyticsService.mockGroupBalances = {
result: [
{
groupBySHopResults: [],
},
],
};
expect(service.balances$).toBeObservable(expected$);
});
it('should return empty balances list if there was an error', () => {
const expected$ = cold('a', {
a: [],
});
analyticsService.getGroupBalances = () => {
return of({
result: [],
}).pipe(
map(() => {
throw new Error('[TEST ERROR] Mock Error');
})
);
};
expect(service.balances$).toBeObservable(expected$);
});
});
// setShopIds
});

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
import { isNil } from '@ngneat/transloco';
import { Observable, of, ReplaySubject } from 'rxjs';
import { catchError, distinctUntilChanged, map, shareReplay, switchMap } from 'rxjs/operators';
import { AnalyticsService } from '../../../../../../api/analytics';
import { SHARE_REPLAY_CONF } from '../../../../../../custom-operators';
import { ShopBalance } from '../../types/shop-balance';
@Injectable()
export class ShopsBalanceService {
balances$: Observable<ShopBalance[]>;
private shopIDsChange$ = new ReplaySubject<string[]>(1);
constructor(private analyticsService: AnalyticsService) {
this.balances$ = this.shopIDsChange$.pipe(
distinctUntilChanged(),
switchMap((shopIDs: string[]) => {
return this.analyticsService.getGroupBalances({ shopIDs }).pipe(
map(({ result }) => {
return result
.flatMap(({ groupBySHopResults }) => groupBySHopResults)
.map(({ id, amountResults = [] }) => {
return {
id,
data: isNil(amountResults) || isNil(amountResults[0]) ? null : amountResults[0],
};
});
}),
catchError((err) => {
console.error(err);
return of([]);
})
);
}),
shareReplay(SHARE_REPLAY_CONF)
);
}
setShopIds(shopIDs: string[]): void {
this.shopIDsChange$.next(shopIDs);
}
}

View File

@ -0,0 +1,10 @@
<dsh-row
*transloco="let s; scope: 'shops'; read: 'shops'"
fxLayout="row"
fxLayoutAlign="space-between center"
fxLayoutGap="24px"
color="primary"
>
<dsh-row-header-label fxFlex>{{ s('panel.name') }}</dsh-row-header-label>
<dsh-row-header-label fxFlex fxHide.lt-md> {{ s('panel.balance') }} </dsh-row-header-label>
</dsh-row>

View File

@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'dsh-shop-row-header',
templateUrl: 'shop-row-header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopRowHeaderComponent {}

View File

@ -0,0 +1,14 @@
<dsh-row fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="24px">
<ng-container *ngTemplateOutlet="shop ? tableGrid : loading; context: { $implicit: shop }"></ng-container>
</dsh-row>
<ng-template #loading>
<dsh-row-label>Loading ...</dsh-row-label>
</ng-template>
<ng-template #tableGrid let-item>
<dsh-row-label fxFlex>{{ item.details.name }}</dsh-row-label>
<dsh-row-label fxFlex fxHide.lt-md>
<dsh-shop-balance [shop]="shop"></dsh-shop-balance>
</dsh-row-label>
</ng-template>

View File

@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ShopItem } from '../../../types/shop-item';
@Component({
selector: 'dsh-shop-row',
templateUrl: 'shop-row.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopRowComponent {
@Input() shop: ShopItem;
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ShopsExpandedIdManagerService } from './shops-expanded-id-manager.service';
describe('ShopsExpandedIdManagerService', () => {
let service: ShopsExpandedIdManagerService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ShopsExpandedIdManagerService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ExpandedIdManager } from '@dsh/app/shared/services';
import { Shop } from '../../../../../../../api-codegen/capi/swagger-codegen';
import { FetchShopsService } from '../../../services/fetch-shops/fetch-shops.service';
@Injectable()
export class ShopsExpandedIdManagerService extends ExpandedIdManager<Shop> {
constructor(protected route: ActivatedRoute, protected router: Router, private shopsService: FetchShopsService) {
super(route, router);
}
protected get dataSet$(): Observable<Shop[]> {
return this.shopsService.allShops$;
}
}

View File

@ -0,0 +1 @@
export * from './shop-balance.module';

View File

@ -0,0 +1,8 @@
<ng-container
*ngTemplateOutlet="shop.balance ? balanceRow : emptyBalance; context: { $implicit: shop.balance }"
></ng-container>
<ng-template #balanceRow let-balance>
{{ balance.amount | toMajor | currency: balance.currency:'symbol' }}
</ng-template>
<ng-template #emptyBalance>--/--</ng-template>

View File

@ -0,0 +1,58 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ShopLocation } from '../../../../../../api-codegen/anapi/swagger-codegen';
import { ToMajorModule } from '../../../../../../to-major';
import { ShopItem } from '../../types/shop-item';
import { ShopBalanceComponent } from './shop-balance.component';
const mockShop: ShopItem = {
id: 'mock',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
url: 'example.com',
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
balance: {
amount: 20,
currency: 'USD',
},
};
describe('ShopBalanceComponent', () => {
let component: ShopBalanceComponent;
let fixture: ComponentFixture<ShopBalanceComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ToMajorModule],
declarations: [ShopBalanceComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShopBalanceComponent);
component = fixture.componentInstance;
component.shop = mockShop;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ShopItem } from '../../types/shop-item';
@Component({
selector: 'dsh-shop-balance',
templateUrl: 'shop-balance.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopBalanceComponent {
@Input() shop: ShopItem;
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ToMajorModule } from '../../../../../../to-major';
import { ShopBalanceComponent } from './shop-balance.component';
@NgModule({
imports: [CommonModule, ToMajorModule],
declarations: [ShopBalanceComponent],
exports: [ShopBalanceComponent],
})
export class ShopBalanceModule {}

View File

@ -0,0 +1,22 @@
<ng-container *transloco="let p; scope: 'shops'; read: 'shops.panel'">
<div class="mat-title">{{ p('actions') }}</div>
<div>
<ng-container *ngTemplateOutlet="shop.isSuspended ? activateButton : suspendButton"></ng-container>
</div>
</ng-container>
<ng-template #activateButton>
<ng-container *transloco="let p; scope: 'shops'; read: 'shops.panel'">
<button (click)="activate(shop.id)" color="accent" dsh-stroked-button>
{{ p('activate') }}
</button>
</ng-container>
</ng-template>
<ng-template #suspendButton>
<ng-container *transloco="let p; scope: 'shops'; read: 'shops.panel'">
<button (click)="suspend(shop.id)" color="warn" dsh-stroked-button>
{{ p('suspend') }}
</button>
</ng-container>
</ng-template>

View File

@ -0,0 +1,226 @@
import { Injectable } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoTestingModule } from '@ngneat/transloco';
import cloneDeep from 'lodash.clonedeep';
import { Observable, of } from 'rxjs';
import { ShopLocation } from '../../../../../../../../api-codegen/anapi/swagger-codegen';
import { ShopsService } from '../../../../../../../../api-codegen/capi/shops.service';
import { Shop } from '../../../../../../../../api-codegen/capi/swagger-codegen';
import { ApiShopsService } from '../../../../../../../../api/shop';
import { ShopItem } from '../../../../types/shop-item';
import { ShopActionsService } from '../../services/shop-actions/shop-actions.service';
import { ShopActionResult } from '../../types/shop-action-result';
import { ShopActionsComponent } from './shop-actions.component';
const mockShop: ShopItem = {
id: 'mock',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
url: 'example.com',
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
balance: {
amount: 20,
currency: 'USD',
},
};
class MockShopsService {
getShops(): Observable<Shop[]> {
return of([]);
}
}
@Injectable()
class MockApiShopsService extends ApiShopsService {
set mockShops(shops: Shop[]) {
this._shops = shops;
}
private _shops: Shop[] = [];
set mockActionResponse(response: any) {
this._actionResponse = response;
}
private _actionResponse: any;
getShopByID(shopID: string): Observable<Shop> {
return of(this._shops.find(({ id }) => id === shopID));
}
getShops(): Observable<Shop[]> {
return of(this._shops);
}
reloadShops() {
this._shops = cloneDeep(this._shops);
}
suspendShop() {
return of(this._actionResponse);
}
activateShop() {
return of(this._actionResponse);
}
}
class MockMatDialogRef<T = any, R = any> extends MatDialogRef<T, R> {}
class MockMatDialog {
private _dialogRef: MockMatDialogRef;
get dialogRef(): MockMatDialogRef {
return this._dialogRef;
}
open<T, R = any>(): MockMatDialogRef<T, R> {
this._dialogRef = new MockMatDialogRef(null, null);
return this._dialogRef;
}
}
describe('ShopActionsComponent', () => {
let component: ShopActionsComponent;
let fixture: ComponentFixture<ShopActionsComponent>;
let mockDialog: MockMatDialog;
let actionsService: ShopActionsService;
beforeEach(async(() => {
mockDialog = new MockMatDialog();
TestBed.configureTestingModule({
imports: [
TranslocoTestingModule.withLangs({
en: {
shops: {
panel: {
activate: 'activate',
suspend: 'suspend',
},
suspend: {
success: 'success suspend',
error: 'error suspend',
},
activate: {
success: 'success activate',
error: 'error activate',
},
},
},
}),
MatSnackBarModule,
],
declarations: [ShopActionsComponent],
providers: [
ShopActionsService,
{
provide: ApiShopsService,
useClass: MockApiShopsService,
},
{
provide: ShopsService,
useClass: MockShopsService,
},
{
provide: MatDialog,
useValue: mockDialog,
},
],
})
.overrideComponent(ShopActionsComponent, { set: { providers: [] } })
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShopActionsComponent);
component = fixture.componentInstance;
actionsService = TestBed.inject(ShopActionsService);
component.shop = mockShop;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('suspend', () => {
it('should call service suspend method', () => {
const spyOnSuspend = spyOn(actionsService, 'suspend').and.returnValue(of(ShopActionResult.SUCCESS));
component.suspend('id');
expect(spyOnSuspend).toHaveBeenCalledTimes(1);
expect(spyOnSuspend).toHaveBeenCalledWith('id');
});
it('should emit update data if suspend was successful', () => {
const spyOnSuspend = spyOn(actionsService, 'suspend').and.returnValue(of(ShopActionResult.SUCCESS));
const spyOnUpdateData = spyOn(component.updateData, 'emit');
component.suspend('id');
expect(spyOnSuspend).toHaveBeenCalledTimes(1);
expect(spyOnUpdateData).toHaveBeenCalledTimes(1);
});
it('should emit update data if suspend was not successful', () => {
const spyOnSuspend = spyOn(actionsService, 'suspend').and.returnValue(of(ShopActionResult.ERROR));
const spyOnUpdateData = spyOn(component.updateData, 'emit');
component.suspend('id');
expect(spyOnSuspend).toHaveBeenCalledTimes(1);
expect(spyOnUpdateData).not.toHaveBeenCalledTimes(1);
});
});
describe('activate', () => {
it('should call service activate method', () => {
const spyOnActivate = spyOn(actionsService, 'activate').and.returnValue(of(ShopActionResult.SUCCESS));
component.activate('id');
expect(spyOnActivate).toHaveBeenCalledTimes(1);
expect(spyOnActivate).toHaveBeenCalledWith('id');
});
it('should emit update data if activate was successful', () => {
const spyOnActivate = spyOn(actionsService, 'activate').and.returnValue(of(ShopActionResult.SUCCESS));
const spyOnUpdateData = spyOn(component.updateData, 'emit');
component.activate('id');
expect(spyOnActivate).toHaveBeenCalledTimes(1);
expect(spyOnUpdateData).toHaveBeenCalledTimes(1);
});
it('should emit update data if activate was not successful', () => {
const spyOnActivate = spyOn(actionsService, 'activate').and.returnValue(of(ShopActionResult.ERROR));
const spyOnUpdateData = spyOn(component.updateData, 'emit');
component.activate('id');
expect(spyOnActivate).toHaveBeenCalledTimes(1);
expect(spyOnUpdateData).not.toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { filter, take } from 'rxjs/operators';
import { ShopItem } from '../../../../types/shop-item';
import { isSuccessfulShopAction } from '../../services/shop-actions/is-successful-shop-action';
import { ShopActionsService } from '../../services/shop-actions/shop-actions.service';
@Component({
selector: 'dsh-shop-actions',
templateUrl: 'shop-actions.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ShopActionsService],
})
export class ShopActionsComponent {
@Input() shop: ShopItem;
@Output() updateData = new EventEmitter<void>();
constructor(private shopActions: ShopActionsService) {}
suspend(id: string): void {
this.shopActions
.suspend(id)
.pipe(take(1), filter(isSuccessfulShopAction))
.subscribe(() => {
this.updateData.emit();
});
}
activate(id: string): void {
this.shopActions
.activate(id)
.pipe(take(1), filter(isSuccessfulShopAction))
.subscribe(() => {
this.updateData.emit();
});
}
}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ShopContractDetailsService } from './shop-contract-details.service';
import { ShopContractDetailsService } from '../../services/shop-contract-details/shop-contract-details.service';
@Component({
selector: 'dsh-shop-contract-details',

View File

@ -0,0 +1,9 @@
<ng-container *transloco="let p; scope: 'shops'; read: 'shops.panel'">
<div class="mat-title">{{ p('id') }}</div>
<div class="mat-subheading-2">{{ id }}</div>
<div>
<button dsh-button [cdkCopyToClipboard]="id" (cdkCopyToClipboardCopied)="copied($event)">
{{ p('copy') }}
</button>
</div>
</ng-container>

View File

@ -0,0 +1,60 @@
import { ClipboardModule } from '@angular/cdk/clipboard';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { TranslocoTestingModule } from '@ngneat/transloco';
import { ShopIdComponent } from './shop-id.component';
describe('ShopIdComponent', () => {
let component: ShopIdComponent;
let fixture: ComponentFixture<ShopIdComponent>;
let snackbar: MatSnackBar;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslocoTestingModule.withLangs({
en: {
copied: 'Copied!',
copyFailed: 'CopyFailed!',
},
}),
MatSnackBarModule,
ClipboardModule,
],
declarations: [ShopIdComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShopIdComponent);
component = fixture.componentInstance;
snackbar = TestBed.inject(MatSnackBar);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('copied', () => {
it('should open snackbar with successful copy text', () => {
const spyOnSnackBar = spyOn(snackbar, 'open').and.returnValue(null);
component.copied(true);
expect(spyOnSnackBar).toHaveBeenCalledTimes(1);
expect(spyOnSnackBar).toHaveBeenCalledWith('en.copied', 'OK', { duration: 1000 });
});
it('should open snackbar with error copy text', () => {
const spyOnSnackBar = spyOn(snackbar, 'open').and.returnValue(null);
component.copied(false);
expect(spyOnSnackBar).toHaveBeenCalledTimes(1);
expect(spyOnSnackBar).toHaveBeenCalledWith('en.copyFailed', 'OK', { duration: 1000 });
});
});
});

View File

@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
@Component({
selector: 'dsh-shop-id',
templateUrl: 'shop-id.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopIdComponent {
@Input() id: string;
constructor(private snackBar: MatSnackBar, private transloco: TranslocoService) {}
copied(isCopied: boolean) {
this.snackBar.open(this.transloco.translate(isCopied ? 'copied' : 'copyFailed'), 'OK', { duration: 1000 });
}
}

View File

@ -0,0 +1,15 @@
<div *transloco="let p; scope: 'shops'; read: 'shops.panel'" fxLayout="column" fxLayoutGap="24px">
<dsh-details-item [title]="p('name')" fxFlex fxFlex.lt-md="100">{{ shop.details.name }}</dsh-details-item>
<dsh-details-item [title]="p('url')" fxFlex fxFlex.lt-md="100">{{ shop.location.url }}</dsh-details-item>
<div fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="space-between" fxLayoutGap="24px">
<dsh-details-item [title]="p('balance')" fxFlex fxFlex.lt-md="100">
<dsh-shop-balance [shop]="shop"></dsh-shop-balance>
</dsh-details-item>
<dsh-details-item [title]="p('createdAt')" fxFlex fxFlex.lt-md="100">{{
shop.createdAt | date: 'dd MMMM yyyy, HH:mm:ss'
}}</dsh-details-item>
<dsh-details-item [title]="p('category')" fxFlex fxFlex.lt-md="100">{{
(category$ | async)?.name
}}</dsh-details-item>
</div>
</div>

View File

@ -0,0 +1,102 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslocoTestingModule } from '@ngneat/transloco';
import { Subject } from 'rxjs';
import { DetailsItemModule } from '@dsh/components/layout';
import { ShopLocation } from '../../../../../../../../api-codegen/anapi/swagger-codegen';
import { Category } from '../../../../../../../../api-codegen/capi/swagger-codegen';
import { ShopItem } from '../../../../types/shop-item';
import { ShopBalanceModule } from '../../../shop-balance';
import { CategoryService } from '../../services/category/category.service';
import { ShopInfoComponent } from './shop-info.component';
const mockShop: ShopItem = {
id: 'mock',
createdAt: new Date(),
isBlocked: false,
isSuspended: false,
categoryID: 1,
location: {
locationType: ShopLocation.LocationTypeEnum.ShopLocationUrl,
url: 'example.com',
},
details: {
name: 'my name',
description: 'some description',
},
contractID: 'contractID',
payoutToolID: 'payoutToolID',
scheduleID: 1,
account: {
currency: 'USD',
guaranteeID: 2,
settlementID: 2,
},
balance: {
amount: 20,
currency: 'USD',
},
};
class MockCategoryService {
category$ = new Subject<Category>();
updateID(categoryID: number) {
this.category$.next({
categoryID,
name: 'Mock Category',
});
}
}
describe('ShopInfoComponent', () => {
let component: ShopInfoComponent;
let fixture: ComponentFixture<ShopInfoComponent>;
let mockCategoryService: MockCategoryService;
beforeEach(async(() => {
mockCategoryService = new MockCategoryService();
TestBed.configureTestingModule({
imports: [
TranslocoTestingModule.withLangs({
en: {
shops: {
panel: {
name: 'PanelName',
url: 'PanelUrl',
balance: 'PanelBalance',
createdAt: 'PanelCreatedAt',
category: 'PanelCategory',
},
},
},
}),
FlexLayoutModule,
DetailsItemModule,
ShopBalanceModule,
],
declarations: [ShopInfoComponent],
providers: [
{
provide: CategoryService,
useValue: mockCategoryService,
},
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShopInfoComponent);
component = fixture.componentInstance;
component.shop = mockShop;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import isNil from 'lodash.isnil';
import { Observable } from 'rxjs';
import { Category } from '../../../../../../../../api-codegen/capi/swagger-codegen';
import { ShopItem } from '../../../../types/shop-item';
import { CategoryService } from '../../services/category/category.service';
@Component({
selector: 'dsh-shop-info',
templateUrl: 'shop-info.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopInfoComponent {
@Input()
get shop(): ShopItem {
return this._shop;
}
set shop(shopItem: ShopItem) {
this._shop = shopItem;
if (isNil(shopItem)) {
return;
}
this.categoryService.updateID(shopItem.categoryID);
}
category$: Observable<Category> = this.categoryService.category$;
private _shop: ShopItem;
constructor(private categoryService: CategoryService) {}
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { PayoutToolParams } from './payout-tool-params';
import { ShopPayoutToolDetailsService } from './shop-payout-tool-details.service';
import { ShopPayoutToolDetailsService } from '../../services/shop-payout-tool-details/shop-payout-tool-details.service';
import { PayoutToolParams } from '../../types/payout-tool-params';
@Component({
selector: 'dsh-shop-payout-tool-details',

View File

@ -0,0 +1,2 @@
export * from './shop-details.component';
export * from './shop-details.module';

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { CategoryService } from './category.service';
describe('CategoryService', () => {
let service: CategoryService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CategoryService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { combineLatest, Observable, Subject } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { Category } from '../../../../../../../../api-codegen/capi/swagger-codegen';
import { CategoriesService } from '../../../../../../../../api/categories';
import { SHARE_REPLAY_CONF } from '../../../../../../../../custom-operators';
@Injectable()
export class CategoryService {
category$: Observable<Category>;
private categoryID$ = new Subject<number>();
constructor(private categoriesService: CategoriesService) {
this.category$ = combineLatest([this.categoryID$, this.categoriesService.categories$]).pipe(
map(([categoryID, categories]: [number, Category[]]) =>
categories.find((c) => c.categoryID === categoryID)
),
shareReplay(SHARE_REPLAY_CONF)
);
}
updateID(categoryID: number): void {
this.categoryID$.next(categoryID);
}
}

View File

@ -0,0 +1,5 @@
import { ShopActionResult } from '../../types/shop-action-result';
export function isSuccessfulShopAction(action: ShopActionResult): boolean {
return action === ShopActionResult.SUCCESS;
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ShopActionsService } from './shop-actions.service';
describe('ShopActionsService', () => {
let service: ShopActionsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ShopActionsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { Observable, of } from 'rxjs';
import { catchError, filter, map, switchMap } from 'rxjs/operators';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { ApiShopsService } from '../../../../../../../../api/shop';
import { ShopActionResult } from '../../types/shop-action-result';
@Injectable()
export class ShopActionsService {
constructor(
private shopService: ApiShopsService,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private transloco: TranslocoService
) {}
suspend(shopID: string): Observable<ShopActionResult> {
return this.dialog
.open(ConfirmActionDialogComponent)
.afterClosed()
.pipe(
filter((r) => r === 'confirm'),
switchMap(() => this.shopService.suspendShop(shopID)),
map(() => {
this.snackBar.open(this.transloco.translate('suspend.success', null, 'shops'), 'OK', {
duration: 3000,
});
return ShopActionResult.SUCCESS;
}),
catchError(() => {
this.snackBar.open(this.transloco.translate('suspend.error', null, 'shops'), 'OK');
return of(ShopActionResult.ERROR);
})
);
}
activate(shopID: string): Observable<ShopActionResult> {
return this.dialog
.open(ConfirmActionDialogComponent)
.afterClosed()
.pipe(
filter((r) => r === 'confirm'),
switchMap(() => this.shopService.activateShop(shopID)),
map(() => {
this.snackBar.open(this.transloco.translate('activate.success', null, 'shops'), 'OK', {
duration: 3000,
});
return ShopActionResult.SUCCESS;
}),
catchError(() => {
this.snackBar.open(this.transloco.translate('activate.error', null, 'shops'), 'OK');
return of(ShopActionResult.ERROR);
})
);
}
}

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