Chargebacks List & Details (#195)

This commit is contained in:
Rinat Arsaev 2020-10-15 17:35:44 +03:00 committed by GitHub
parent 526b46aa1f
commit 2e477bba09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 2368 additions and 326 deletions

6
.genryrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"include": "node_modules/@rbkmoney/angular-templates/**",
"template": {
"prefix": "cc"
}
}

View File

@ -1,6 +0,0 @@
# VSC scaffolding
- [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=alfnielsen.vsc-scaffolding)
- [Documentation](http://vsc-base.org/)
- [Source code](https://github.com/alfnielsen/vsc-base)

View File

@ -1,75 +0,0 @@
import * as vsc from 'vsc-base';
const PREFIX = 'cc';
function getComponentName(name: string) {
return `${vsc.toPascalCase(name)}Component`;
}
function getComponentPath(name: string) {
return `${vsc.toKebabCase(name)}.component`;
}
function getClassName(name: string) {
return `${PREFIX}-${vsc.toKebabCase(name)}`;
}
export function Template(path: string, templatePath: string): vsc.vscTemplate {
return {
userInputs: [
{
title: 'Component name',
argumentName: 'name',
defaultValue: '',
},
],
template: [
{
type: 'folder',
name: (inputs) => vsc.toKebabCase(inputs.name),
children: [
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.html`,
content: (inputs) => `
<div class="${PREFIX}-${vsc.toKebabCase(inputs.name)}">
<ng-content></ng-content>
</div>
`,
},
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.scss`,
content: (inputs) => `
.${PREFIX}-${vsc.toKebabCase(inputs.name)} {
}
`,
},
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.ts`,
content: (inputs) => `
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: '${PREFIX}-${vsc.toKebabCase(inputs.name)}',
templateUrl: '${getComponentPath(inputs.name)}.html',
styleUrls: ['${getComponentPath(inputs.name)}.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ${getComponentName(inputs.name)} {
}
`,
},
{
type: 'file',
name: (inputs) => `index.ts`,
content: (inputs) => `
export * from './${getComponentPath(inputs.name)}'
`,
},
],
},
],
};
}

View File

@ -1,117 +0,0 @@
import * as vsc from 'vsc-base';
const PREFIX = 'cc';
function getModuletName(name: string) {
return `${vsc.toPascalCase(name)}Module`;
}
function getModulePath(name: string) {
return `${vsc.toKebabCase(name)}.module`;
}
function getComponentName(name: string) {
return `${vsc.toPascalCase(name)}Component`;
}
function getComponentPath(name: string) {
return `${vsc.toKebabCase(name)}.component`;
}
function getCssClassName(name: string) {
return `${PREFIX}-${vsc.toKebabCase(name)}`;
}
export function Template(path: string, templatePath: string): vsc.vscTemplate {
return {
userInputs: [
{
title: 'Component name',
argumentName: 'name',
defaultValue: '',
},
],
template: [
{
type: 'folder',
name: (inputs) => vsc.toKebabCase(inputs.name),
children: [
{
type: 'file',
name: (inputs) => `${getModulePath(inputs.name)}.ts`,
content: (inputs) => `
import { NgModule } from '@angular/core';
import { ${getComponentName(inputs.name)} } from './${getComponentPath(inputs.name)}';
@NgModule({
imports: [],
declarations: [${getComponentName(inputs.name)}],
exports: [${getComponentName(inputs.name)}]
})
export class ${getModuletName(inputs.name)} {}
`,
},
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.html`,
content: (inputs) => `
<div class="${PREFIX}-${vsc.toKebabCase(inputs.name)}">
<ng-content></ng-content>
</div>
`,
},
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.scss`,
content: (inputs) => `
.${getCssClassName(inputs.name)} {
}
`,
},
{
type: 'file',
name: (inputs) => `${getComponentPath(inputs.name)}.ts`,
content: (inputs) => `
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: '${PREFIX}-${vsc.toKebabCase(inputs.name)}',
templateUrl: '${getComponentPath(inputs.name)}.html',
styleUrls: ['${getComponentPath(inputs.name)}.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ${getComponentName(inputs.name)} {
}
`,
},
{
type: 'file',
name: (inputs) => `index.ts`,
content: (inputs) => `
export * from './${getModulePath(inputs.name)}';
export * from './${getComponentPath(inputs.name)}'
`,
},
{
type: 'file',
name: (inputs) => `_${vsc.toKebabCase(inputs.name)}-theme.scss`,
content: (inputs) => `
@import '~@angular/material/theming';
@mixin ${getCssClassName(inputs.name)}-theme($theme) {
.${getCssClassName(inputs.name)} {
}
}
@mixin ${getCssClassName(inputs.name)}-typography($config) {
.${getCssClassName(inputs.name)} {
}
}
`,
},
],
},
],
};
}

416
package-lock.json generated
View File

@ -3742,6 +3742,16 @@
}
}
},
"@rbkmoney/angular-templates": {
"version": "0.1.2",
"resolved": "https://npm.pkg.github.com/download/@rbkmoney/angular-templates/0.1.2/03923ba6c89fd4ba87e9acf77f1074d3693dbcfdc0e775f707d0338b7ad4b997",
"integrity": "sha512-io9DKeEbTQ0/h1UVkl18KjqWg3I2BxwHqvbSTpR7xkJ9J4VKqXS92BB1JYpOinQzY8W7qAxVYDosGBazhRIftA==",
"dev": true,
"requires": {
"genry": "^0.15.0",
"lodash": "^4.17.15"
}
},
"@rbkmoney/partial-fetcher": {
"version": "1.0.4",
"resolved": "https://npm.pkg.github.com/download/@rbkmoney/partial-fetcher/1.0.4/525ae43ea6950ba9a0282b5396cf094071b619850d6fe1b622d5d5d1c6079021",
@ -3785,15 +3795,6 @@
}
}
},
"@types/child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha512-xZ4kkF82YkmqPCERqV9Tj0bVQj3Tk36BqGlNgxv5XhifgDRhwAqp+of+sccksdpZRbbPsNwMOkmUqOnLgxKtGw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -3808,15 +3809,6 @@
"del": "*"
}
},
"@types/fs-extra": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz",
"integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@ -3887,6 +3879,12 @@
"integrity": "sha512-BneGN0J9ke24lBRn44hVHNeDlrXRYF+VRp0HbSUNnEZahXGAysHZIqnf/hER6aabdBgzM4YOV4jrR8gj4Zfi0g==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/pdfmake": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/pdfmake/-/pdfmake-0.1.9.tgz",
@ -6135,29 +6133,6 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=",
"dev": true,
"requires": {
"cross-spawn": "^4.0.2",
"node-version": "^1.0.0",
"promise-polyfill": "^6.0.1"
},
"dependencies": {
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
"integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"which": "^1.2.9"
}
}
}
},
"chokidar": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.2.1.tgz",
@ -6432,6 +6407,11 @@
}
}
},
"coerce-property": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/coerce-property/-/coerce-property-0.3.2.tgz",
"integrity": "sha512-jPD0ItShAyzLuvwNE9LzSBzcE5n0iD9ob2YaES6EkNhSEm/BZFU+NO9rVU9EL1s+fXesIVc7VP5OHOR7Kl82BA=="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@ -7635,6 +7615,12 @@
"stream-shift": "^1.0.0"
}
},
"easy-stack": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.0.tgz",
"integrity": "sha1-EskbMIWjfwuqM26UhurEv5Tj54g=",
"dev": true
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -8057,6 +8043,12 @@
"es5-ext": "~0.10.14"
}
},
"event-pubsub": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
"dev": true
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@ -8811,17 +8803,6 @@
"readable-stream": "^2.0.0"
}
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@ -8864,6 +8845,220 @@
"integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==",
"dev": true
},
"genry": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/genry/-/genry-0.15.0.tgz",
"integrity": "sha512-XWjWNZzeXTKDtGsVyQSS6hnBlBDY/H7wMi0spj3bQv2yZ8+fVZy9M0TaS3W23NvRdNIZWx2iL6WGbLmqDQcNxA==",
"dev": true,
"requires": {
"cosmiconfig": "^6.0.0",
"glob": "^7.1.6",
"node-ipc": "^9.1.1",
"ora": "^4.0.3",
"pkg-up": "^3.1.0",
"prettier": "^2.0.4",
"prompts": "^2.3.2",
"ts-node": "^8.8.2",
"yargs": "^15.3.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
"integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.7.2"
}
},
"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-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
}
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"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"
}
},
"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"
}
},
"parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
"integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
}
},
"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
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"dev": true,
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"gensync": {
"version": "1.0.0-beta.1",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
@ -10146,6 +10341,21 @@
}
}
},
"js-message": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz",
"integrity": "sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=",
"dev": true
},
"js-queue": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.0.tgz",
"integrity": "sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=",
"dev": true,
"requires": {
"easy-stack": "^1.0.0"
}
},
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
@ -10183,6 +10393,12 @@
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@ -10592,6 +10808,12 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
"dev": true
},
"less": {
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
@ -10729,6 +10951,12 @@
}
}
},
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"loader-runner": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@ -11551,6 +11779,17 @@
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
"integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs="
},
"node-ipc": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz",
"integrity": "sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==",
"dev": true,
"requires": {
"event-pubsub": "4.3.0",
"js-message": "1.0.5",
"js-queue": "2.0.0"
}
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@ -11594,12 +11833,6 @@
"integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==",
"dev": true
},
"node-version": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz",
"integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==",
"dev": true
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -12459,6 +12692,23 @@
"readable-stream": "^2.1.5"
}
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"requires": {
"callsites": "^3.0.0"
},
"dependencies": {
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
}
}
},
"parse-asn1": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz",
@ -12645,6 +12895,15 @@
"find-up": "^3.0.0"
}
},
"pkg-up": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
"dev": true,
"requires": {
"find-up": "^3.0.0"
}
},
"png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
@ -13413,12 +13672,6 @@
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM="
},
"promise-polyfill": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz",
"integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=",
"dev": true
},
"promise-retry": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz",
@ -13437,6 +13690,16 @@
}
}
},
"prompts": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz",
"integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==",
"dev": true,
"requires": {
"kleur": "^3.0.3",
"sisteransi": "^1.0.4"
}
},
"protoduck": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz",
@ -14836,6 +15099,12 @@
}
}
},
"sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"dev": true
},
"skipper-proto": {
"version": "github:rbkmoney/skipper-proto#d33d87cd9080925861f755b11ee1f16378076f74",
"from": "github:rbkmoney/skipper-proto#d33d87cd9080925861f755b11ee1f16378076f74"
@ -17133,19 +17402,6 @@
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
},
"vsc-base": {
"version": "0.9.10",
"resolved": "https://registry.npmjs.org/vsc-base/-/vsc-base-0.9.10.tgz",
"integrity": "sha512-2fINBTgn4j4COH4vh+VkRiyHjvvyk+uFeSJQtizorqzUox0xocmgCEPTugwusE55rQVet2VqmqDF6QjFQw9pGg==",
"dev": true,
"requires": {
"@types/child-process-promise": "^2.2.1",
"@types/fs-extra": "^5.1.0",
"child-process-promise": "^2.2.1",
"fs-extra": "^7.0.1",
"typescript": "^3.4.5"
}
},
"watchpack": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
@ -18578,6 +18834,12 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
"dev": true
},
"yargs": {
"version": "12.0.5",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",

View File

@ -34,6 +34,7 @@
"angular-file": "3.0.1",
"angular2-prettyjson": "3.0.1",
"ank-proto": "git+ssh://git@github.com:rbkmoney/ank-proto.git#d638e44eb8632fd62f0d6730294e51637babcc78",
"coerce-property": "^0.3.2",
"damsel": "git+ssh://git@github.com/rbkmoney/damsel.git#895f01ac1b71e108eb8d5dce1df0ef2e31dc0152",
"file-storage-proto": "git+ssh://git@github.com:rbkmoney/file-storage-proto.git#281e1ca4cea9bf32229a6c389f0dcf5d49c05a0b",
"fistful-proto": "git+ssh://git@github.com/rbkmoney/fistful-proto.git#c2113c853ed71a34bb6468b9a6cf9b468967af84",
@ -61,6 +62,7 @@
"@angular-devkit/build-angular": "^0.1000.6",
"@angular/cli": "^10.0.6",
"@angular/compiler-cli": "^10.0.10",
"@rbkmoney/angular-templates": "^0.1.2",
"@types/del": "^4.0.0",
"@types/glob": "^7.1.3",
"@types/humanize-duration": "~3.18.0",
@ -87,7 +89,6 @@
"ts-node": "~8.8.2",
"tslint": "~6.1.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "~3.9.7",
"vsc-base": "^0.9.10"
"typescript": "~3.9.7"
}
}

View File

@ -0,0 +1,42 @@
import {
InvoicePaymentChargebackCategory,
InvoicePaymentChargebackStage,
InvoicePaymentChargebackStatus,
} from '../thrift-services/damsel/gen-model/domain';
export const chargebackStatuses: (keyof InvoicePaymentChargebackStatus)[] = [
'accepted',
'cancelled',
'pending',
'rejected',
];
export const chargebackCategories: (keyof InvoicePaymentChargebackCategory)[] = [
'authorisation',
'dispute',
'fraud',
'processing_error',
];
export const chargebackStages: (keyof InvoicePaymentChargebackStage)[] = [
'arbitration',
'chargeback',
'pre_arbitration',
];
// https://github.com/rbkmoney/magista#chargebacks
export interface Chargebacks {
merchant_id?: string;
shop_ids?: string;
invoice_id?: string;
payment_id?: string;
chargeback_id?: string;
from_time?: string;
to_time?: string;
// список интересующих статусов
chargeback_statuses?: (keyof InvoicePaymentChargebackStatus)[];
// список интересующих категорий
chargeback_categories?: (keyof InvoicePaymentChargebackCategory)[];
// список интересующих этапов
chargeback_stages?: (keyof InvoicePaymentChargebackStage)[];
}

View File

@ -0,0 +1,5 @@
import { QueryDSL } from './query-dsl';
export function createDSL(query: QueryDSL['query']): string {
return JSON.stringify({ query });
}

View File

@ -1 +1,3 @@
export * from './query-dsl';
export * from './create-dsl';
export * from './chargebacks';

View File

@ -1,11 +1,15 @@
import { Chargebacks } from './chargebacks';
import { Deposit } from './deposit';
import { ModelParams } from './model-params';
import { Params } from './params';
import { Payment } from './payment';
export type ChargebacksParams = Params & ModelParams & Chargebacks;
export interface QueryDSL {
query: {
payments?: Payment & Params & ModelParams;
deposits?: Deposit & Params & ModelParams;
chargebacks?: ChargebacksParams;
};
}

View File

@ -0,0 +1,9 @@
@import '~@angular/material/theming';
@mixin cc-chargeback-details-theme($theme) {
$foreground: map-get($theme, foreground);
.cc-chargeback-details-caption {
color: mat-color($foreground, secondary-text);
}
}

View File

@ -0,0 +1,29 @@
<div fxLayout="column" fxLayoutGap="32px">
<h1 class="mat-headline">Change chargeback status</h1>
<mat-dialog-content [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field fxFlex>
<mat-label>Status</mat-label>
<mat-select formControlName="status" required>
<mat-option *ngFor="let c of statuses" [value]="c">
{{ c }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<input
matInput
formControlName="date"
[matDatepicker]="datepicker"
placeholder="Date of decision (optional)"
/>
<mat-datepicker-toggle matSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker></mat-datepicker>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" [disabled]="form.invalid" (click)="changeStatus()">
CHANGE STATUS
</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,64 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatDialogConfig, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
ChangeChargebackStatusDialogService,
Statuses,
} from './change-chargeback-status-dialog.service';
@Component({
selector: 'cc-change-chargeback-status-dialog',
templateUrl: 'change-chargeback-status-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ChangeChargebackStatusDialogService],
})
export class ChangeChargebackStatusDialogComponent {
static defaultConfig: MatDialogConfig = {
disableClose: true,
width: '552px',
};
form = this.fb.group({
status: '',
date: '',
});
statuses = Statuses;
constructor(
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA)
public params: {
invoiceID: string;
paymentID: string;
chargebackID: string;
},
private dialogRef: MatDialogRef<ChangeChargebackStatusDialogComponent>,
private snackBar: MatSnackBar,
private changeChargebackStatusDialogService: ChangeChargebackStatusDialogService
) {}
changeStatus() {
const { status, date } = this.form.value;
this.changeChargebackStatusDialogService
.changeStatus({
status,
date: date ? date.utc().format() : undefined,
invoiceID: this.params.invoiceID,
paymentID: this.params.paymentID,
chargebackID: this.params.chargebackID,
})
.subscribe(
() => this.dialogRef.close(),
(error) => {
console.error(error);
this.snackBar.open('An error occurred while chargeback changing status', 'OK');
}
);
}
cancel() {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { PaymentProcessingService } from 'src/app/thrift-services/damsel/payment-processing.service';
export const Statuses = ['Accept', 'Cancell', 'Reject'] as const;
@Injectable()
export class ChangeChargebackStatusDialogService {
constructor(private paymentProcessingService: PaymentProcessingService) {}
changeStatus({
status,
date,
invoiceID,
paymentID,
chargebackID,
}: {
status: string;
date: string;
invoiceID: string;
paymentID: string;
chargebackID: string;
}) {
const fnByStatus = {
Accept: this.paymentProcessingService.acceptChargeback,
Cancell: this.paymentProcessingService.cancelChargeback,
Reject: this.paymentProcessingService.rejectChargeback,
};
return fnByStatus[status as typeof Statuses[number]].bind(this.paymentProcessingService)(
invoiceID,
paymentID,
chargebackID,
Object.assign({}, !!date && { occurred_at: date })
);
}
}

View File

@ -0,0 +1,2 @@
export * from './change-chargeback-status-dialog.component';
export * from './change-chargeback-status-dialog.service';

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../app-auth-guard.service';
import { ChargebackDetailsComponent } from './chargeback-details.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: ChargebackDetailsComponent,
canActivate: [AppAuthGuardService],
},
]),
],
})
export class ChargebackDetailsRoutingModule {}

View File

@ -0,0 +1,67 @@
<div fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="8px">
<div fxLayout fxLayoutAlign="space-between">
<cc-headline fxLayout fxLayoutAlign="space-between">Chargeback</cc-headline>
<div class="cc-headline">
{{ (chargeback$ | async)?.status | ccMapUnion: mapStatus }}
</div>
</div>
<div fxLayout fxLayoutAlign="space-between">
<div class="cc-subheading-1 cc-chargeback-details-caption">
#{{ (chargeback$ | async)?.id }}
</div>
<div class="cc-subheading-1 cc-chargeback-details-caption">
{{ (chargeback$ | async)?.stage | ccMapUnion: mapStage }}
</div>
</div>
</div>
<mat-card *ngIf="chargeback$ | async as chargeback">
<mat-card-content fxLayout="column" fxLayoutGap="24px">
<h2 class="cc-subheading-1">Chargeback data</h2>
<div gdColumns="1fr 1fr 1fr" gdGap="16px">
<cc-details-item gdColumn="1 / span 3" title="Created At">
{{ chargeback.created_at | date: 'dd.MM.yy HH:mm:ss' }}
</cc-details-item>
<cc-details-item *ngIf="chargeback.levy" title="Levy Amount">
{{ chargeback.levy.amount | ccFormatAmount }}
{{ chargeback.levy.currency.symbolic_code | ccCurrency }}
</cc-details-item>
<cc-details-item *ngIf="chargeback.body" title="Body Amount">
{{ chargeback.body.amount | ccFormatAmount }}
{{ chargeback.body.currency.symbolic_code | ccCurrency }}
</cc-details-item>
</div>
<mat-divider inset></mat-divider>
<h2 class="cc-title">Reason</h2>
<div gdColumns="1fr 1fr 1fr" gdGap="16px">
<cc-details-item title="Category">
{{ chargeback.reason.category | ccUnionKey }}
</cc-details-item>
<cc-details-item title="Code">
{{ chargeback.reason.code }}
</cc-details-item>
</div>
<ng-container *ngIf="(shop$ | async) && (payment$ | async)">
<mat-divider inset></mat-divider>
<div fxLayout fxLayoutAlign="space-between center">
<h2 class="cc-title">Payment</h2>
<button mat-icon-button (click)="navigateToPayment()">
<mat-icon>open_in_new</mat-icon>
</button>
</div>
<cc-payment-main-info
[shop]="shop$ | async"
[payment]="payment$ | async"
></cc-payment-main-info>
</ng-container>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="changeStatus()">CHANGE STATUS</button>
<button mat-button (click)="reopen()">REOPEN</button>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,99 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { combineLatest } from 'rxjs';
import { first, switchMap } from 'rxjs/operators';
import {
InvoicePaymentChargebackStage,
InvoicePaymentChargebackStatus,
} from 'src/app/thrift-services/damsel/gen-model/domain';
import { ChangeChargebackStatusDialogComponent } from './change-chargeback-status-dialog';
import { ChargebackDetailsService } from './chargeback-details.service';
import { ReopenChargebackDialogComponent } from './reopen-chargeback-dialog';
@Component({
selector: 'cc-chargeback-details',
templateUrl: 'chargeback-details.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ChargebackDetailsService],
})
export class ChargebackDetailsComponent {
mapStatus: { [N in keyof InvoicePaymentChargebackStatus] } = {
accepted: 'Accepted',
cancelled: 'Cancelled',
pending: 'Pending',
rejected: 'Rejected',
};
mapStage: { [N in keyof InvoicePaymentChargebackStage] } = {
arbitration: 'Arbitration',
chargeback: 'Chargeback',
pre_arbitration: 'Pre-arbitration',
};
chargeback$ = this.chargebackDetailsService.chargeback$;
shop$ = this.chargebackDetailsService.shop$;
payment$ = this.chargebackDetailsService.payment$;
constructor(
private chargebackDetailsService: ChargebackDetailsService,
private dialog: MatDialog,
private router: Router
) {}
changeStatus() {
combineLatest([this.chargeback$, this.payment$])
.pipe(
first(),
switchMap(([{ id: chargebackID }, { id: paymentID, invoice_id: invoiceID }]) =>
this.dialog
.open(ChangeChargebackStatusDialogComponent, {
...ChangeChargebackStatusDialogComponent.defaultConfig,
data: {
invoiceID,
paymentID,
chargebackID,
},
})
.afterClosed()
)
)
.subscribe(() => this.chargebackDetailsService.loadChargeback());
}
reopen() {
combineLatest([this.chargeback$, this.payment$])
.pipe(
first(),
switchMap(([{ id: chargebackID }, { id: paymentID, invoice_id: invoiceID }]) =>
this.dialog
.open(ReopenChargebackDialogComponent, {
...ReopenChargebackDialogComponent.defaultConfig,
data: {
invoiceID,
paymentID,
chargebackID,
},
})
.afterClosed()
)
)
.subscribe(() => this.chargebackDetailsService.loadChargeback());
}
navigateToPayment() {
this.payment$
.pipe(first())
.subscribe((p) =>
this.router.navigate([
'party',
p.owner_id,
'invoice',
p.invoice_id,
'payment',
p.id,
])
);
}
}

View File

@ -0,0 +1,56 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { DetailsItemModule } from '@cc/components/details-item';
import { HeadlineModule } from '@cc/components/headline';
import { CommonPipesModule } from '@cc/pipes/common-pipes.module';
import { PaymentMainInfoModule } from '../payment-details/payment-main-info';
import { ChangeChargebackStatusDialogComponent } from './change-chargeback-status-dialog';
import { ChargebackDetailsRoutingModule } from './chargeback-details-routing.module';
import { ChargebackDetailsComponent } from './chargeback-details.component';
import { ReopenChargebackDialogComponent } from './reopen-chargeback-dialog';
const EXPORTED_DECLARATIONS = [
ChargebackDetailsComponent,
ChangeChargebackStatusDialogComponent,
ReopenChargebackDialogComponent,
];
@NgModule({
imports: [
ChargebackDetailsRoutingModule,
CommonModule,
HeadlineModule,
MatCardModule,
MatDividerModule,
DetailsItemModule,
CommonPipesModule,
FlexLayoutModule,
PaymentMainInfoModule,
MatButtonModule,
ReactiveFormsModule,
MatSnackBarModule,
MatDialogModule,
MatDatepickerModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatIconModule,
],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,
})
export class ChargebackDetailsModule {}

View File

@ -0,0 +1,59 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest, merge, Subject } from 'rxjs';
import { map, pluck, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { PartyService } from 'src/app/party/party.service';
import { createDSL } from 'src/app/query-dsl';
import { MerchantStatisticsService } from 'src/app/thrift-services/damsel/merchant-statistics.service';
import { PaymentProcessingService } from 'src/app/thrift-services/damsel/payment-processing.service';
@Injectable()
export class ChargebackDetailsService {
private loadChargeback$ = new Subject<void>();
payment$ = this.route.params.pipe(
switchMap(({ partyID, invoiceID, paymentID }) => {
return this.merchantStatisticsService
.getPayments({
dsl: createDSL({
payments: {
...(paymentID ? { payment_id: paymentID } : {}),
...(partyID ? { merchant_id: partyID } : {}),
...(invoiceID ? { invoice_id: invoiceID } : {}),
},
}),
})
.pipe(map(({ data }) => data.payments[0]));
}),
shareReplay(1)
);
shop$ = combineLatest([
this.route.params.pipe(pluck('partyID')),
this.payment$.pipe(pluck('shop_id')),
]).pipe(
switchMap(([partyID, shopID]) => this.partyService.getShop(partyID, shopID)),
shareReplay(1)
);
chargeback$ = merge(
this.route.params,
this.loadChargeback$.pipe(withLatestFrom(this.route.params, (_, p) => p))
).pipe(
switchMap((p) =>
this.paymentProcessingService.getChargeback(p.invoiceID, p.paymentID, p.chargebackID)
),
shareReplay(1)
);
constructor(
private partyService: PartyService,
private merchantStatisticsService: MerchantStatisticsService,
private route: ActivatedRoute,
private paymentProcessingService: PaymentProcessingService
) {}
loadChargeback() {
this.loadChargeback$.next();
}
}

View File

@ -0,0 +1,3 @@
export * from './chargeback-details.component';
export * from './chargeback-details.module';
export * from './chargeback-details.service';

View File

@ -0,0 +1 @@
export * from './reopen-chargeback-dialog.component';

View File

@ -0,0 +1,29 @@
<div fxLayout="column" fxLayoutGap="32px">
<h1 class="mat-headline">Reopen chargeback</h1>
<mat-dialog-content [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field fxFlex>
<mat-label>Reopen stage (optional)</mat-label>
<mat-select formControlName="stage">
<mat-option *ngFor="let c of stages" [value]="c">
{{ c }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout fxLayoutGap="24px">
<mat-form-field fxFlex>
<mat-label>Leavy amount (optional)</mat-label>
<input matInput type="number" formControlName="leavyAmount" />
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Body amount (optional)</mat-label>
<input matInput type="number" formControlName="bodyAmount" />
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" [disabled]="form.invalid" (click)="changeStatus()">
REOPEN
</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,76 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatDialogConfig, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { InvoicePaymentChargebackStage } from 'src/app/thrift-services/damsel/gen-model/domain';
import { PaymentProcessingService } from 'src/app/thrift-services/damsel/payment-processing.service';
@Component({
selector: 'cc-reopen-chargeback-dialog',
templateUrl: 'reopen-chargeback-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReopenChargebackDialogComponent {
static defaultConfig: MatDialogConfig = {
disableClose: true,
width: '552px',
};
form = this.fb.group({
stage: '',
leavyAmount: '',
bodyAmount: '',
date: '',
});
stages: (keyof InvoicePaymentChargebackStage)[] = [
'arbitration',
'chargeback',
'pre_arbitration',
];
constructor(
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA)
public params: {
invoiceID: string;
paymentID: string;
chargebackID: string;
},
private dialogRef: MatDialogRef<ReopenChargebackDialogComponent>,
private snackBar: MatSnackBar,
private paymentProcessingService: PaymentProcessingService
) {}
changeStatus() {
const { stage, leavyAmount, bodyAmount } = this.form.value;
this.paymentProcessingService
.reopenChargeback(
this.params.invoiceID,
this.params.paymentID,
this.params.chargebackID,
Object.assign(
{},
!!stage && { move_to_stage: { [stage]: {} } },
!!bodyAmount && {
amount: bodyAmount,
currency: { symbolic_code: 'RUB' },
},
!!leavyAmount && {
amount: leavyAmount,
currency: { symbolic_code: 'RUB' },
}
)
)
.subscribe(
() => this.dialogRef.close(),
(error) => {
console.error(error);
this.snackBar.open('An error occurred while reopen chargeback', 'OK');
}
);
}
cancel() {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import pickBy from 'lodash-es/pickBy';
import { QueryParamsStore } from '@cc/app/shared/services';
import { wrapValuesToArray } from '@cc/utils/index';
import { FormValue } from './chargebacks-search-filters';
const ARRAY_PARAMS: (keyof FormValue)[] = [
'shop_ids',
'chargeback_statuses',
'chargeback_stages',
'chargeback_categories',
];
@Injectable()
export class ChargebacksSearchFiltersStore extends QueryParamsStore<FormValue> {
constructor(protected router: Router, protected route: ActivatedRoute) {
super(router, route);
}
mapToData(queryParams: Params = {}) {
return {
...queryParams,
...wrapValuesToArray(
pickBy(
queryParams,
(v, k: keyof FormValue) => typeof v === 'string' && ARRAY_PARAMS.includes(k)
)
),
} as FormValue;
}
mapToParams(data: Partial<FormValue> = {}): Params {
return data;
}
}

View File

@ -0,0 +1,30 @@
<form [formGroup]="form" fxLayout fxLayoutGap="16px">
<mat-form-field fxFlex>
<mat-label>Date Range</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input matStartDate matInput formControlName="from_time" placeholder="Start Date" />
<input matEndDate matInput formControlName="to_time" placeholder="End Date" />
</mat-date-range-input>
<mat-datepicker-toggle matSuffix [for]="picker">
<mat-icon matDatepickerToggleIcon>keyboard_arrow_down</mat-icon>
</mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<mat-form-field fxFlex>
<input
matInput
placeholder="Invoice ID"
formControlName="invoice_id"
type="string"
autocomplete="false"
/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Shops</mat-label>
<mat-select multiple formControlName="shop_ids" class="select">
<mat-option *ngFor="let shop of shops$ | async" [value]="shop.id">
{{ shop.details.name }}
</mat-option>
</mat-select>
</mat-form-field>
</form>

View File

@ -0,0 +1,47 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import moment from 'moment';
import { ChargebacksParams } from '../../../../query-dsl';
import { ChargebacksMainSearchFiltersService } from './chargebacks-main-search-filters.service';
@Component({
selector: 'cc-chargebacks-main-search-filters',
templateUrl: 'chargebacks-main-search-filters.component.html',
styleUrls: ['chargebacks-main-search-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ChargebacksMainSearchFiltersService],
})
export class ChargebacksMainSearchFiltersComponent implements OnInit {
@Input() partyID: string;
@Input() initParams: ChargebacksParams;
@Output() valueChanges = new EventEmitter<ChargebacksParams>();
shops$ = this.chargebacksMainSearchFiltersService.shops$;
form = this.chargebacksMainSearchFiltersService.form;
constructor(private chargebacksMainSearchFiltersService: ChargebacksMainSearchFiltersService) {
this.chargebacksMainSearchFiltersService.searchParamsChanges$.subscribe((params) =>
this.valueChanges.emit(params)
);
}
ngOnInit() {
const { from_time, to_time, ...params } = this.initParams || {};
this.chargebacksMainSearchFiltersService.init(
Object.assign(
params,
!!from_time && { from_time: moment(from_time) },
!!to_time && { to_time: moment(to_time) }
)
);
this.chargebacksMainSearchFiltersService.getShops(this.partyID);
}
}

View File

@ -0,0 +1,52 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { ChargebacksMainSearchFiltersComponent } from './chargebacks-main-search-filters.component';
export const RU_DATE_FORMATS = {
parse: {
dateInput: ['l', 'LL'],
},
display: {
dateInput: 'DD.MM.YYYY',
monthYearLabel: 'DD.MM.YYYY',
dateA11yLabel: 'DD.MM.YYYY',
monthYearA11yLabel: 'DD.MM.YYYY',
},
};
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatButtonModule,
MatBadgeModule,
MatDialogModule,
MatDividerModule,
FlexLayoutModule,
MatSelectModule,
MatDialogModule,
FormsModule,
MatOptionModule,
],
declarations: [ChargebacksMainSearchFiltersComponent],
exports: [ChargebacksMainSearchFiltersComponent],
providers: [{ provide: MAT_DATE_FORMATS, useValue: RU_DATE_FORMATS }],
})
export class ChargebacksMainSearchFiltersModule {}

View File

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import * as moment from 'moment';
import { ReplaySubject } from 'rxjs';
import { debounceTime, filter, shareReplay, switchMap } from 'rxjs/operators';
import { PartyService } from '../../../../party/party.service';
import { FormValue } from '../form-value';
@Injectable()
export class ChargebacksMainSearchFiltersService {
private getShops$ = new ReplaySubject<string>();
form = this.fb.group({
from_time: [moment().subtract(1, 'month').startOf('d'), Validators.required],
to_time: [moment().endOf('d'), Validators.required],
invoice_id: '',
shop_ids: '',
});
searchParamsChanges$ = this.form.valueChanges.pipe(
debounceTime(600),
filter(() => this.form.valid),
shareReplay(1)
);
shops$ = this.getShops$.pipe(
switchMap((partyID) => this.partyService.getShops(partyID)),
shareReplay(1)
);
constructor(private partyService: PartyService, private fb: FormBuilder) {}
getShops(id: string) {
this.getShops$.next(id);
}
init(params: FormValue) {
this.form.patchValue(params);
}
}

View File

@ -0,0 +1,2 @@
export * from './chargebacks-main-search-filters.component';
export * from './chargebacks-main-search-filters.module';

View File

@ -0,0 +1,10 @@
<button
class="other-filters-button"
mat-button
[matBadge]="count$ | async"
matBadgePosition="after"
matBadgeColor="accent"
(click)="openOtherFiltersDialog()"
>
OTHER FILTERS
</button>

View File

@ -0,0 +1,40 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ChargebacksParams } from 'src/app/query-dsl';
import { ChargebacksOtherSearchFiltersService } from './chargebacks-other-search-filters.service';
@Component({
selector: 'cc-chargebacks-other-search-filters',
templateUrl: 'chargebacks-other-search-filters.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ChargebacksOtherSearchFiltersService],
})
export class ChargebacksOtherSearchFiltersComponent implements OnInit {
@Input() initParams: ChargebacksParams;
@Output() valueChanges = new EventEmitter<ChargebacksParams>();
count$ = this.chargebacksOtherSearchFiltersService.filtersCount$;
constructor(
private chargebacksOtherSearchFiltersService: ChargebacksOtherSearchFiltersService
) {
this.chargebacksOtherSearchFiltersService.formParams$.subscribe((params) => {
this.valueChanges.emit(params);
});
}
ngOnInit() {
this.chargebacksOtherSearchFiltersService.init(this.initParams);
}
openOtherFiltersDialog() {
this.chargebacksOtherSearchFiltersService.openOtherFiltersDialog();
}
}

View File

@ -0,0 +1,41 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { ChargebacksOtherSearchFiltersComponent } from './chargebacks-other-search-filters.component';
import { OtherFiltersDialogModule } from './other-filters-dialog';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatButtonModule,
MatBadgeModule,
MatDialogModule,
MatDividerModule,
FlexLayoutModule,
MatSelectModule,
MatDialogModule,
FormsModule,
MatOptionModule,
OtherFiltersDialogModule,
],
declarations: [ChargebacksOtherSearchFiltersComponent],
exports: [ChargebacksOtherSearchFiltersComponent],
})
export class ChargebacksOtherSearchFiltersModule {}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import pick from 'lodash-es/pick';
import { ReplaySubject } from 'rxjs';
import { filter, map, shareReplay, switchMap, take } from 'rxjs/operators';
import { ChargebacksParams } from 'src/app/query-dsl';
import { OtherFiltersDialogComponent } from './other-filters-dialog';
@Injectable()
export class ChargebacksOtherSearchFiltersService {
formParams$ = new ReplaySubject<ChargebacksParams>(1);
filtersCount$ = this.formParams$.pipe(
map((p) => this.getActiveParamsCount(p) || null),
shareReplay(1)
);
constructor(private dialog: MatDialog) {}
init(params: ChargebacksParams) {
this.formParams$.next(
pick(params, ['chargeback_statuses', 'chargeback_categories', 'chargeback_stages'])
);
}
openOtherFiltersDialog() {
this.formParams$
.pipe(
take(1),
switchMap((data) =>
this.dialog
.open(OtherFiltersDialogComponent, {
disableClose: true,
width: '552px',
data,
})
.afterClosed()
),
filter((v) => v)
)
.subscribe((p) => this.formParams$.next(p));
}
private getActiveParamsCount(params: any) {
return Object.values(params).filter((p) => (Array.isArray(p) ? p?.length : p)).length;
}
}

View File

@ -0,0 +1,3 @@
export * from './chargebacks-other-search-filters.component';
export * from './chargebacks-other-search-filters.module';
export * from './other-filters-dialog';

View File

@ -0,0 +1,2 @@
export * from './other-filters-dialog.component';
export * from './other-filters-dialog.module';

View File

@ -0,0 +1,35 @@
<div fxLayout="column" fxLayoutGap="32px">
<h1 class="mat-headline">Other filters</h1>
<mat-dialog-content [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field>
<mat-label>Chargeback Statuses</mat-label>
<mat-select multiple formControlName="chargeback_statuses" class="select">
<mat-option *ngFor="let s of chargebackStatuses" [value]="s">
{{ s }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Chargeback Categories</mat-label>
<mat-select multiple formControlName="chargeback_categories" class="select">
<mat-option *ngFor="let c of chargebackCategories" [value]="c">
{{ c }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Chargeback Stages</mat-label>
<mat-select multiple formControlName="chargeback_stages" class="select">
<mat-option *ngFor="let s of chargebackStages" [value]="s">
{{ s }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" [disabled]="form.invalid" (click)="save()">SAVE</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,42 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {
chargebackCategories,
ChargebacksParams,
chargebackStages,
chargebackStatuses,
} from '../../../../../query-dsl';
@Component({
templateUrl: 'other-filters-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OtherFiltersDialogComponent {
chargebackStatuses = chargebackStatuses;
chargebackCategories = chargebackCategories;
chargebackStages = chargebackStages;
form = this.fb.group({
chargeback_statuses: '',
chargeback_categories: '',
chargeback_stages: '',
});
constructor(
private dialogRef: MatDialogRef<OtherFiltersDialogComponent>,
@Inject(MAT_DIALOG_DATA) public initParams: ChargebacksParams,
private fb: FormBuilder
) {
this.form.patchValue(this.initParams);
}
cancel() {
this.dialogRef.close();
}
save() {
this.dialogRef.close(this.form.value);
}
}

View File

@ -0,0 +1,36 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { OtherFiltersDialogComponent } from './other-filters-dialog.component';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatButtonModule,
MatBadgeModule,
MatDialogModule,
MatDividerModule,
FlexLayoutModule,
MatSelectModule,
],
declarations: [OtherFiltersDialogComponent],
exports: [OtherFiltersDialogComponent],
entryComponents: [OtherFiltersDialogComponent],
})
export class OtherFiltersDialogModule {}

View File

@ -0,0 +1,8 @@
import { Moment } from 'moment';
import { ChargebacksParams } from '../../../query-dsl';
export type FormValue = Omit<ChargebacksParams, 'from_time' | 'to_time'> & {
from_time: Moment;
to_time: Moment;
};

View File

@ -0,0 +1,3 @@
export * from './chargebacks-other-search-filters';
export * from './chargebacks-main-search-filters';
export * from './form-value';

View File

@ -0,0 +1 @@
export * from './party-chargebacks.module';

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../app-auth-guard.service';
import { PartyChargebacksComponent } from './party-chargebacks.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: PartyChargebacksComponent,
canActivate: [AppAuthGuardService],
},
]),
],
})
export class PartyChargebacksRoutingModule {}

View File

@ -0,0 +1,28 @@
<div fxLayout="column" fxLayoutGap="24px">
<div class="cc-headline">Merchants chargebacks</div>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content fxLayout fxLayoutAlign=" center">
<cc-chargebacks-main-search-filters
fxFlex="75"
[partyID]="partyID$ | async"
(valueChanges)="searchParamsChanges$.next($event)"
[initParams]="initSearchParams$ | async"
></cc-chargebacks-main-search-filters>
<cc-chargebacks-other-search-filters
class="other-filters-button"
fxFlex
fxLayoutAlign="center start"
(valueChanges)="otherSearchParamsChanges$.next($event)"
[initParams]="initSearchParams$ | async"
></cc-chargebacks-other-search-filters>
</mat-card-content>
</mat-card>
<mat-card>
<cc-chargebacks-table
[partyID]="partyID$ | async"
[searchParams]="searchParams$ | async"
></cc-chargebacks-table>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest, ReplaySubject } from 'rxjs';
import { map, pluck, shareReplay } from 'rxjs/operators';
import { FormValue } from './chargebacks-search-filters';
import { ChargebacksSearchFiltersStore } from './chargebacks-search-filters-store.service';
@Component({
templateUrl: 'party-chargebacks.component.html',
providers: [ChargebacksSearchFiltersStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PartyChargebacksComponent {
partyID$ = this.route.params.pipe(pluck('partyID'), shareReplay(1));
initSearchParams$ = this.chargebacksSearchFiltersStore.data$;
searchParamsChanges$ = new ReplaySubject<FormValue>();
otherSearchParamsChanges$ = new ReplaySubject<FormValue>();
searchParams$ = combineLatest([this.searchParamsChanges$, this.otherSearchParamsChanges$]).pipe(
map(([a, b]) => ({ ...a, ...b })),
shareReplay(1)
);
constructor(
private route: ActivatedRoute,
private chargebacksSearchFiltersStore: ChargebacksSearchFiltersStore
) {
this.searchParams$.subscribe((params) =>
this.chargebacksSearchFiltersStore.preserve(params)
);
}
}

View File

@ -0,0 +1,47 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTableModule } from '@angular/material/table';
import { ChargebacksTableModule } from '@cc/app/shared/components/chargebacks-table';
import { StatusModule } from '@cc/components/status';
import {
ChargebacksMainSearchFiltersModule,
ChargebacksOtherSearchFiltersModule,
} from './chargebacks-search-filters';
import { PartyChargebacksRoutingModule } from './party-chargebacks-routing.module';
import { PartyChargebacksComponent } from './party-chargebacks.component';
@NgModule({
imports: [
FlexModule,
MatCardModule,
MatProgressBarModule,
CommonModule,
MatButtonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatMenuModule,
MatIconModule,
PartyChargebacksRoutingModule,
StatusModule,
ChargebacksTableModule,
MatBadgeModule,
ChargebacksMainSearchFiltersModule,
ChargebacksOtherSearchFiltersModule,
],
declarations: [PartyChargebacksComponent],
})
export class PartyChargebacksModule {}

View File

@ -3,10 +3,9 @@ import { ActivatedRoute, Params, Router } from '@angular/router';
import pickBy from 'lodash-es/pickBy';
import { SearchFiltersParams } from '@cc/app/shared/components/payments-search-filters/search-filters-params';
import { QueryParamsStore } from '@cc/app/shared/services';
import { wrapValuesToArray } from '@cc/utils/index';
import { QueryParamsStore } from './query-params-store';
const shopIDsAndPrimitives = (v, k) => typeof v === 'string' && k === 'shopIDs';
@Injectable()

View File

@ -64,9 +64,18 @@ import { PartyComponent } from './party.component';
(m) => m.PaymentRoutingRulesModule
),
canActivate: [AppAuthGuardService],
data: {
roles: [],
},
},
{
path: 'chargebacks',
loadChildren: () =>
import('../party-chargebacks').then((m) => m.PartyChargebacksModule),
canActivate: [AppAuthGuardService],
},
{
path: 'invoice/:invoiceID/payment/:paymentID/chargeback/:chargebackID',
loadChildren: () =>
import('../chargeback-details').then((m) => m.ChargebackDetailsModule),
canActivate: [AppAuthGuardService],
},
{ path: '', redirectTo: 'payments', pathMatch: 'full' },
],

View File

@ -9,7 +9,7 @@
[routerLink]="link.url"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive || hasActiveFragments(link.otherActiveUrlFragments)"
[active]="rla.isActive || (activeLinkByFragment$ | async) === link"
>
{{ link.name }}
</a>

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { pluck, shareReplay } from 'rxjs/operators';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { filter, map, pluck, shareReplay, startWith } from 'rxjs/operators';
import { hasActiveFragments, SHARE_REPLAY_CONF } from '@cc/utils/index';
import { SHARE_REPLAY_CONF } from '@cc/utils/index';
@Component({
templateUrl: 'party.component.html',
@ -14,14 +14,41 @@ export class PartyComponent {
{ name: 'Claims', url: 'claims', otherActiveUrlFragments: ['claim'] },
{ name: 'Shops', url: 'shops' },
{ name: 'Payment Routing Rules', url: 'payment-routing-rules' },
{
name: 'Chargebacks',
url: 'chargebacks',
otherActiveUrlFragments: ['payment', 'invoice', 'chargeback'],
},
];
partyID$ = this.route.params.pipe(pluck('partyID'), shareReplay(SHARE_REPLAY_CONF));
activeLinkByFragment$ = this.router.events.pipe(
filter((e) => e instanceof NavigationEnd),
startWith(undefined),
map(() => this.findLinkWithMaxActiveFragments()),
shareReplay(1)
);
constructor(private route: ActivatedRoute, private router: Router) {}
hasActiveFragments(fragments: string[]): boolean {
const ulrFragments = this.router.url.split('/');
return hasActiveFragments(fragments, ulrFragments);
private activeFragments(fragments: string[]): number {
if (fragments?.length) {
const ulrFragments = this.router.url.split('/');
if (
ulrFragments.filter((fragment) => fragments.includes(fragment)).length ===
fragments.length
) {
return fragments.length;
}
}
return 0;
}
private findLinkWithMaxActiveFragments() {
return this.links.reduce(([maxLink, maxActiveFragments], link) => {
const activeFragments = this.activeFragments(link.otherActiveUrlFragments);
return maxActiveFragments > activeFragments
? [maxLink, maxActiveFragments]
: [link, activeFragments];
}, [])?.[0];
}
}

View File

@ -1,10 +1,28 @@
<div fxLayout="column" fxLayoutGap="24px">
<cc-headline>Payment details</cc-headline>
<mat-card *ngIf="payment$ | async as payment">
<mat-card-content>
<cc-payment-main-info [payment]="payment" [shop]="shop$ | async"></cc-payment-main-info>
</mat-card-content>
</mat-card>
<ng-container *ngIf="payment$ | async as payment">
<mat-card>
<mat-card-content>
<cc-payment-main-info
[payment]="payment"
[shop]="shop$ | async"
></cc-payment-main-info>
</mat-card-content>
</mat-card>
<mat-card *ngIf="payment$ | async as payment" fxLayout="column" fxLayoutGap="32px">
<div fxLayout fxLayoutAlign="space-between">
<h2 class="cc-headline">Chargebacks</h2>
<button mat-raised-button color="primary" (click)="createChargeback()">
CREATE CHARGEBACK
</button>
</div>
<cc-chargebacks-table
[partyID]="partyID$ | async"
[searchParams]="chargebackSerchParams$ | async"
[displayedColumns]="['createdAt', 'status', 'stage', 'levyAmount', 'actions']"
></cc-chargebacks-table>
</mat-card>
</ng-container>
<div *ngIf="isLoading$ | async" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>

View File

@ -1,5 +1,12 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { merge, Subject } from 'rxjs';
import { map, pluck, shareReplay, switchMap, take, withLatestFrom } from 'rxjs/operators';
import { CreateChargebackDialogComponent } from '@cc/app/shared/components/create-chargeback-dialog';
import { ChargebacksParams } from '../../query-dsl';
import { PaymentDetailsService } from './payment-details.service';
@Component({
@ -8,11 +15,38 @@ import { PaymentDetailsService } from './payment-details.service';
providers: [PaymentDetailsService],
})
export class PaymentDetailsComponent {
partyID$ = this.route.params.pipe(pluck('partyID'), shareReplay(1));
payment$ = this.paymentDetailsService.payment$;
isLoading$ = this.paymentDetailsService.isLoading$;
shop$ = this.paymentDetailsService.shop$;
updateSearchParams$ = new Subject();
chargebackSerchParams$ = merge(
this.payment$,
this.updateSearchParams$.pipe(withLatestFrom(this.payment$, (_, p) => p))
).pipe(
map(({ id: payment_id, invoice_id }) => ({ invoice_id, payment_id } as ChargebacksParams)),
shareReplay(1)
);
constructor(private paymentDetailsService: PaymentDetailsService) {}
constructor(
private paymentDetailsService: PaymentDetailsService,
private route: ActivatedRoute,
private dialog: MatDialog
) {}
createChargeback() {
this.payment$
.pipe(
take(1),
switchMap(({ id: paymentID, invoice_id: invoiceID }) =>
this.dialog
.open(CreateChargebackDialogComponent, {
...CreateChargebackDialogComponent.defaultConfig,
data: { invoiceID, paymentID },
})
.afterClosed()
)
)
.subscribe(() => this.updateSearchParams$.next());
}
}

View File

@ -1,9 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ChargebacksTableModule } from '@cc/app/shared/components/chargebacks-table';
import { CreateChargebackDialogModule } from '@cc/app/shared/components/create-chargeback-dialog';
import { SharedPipesModule } from '@cc/app/shared/pipes';
import { DetailsItemModule } from '@cc/components/details-item';
import { HeadlineModule } from '@cc/components/headline';
@ -27,6 +31,10 @@ import { PaymentToolModule } from './payment-main-info/payment-tool';
PaymentToolModule,
MatProgressSpinnerModule,
PaymentMainInfoModule,
ChargebacksTableModule,
MatButtonModule,
MatDialogModule,
CreateChargebackDialogModule,
],
declarations: [PaymentDetailsComponent],
})

View File

@ -0,0 +1,73 @@
<div *ngIf="(chargebacks$ | async)?.length; else empty" fxLayout="column" fxLayoutGap="24px">
<table mat-table [dataSource]="chargebacks$ | async">
<ng-container matColumnDef="createdAt">
<th mat-header-cell *matHeaderCellDef>Created At</th>
<td mat-cell *matCellDef="let chargeback">
{{ chargeback.created_at | date: 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let chargeback">
<cc-status>{{ chargeback.chargeback_status | ccMapUnion: mapStatus }}</cc-status>
</td>
</ng-container>
<ng-container matColumnDef="stage">
<th mat-header-cell *matHeaderCellDef>Stage</th>
<td mat-cell *matCellDef="let chargeback">
{{ chargeback.stage | ccMapUnion: mapStage }}
</td>
</ng-container>
<ng-container matColumnDef="levyAmount">
<th mat-header-cell *matHeaderCellDef>Levy Amount</th>
<td mat-cell *matCellDef="let chargeback">
{{ chargeback.levy_amount | ccFormatAmount }}
{{ chargeback.levy_currency_code.symbolic_code | ccCurrency }}
</td>
</ng-container>
<ng-container matColumnDef="shop">
<th mat-header-cell *matHeaderCellDef>Shop</th>
<td mat-cell *matCellDef="let chargeback">
{{ chargeback.shop_id | shopName: partyID }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="action-cell"></th>
<td mat-cell *matCellDef="let chargeback" class="action-cell">
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="navigateToChargeback(chargeback)">
Chargeback Details
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<button
fxFlex="100"
mat-button
*ngIf="hasMore$ | async"
(click)="fetchMore()"
[disabled]="doAction$ | async"
>
{{ (doAction$ | async) ? 'LOADING...' : 'SHOW MORE' }}
</button>
</div>
<ng-template #empty>
<div *ngIf="isLoading$ | async; else emptyResult" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>
<ng-template #emptyResult>
<cc-empty-search-result unwrapped></cc-empty-search-result>
</ng-template>
</ng-template>

View File

@ -0,0 +1,7 @@
:host {
display: block;
}
.action-cell {
width: 8px;
}

View File

@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { StatChargeback } from 'src/app/thrift-services/damsel/gen-model/merch_stat';
import { ChargebacksParams } from '../../../query-dsl';
import { ComponentChanges } from '../../../shared/utils';
import {
InvoicePaymentChargebackStage,
InvoicePaymentChargebackStatus,
} from '../../../thrift-services/damsel/gen-model/domain';
import { FetchChargebacksService } from './fetch-chargebacks.service';
@Component({
selector: 'cc-chargebacks-table',
templateUrl: 'chargebacks-table.component.html',
styleUrls: ['chargebacks-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChargebacksTableComponent implements OnInit, OnChanges {
@Input() partyID: string;
@Input() searchParams: ChargebacksParams;
@Input() displayedColumns = ['createdAt', 'status', 'stage', 'levyAmount', 'shop', 'actions'];
chargebacks$ = this.fetchChargebacksService.searchResult$;
doAction$ = this.fetchChargebacksService.doAction$;
isLoading$ = this.fetchChargebacksService.isLoading$;
hasMore$ = this.fetchChargebacksService.hasMore$;
mapStatus: { [N in keyof InvoicePaymentChargebackStatus] } = {
accepted: 'Accepted',
cancelled: 'Cancelled',
pending: 'Pending',
rejected: 'Rejected',
};
mapStage: { [N in keyof InvoicePaymentChargebackStage] } = {
arbitration: 'Arbitration',
chargeback: 'Chargeback',
pre_arbitration: 'Pre-arbitration',
};
constructor(
private router: Router,
private fetchChargebacksService: FetchChargebacksService,
private snackBar: MatSnackBar
) {}
ngOnInit() {
this.fetchChargebacksService.errors$.subscribe((e) =>
this.snackBar.open(`An error occurred while search chargebacks (${e})`, 'OK')
);
}
ngOnChanges({ searchParams }: ComponentChanges<ChargebacksTableComponent>) {
if (searchParams) {
this.fetchChargebacksService.search({
...searchParams.currentValue,
merchant_id: this.partyID,
});
}
}
navigateToChargeback({ chargeback_id, invoice_id, payment_id }: StatChargeback) {
this.router.navigate([
`/party/${this.partyID}/invoice/${invoice_id}/payment/${payment_id}/chargeback/${chargeback_id}`,
]);
}
fetchMore() {
this.fetchChargebacksService.fetchMore();
}
}

View File

@ -0,0 +1,38 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { StatusModule } from '@cc/components/status';
import { CommonPipesModule } from '@cc/pipes/common-pipes.module';
import { SharedPipesModule } from '../../../shared';
import { ChargebacksTableComponent } from './chargebacks-table.component';
import { FetchChargebacksService } from './fetch-chargebacks.service';
@NgModule({
imports: [
CommonModule,
MatTableModule,
FlexModule,
StatusModule,
MatButtonModule,
MatIconModule,
MatMenuModule,
MatProgressSpinnerModule,
EmptySearchResultModule,
CommonPipesModule,
SharedPipesModule,
MatSnackBarModule,
],
declarations: [ChargebacksTableComponent],
exports: [ChargebacksTableComponent],
providers: [FetchChargebacksService],
})
export class ChargebacksTableModule {}

View File

@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { FetchResult, PartialFetcher } from '@rbkmoney/partial-fetcher';
import pickBy from 'lodash-es/pickBy';
import moment from 'moment';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { booleanDelay } from '@cc/operators/index';
import { ChargebacksParams, createDSL } from '../../../query-dsl';
import { StatChargeback } from '../../../thrift-services/damsel/gen-model/merch_stat';
import { MerchantStatisticsService } from '../../../thrift-services/damsel/merchant-statistics.service';
const SEARCH_LIMIT = 10;
@Injectable()
export class FetchChargebacksService extends PartialFetcher<StatChargeback, ChargebacksParams> {
isLoading$ = this.doAction$.pipe(booleanDelay(), shareReplay(1));
constructor(private merchantStatisticsService: MerchantStatisticsService) {
super();
}
protected fetch(
{ from_time, to_time, ...params }: ChargebacksParams,
continuationToken: string
): Observable<FetchResult<StatChargeback>> {
return this.merchantStatisticsService
.getChargebacks({
dsl: createDSL({
chargebacks: Object.assign(
pickBy(params, (v) => (Array.isArray(v) ? v.length : v)),
!!from_time && { from_time: moment(from_time).utc().format() },
!!to_time && { to_time: moment(to_time).utc().format() },
{
size: SEARCH_LIMIT,
}
),
}),
...(!!continuationToken && { continuation_token: continuationToken }),
})
.pipe(
map(({ data, continuation_token }) => ({
result: data.chargebacks,
continuationToken: continuation_token,
}))
);
}
}

View File

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

View File

@ -0,0 +1,45 @@
<div fxLayout="column" fxLayoutGap="32px">
<h1 class="mat-headline">Create chargeback params</h1>
<mat-dialog-content [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div fxLayout fxLayoutGap="16px">
<mat-form-field fxFlex>
<input
matInput
formControlName="date"
[matDatepicker]="datepicker"
placeholder="Pretension date"
required
/>
<mat-datepicker-toggle matSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker></mat-datepicker>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Leavy amount</mat-label>
<input matInput type="number" formControlName="leavyAmount" required />
</mat-form-field>
</div>
<mat-divider></mat-divider>
<h2 class="cc-title">Reason (optional)</h2>
<div fxLayout fxLayoutGap="16px">
<mat-form-field fxFlex>
<mat-label>Category</mat-label>
<mat-select formControlName="category" required>
<mat-option *ngFor="let c of categories" [value]="c">
{{ c }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>Code (optional)</mat-label>
<input matInput formControlName="code" />
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" [disabled]="form.invalid" (click)="create()">
CREATE
</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatDialogConfig, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Moment } from 'moment';
import uuid from 'uuid';
import { InvoicePaymentChargebackCategory } from '../../../thrift-services/damsel/gen-model/domain';
import { PaymentProcessingService } from '../../../thrift-services/damsel/payment-processing.service';
@Component({
selector: 'cc-create-chargeback-dialog',
templateUrl: 'create-chargeback-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateChargebackDialogComponent {
static defaultConfig: MatDialogConfig = {
disableClose: true,
width: '552px',
};
form = this.fb.group({
date: '',
leavyAmount: '',
category: '',
code: '',
});
categories: (keyof InvoicePaymentChargebackCategory)[] = [
'authorisation',
'dispute',
'fraud',
'processing_error',
];
constructor(
private paymentProcessingService: PaymentProcessingService,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA)
public params: {
invoiceID: string;
paymentID: string;
},
private dialogRef: MatDialogRef<CreateChargebackDialogComponent>,
private snackBar: MatSnackBar
) {}
create() {
const { leavyAmount, code, category, date } = this.form.value;
this.paymentProcessingService
.createChargeback(this.params.invoiceID, this.params.paymentID, {
id: uuid(),
occurred_at: (date as Moment).utc().format(),
reason: {
code,
category: { [category]: {} },
},
levy: {
amount: (leavyAmount * 100) as any,
currency: { symbolic_code: 'RUB' },
},
})
.subscribe(
() => this.dialogRef.close(),
(error) => {
console.error(error);
this.snackBar.open('An error occurred while chargeback creating', 'OK');
}
);
}
cancel() {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,37 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { DamselModule } from '../../../thrift-services/damsel';
import { CreateChargebackDialogComponent } from './create-chargeback-dialog.component';
const EXPORTED_DECLARATIONS = [CreateChargebackDialogComponent];
@NgModule({
imports: [
CommonModule,
FlexModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatDialogModule,
DamselModule,
MatDividerModule,
MatSelectModule,
MatDatepickerModule,
MatButtonModule,
MatSnackBarModule,
],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,
})
export class CreateChargebackDialogModule {}

View File

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

View File

@ -0,0 +1 @@
export * from './query-params-store';

View File

@ -3,8 +3,10 @@ import isEqual from 'lodash-es/isEqual';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { removeEmptyProperties } from '@cc/utils/index';
export abstract class QueryParamsStore<D> {
data$: Observable<D> = this.route.queryParams.pipe(
data$: Observable<Partial<D>> = this.route.queryParams.pipe(
distinctUntilChanged(isEqual),
map((p) => this.mapToData(p)),
shareReplay(1)
@ -12,11 +14,12 @@ export abstract class QueryParamsStore<D> {
constructor(protected router: Router, protected route: ActivatedRoute) {}
abstract mapToData(queryParams: Params): D;
abstract mapToData(queryParams: Params): Partial<D>;
abstract mapToParams(data: D): Params;
preserve(data: D) {
this.router.navigate([], { queryParams: this.mapToParams(data), preserveFragment: true });
const queryParams = removeEmptyProperties(this.mapToParams(data));
this.router.navigate([], { queryParams, preserveFragment: true });
}
}

View File

@ -0,0 +1,11 @@
import { SimpleChange } from '@angular/core';
export interface ComponentChange<T, P extends keyof T>
extends Omit<SimpleChange, 'previousValue' | 'currentValue'> {
previousValue: T[P];
currentValue: T[P];
}
export type ComponentChanges<T> = {
[P in keyof T]?: ComponentChange<T, P>;
};

View File

@ -1 +1,2 @@
export * from './extract-claim-status';
export * from './component-changes';

View File

@ -3,6 +3,7 @@
@import '../../../components/components-themes';
@import '../../sections/party/party-theme';
@import '../../sections/chargeback-details/chargeback-details-theme';
@import '../../sections/payment-routing-rules/payment-routing-rules-theme';
@import '../../sections/party-claim/party-claim-theme';
@ -12,6 +13,7 @@
body.#{map-get($theme, name)} {
@include angular-material-theme($theme);
@include cc-party-theme($theme);
@include cc-chargeback-details-theme($theme);
@include cc-components-themes($theme);
@include cc-payment-routing-rules-theme($theme);
@include cc-party-claim-theme($theme);

View File

@ -5,13 +5,17 @@ import * as claim_management from './gen-nodejs/claim_management_types';
import * as domain_config from './gen-nodejs/domain_config_types';
import * as domain from './gen-nodejs/domain_types';
import * as geo_ip from './gen-nodejs/geo_ip_types';
import * as merch_stat from './gen-nodejs/merch_stat_types';
import * as payment_processing from './gen-nodejs/payment_processing_types';
export const namespaces = {
const namespaces = {
base,
domain,
domain_config,
claim_management,
geo_ip,
merch_stat,
payment_processing,
};
export const {

View File

@ -1,8 +1,10 @@
import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../thrift-service';
import { createDamselInstance, damselInstanceToObject } from './create-damsel-instance';
import { StatRequest, StatResponse } from './gen-model/merch_stat';
import * as MerchantStatistics from './gen-nodejs/MerchantStatistics';
import { StatRequest as ThriftStatRequest } from './gen-nodejs/merch_stat_types';
@ -15,4 +17,9 @@ export class MerchantStatisticsService extends ThriftService {
getPayments = (req: StatRequest): Observable<StatResponse> =>
this.toObservableAction('GetPayments')(new ThriftStatRequest(req));
getChargebacks = (req: StatRequest): Observable<StatResponse> =>
this.toObservableAction('GetChargebacks')(
createDamselInstance('merch_stat', 'StatRequest', req)
).pipe(map((r) => damselInstanceToObject('merch_stat', 'StatResponse', r)));
}

View File

@ -1,13 +1,25 @@
import { Injectable, NgZone } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { Observable, timer } from 'rxjs';
import { first, share, switchMap } from 'rxjs/operators';
import { first, map, share, switchMap } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../thrift-service';
import { InvoiceID } from './gen-model/domain';
import { createDamselInstance, damselInstanceToObject } from './create-damsel-instance';
import {
InvoiceID,
InvoicePaymentChargeback,
InvoicePaymentChargebackID,
InvoicePaymentID,
} from './gen-model/domain';
import {
InvoicePaymentAdjustment,
InvoicePaymentAdjustmentParams,
InvoicePaymentChargebackAcceptParams,
InvoicePaymentChargebackCancelParams,
InvoicePaymentChargebackParams,
InvoicePaymentChargebackRejectParams,
InvoicePaymentChargebackReopenParams,
InvoiceRepairScenario,
UserInfo,
} from './gen-model/payment_processing';
@ -20,7 +32,11 @@ import {
@Injectable()
export class PaymentProcessingService extends ThriftService {
constructor(zone: NgZone, keycloakTokenInfoService: KeycloakTokenInfoService) {
constructor(
zone: NgZone,
keycloakTokenInfoService: KeycloakTokenInfoService,
private keycloakService: KeycloakService
) {
super(zone, keycloakTokenInfoService, '/v1/processing/invoicing', Invoicing);
}
@ -96,4 +112,107 @@ export class PaymentProcessingService extends ThriftService {
id,
new InvoiceRepairScenarioObject(scenario)
);
createChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
params: InvoicePaymentChargebackParams
): Observable<InvoicePaymentChargeback> =>
this.toObservableAction('CreateChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('payment_processing', 'InvoicePaymentChargebackParams', params)
).pipe(map((r) => damselInstanceToObject('domain', 'InvoicePaymentChargeback', r)));
acceptChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
chargebackID: InvoicePaymentChargebackID,
params: InvoicePaymentChargebackAcceptParams = {}
): Observable<void> =>
this.toObservableAction('AcceptChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('domain', 'InvoicePaymentChargebackID', chargebackID),
createDamselInstance(
'payment_processing',
'InvoicePaymentChargebackAcceptParams',
params
)
);
rejectChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
chargebackID: InvoicePaymentChargebackID,
params: InvoicePaymentChargebackRejectParams = {}
): Observable<void> =>
this.toObservableAction('RejectChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('domain', 'InvoicePaymentChargebackID', chargebackID),
createDamselInstance(
'payment_processing',
'InvoicePaymentChargebackRejectParams',
params
)
);
reopenChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
chargebackID: InvoicePaymentChargebackID,
params: InvoicePaymentChargebackReopenParams = {}
): Observable<void> =>
this.toObservableAction('ReopenChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('domain', 'InvoicePaymentChargebackID', chargebackID),
createDamselInstance(
'payment_processing',
'InvoicePaymentChargebackReopenParams',
params
)
);
cancelChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
chargebackID: InvoicePaymentChargebackID,
params: InvoicePaymentChargebackCancelParams = {}
): Observable<void> =>
this.toObservableAction('CancelChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('domain', 'InvoicePaymentChargebackID', chargebackID),
createDamselInstance(
'payment_processing',
'InvoicePaymentChargebackCancelParams',
params
)
);
getChargeback = (
invoiceID: InvoiceID,
paymentID: InvoicePaymentID,
chargebackID: InvoicePaymentChargebackID
): Observable<InvoicePaymentChargeback> =>
this.toObservableAction('GetPaymentChargeback')(
this.getUser(),
createDamselInstance('domain', 'InvoiceID', invoiceID),
createDamselInstance('domain', 'InvoicePaymentID', paymentID),
createDamselInstance('domain', 'InvoicePaymentChargebackID', chargebackID)
).pipe(map((r) => damselInstanceToObject('domain', 'InvoicePaymentChargeback', r)));
private getUser(): UserInfo {
return createDamselInstance('payment_processing', 'UserInfo', {
id: this.keycloakService.getUsername(),
type: { internal_user: {} },
});
}
}

View File

@ -5,3 +5,4 @@ export * from './ank';
export * from './file-storage';
export * from './messages';
export * from './damsel';
export * from './skipper';

View File

@ -0,0 +1,38 @@
import { Injectable, NgZone } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '../../keycloak-token-info.service';
import { ThriftService } from '../thrift-service';
import { ID } from './gen-model/base';
import { ChargebackData, ChargebackEvent, ChargebackFilter } from './gen-model/skipper';
import * as Skipper from './gen-nodejs/Skipper';
import { createSkipperInstance, skipperInstanceToObject } from './skipper-instance-utils';
@Injectable()
export class ChargebacksService extends ThriftService {
constructor(zone: NgZone, keycloakTokenInfoService: KeycloakTokenInfoService) {
super(zone, keycloakTokenInfoService, '/v1/skipper', Skipper);
}
getChargebacks = (filter: ChargebackFilter): Observable<ChargebackData> =>
this.toObservableAction(
'getChargebacks',
false
)(createSkipperInstance('skipper', 'ChargebackFilter', filter)).pipe(
map((d) => skipperInstanceToObject('skipper', 'ChargebackData', d))
);
getChargeback = (invoiceID: ID, paymentID: ID, chargebackID: ID): Observable<ChargebackData> =>
this.toObservableAction('getChargeback', false)(
createSkipperInstance('base', 'ID', invoiceID),
createSkipperInstance('base', 'ID', paymentID),
createSkipperInstance('base', 'ID', chargebackID)
).pipe(map((d) => skipperInstanceToObject('skipper', 'ChargebackData', d)));
processChargebackData = (event: ChargebackEvent): Observable<void> =>
this.toObservableAction(
'processChargebackData',
false
)(createSkipperInstance('skipper', 'ChargebackEvent', event));
}

View File

@ -0,0 +1,3 @@
export * from './skipper.module';
export * from './chargebacks.service';
export * from './skipper-instance-utils';

View File

@ -0,0 +1,16 @@
import metadata from '../../../assets/meta-skipper.json';
import { createThriftInstanceUtils } from '../thrift-instance';
import * as base from './gen-nodejs/base_types';
import * as chargeback from './gen-nodejs/chargeback_types';
import * as skipper from './gen-nodejs/skipper_types';
const namespaces = {
base,
chargeback,
skipper,
};
export const {
createThriftInstance: createSkipperInstance,
thriftInstanceToObject: skipperInstanceToObject,
} = createThriftInstanceUtils(metadata, namespaces);

View File

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { ChargebacksService } from './chargebacks.service';
@NgModule({
providers: [ChargebacksService],
})
export class SkipperModule {}

View File

@ -59,15 +59,25 @@ export function createThriftInstance<T extends { [N in string]: any }, V extends
case 'enum':
return value;
default:
const typeMeta = namespaceMeta.ast[structureType][type];
if (structureType === 'typedef') {
return internalCreateThriftInstance(typeMeta.type, value);
try {
const typeMeta = namespaceMeta.ast[structureType][type];
if (structureType === 'typedef') {
return internalCreateThriftInstance(typeMeta.type, value);
}
const instance = new namespaces[namespace][type]();
for (const [k, v] of Object.entries(value)) {
const fieldTypeMeta = typeMeta.find((m) => m.name === k);
instance[k] = internalCreateThriftInstance(fieldTypeMeta.type, v);
}
return instance;
} catch (e) {
console.error(
`Thrift ${namespace}`,
type,
`instance creation error, value:`,
value
);
throw e;
}
const instance = new namespaces[namespace][type]();
for (const [k, v] of Object.entries(value)) {
const fieldTypeMeta = typeMeta.find((m) => m.name === k);
instance[k] = internalCreateThriftInstance(fieldTypeMeta.type, v);
}
return instance;
}
}

View File

@ -1,7 +1,16 @@
<mat-card>
<mat-card-content>
<div fxLayout="row" fxLayoutAlign="center center">
<h2 class="mat-headline empty-result">Search result is empty</h2>
</div>
</mat-card-content>
</mat-card>
<ng-template #content>
<div fxLayout="row" fxLayoutAlign="center center">
<h2 class="mat-headline empty-result">Search result is empty</h2>
</div>
</ng-template>
<ng-template #wrapped>
<mat-card>
<mat-card-content>
<ng-container *ngTemplateOutlet="content"></ng-container>
</mat-card-content>
</mat-card>
</ng-template>
<ng-container *ngIf="unwrapped; else wrapped">
<ng-container *ngTemplateOutlet="content"></ng-container>
</ng-container>

View File

@ -1,8 +1,11 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { coerceBoolean } from 'coerce-property';
@Component({
selector: 'cc-empty-search-result',
templateUrl: 'empty-search-result.component.html',
styleUrls: ['empty-search-result.component.scss'],
})
export class EmptySearchResultComponent {}
export class EmptySearchResultComponent {
@Input() @coerceBoolean unwrapped = false;
}

View File

@ -1,3 +1,4 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
@ -7,6 +8,6 @@ import { EmptySearchResultComponent } from './empty-search-result.component';
@NgModule({
declarations: [EmptySearchResultComponent],
exports: [EmptySearchResultComponent],
imports: [MatCardModule, FlexModule],
imports: [MatCardModule, FlexModule, CommonModule],
})
export class EmptySearchResultModule {}

View File

@ -2,10 +2,19 @@ import { NgModule } from '@angular/core';
import { CurrencyPipe } from './currency.pipe';
import { FormatAmountPipe } from './format-amount.pipe';
import { MapUnionPipe } from './map-union.pipe';
import { ThriftInt64Pipe } from './thrift-int64.pipe';
import { ThriftViewPipe } from './thrift-view.pipe';
import { UnionKeyPipe } from './union-key';
const declarations = [FormatAmountPipe, CurrencyPipe, ThriftInt64Pipe, ThriftViewPipe];
const declarations = [
FormatAmountPipe,
CurrencyPipe,
ThriftInt64Pipe,
ThriftViewPipe,
MapUnionPipe,
UnionKeyPipe,
];
@NgModule({
declarations,

View File

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { getUnionKey } from '../utils';
@Pipe({
name: 'ccMapUnion',
})
export class MapUnionPipe<T extends object> implements PipeTransform {
public transform(union: T, mapObject: { [N in keyof T]: string | number }) {
return mapObject[getUnionKey(union)];
}
}

12
src/pipes/union-key.ts Normal file
View File

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { getUnionKey } from '../utils';
@Pipe({
name: 'ccUnionKey',
})
export class UnionKeyPipe<T extends object> implements PipeTransform {
public transform(union: T) {
return getUnionKey(union);
}
}

View File

@ -6,6 +6,6 @@
"messages": "./node_modules/messages-proto/proto",
"file-storage": "./node_modules/file-storage-proto/proto",
"ank": "./node_modules/ank-proto/proto",
"skipper": "./node_modules/skipper-proto/proto"
"skipper": { "path": "./node_modules/skipper-proto/proto", "meta": true }
}
}