mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 10:35:21 +00:00
FE-902: Сreate questionary document 📮 🗃 👉📄 (#74)
This commit is contained in:
parent
1232b5ecb3
commit
3f19e0e0b8
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -8,10 +8,13 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"cSpell.words": [
|
||||
"CAPI",
|
||||
"FATCA",
|
||||
"SNILS",
|
||||
"OGRN",
|
||||
"OKVED",
|
||||
"actionbar",
|
||||
"anapi",
|
||||
"anthroponym",
|
||||
"codegen",
|
||||
"dadata",
|
||||
"datepicker",
|
||||
@ -30,6 +33,7 @@
|
||||
"snils",
|
||||
"submodule",
|
||||
"transloco",
|
||||
"ЕГРЮЛ",
|
||||
"СНИЛС",
|
||||
"бенефициарного",
|
||||
"бенефициарные",
|
||||
|
@ -7,6 +7,7 @@
|
||||
- To get more help use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
- [Angular Material](https://material.angular.io/)
|
||||
- [Prettier](https://prettier.io/)
|
||||
- [PDFMake](https://pdfmake.github.io/docs/)
|
||||
|
||||
## Initialization
|
||||
|
||||
|
27
package-lock.json
generated
27
package-lock.json
generated
@ -6030,9 +6030,9 @@
|
||||
}
|
||||
},
|
||||
"es5-ext": {
|
||||
"version": "0.10.51",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.51.tgz",
|
||||
"integrity": "sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ==",
|
||||
"version": "0.10.50",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz",
|
||||
"integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==",
|
||||
"requires": {
|
||||
"es6-iterator": "~2.0.3",
|
||||
"es6-symbol": "~3.1.1",
|
||||
@ -6087,26 +6087,15 @@
|
||||
"es6-iterator": "~2.0.1",
|
||||
"es6-symbol": "3.1.1",
|
||||
"event-emitter": "~0.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"es6-symbol": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
|
||||
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
|
||||
"requires": {
|
||||
"d": "1",
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.2.tgz",
|
||||
"integrity": "sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
|
||||
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
|
||||
"requires": {
|
||||
"d": "^1.0.1",
|
||||
"es5-ext": "^0.10.51"
|
||||
"d": "1",
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
},
|
||||
"escape-html": {
|
||||
|
@ -2,7 +2,6 @@
|
||||
"name": "dashboard",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"prepush": "npm run test-silent",
|
||||
"start": "ng serve --port 8000",
|
||||
"stub": "ng serve --port 8000 --configuration=stub-keycloak",
|
||||
"aot": "ng serve --port 8000 --aot",
|
||||
@ -12,7 +11,7 @@
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix",
|
||||
"e2e": "ng e2e",
|
||||
"prettier": "prettier \"**/*.{html,js,ts,css,scss,md,json,prettierrc,svg}\"",
|
||||
"prettier": "prettier \"**/*.{html,js,ts,css,scss,md,json,prettierrc,svg,huskyrc}\"",
|
||||
"prettify": "npm run prettier -- --write",
|
||||
"check": "npm run prettier -- --list-different",
|
||||
"fix": "npm run lint-fix; npm run prettify",
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './questionary.module';
|
||||
export * from './questionary.service';
|
||||
export * from './type-guards';
|
||||
|
2
src/app/api/questionary/type-guards/index.ts
Normal file
2
src/app/api/questionary/type-guards/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './russian-individual-entity-questionary';
|
||||
export * from './russian-legal-entity-questionary';
|
@ -0,0 +1,35 @@
|
||||
import {
|
||||
Questionary,
|
||||
IndividualEntityContractor,
|
||||
RussianIndividualEntity,
|
||||
QuestionaryData,
|
||||
Contractor,
|
||||
IndividualResidencyInfo,
|
||||
IndividualRegistrationInfo
|
||||
} from '../../../api-codegen/questionary';
|
||||
import { Replace } from '../../../../type-utils';
|
||||
|
||||
type RussianIndividualEntityContractor = Replace<
|
||||
IndividualEntityContractor,
|
||||
{
|
||||
individualEntity: Replace<
|
||||
RussianIndividualEntity,
|
||||
{
|
||||
residencyInfo: IndividualResidencyInfo;
|
||||
registrationInfo: IndividualRegistrationInfo;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
type RussianIndividualEntityQuestionaryData = Replace<
|
||||
QuestionaryData,
|
||||
{ contractor: RussianIndividualEntityContractor }
|
||||
>;
|
||||
|
||||
export type RussianIndividualEntityQuestionary = Replace<Questionary, { data: RussianIndividualEntityQuestionaryData }>;
|
||||
|
||||
export function isRussianIndividualEntityQuestionary(
|
||||
questionary: Questionary
|
||||
): questionary is RussianIndividualEntityQuestionary {
|
||||
return questionary.data.contractor.contractorType === Contractor.ContractorTypeEnum.IndividualEntityContractor;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import {
|
||||
Questionary,
|
||||
LegalEntityContractor,
|
||||
RussianLegalEntity,
|
||||
QuestionaryData,
|
||||
Contractor,
|
||||
LegalResidencyInfo,
|
||||
LegalRegistrationInfo
|
||||
} from '../../../api-codegen/questionary';
|
||||
import { Replace } from '../../../../type-utils';
|
||||
|
||||
type RussianLegalEntityContractor = Replace<
|
||||
LegalEntityContractor,
|
||||
{
|
||||
legalEntity: Replace<
|
||||
RussianLegalEntity,
|
||||
{
|
||||
residencyInfo: LegalResidencyInfo;
|
||||
registrationInfo: LegalRegistrationInfo;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
type RussianLegalEntityQuestionaryData = Replace<
|
||||
QuestionaryData,
|
||||
{
|
||||
contractor: RussianLegalEntityContractor;
|
||||
}
|
||||
>;
|
||||
export type RussianLegalEntityQuestionary = Replace<
|
||||
Questionary,
|
||||
{
|
||||
data: RussianLegalEntityQuestionaryData;
|
||||
}
|
||||
>;
|
||||
|
||||
export function isRussianLegalEntityQuestionary(
|
||||
questionary: Questionary
|
||||
): questionary is RussianLegalEntityQuestionary {
|
||||
return questionary.data.contractor.contractorType === Contractor.ContractorTypeEnum.LegalEntityContractor;
|
||||
}
|
18
src/app/document/cm-margins-to-in.spec.ts
Normal file
18
src/app/document/cm-margins-to-in.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { cmMarginsToIn } from './cm-margins-to-in';
|
||||
|
||||
describe('cmMarginsToIn', () => {
|
||||
it('should convert 4 cm margins to in', () => {
|
||||
const margins = cmMarginsToIn(5.08, 2.54, 5.08, 7.62);
|
||||
expect(margins).toEqual([144, 72, 144, 216]);
|
||||
});
|
||||
|
||||
it('should convert 2 cm margins to in', () => {
|
||||
const margins = cmMarginsToIn(5.08, 7.62);
|
||||
expect(margins).toEqual([144, 216]);
|
||||
});
|
||||
|
||||
it('should convert 1 cm margins to in', () => {
|
||||
const margins = cmMarginsToIn(7.62);
|
||||
expect(margins).toBe(216);
|
||||
});
|
||||
});
|
13
src/app/document/cm-margins-to-in.ts
Normal file
13
src/app/document/cm-margins-to-in.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Margins } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { cmToIn } from './cm-to-in';
|
||||
|
||||
/**
|
||||
* left, top, right, bottom
|
||||
*/
|
||||
export function cmMarginsToIn(
|
||||
...marginsCm: [number] | [number, number] | [number, number, number, number] | number[]
|
||||
): Margins {
|
||||
const marginsIn = marginsCm.slice(0, 4).map(cm => cmToIn(cm));
|
||||
return (marginsIn.length === 1 ? marginsIn[0] : marginsIn) as Margins;
|
||||
}
|
13
src/app/document/cm-to-in.spec.ts
Normal file
13
src/app/document/cm-to-in.spec.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { cmToIn } from './cm-to-in';
|
||||
|
||||
describe('cmToIn', () => {
|
||||
it('should convert cm to in', () => {
|
||||
const cm = cmToIn(5.08, 72);
|
||||
expect(cm).toBe(144);
|
||||
});
|
||||
|
||||
it('should convert cm to in with selected dpi', () => {
|
||||
const cm = cmToIn(5.08, 100);
|
||||
expect(cm).toBe(200);
|
||||
});
|
||||
});
|
3
src/app/document/cm-to-in.ts
Normal file
3
src/app/document/cm-to-in.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function cmToIn(cm: number, dpi = 72): number {
|
||||
return (cm / 2.54) * dpi;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { createFontFamily } from './font/font-family';
|
||||
|
||||
export enum Family {
|
||||
serif = 'serif'
|
||||
}
|
||||
|
||||
export const fonts = [
|
||||
createFontFamily(Family.serif, {
|
||||
normal: '/assets/fonts/Tinos regular.ttf',
|
||||
bold: '/assets/fonts/Tinos 700.ttf',
|
||||
italics: '/assets/fonts/Tinos italic.ttf',
|
||||
bolditalics: '/assets/fonts/Tinos 700italic.ttf'
|
||||
})
|
||||
];
|
@ -1,12 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { DocumentService } from './document.service';
|
||||
import { DocumentFontsService } from './font/document-fonts.service';
|
||||
import { FontsService } from './fonts';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [],
|
||||
entryComponents: [],
|
||||
providers: [DocumentService, DocumentFontsService]
|
||||
providers: [DocumentService, FontsService]
|
||||
})
|
||||
export class DocumentModule {}
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { createPdf, TDocumentDefinitions, TCreatedPdf } from 'pdfmake/build/pdfmake';
|
||||
import { createPdf, TDocumentDefinitions, TCreatedPdf, TableLayoutFunctions } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { DocumentFontsService } from './font/document-fonts.service';
|
||||
import { fonts } from './document-fonts-config';
|
||||
import { FontsService } from './fonts';
|
||||
import { fontsConfig } from './fonts-config';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentService {
|
||||
constructor(private documentFontService: DocumentFontsService) {
|
||||
this.documentFontService.init(fonts);
|
||||
constructor(private fontsService: FontsService) {
|
||||
this.fontsService.loadFonts(fontsConfig);
|
||||
}
|
||||
|
||||
createPdf(docDefinition: TDocumentDefinitions): Observable<TCreatedPdf> {
|
||||
return this.documentFontService.init$.pipe(map(() => createPdf(docDefinition)));
|
||||
createPdf(
|
||||
docDefinition: TDocumentDefinitions,
|
||||
tableLayouts?: { [name: string]: TableLayoutFunctions }
|
||||
): Observable<TCreatedPdf> {
|
||||
return this.fontsService.fontsData$.pipe(
|
||||
map(({ fonts, vfs }) => createPdf(docDefinition, tableLayouts, fonts, vfs))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import pdfMake, { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { switchMap, map, tap, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { Font } from './font';
|
||||
import { FontFamily } from './font-family';
|
||||
import { blobToBase64 } from './blob-to-base64';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentFontsService<F extends FontFamily[] = FontFamily[]> {
|
||||
init$: Observable<boolean> = this.init([] as F);
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
init(loadedFonts: F): Observable<boolean> {
|
||||
this.init$ = this.loadFonts(loadedFonts).pipe(
|
||||
shareReplay(1),
|
||||
map(() => true)
|
||||
);
|
||||
return this.init$;
|
||||
}
|
||||
|
||||
private loadFont(fontUrl: string): Observable<string> {
|
||||
return this.http.request('GET', fontUrl, { responseType: 'blob' }).pipe(switchMap(blob => blobToBase64(blob)));
|
||||
}
|
||||
|
||||
private loadFonts(loadedFonts: F): Observable<string[]> {
|
||||
const fonts: Font[] = loadedFonts.reduce((r, family) => r.concat(Object.values(family)), []);
|
||||
return forkJoin(fonts.map(font => this.loadFont(font.url))).pipe(
|
||||
tap(fontsBase64 => {
|
||||
const vfs = {};
|
||||
for (let i = 0; i < fonts.length; ++i) {
|
||||
fonts[i].base64 = fontsBase64[i];
|
||||
vfs[fonts[i].hash] = fonts[i].base64;
|
||||
}
|
||||
pdfMake.vfs = vfs;
|
||||
pdfMake.fonts = this.getPdfMakeFonts(loadedFonts);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getFontFamilyHashMap<T extends string>(fonts: FontFamily<T>) {
|
||||
return Object.entries(fonts).reduce((fontHashMap, [type, font]) => {
|
||||
fontHashMap[type] = font.hash;
|
||||
return fontHashMap;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private getPdfMakeFonts(loadedFonts: F) {
|
||||
return loadedFonts.reduce(
|
||||
(currentFonts, family) => {
|
||||
currentFonts[Object.values(family)[0].family] = this.getFontFamilyHashMap(family);
|
||||
return currentFonts;
|
||||
},
|
||||
{} as { [name: string]: TFontFamilyTypes }
|
||||
);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { FontFamily, createFontFamily } from './font-family';
|
||||
import { Font } from './font';
|
||||
|
||||
describe('createFontFamily', () => {
|
||||
it('should return empty FontFamily', () => {
|
||||
const family: FontFamily = createFontFamily('test', {});
|
||||
expect(family).toEqual({});
|
||||
});
|
||||
|
||||
it('should return FontFamily structure', () => {
|
||||
const family: FontFamily = createFontFamily('test', { normal: 'a/b/c d.ttf' });
|
||||
expect(family).toEqual({ normal: new Font('test', 'normal', 'a/b/c d.ttf') });
|
||||
});
|
||||
|
||||
it('should return FontFamily with 2 style', () => {
|
||||
const family: FontFamily = createFontFamily('test', { normal: 'a/b/c d.ttf', bold: 'bold.ttf' });
|
||||
expect(family).toEqual({
|
||||
normal: new Font('test', 'normal', 'a/b/c d.ttf'),
|
||||
bold: new Font('test', 'bold', 'bold.ttf')
|
||||
});
|
||||
});
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
import { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { Font } from './font';
|
||||
|
||||
export type FontFamily<T extends string = string> = Partial<Record<keyof TFontFamilyTypes, Font<T>>>;
|
||||
|
||||
export function createFontFamily<T extends string>(
|
||||
family: T,
|
||||
urlMap: Partial<Record<keyof TFontFamilyTypes, string>> = {}
|
||||
): FontFamily<T> {
|
||||
return Object.entries(urlMap).reduce((fontFamily, [type, url]: [keyof TFontFamilyTypes, string]) => {
|
||||
fontFamily[type] = new Font(family, type, url);
|
||||
return fontFamily;
|
||||
}, {});
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { Font } from './font';
|
||||
|
||||
describe('Font', () => {
|
||||
it('should return hash', () => {
|
||||
const font = new Font('test', 'normal', 'a/b/c d.ttf');
|
||||
expect(font.hash).toBe('test_normal_(a_b_c+d.ttf)');
|
||||
});
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
|
||||
export class Font<T extends string = string> {
|
||||
base64: string;
|
||||
|
||||
get hash() {
|
||||
return `${this.family}_${this.type.toString()}_(${this.url.replace(/\//g, '_').replace(/ /g, '+')})`;
|
||||
}
|
||||
|
||||
constructor(public family: T, public type: keyof TFontFamilyTypes, public url: string) {}
|
||||
}
|
21
src/app/document/fonts-config.ts
Normal file
21
src/app/document/fonts-config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
|
||||
const fontsDir = '/assets/fonts/';
|
||||
const robotoDir = `${fontsDir}Roboto/`;
|
||||
|
||||
export enum FontFamily {
|
||||
serif,
|
||||
fa
|
||||
}
|
||||
|
||||
export const fontsConfig: { [name in FontFamily]: TFontFamilyTypes } = {
|
||||
[FontFamily.serif]: {
|
||||
normal: `${robotoDir}Roboto-Regular.ttf`,
|
||||
bold: `${robotoDir}Roboto-Bold.ttf`,
|
||||
italics: `${robotoDir}Roboto-RegularItalic.ttf`,
|
||||
bolditalics: `${robotoDir}Roboto-BoldItalic.ttf`
|
||||
},
|
||||
[FontFamily.fa]: {
|
||||
normal: `${fontsDir}font-awesome5/fa-regular-400.ttf`
|
||||
}
|
||||
};
|
6
src/app/document/fonts/font.ts
Normal file
6
src/app/document/fonts/font.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Font {
|
||||
family: string | number;
|
||||
type: string;
|
||||
url: string;
|
||||
hash: string;
|
||||
}
|
6
src/app/document/fonts/fonts-data.ts
Normal file
6
src/app/document/fonts/fonts-data.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { vfs, fonts } from 'pdfmake/build/pdfmake';
|
||||
|
||||
export interface FontsData {
|
||||
vfs: typeof vfs;
|
||||
fonts: typeof fonts;
|
||||
}
|
49
src/app/document/fonts/fonts.service.ts
Normal file
49
src/app/document/fonts/fonts.service.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { switchMap, map, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { blobToBase64 } from './blob-to-base64';
|
||||
import { toFonts } from './to-fonts';
|
||||
import { Font } from './font';
|
||||
import { FontsData } from './fonts-data';
|
||||
|
||||
@Injectable()
|
||||
export class FontsService {
|
||||
fontsData$: Observable<FontsData>;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
loadFonts(fontsUrls: FontsData['fonts']): Observable<FontsData> {
|
||||
const fonts = toFonts(fontsUrls);
|
||||
return (this.fontsData$ = forkJoin(fonts.map(({ url }) => this.loadFont(url))).pipe(
|
||||
map(fontsBase64 => ({ vfs: this.getVFS(fonts, fontsBase64), fonts: this.getFonts(fonts) })),
|
||||
shareReplay(1)
|
||||
));
|
||||
}
|
||||
|
||||
private loadFont(url: string): Observable<string> {
|
||||
return this.http.get(url, { responseType: 'blob' }).pipe(switchMap(blob => blobToBase64(blob)));
|
||||
}
|
||||
|
||||
private getFonts(fonts: Font[]): FontsData['fonts'] {
|
||||
return fonts.reduce(
|
||||
(accFonts, { family, type, hash }) => {
|
||||
if (accFonts[family]) accFonts[family][type] = hash;
|
||||
else accFonts[family] = { [type]: hash };
|
||||
return accFonts;
|
||||
},
|
||||
{} as FontsData['fonts']
|
||||
);
|
||||
}
|
||||
|
||||
private getVFS(fonts: Font[], fontsBase64: string[]): FontsData['vfs'] {
|
||||
return fonts.reduce(
|
||||
(accVFS, font, idx) => {
|
||||
accVFS[font.hash] = fontsBase64[idx];
|
||||
return accVFS;
|
||||
},
|
||||
{} as FontsData['vfs']
|
||||
);
|
||||
}
|
||||
}
|
@ -1,35 +1,38 @@
|
||||
import { TestBed, getTestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { DocumentFontsService } from './document-fonts.service';
|
||||
import { createFontFamily } from './font-family';
|
||||
import { FontsService } from './fonts.service';
|
||||
|
||||
describe('DocumentFontsService', () => {
|
||||
const fonts = [
|
||||
createFontFamily('serif', {
|
||||
describe('FontsService', () => {
|
||||
const fonts = {
|
||||
serif: {
|
||||
normal: '/assets/regular.ttf'
|
||||
})
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function createDocumentFontsServiceService() {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [DocumentFontsService]
|
||||
providers: [FontsService]
|
||||
});
|
||||
const injector = getTestBed();
|
||||
const service: DocumentFontsService = injector.get(DocumentFontsService);
|
||||
const service: FontsService = injector.get(FontsService);
|
||||
const httpMock: HttpTestingController = injector.get(HttpTestingController);
|
||||
return { injector, service, httpMock };
|
||||
}
|
||||
|
||||
it('should load fonts', () => {
|
||||
const { service, httpMock } = createDocumentFontsServiceService();
|
||||
service.init(fonts);
|
||||
service.init$.subscribe(result => {
|
||||
expect(result).toEqual(true);
|
||||
const blob = new Blob([new Uint8Array(2)]);
|
||||
service.loadFonts(fonts);
|
||||
service.fontsData$.subscribe(result => {
|
||||
expect(result).toEqual({
|
||||
vfs: { serif_normal: 'AAA=' },
|
||||
fonts: { serif: { normal: 'serif_normal' } }
|
||||
});
|
||||
});
|
||||
const req = httpMock.expectOne('/assets/regular.ttf');
|
||||
req.flush(new Blob([new Uint8Array(2)], { type: 'blob' }));
|
||||
req.flush(blob);
|
||||
expect(req.request.method).toBe('GET');
|
||||
});
|
||||
});
|
2
src/app/document/fonts/index.ts
Normal file
2
src/app/document/fonts/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './font';
|
||||
export * from './fonts.service';
|
27
src/app/document/fonts/to-fonts.spec.ts
Normal file
27
src/app/document/fonts/to-fonts.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { toFonts } from './to-fonts';
|
||||
|
||||
describe('toFonts', () => {
|
||||
const fonts: { [name in string | number]: TFontFamilyTypes } = {
|
||||
test: {
|
||||
normal: 'normal',
|
||||
bold: 'bold',
|
||||
italics: '🥳',
|
||||
bolditalics: 'url5'
|
||||
},
|
||||
1: {
|
||||
normal: 'url'
|
||||
}
|
||||
};
|
||||
|
||||
it('should return fonts list', () => {
|
||||
expect(toFonts(fonts)).toEqual([
|
||||
{ family: '1', type: 'normal', hash: '1_normal', url: 'url' },
|
||||
{ family: 'test', type: 'normal', hash: 'test_normal', url: 'normal' },
|
||||
{ family: 'test', type: 'bold', hash: 'test_bold', url: 'bold' },
|
||||
{ family: 'test', type: 'italics', hash: 'test_italics', url: '🥳' },
|
||||
{ family: 'test', type: 'bolditalics', hash: 'test_bolditalics', url: 'url5' }
|
||||
]);
|
||||
});
|
||||
});
|
19
src/app/document/fonts/to-fonts.ts
Normal file
19
src/app/document/fonts/to-fonts.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { TFontFamilyTypes } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { Font } from './font';
|
||||
|
||||
function familyToFonts(familyName: string | number, family: TFontFamilyTypes): Font[] {
|
||||
return (Object.entries(family) as [string, string][]).map(([type, url]) => ({
|
||||
family: familyName,
|
||||
type,
|
||||
hash: `${familyName}_${type}`,
|
||||
url
|
||||
}));
|
||||
}
|
||||
|
||||
export function toFonts(pdfMakeFonts: { [name in string | number]: TFontFamilyTypes }): Font[] {
|
||||
return Object.entries(pdfMakeFonts).reduce(
|
||||
(accFonts, [familyName, familyFonts]) => accFonts.concat(familyToFonts(familyName, familyFonts)),
|
||||
[] as Font[]
|
||||
);
|
||||
}
|
7
src/app/document/index.ts
Normal file
7
src/app/document/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './document.module';
|
||||
export * from './document.service';
|
||||
export * from './fonts';
|
||||
export * from './fonts-config';
|
||||
export * from './pdfmake-typings';
|
||||
export * from './cm-to-in';
|
||||
export * from './cm-margins-to-in';
|
14
src/app/document/pdfmake-typings.ts
Normal file
14
src/app/document/pdfmake-typings.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {
|
||||
Content as PDFMakeContent,
|
||||
Style as PDFMakeStyle,
|
||||
Table as PDFMakeTable,
|
||||
TableCell as PDFMakeTableCell
|
||||
} from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { Replace } from '../../type-utils';
|
||||
|
||||
type Style = PDFMakeStyle | string | string[];
|
||||
|
||||
export type Content = Replace<PDFMakeContent, { style?: Style }>;
|
||||
export type TableCell = Replace<PDFMakeTableCell, { style?: Style }>;
|
||||
export type Table = Replace<PDFMakeTable, { body: (Content | PDFMakeTableCell | string)[][] }>;
|
@ -0,0 +1,12 @@
|
||||
import { cmToIn, Content } from '../../document';
|
||||
import { createCaptionedText } from '../create-content';
|
||||
|
||||
export function createCompanyHeader(companyName: string, companyInn: string): Content {
|
||||
return {
|
||||
columns: [
|
||||
{ ...createCaptionedText(companyName, 'Наименование вашей компании'), width: 'auto' },
|
||||
{ ...createCaptionedText(companyInn, 'ИНН'), width: 'auto' }
|
||||
],
|
||||
columnGap: cmToIn(2)
|
||||
};
|
||||
}
|
143
src/app/questionary-document/beneficial-owner/get-doc-def.ts
Normal file
143
src/app/questionary-document/beneficial-owner/get-doc-def.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { DocDef } from '../create-questionary';
|
||||
import {
|
||||
createVerticalParagraph,
|
||||
createHeader,
|
||||
createHeadline,
|
||||
createEnding,
|
||||
createInlineCheckboxWithTitle,
|
||||
createInlineParagraph,
|
||||
createHorizontalCheckbox
|
||||
} from '../create-content';
|
||||
import { getContactInfo, toYesNo, getIdentityDocument, getDate, getPercent, simpleYesNo } from '../select-data';
|
||||
import { BeneficialOwner } from '../../api-codegen/questionary';
|
||||
import { getIndividualResidencyInfo } from './get-individual-residency-info';
|
||||
import { createCompanyHeader } from './create-company-header';
|
||||
import { toOptional } from '../../../utils';
|
||||
|
||||
const EMPTY = '';
|
||||
|
||||
export function getDocDef(beneficialOwner: BeneficialOwner, companyName: string, companyInn: string): DocDef {
|
||||
const {
|
||||
russianPrivateEntity,
|
||||
migrationCardInfo,
|
||||
residenceApprove,
|
||||
ownershipPercentage,
|
||||
inn,
|
||||
snils,
|
||||
pdlRelationDegree,
|
||||
pdlCategory,
|
||||
residencyInfo,
|
||||
identityDocument
|
||||
} = toOptional(beneficialOwner);
|
||||
const { birthDate, fio, birthPlace, citizenship, actualAddress, contactInfo } = toOptional(russianPrivateEntity);
|
||||
const optionalMigrationCardInfo = toOptional(migrationCardInfo);
|
||||
const optionalResidenceApprove = toOptional(residenceApprove);
|
||||
|
||||
const identityDocumentInfo = getIdentityDocument(identityDocument);
|
||||
const { usaTaxResident, exceptUsaTaxResident } = getIndividualResidencyInfo(residencyInfo);
|
||||
const contact = getContactInfo(contactInfo);
|
||||
|
||||
return {
|
||||
content: [
|
||||
createHeader('Приложение №'),
|
||||
createCompanyHeader(companyName, companyInn),
|
||||
createHeadline('АНКЕТА ФИЗИЧЕСКОГО ЛИЦА - БЕНЕФИЦИАРНОГО ВЛАДЕЛЬЦА'),
|
||||
createVerticalParagraph('1. Бенефициарный владелец', [
|
||||
[
|
||||
createHorizontalCheckbox(
|
||||
[
|
||||
'лицо, не указанное в учредительных документах/выписке ЕГРЮЛ/списке участников/реестре акционеров, но которое косвенно владеет более 25% в капитале компании или контролирует действия компании',
|
||||
'лицо, указанное в учредительных документах/выписке ЕГРЮЛ/списке участников/реестре акционеров (участник/акционер), владеющее в конечном счете более 25% в капитале компании'
|
||||
],
|
||||
-1 // TODO
|
||||
)
|
||||
]
|
||||
]),
|
||||
createInlineParagraph(
|
||||
'2. Процент владения капиталом юридического лица',
|
||||
getPercent(ownershipPercentage) || EMPTY
|
||||
),
|
||||
createInlineParagraph('3. Фамилия, Имя, Отчество (при наличии)', fio || EMPTY),
|
||||
createInlineParagraph('4. Дата рождения', getDate(birthDate) || EMPTY),
|
||||
createInlineParagraph('5. Место рождения', birthPlace || EMPTY),
|
||||
createInlineParagraph('6. Гражданство', citizenship || 'РФ'), // TODO: move to questionary input
|
||||
createInlineParagraph('7. ИНН (при наличии)', inn || EMPTY),
|
||||
createInlineParagraph('8. Реквизиты документа, удостоверяющего личность', [
|
||||
[`8.1. Вид документа: ${identityDocumentInfo.name || EMPTY}`],
|
||||
[`8.2. Серия и номер: ${identityDocumentInfo.seriesNumber || EMPTY}`],
|
||||
[
|
||||
`8.3. Наименование органа, выдавшего документ, код подразделения (при наличии): ${identityDocumentInfo.issuer ||
|
||||
EMPTY}`
|
||||
],
|
||||
[`8.4. Дата выдачи: ${getDate(identityDocumentInfo.issuedAt) || EMPTY}`]
|
||||
]),
|
||||
createInlineParagraph('9. Данные миграционной карты¹', [
|
||||
[`9.1. Номер карты: ${optionalMigrationCardInfo.cardNumber || EMPTY}`],
|
||||
[
|
||||
`9.2. Дата начала срока пребывания в РФ: ${getDate(optionalMigrationCardInfo.beginningDate) ||
|
||||
EMPTY}`
|
||||
],
|
||||
[
|
||||
`9.3. Дата окончания срока пребывания в РФ: ${getDate(optionalMigrationCardInfo.expirationDate) ||
|
||||
EMPTY}`
|
||||
]
|
||||
]),
|
||||
createInlineParagraph(
|
||||
'10. Данные документа, подтверждающего право иностранного гражданина или лица без гражданства на пребывание (проживание) в РФ1¹',
|
||||
[
|
||||
[`10.1. Вид документа: ${optionalResidenceApprove.name || EMPTY}`],
|
||||
[`10.2. Серия (при наличии): ${optionalResidenceApprove.series || EMPTY}`],
|
||||
[`10.3. Номер документа: ${optionalResidenceApprove.number || EMPTY}`],
|
||||
[`10.4. Дата начала срока действия: ${getDate(optionalResidenceApprove.beginningDate) || EMPTY}`],
|
||||
[
|
||||
`10.5. Дата окончания срока действия: ${getDate(optionalResidenceApprove.expirationDate) ||
|
||||
EMPTY}`
|
||||
]
|
||||
]
|
||||
),
|
||||
createInlineParagraph(
|
||||
'11. Адрес места жительства (регистрации) или места пребывания',
|
||||
actualAddress || EMPTY
|
||||
),
|
||||
createInlineParagraph('12. СНИЛС (при наличии)', snils || EMPTY),
|
||||
createInlineParagraph('13. Контактная информация (телефон/email)', contact || EMPTY),
|
||||
createVerticalParagraph('14. Принадлежность физического лица к некоторым категориям лиц', [
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'14.1. Принадлежность к категории ПДЛ²',
|
||||
simpleYesNo,
|
||||
toYesNo(pdlCategory)
|
||||
)
|
||||
],
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'14.2. Является родственником ПДЛ',
|
||||
simpleYesNo,
|
||||
toYesNo(!!pdlRelationDegree)
|
||||
),
|
||||
`14.3. Степень родства: ${pdlRelationDegree || EMPTY}`
|
||||
],
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'14.4. Является ли бенефициарный владелец налогоплательщиком/налоговым резидентом США?',
|
||||
simpleYesNo,
|
||||
toYesNo(usaTaxResident)
|
||||
)
|
||||
],
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'14.5. Является ли бенефициарный владелец налогоплательщиком/налоговым резидентом иного иностранного государства (кроме США)?',
|
||||
simpleYesNo,
|
||||
toYesNo(exceptUsaTaxResident)
|
||||
)
|
||||
]
|
||||
])
|
||||
],
|
||||
prefooter: createEnding(),
|
||||
footer: [
|
||||
'¹ Заполняется только для иностранных граждан и лиц без гражданства, находящихся на территории РФ в случае, если необходимость наличия у них данного документа предусмотрена законодательством РФ',
|
||||
'² Публичные должностные лица, включая российские, иностранные и международные.'
|
||||
].join('\n'),
|
||||
footerHeight: 3.1
|
||||
};
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { ResidencyInfo, IndividualResidencyInfo } from '../../api-codegen/questionary';
|
||||
|
||||
export function getIndividualResidencyInfo(
|
||||
residencyInfo: ResidencyInfo
|
||||
): {
|
||||
usaTaxResident: boolean;
|
||||
exceptUsaTaxResident: boolean;
|
||||
} {
|
||||
if (get(residencyInfo, 'residencyInfoType') === 'IndividualResidencyInfo') {
|
||||
const { usaTaxResident, exceptUsaTaxResident } = residencyInfo as IndividualResidencyInfo;
|
||||
return {
|
||||
usaTaxResident,
|
||||
exceptUsaTaxResident
|
||||
};
|
||||
}
|
||||
console.error("ResidencyInfo isn't IndividualResidencyInfo");
|
||||
return null;
|
||||
}
|
1
src/app/questionary-document/beneficial-owner/index.ts
Normal file
1
src/app/questionary-document/beneficial-owner/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './get-doc-def';
|
@ -0,0 +1,31 @@
|
||||
import { Layout } from '../create-questionary';
|
||||
import { Content } from '../../document';
|
||||
|
||||
export function createCaptionedText(text: string, caption: string): Content {
|
||||
return {
|
||||
layout: Layout.underline,
|
||||
table: {
|
||||
body: [
|
||||
[
|
||||
{
|
||||
text,
|
||||
style: {
|
||||
bold: true,
|
||||
alignment: 'center'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
text: caption,
|
||||
style: {
|
||||
bold: true,
|
||||
alignment: 'center',
|
||||
italics: true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import { icons } from './icons';
|
||||
import { Content } from '../../document';
|
||||
import { Layout } from '../create-questionary';
|
||||
import { createGrid } from './create-grid';
|
||||
|
||||
type Items<T extends any> = string[] | [T, string][];
|
||||
|
||||
function itemsIsKeyValue<T extends any>(srcItems: Items<T>): srcItems is [T, string][] {
|
||||
return Array.isArray(srcItems[0]);
|
||||
}
|
||||
|
||||
function itemsToMap<T extends any>(srcItems: Items<T>): Map<T, string> {
|
||||
return itemsIsKeyValue(srcItems)
|
||||
? new Map(srcItems)
|
||||
: new Map<T, string>(srcItems.map((i, idx) => [idx, i] as any));
|
||||
}
|
||||
|
||||
function itemsWithActive<T extends any>(
|
||||
itemsSrc: Items<T>,
|
||||
activeKey?: T
|
||||
): { key: T; value: string; isActive: boolean }[] {
|
||||
const items = itemsToMap(itemsSrc);
|
||||
return Array.from(items).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
isActive: activeKey === key
|
||||
}));
|
||||
}
|
||||
|
||||
export function createCheckbox(text: string, active = false): Content {
|
||||
return { text: [active ? icons.checkSquare : icons.square, ' ', text] };
|
||||
}
|
||||
|
||||
export function createInlineCheckbox<T extends any>(itemsSrc: Items<T>, activeKey?: T): Content {
|
||||
const text = itemsWithActive(itemsSrc, activeKey)
|
||||
.reduce((acc, { value, isActive }) => [...acc, createCheckbox(value, isActive), ' '], [])
|
||||
.slice(0, -1);
|
||||
return { text };
|
||||
}
|
||||
|
||||
export function createInlineCheckboxWithTitle<T extends any>(title: string, items: Items<T>, activeKey?: T): Content {
|
||||
const inlineCheckbox = createInlineCheckbox(items, activeKey);
|
||||
return { ...inlineCheckbox, text: [`${title}: `, ...inlineCheckbox.text] };
|
||||
}
|
||||
|
||||
export function createVerticalCheckbox<T extends any>(itemsSrc: Items<T>, activeKey?: T): Content {
|
||||
return {
|
||||
layout: Layout.noBorders,
|
||||
table: {
|
||||
widths: ['*'],
|
||||
body: itemsWithActive(itemsSrc, activeKey).map(({ value, isActive }) => [createCheckbox(value, isActive)])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createVerticalCheckboxWithTitle<T extends any>(title: string, items: Items<T>, activeKey?: T): Content {
|
||||
const verticalCheckbox = createVerticalCheckbox(items, activeKey);
|
||||
const { table } = verticalCheckbox;
|
||||
return {
|
||||
...verticalCheckbox,
|
||||
table: {
|
||||
...table,
|
||||
widths: ['auto', ...table.widths],
|
||||
body: table.body.map((row, idx) => [idx === 0 ? title : null, ...row])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createHorizontalCheckbox<T extends any>(itemsSrc: Items<T>, activeKey?: T): Content {
|
||||
return createGrid(
|
||||
itemsWithActive(itemsSrc, activeKey).map(({ value, isActive }) => createCheckbox(value, isActive)),
|
||||
0.25
|
||||
);
|
||||
}
|
22
src/app/questionary-document/create-content/create-ending.ts
Normal file
22
src/app/questionary-document/create-content/create-ending.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { cmMarginsToIn, Content } from '../../document';
|
||||
|
||||
export function createEnding(): Content {
|
||||
return {
|
||||
layout: 'noBorders',
|
||||
margin: cmMarginsToIn(0, 0.2, 0, 1.1),
|
||||
table: {
|
||||
widths: ['*', 'auto'],
|
||||
body: [
|
||||
[
|
||||
'М.П.',
|
||||
{
|
||||
text: moment().format('LL') + '\n\n\n_____________________/______________/',
|
||||
style: { alignment: 'right' }
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
30
src/app/questionary-document/create-content/create-grid.ts
Normal file
30
src/app/questionary-document/create-content/create-grid.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Layout } from '../create-questionary';
|
||||
import { Content, Table, cmMarginsToIn } from '../../document';
|
||||
import { createTableBody } from './create-table-body';
|
||||
import { getColumnsCount } from './get-columns-count';
|
||||
|
||||
type Item = Table['body'][number][number] | [Table['body'][number][number], number];
|
||||
|
||||
function getMargin(idx: number, count: number, gapCm: number) {
|
||||
const marginFirst = cmMarginsToIn(0, 0, gapCm / 2, 0);
|
||||
const marginInner = cmMarginsToIn(gapCm / 2, 0, gapCm / 2, 0);
|
||||
const marginLast = cmMarginsToIn(gapCm / 2, 0, 0, 0);
|
||||
return idx === 0 ? marginFirst : idx === count - 1 ? marginLast : marginInner;
|
||||
}
|
||||
|
||||
function getTableCell(i: Item): Content {
|
||||
const [item, colSpan] = Array.isArray(i) ? i : [i, 1];
|
||||
const content = typeof item === 'object' ? item : { text: item };
|
||||
return { ...content, colSpan };
|
||||
}
|
||||
|
||||
export function createGrid(items: Item[], gapCm: number = 0): Content {
|
||||
const row = items.map((i, idx) => ({ ...getTableCell(i), margin: getMargin(idx, items.length, gapCm) }));
|
||||
return {
|
||||
layout: Layout.wrapper,
|
||||
table: {
|
||||
widths: new Array(getColumnsCount(row)).fill('*'),
|
||||
body: createTableBody([row])
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { Content } from '../../document';
|
||||
|
||||
export function createHeader(text: string): Content {
|
||||
return {
|
||||
text,
|
||||
style: { alignment: 'right' }
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Content, cmMarginsToIn } from '../../document';
|
||||
|
||||
export function createHeadline(text: string): Content {
|
||||
return {
|
||||
text,
|
||||
style: {
|
||||
alignment: 'center',
|
||||
bold: true
|
||||
},
|
||||
margin: cmMarginsToIn(0, 0.1, 0, 0.1)
|
||||
};
|
||||
}
|
22
src/app/questionary-document/create-content/create-icons.ts
Normal file
22
src/app/questionary-document/create-content/create-icons.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { FontFamily, Content } from '../../document';
|
||||
|
||||
interface SourceIcons {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
type Icons<T extends SourceIcons> = Record<keyof T, Content>;
|
||||
|
||||
export function createIcons<T extends SourceIcons, R = Icons<T>>(iconsObj: T): R {
|
||||
return Object.entries(iconsObj).reduce(
|
||||
(acc, [name, text]) => {
|
||||
acc[name] = {
|
||||
text,
|
||||
style: {
|
||||
font: FontFamily.fa
|
||||
}
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as R
|
||||
);
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import { Content, Table, cmMarginsToIn } from '../../document';
|
||||
import { Layout } from '../create-questionary';
|
||||
import { createTableBody } from './create-table-body';
|
||||
import { PRIMARY_COLOR } from '../create-questionary';
|
||||
import { getColumnsCount } from './get-columns-count';
|
||||
import { createGrid } from './create-grid';
|
||||
|
||||
const MARGIN = cmMarginsToIn(0, 0.1);
|
||||
|
||||
function prepareBody(body: Table['body'] | string): Table['body'] {
|
||||
body = typeof body === 'string' ? [[body]] : body;
|
||||
return body.map(i => i.map(j => j || ''));
|
||||
}
|
||||
|
||||
function setBodyColSpans(body: Table['body'], columnsCount: number): Table['body'] {
|
||||
return body.map(row => {
|
||||
const rowColumnsCount = row.reduce((sum, col) => sum + (typeof col === 'object' ? col.colSpan || 1 : 1), 0);
|
||||
if (rowColumnsCount === columnsCount) {
|
||||
return row;
|
||||
}
|
||||
const lastCol = row[row.length - 1];
|
||||
const resultLastCol =
|
||||
typeof lastCol === 'object' ? { ...lastCol, colSpan: lastCol.colSpan || 1 } : { text: lastCol, colSpan: 1 };
|
||||
resultLastCol.colSpan += columnsCount - rowColumnsCount;
|
||||
return [...row.slice(0, -1), resultLastCol];
|
||||
});
|
||||
}
|
||||
|
||||
function getColumnsCountByBody(body: Table['body']): number {
|
||||
return body.reduce((maxCount, row) => Math.max(maxCount, getColumnsCount(row)), 1);
|
||||
}
|
||||
|
||||
export function createVerticalParagraph(
|
||||
header: string,
|
||||
body: Table['body'] | string = [[]],
|
||||
columnsCount?: number
|
||||
): Content {
|
||||
body = prepareBody(body);
|
||||
if (!columnsCount) {
|
||||
columnsCount = getColumnsCountByBody(body);
|
||||
}
|
||||
body = setBodyColSpans(body, columnsCount);
|
||||
const headerRow: Table['body'][number] = [
|
||||
{
|
||||
colSpan: columnsCount,
|
||||
style: { color: 'white' },
|
||||
text: header
|
||||
}
|
||||
];
|
||||
return {
|
||||
layout: Layout.header,
|
||||
margin: MARGIN,
|
||||
table: {
|
||||
widths: new Array(columnsCount).fill('*'),
|
||||
body: createTableBody([headerRow, ...body])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createInlineParagraph(header: string, body: Table['body'] | string = [[]]): Content {
|
||||
body = prepareBody(body);
|
||||
const headerTable: Content = {
|
||||
layout: Layout.noBorders,
|
||||
table: {
|
||||
rowSpan: body.length,
|
||||
widths: ['*'],
|
||||
body: [
|
||||
[
|
||||
{
|
||||
text: header,
|
||||
style: { color: 'white' },
|
||||
fillColor: PRIMARY_COLOR
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
const bodyTable = {
|
||||
layout: Layout.noBorders,
|
||||
table: {
|
||||
rowSpan: body.length,
|
||||
widths: ['*'],
|
||||
body
|
||||
}
|
||||
};
|
||||
return { ...createGrid([headerTable, bodyTable]), margin: MARGIN };
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { Table } from '../../document';
|
||||
|
||||
export function createTableBody(body: Table['body']): Table['body'] {
|
||||
/**
|
||||
* Магия ✨ таблиц PDFMake (TODO: исправить если что-то изменится)
|
||||
* В таблице первая колонка с `colSpan` свойством должна иметь после себя `colSpan - 1` пустых колонок
|
||||
* похоже она использует их для расширения первой колонки
|
||||
*/
|
||||
return body.reduce((accBody, row, idx) => {
|
||||
const [firstColumn, ...otherColumns] = row;
|
||||
if (typeof firstColumn === 'object' && firstColumn.colSpan && firstColumn.colSpan > 1) {
|
||||
const firstColumnFiller = new Array(firstColumn.colSpan - 1).fill(null);
|
||||
accBody[idx] = [firstColumn, ...firstColumnFiller, ...otherColumns];
|
||||
}
|
||||
return accBody;
|
||||
}, body.slice());
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { Table } from '../../document';
|
||||
|
||||
export function getColumnsCount(row: Table['body'][number]): number {
|
||||
return row.reduce((accCount, col) => accCount + (typeof col === 'object' && col.colSpan ? col.colSpan : 1), 0);
|
||||
}
|
6
src/app/questionary-document/create-content/icons.ts
Normal file
6
src/app/questionary-document/create-content/icons.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { createIcons } from './create-icons';
|
||||
|
||||
export const icons = createIcons({
|
||||
checkSquare: '',
|
||||
square: ''
|
||||
});
|
12
src/app/questionary-document/create-content/index.ts
Normal file
12
src/app/questionary-document/create-content/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from './create-checkbox';
|
||||
export * from './create-paragraph';
|
||||
export * from './create-icons';
|
||||
export * from './create-header';
|
||||
export * from './create-headline';
|
||||
export * from './create-ending';
|
||||
export * from './create-grid';
|
||||
export * from './create-captioned-text';
|
||||
|
||||
export * from './icons';
|
||||
|
||||
export * from './text';
|
11
src/app/questionary-document/create-content/text.ts
Normal file
11
src/app/questionary-document/create-content/text.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function text(literals: TemplateStringsArray, ...placeholders: (string | number | null | undefined)[]): string {
|
||||
const resultPlaceholders = placeholders.map(p => {
|
||||
switch (p) {
|
||||
case null:
|
||||
case undefined:
|
||||
return '';
|
||||
}
|
||||
return String(p);
|
||||
});
|
||||
return resultPlaceholders.map((p, i) => literals[i] + p).join('') + literals[literals.length - 1];
|
||||
}
|
@ -0,0 +1 @@
|
||||
export const PRIMARY_COLOR = '#203764';
|
@ -0,0 +1,33 @@
|
||||
import { Margins, TDocumentHeaderFooterFunction } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { Content } from '../../document';
|
||||
|
||||
export function createFooter({
|
||||
margin,
|
||||
text,
|
||||
content
|
||||
}: {
|
||||
margin: Margins;
|
||||
text: string;
|
||||
content: Content;
|
||||
}): TDocumentHeaderFooterFunction {
|
||||
const lineOffsetIn = 5;
|
||||
const lineSizeIn = 100;
|
||||
return () => ({
|
||||
margin: [margin[0], margin[1] + lineOffsetIn, margin[2], margin[3]],
|
||||
columns: [
|
||||
[
|
||||
content,
|
||||
{
|
||||
canvas: [
|
||||
{ type: 'line', x1: 0, y1: -lineOffsetIn, x2: lineSizeIn, y2: -lineOffsetIn, lineWidth: 0.5 }
|
||||
]
|
||||
},
|
||||
{
|
||||
style: { fontSize: 6 },
|
||||
text
|
||||
}
|
||||
]
|
||||
]
|
||||
});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { TableLayoutFunctions, TDocumentDefinitions, PageSize } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { createStyles, createDefaultStyle } from './create-styles';
|
||||
import { createTableLayouts } from './create-table-layouts';
|
||||
import { DocDef } from './doc-def';
|
||||
import { cmMarginsToIn } from '../../document/cm-margins-to-in';
|
||||
import { createFooter } from './create-footer';
|
||||
|
||||
export function createQuestionary(data: DocDef): [TDocumentDefinitions, { [name: string]: TableLayoutFunctions }] {
|
||||
const leftMarginCm = 3;
|
||||
const topMarginCm = 2;
|
||||
const rightMarginCm = 1.5;
|
||||
const footerMarginCm = 2;
|
||||
|
||||
const pageMarginsIn = cmMarginsToIn(leftMarginCm, topMarginCm, rightMarginCm, footerMarginCm + data.footerHeight);
|
||||
const footerMarginsIn = cmMarginsToIn(leftMarginCm, 0, rightMarginCm, 0);
|
||||
|
||||
return [
|
||||
{
|
||||
pageSize: 'A4' as PageSize,
|
||||
pageMargins: pageMarginsIn,
|
||||
content: data.content,
|
||||
footer: createFooter({ margin: footerMarginsIn, text: data.footer, content: data.prefooter }),
|
||||
styles: createStyles(),
|
||||
defaultStyle: createDefaultStyle()
|
||||
},
|
||||
createTableLayouts()
|
||||
];
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { Style as PDFMakeStyle } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { FontFamily } from '../../document';
|
||||
|
||||
export enum Style {}
|
||||
|
||||
export function createStyles(): { [name in Style]: PDFMakeStyle } {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function createDefaultStyle(): PDFMakeStyle {
|
||||
return {
|
||||
font: FontFamily.serif,
|
||||
fontSize: 8,
|
||||
lineHeight: 1
|
||||
};
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { TableLayoutFunctions } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { PRIMARY_COLOR } from './colors';
|
||||
|
||||
export enum Layout {
|
||||
noBorders = 'noBorders',
|
||||
noPaddings = 'noPaddings',
|
||||
header = 'header',
|
||||
wrapper = 'wrapper',
|
||||
underline = 'underline'
|
||||
}
|
||||
|
||||
const noPaddings: TableLayoutFunctions = {
|
||||
paddingLeft: () => 0,
|
||||
paddingRight: () => 0,
|
||||
paddingTop: () => 0,
|
||||
paddingBottom: () => 0
|
||||
};
|
||||
|
||||
const noBorders: TableLayoutFunctions = {
|
||||
hLineWidth: () => 0,
|
||||
vLineWidth: () => 0
|
||||
};
|
||||
|
||||
export function createTableLayouts(): { [name in Layout]: TableLayoutFunctions } {
|
||||
return {
|
||||
[Layout.noBorders]: noBorders,
|
||||
[Layout.noPaddings]: noPaddings,
|
||||
[Layout.wrapper]: {
|
||||
...noBorders,
|
||||
...noPaddings
|
||||
},
|
||||
[Layout.header]: {
|
||||
fillColor(rowIdx) {
|
||||
return rowIdx === 0 ? PRIMARY_COLOR : null;
|
||||
},
|
||||
...noBorders
|
||||
},
|
||||
[Layout.underline]: {
|
||||
hLineWidth: idx => (idx === 1 ? 0.5 : 0),
|
||||
vLineWidth: () => 0,
|
||||
paddingLeft: () => 0,
|
||||
paddingRight: () => 0,
|
||||
paddingTop: () => 0
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { Content } from '../../document';
|
||||
|
||||
export interface DocDef {
|
||||
content: (Content | string)[];
|
||||
prefooter?: Content;
|
||||
footer?: string;
|
||||
footerHeight?: number;
|
||||
}
|
4
src/app/questionary-document/create-questionary/index.ts
Normal file
4
src/app/questionary-document/create-questionary/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './create-questionary';
|
||||
export * from './create-table-layouts';
|
||||
export * from './doc-def';
|
||||
export * from './colors';
|
11
src/app/questionary-document/get-beneficial-owner-doc-def.ts
Normal file
11
src/app/questionary-document/get-beneficial-owner-doc-def.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { BeneficialOwner } from '../api-codegen/questionary';
|
||||
import { DocDef } from './create-questionary';
|
||||
import { getDocDef } from './beneficial-owner';
|
||||
|
||||
export function getBeneficialOwnerDocDef(
|
||||
beneficialOwner: BeneficialOwner,
|
||||
companyName: string,
|
||||
companyInn: string
|
||||
): DocDef {
|
||||
return getDocDef(beneficialOwner, companyName, companyInn);
|
||||
}
|
15
src/app/questionary-document/get-beneficial-owners.ts
Normal file
15
src/app/questionary-document/get-beneficial-owners.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Questionary, BeneficialOwner } from '../api-codegen/questionary';
|
||||
import { isRussianIndividualEntityQuestionary, isRussianLegalEntityQuestionary } from '../api';
|
||||
|
||||
export function getBeneficialOwners(questionary: Questionary): BeneficialOwner[] | null {
|
||||
let beneficialOwners: BeneficialOwner[] = [];
|
||||
if (isRussianIndividualEntityQuestionary(questionary)) {
|
||||
beneficialOwners = questionary.data.contractor.individualEntity.beneficialOwners;
|
||||
} else if (isRussianLegalEntityQuestionary(questionary)) {
|
||||
beneficialOwners = questionary.data.contractor.legalEntity.beneficialOwner;
|
||||
} else {
|
||||
console.error('Unknown questionary');
|
||||
return null;
|
||||
}
|
||||
return beneficialOwners;
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Questionary } from '../api-codegen/questionary';
|
||||
import { getDocDef as getRussianIndividualEntityDocDef } from './russian-individual-entity';
|
||||
import { getDocDef as getRussianLegalEntityDocDef } from './russian-legal-entity';
|
||||
import { DocDef } from './create-questionary';
|
||||
import { isRussianLegalEntityQuestionary, isRussianIndividualEntityQuestionary } from '../api';
|
||||
|
||||
export function getEntityQuestionaryDocDef(questionary: Questionary): DocDef {
|
||||
if (isRussianIndividualEntityQuestionary(questionary)) {
|
||||
return getRussianIndividualEntityDocDef(questionary);
|
||||
} else if (isRussianLegalEntityQuestionary(questionary)) {
|
||||
return getRussianLegalEntityDocDef(questionary);
|
||||
}
|
||||
console.error('Unknown questionary');
|
||||
return null;
|
||||
}
|
2
src/app/questionary-document/index.ts
Normal file
2
src/app/questionary-document/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './questionary-document.module';
|
||||
export * from './questionary-document.service';
|
12
src/app/questionary-document/questionary-document.module.ts
Normal file
12
src/app/questionary-document/questionary-document.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CardModule } from '../layout/card';
|
||||
import { ButtonModule } from '../button';
|
||||
import { DocumentModule } from '../document/document.module';
|
||||
import { QuestionaryDocumentService } from './questionary-document.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [CardModule, ButtonModule, DocumentModule],
|
||||
providers: [QuestionaryDocumentService]
|
||||
})
|
||||
export class QuestionaryDocumentModule {}
|
42
src/app/questionary-document/questionary-document.service.ts
Normal file
42
src/app/questionary-document/questionary-document.service.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { TCreatedPdf } from 'pdfmake/build/pdfmake';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { DocumentService } from '../document';
|
||||
import { createQuestionary } from './create-questionary';
|
||||
import { Questionary, BeneficialOwner } from '../api-codegen/questionary';
|
||||
import { getEntityQuestionaryDocDef } from './get-entity-questionary-doc-def';
|
||||
import { getBeneficialOwners } from './get-beneficial-owners';
|
||||
import { getBeneficialOwnerDocDef } from './get-beneficial-owner-doc-def';
|
||||
import { getCompanyInfo } from './select-data';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionaryDocumentService {
|
||||
constructor(private documentService: DocumentService) {}
|
||||
|
||||
createDoc(questionary: Questionary): Observable<TCreatedPdf> {
|
||||
return this.documentService.createPdf(...createQuestionary(getEntityQuestionaryDocDef(questionary)));
|
||||
}
|
||||
|
||||
createBeneficialOwnerDoc(
|
||||
beneficialOwner: BeneficialOwner,
|
||||
companyName: string,
|
||||
companyInn: string
|
||||
): Observable<TCreatedPdf> {
|
||||
const docDef = getBeneficialOwnerDocDef(beneficialOwner, companyName, companyInn);
|
||||
return this.documentService.createPdf(...createQuestionary(docDef));
|
||||
}
|
||||
|
||||
createBeneficialOwnerDocs(questionary: Questionary): Observable<TCreatedPdf[]> {
|
||||
const beneficialOwners = getBeneficialOwners(questionary);
|
||||
const { companyName, companyInn } = getCompanyInfo(questionary);
|
||||
if (!isEmpty(beneficialOwners)) {
|
||||
const beneficialOwnersDocs = beneficialOwners.map(beneficialOwner =>
|
||||
this.createBeneficialOwnerDoc(beneficialOwner, companyName, companyInn)
|
||||
);
|
||||
return combineLatest(beneficialOwnersDocs);
|
||||
}
|
||||
return of([]);
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { DocDef } from '../create-questionary';
|
||||
import {
|
||||
createVerticalCheckboxWithTitle,
|
||||
createInlineCheckboxWithTitle,
|
||||
createInlineCheckbox,
|
||||
createVerticalParagraph,
|
||||
createHeader,
|
||||
createHeadline,
|
||||
createEnding
|
||||
} from '../create-content';
|
||||
import { YesNo, getShopLocationURL, getBusinessInfo, toYesNo, simpleYesNo } from '../select-data';
|
||||
import {
|
||||
AccountantInfo,
|
||||
MonthOperationCount,
|
||||
MonthOperationSum,
|
||||
PropertyInfoDocumentType
|
||||
} from '../../api-codegen/questionary';
|
||||
import { getIndividualEntityName } from './get-individual-entity-name';
|
||||
import { RussianIndividualEntityQuestionary } from '../../api';
|
||||
import { toOptional } from '../../../utils';
|
||||
|
||||
const DocumentType = PropertyInfoDocumentType.DocumentTypeEnum;
|
||||
const AccountantInfoType = AccountantInfo.AccountantInfoTypeEnum;
|
||||
|
||||
const EMPTY = '';
|
||||
|
||||
export function getDocDef(questionary: RussianIndividualEntityQuestionary): DocDef {
|
||||
const { data } = toOptional(questionary);
|
||||
const { contractor, shopInfo, contactInfo } = toOptional(data);
|
||||
const { individualEntity } = toOptional(contractor);
|
||||
const { location, details } = toOptional(shopInfo);
|
||||
const { name: brandName } = toOptional(details);
|
||||
const { phoneNumber, email } = toOptional(contactInfo);
|
||||
const {
|
||||
additionalInfo,
|
||||
russianPrivateEntity,
|
||||
registrationInfo,
|
||||
inn,
|
||||
snils,
|
||||
propertyInfoDocumentType,
|
||||
individualPersonCategories,
|
||||
beneficialOwners,
|
||||
residencyInfo
|
||||
} = toOptional(individualEntity);
|
||||
const {
|
||||
nkoRelationTarget,
|
||||
relationshipWithNko,
|
||||
monthOperationSum,
|
||||
monthOperationCount,
|
||||
benefitThirdParties,
|
||||
relationIndividualEntity
|
||||
} = toOptional(additionalInfo);
|
||||
const { fio } = toOptional(russianPrivateEntity);
|
||||
const { registrationPlace } = toOptional(registrationInfo);
|
||||
const { documentType } = toOptional(propertyInfoDocumentType);
|
||||
const { foreignPublicPerson, foreignRelativePerson } = toOptional(individualPersonCategories);
|
||||
const { usaTaxResident } = toOptional(residencyInfo);
|
||||
|
||||
const name = getIndividualEntityName(fio);
|
||||
const url = getShopLocationURL(location);
|
||||
const { hasChiefAccountant, staffCount, accountingOrgInn, accounting } = getBusinessInfo(additionalInfo);
|
||||
const pdlRelationDegree = EMPTY; // TODO
|
||||
const hasBeneficialOwner = !isEmpty(beneficialOwners);
|
||||
|
||||
return {
|
||||
content: [
|
||||
createHeader('Приложение №'),
|
||||
createHeadline(
|
||||
'ОПРОСНЫЙ ЛИСТ – ИНДИВИДУАЛЬНОГО ПРЕДПРИНИМАТЕЛЯ ИЛИ ФИЗИЧЕСКОГО ЛИЦА, ЗАНИМАЮЩЕГОСЯ В УСТАНОВЛЕННОМ ЗАКОНОДАТЕЛЬСТВОМ РФ ПОРЯДКЕ ЧАСТНОЙ ПРАКТИКОЙ'
|
||||
),
|
||||
createVerticalParagraph('1. Основные сведения о Клиенте', [
|
||||
[`1.1. Наименование: ${name || EMPTY}`, `1.2. ИНН: ${inn || EMPTY}`],
|
||||
[`1.3. Фирменное наименование: ${brandName || EMPTY}`, `1.4. СНИЛС №: ${snils || EMPTY}`]
|
||||
]),
|
||||
createVerticalParagraph('2. Контактная информация', [
|
||||
[
|
||||
`2.1. Телефон: ${phoneNumber || EMPTY}`,
|
||||
`2.2. Сайт (Url): ${url || EMPTY}`,
|
||||
`2.3. Email: ${email || EMPTY}`
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'3. Сведения о целях установления и предполагаемом характере деловых отношений с НКО',
|
||||
[
|
||||
[
|
||||
`3.1. Цели установления отношений: ${nkoRelationTarget || 'подключение интернет-эквайринга'}`,
|
||||
`3.2. Характер отношений: ${relationshipWithNko || 'долгосрочный'}`
|
||||
]
|
||||
]
|
||||
),
|
||||
createVerticalParagraph('4. Планируемые операции по счету, в месяц', [
|
||||
[
|
||||
createVerticalCheckboxWithTitle(
|
||||
'4.1. Количество операций:',
|
||||
[
|
||||
[MonthOperationCount.LtTen, 'до 10'],
|
||||
[MonthOperationCount.BtwTenToFifty, '10 - 50'],
|
||||
[MonthOperationCount.GtFifty, 'свыше 50']
|
||||
],
|
||||
monthOperationCount
|
||||
),
|
||||
createVerticalCheckboxWithTitle(
|
||||
'4.2. Сумма операций:',
|
||||
[
|
||||
[MonthOperationSum.LtFiveHundredThousand, 'до 500 000'],
|
||||
[MonthOperationSum.BtwFiveHundredThousandToOneMillion, '500 000 - 1 000 000'],
|
||||
[MonthOperationSum.GtOneMillion, 'свыше 1 000 000']
|
||||
],
|
||||
monthOperationSum
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'5. Адрес фактического осуществления (ведения) деятельности',
|
||||
registrationPlace || EMPTY
|
||||
),
|
||||
createVerticalParagraph('6. Тип документа, подтверждающий право нахождения по фактическому адресу', [
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[DocumentType.LeaseContract, 'Договор аренды'],
|
||||
[DocumentType.SubleaseContract, 'Договор субаренды'],
|
||||
[DocumentType.CertificateOfOwnership, 'Свидетельство о праве собственности']
|
||||
],
|
||||
documentType
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('7. Сведения о хозяйственной деятельности', [
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'7.1. Наличие в штате главного бухгалтера',
|
||||
simpleYesNo,
|
||||
hasChiefAccountant
|
||||
),
|
||||
`7.2. Штатная численность в организации: ${staffCount || EMPTY}`
|
||||
],
|
||||
[
|
||||
{
|
||||
...createVerticalCheckboxWithTitle(
|
||||
'7.3. Бухгалтерский учет осуществляет:',
|
||||
[
|
||||
[AccountantInfoType.WithoutChiefHeadAccounting, 'ИП лично'],
|
||||
[
|
||||
AccountantInfoType.WithoutChiefAccountingOrganization,
|
||||
`Организация ведущая бух. учет: ИНН: ${accountingOrgInn || EMPTY}`
|
||||
],
|
||||
[
|
||||
AccountantInfoType.WithoutChiefIndividualAccountant,
|
||||
'Бухгалтер – индивидуальный специалист'
|
||||
]
|
||||
],
|
||||
accounting
|
||||
),
|
||||
colSpan: 2
|
||||
}
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('8. Принадлежность физического лица к некоторым категория граждан', [
|
||||
[
|
||||
{
|
||||
...createInlineCheckboxWithTitle(
|
||||
'8.1. Принадлежность к категории ПДЛ¹',
|
||||
simpleYesNo,
|
||||
toYesNo(foreignPublicPerson)
|
||||
),
|
||||
colSpan: 2
|
||||
}
|
||||
],
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'8.2. Является родственником ПДЛ',
|
||||
simpleYesNo,
|
||||
toYesNo(foreignRelativePerson)
|
||||
),
|
||||
`8.3. Степень родства: ${pdlRelationDegree || EMPTY}`
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('9. Наличие выгодоприобретателя²', [
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[YesNo.no, 'Нет'],
|
||||
[YesNo.yes, 'Да (обязательное заполнение анкеты Выгодоприобретателя по форме НКО)']
|
||||
],
|
||||
toYesNo(benefitThirdParties)
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('10. Наличие бенефициарного владельца³', [
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[YesNo.no, 'Нет'],
|
||||
[
|
||||
YesNo.yes,
|
||||
'Да (обязательное заполнение приложение для Бенефициарного владельца по форме НКО)'
|
||||
]
|
||||
],
|
||||
toYesNo(hasBeneficialOwner)
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'11. Имеются ли решения о ликвидации или о любой процедуре, применяемой в деле о банкротстве',
|
||||
[[createInlineCheckbox(simpleYesNo, toYesNo(!!relationIndividualEntity))]]
|
||||
),
|
||||
createVerticalParagraph('12. Являетесь ли Вы налоговым резидентом США или иного иностранного государства', [
|
||||
[createInlineCheckbox(simpleYesNo, toYesNo(usaTaxResident))]
|
||||
])
|
||||
],
|
||||
prefooter: createEnding(),
|
||||
footer: [
|
||||
'¹ Публичные должностные лица, включая российские, иностранные и международные.',
|
||||
'² Выгодоприобретатель - лицо, к выгоде которого действует клиент, в том числе на основании агентского договора, договоров поручения, комиссии и доверительного управления, при проведении операций с денежными средствами и иным имуществом.',
|
||||
'³ Бенефициарный владелец - физическое лицо, которое в конечном счете прямо или косвенно (через третьих лиц) владеет (имеет преобладающее участие более 25 процентов в капитале) клиентом - юридическим лицом либо имеет возможность контролировать действия клиента. Бенефициарным владельцем клиента - физического лица считается это лицо, за исключением случаев, если имеются основания полагать, что бенефициарным владельцем является иное физическое лицо.'
|
||||
].join('\n'),
|
||||
footerHeight: 3.6
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export function getIndividualEntityName(fio: string) {
|
||||
return `ИП ${fio}`;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './get-doc-def';
|
@ -0,0 +1,21 @@
|
||||
import { AuthorityConfirmingDocument } from '../../api-codegen/questionary';
|
||||
import { AuthorityConfirmingDocumentType } from '../../api/questionary/model';
|
||||
import { toOptional } from '../../../utils';
|
||||
import { getDate } from '../select-data';
|
||||
|
||||
const mapAuthorityConfirmingDocumentType: { [name in AuthorityConfirmingDocumentType]: string } = {
|
||||
solePartyDecision: 'Решение единственного участника',
|
||||
meetingOfShareholders: 'Протокол общего собрания участников',
|
||||
meetingOfParticipants: 'Протокол общего собрания акционеров'
|
||||
};
|
||||
|
||||
export function getAuthorityConfirmingDocument(authorityConfirmingDocument: AuthorityConfirmingDocument): string {
|
||||
const { type, number, date } = toOptional(authorityConfirmingDocument);
|
||||
if (type || number || date) {
|
||||
const printedType = mapAuthorityConfirmingDocumentType[type] || type;
|
||||
const printedNumber = number ? `№${number}` : null;
|
||||
const printedDate = date ? `от ${getDate(date)}` : null;
|
||||
return [printedType, printedNumber, printedDate].filter(i => i).join(' ');
|
||||
}
|
||||
return null;
|
||||
}
|
252
src/app/questionary-document/russian-legal-entity/get-doc-def.ts
Normal file
252
src/app/questionary-document/russian-legal-entity/get-doc-def.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import {
|
||||
createInlineCheckboxWithTitle,
|
||||
createVerticalParagraph,
|
||||
createHeader,
|
||||
createHeadline,
|
||||
createEnding
|
||||
} from '../create-content';
|
||||
import { createInlineCheckbox, createVerticalCheckboxWithTitle } from '../create-content';
|
||||
import { YesNo, getShopLocationURL, getContactInfo, getBusinessInfo, toYesNo, simpleYesNo } from '../select-data';
|
||||
import { DocDef } from '../create-questionary';
|
||||
import {
|
||||
AccountantInfo,
|
||||
MonthOperationCount,
|
||||
MonthOperationSum,
|
||||
PropertyInfoDocumentType
|
||||
} from '../../api-codegen/questionary';
|
||||
import { RussianLegalEntityQuestionary } from '../../api';
|
||||
import { getAuthorityConfirmingDocument } from './get-authority-confirming-document';
|
||||
import { toOptional } from '../../../utils';
|
||||
|
||||
const DocumentType = PropertyInfoDocumentType.DocumentTypeEnum;
|
||||
const AccountantInfoType = AccountantInfo.AccountantInfoTypeEnum;
|
||||
|
||||
const EMPTY = '';
|
||||
|
||||
export function getDocDef(questionary: RussianLegalEntityQuestionary): DocDef {
|
||||
const { data } = toOptional(questionary);
|
||||
const { contractor, shopInfo, contactInfo } = toOptional(data);
|
||||
const { location, details } = toOptional(shopInfo);
|
||||
const { name } = toOptional(details);
|
||||
const { phoneNumber, email } = toOptional(contactInfo);
|
||||
const { legalEntity } = toOptional(contractor);
|
||||
const {
|
||||
additionalInfo,
|
||||
inn,
|
||||
name: brandName,
|
||||
legalOwnerInfo,
|
||||
beneficialOwner,
|
||||
propertyInfoDocumentType,
|
||||
registrationInfo,
|
||||
residencyInfo
|
||||
} = toOptional(legalEntity);
|
||||
const { registrationPlace } = toOptional(registrationInfo);
|
||||
const { taxResident, fatca, ownerResident } = toOptional(residencyInfo);
|
||||
const {
|
||||
relationIndividualEntity,
|
||||
benefitThirdParties,
|
||||
nkoRelationTarget,
|
||||
relationshipWithNko,
|
||||
monthOperationSum,
|
||||
monthOperationCount
|
||||
} = toOptional(additionalInfo);
|
||||
const { pdlRelationDegree, pdlCategory, snils, authorityConfirmingDocument, russianPrivateEntity } = toOptional(
|
||||
legalOwnerInfo
|
||||
);
|
||||
const { fio, contactInfo: privateEntityContactInfo } = toOptional(russianPrivateEntity);
|
||||
const { documentType } = toOptional(propertyInfoDocumentType);
|
||||
|
||||
const url = getShopLocationURL(location);
|
||||
const contact = getContactInfo(privateEntityContactInfo);
|
||||
const authorityConfirmingDocumentInfo = getAuthorityConfirmingDocument(authorityConfirmingDocument);
|
||||
const { hasChiefAccountant, staffCount, accounting, accountingOrgInn } = getBusinessInfo(additionalInfo);
|
||||
const hasBeneficialOwner = !isEmpty(beneficialOwner);
|
||||
|
||||
return {
|
||||
content: [
|
||||
createHeader('Приложение №'),
|
||||
createHeadline('ОПРОСНЫЙ ЛИСТ – ЮРИДИЧЕСКОГО ЛИЦА (НЕ ЯВЛЯЮЩЕГОСЯ КРЕДИТНОЙ ОРГАНИЗАЦИЕЙ)'),
|
||||
createVerticalParagraph('1. Основные сведения о Клиенте', [
|
||||
[`1.1. Наименование: ${name || EMPTY}`, `1.2. ИНН: ${inn || EMPTY}`],
|
||||
[{ text: `1.3. Фирменное наименование: ${brandName || EMPTY}`, colSpan: 2 }]
|
||||
]),
|
||||
createVerticalParagraph('2. Контактная информация', [
|
||||
[
|
||||
`2.1. Телефон: ${phoneNumber || EMPTY}`,
|
||||
`2.2. Сайт (Url): ${url || EMPTY}`,
|
||||
`2.3. Email: ${email || EMPTY}`
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'3. Сведения о целях установления и предполагаемом характере деловых отношений с НКО',
|
||||
[
|
||||
[
|
||||
`3.1. Цели установления отношений: ${nkoRelationTarget || 'подключение интернет-эквайринга'}`,
|
||||
`3.2. Характер отношений: ${relationshipWithNko || 'долгосрочный'}`
|
||||
]
|
||||
]
|
||||
),
|
||||
createVerticalParagraph('4. Планируемые операции, в месяц', [
|
||||
[
|
||||
createVerticalCheckboxWithTitle(
|
||||
'4.1. Количество операций:',
|
||||
[
|
||||
[MonthOperationCount.LtTen, 'до 10'],
|
||||
[MonthOperationCount.BtwTenToFifty, '10 - 50'],
|
||||
[MonthOperationCount.GtFifty, 'свыше 50']
|
||||
],
|
||||
monthOperationCount
|
||||
),
|
||||
createVerticalCheckboxWithTitle(
|
||||
'4.2. Сумма операций:',
|
||||
[
|
||||
[MonthOperationSum.LtFiveHundredThousand, 'до 500 000'],
|
||||
[MonthOperationSum.BtwFiveHundredThousandToOneMillion, '500 000 - 1 000 000'],
|
||||
[MonthOperationSum.GtOneMillion, 'свыше 1 000 000']
|
||||
],
|
||||
monthOperationSum
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('5. Сведения о единоличном исполнительном органе юридического лица', [
|
||||
[{ text: `5.1. ФИО Единоличного исполнительного органа: ${fio || EMPTY}`, colSpan: 2 }],
|
||||
[
|
||||
{
|
||||
text: `5.2. Действует на основании: ${authorityConfirmingDocumentInfo || EMPTY}`,
|
||||
colSpan: 2
|
||||
}
|
||||
],
|
||||
[`5.3. СНИЛС №: ${snils || EMPTY}`, `5.4. Контактная информация (телефон, email): ${contact || EMPTY}`]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'6. Данные о фактическом местонахождении органа управления (Руководителя)',
|
||||
registrationPlace || EMPTY
|
||||
),
|
||||
createVerticalParagraph(
|
||||
'7. Тип документа, подтверждающий право нахождения по фактическому адресу органа управления (Руководителя)',
|
||||
[
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[DocumentType.LeaseContract, 'Договор аренды'],
|
||||
[DocumentType.SubleaseContract, 'Договор субаренды'],
|
||||
[DocumentType.CertificateOfOwnership, 'Свидетельство о праве собственности']
|
||||
],
|
||||
documentType
|
||||
)
|
||||
]
|
||||
]
|
||||
),
|
||||
createVerticalParagraph('8. Сведения о хозяйственной деятельности организации', [
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'8.1. Наличие в штате главного бухгалтера',
|
||||
simpleYesNo,
|
||||
hasChiefAccountant
|
||||
),
|
||||
`8.2. Штатная численность в организации: ${staffCount || EMPTY}`
|
||||
],
|
||||
[
|
||||
createVerticalCheckboxWithTitle(
|
||||
'8.3. Бухгалтерский учет осуществляет:',
|
||||
[
|
||||
[AccountantInfoType.WithoutChiefHeadAccounting, 'Руководитель организации'],
|
||||
[
|
||||
AccountantInfoType.WithoutChiefAccountingOrganization,
|
||||
`Организация ведущая бух. учет: ИНН: ${accountingOrgInn || EMPTY}`
|
||||
],
|
||||
[
|
||||
AccountantInfoType.WithoutChiefIndividualAccountant,
|
||||
'Бухгалтер – индивидуальный специалист'
|
||||
]
|
||||
],
|
||||
accounting
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'9. Принадлежность Единоличного исполнительного органа к некоторым категориям граждан',
|
||||
[
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'9.1. Принадлежность к категории ПДЛ¹',
|
||||
simpleYesNo,
|
||||
toYesNo(pdlCategory)
|
||||
)
|
||||
],
|
||||
[
|
||||
createInlineCheckboxWithTitle(
|
||||
'9.2. Является родственником ПДЛ',
|
||||
simpleYesNo,
|
||||
toYesNo(!!pdlRelationDegree)
|
||||
),
|
||||
`9.3. Степень родства: ${pdlRelationDegree || EMPTY}`
|
||||
]
|
||||
]
|
||||
),
|
||||
createVerticalParagraph('10. Наличие выгодоприобретателя²', [
|
||||
[
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[YesNo.no, 'Нет'],
|
||||
[YesNo.yes, 'Да (обязательное заполнение анкеты Выгодоприобретателя по форме НКО)']
|
||||
],
|
||||
toYesNo(benefitThirdParties)
|
||||
)
|
||||
]
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph('11. Наличие бенефициарного владельца³', [
|
||||
[
|
||||
createInlineCheckbox(
|
||||
[
|
||||
[YesNo.no, 'Нет'],
|
||||
[YesNo.yes, 'Да (обязательное заполнение приложение для Бенефициарного владельца)']
|
||||
],
|
||||
toYesNo(hasBeneficialOwner)
|
||||
)
|
||||
]
|
||||
]),
|
||||
createVerticalParagraph(
|
||||
'12. Имеются ли решения о ликвидации или о любой процедуре, применяемой в деле о банкротстве, в отношении Вашей компании',
|
||||
[[createInlineCheckbox(simpleYesNo, toYesNo(!!relationIndividualEntity))]]
|
||||
),
|
||||
createVerticalParagraph('13. Информация об иностранном налоговом резидентстве', [
|
||||
[
|
||||
{
|
||||
text:
|
||||
'13.1. Является ли Ваша организация налоговым резидентом США или иного иностранного государства?',
|
||||
colSpan: 5
|
||||
},
|
||||
createInlineCheckbox(simpleYesNo, toYesNo(taxResident))
|
||||
],
|
||||
[
|
||||
{
|
||||
text:
|
||||
'13.2. Является ли Бенефициарный владелец Вашей организации с долей владения 10% и более налоговым резидентом иностранного государства?',
|
||||
colSpan: 5
|
||||
},
|
||||
createInlineCheckbox(simpleYesNo, toYesNo(ownerResident))
|
||||
],
|
||||
[
|
||||
{
|
||||
text:
|
||||
'13.3. Является ли Ваша организация Финансовым Институтом в соответствии с FATCA и 173-ФЗ от 28.06.2014?',
|
||||
colSpan: 5
|
||||
},
|
||||
createInlineCheckbox(simpleYesNo, toYesNo(fatca))
|
||||
]
|
||||
])
|
||||
],
|
||||
prefooter: createEnding(),
|
||||
footer: [
|
||||
'¹ Публичные должностные лица, включая российские, иностранные и международные.',
|
||||
'² Выгодоприобретатель - лицо, к выгоде которого действует клиент, в том числе на основании агентского договора, договоров поручения, комиссии и доверительного управления, при проведении операций с денежными средствами и иным имуществом.',
|
||||
'³ Бенефициарный владелец - физическое лицо, которое в конечном счете прямо или косвенно (через третьих лиц) владеет (имеет преобладающее участие более 25 процентов в капитале) клиентом - юридическим лицом либо имеет возможность контролировать действия клиента. Бенефициарным владельцем клиента - физического лица считается это лицо, за исключением случаев, если имеются основания полагать, что бенефициарным владельцем является иное физическое лицо.'
|
||||
].join('\n'),
|
||||
footerHeight: 3.2
|
||||
};
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './get-doc-def';
|
@ -0,0 +1,30 @@
|
||||
import { hasChiefAccountant } from './has-chief-accountant';
|
||||
import { AdditionalInfo, AccountantInfo, WithoutChiefAccountingOrganization } from '../../api-codegen/questionary';
|
||||
import { YesNo } from './yes-no';
|
||||
import { toOptional } from '../../../utils';
|
||||
|
||||
export interface BusinessInfo {
|
||||
hasChiefAccountant: YesNo;
|
||||
staffCount: number;
|
||||
accounting: AccountantInfo.AccountantInfoTypeEnum;
|
||||
accountingOrgInn?: string;
|
||||
}
|
||||
|
||||
function isWithoutChiefAccountingOrganization(
|
||||
accountantInfo: AccountantInfo
|
||||
): accountantInfo is WithoutChiefAccountingOrganization {
|
||||
return (
|
||||
accountantInfo &&
|
||||
accountantInfo.accountantInfoType === AccountantInfo.AccountantInfoTypeEnum.WithoutChiefAccountingOrganization
|
||||
);
|
||||
}
|
||||
|
||||
export function getBusinessInfo(additionalInfo: AdditionalInfo): BusinessInfo {
|
||||
const { accountantInfo, staffCount } = toOptional(additionalInfo);
|
||||
return {
|
||||
hasChiefAccountant: hasChiefAccountant(accountantInfo),
|
||||
staffCount,
|
||||
accounting: toOptional(accountantInfo).accountantInfoType,
|
||||
accountingOrgInn: isWithoutChiefAccountingOrganization(accountantInfo) ? toOptional(accountantInfo).inn : null
|
||||
};
|
||||
}
|
20
src/app/questionary-document/select-data/get-company-info.ts
Normal file
20
src/app/questionary-document/select-data/get-company-info.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { Questionary } from '../../api-codegen/questionary';
|
||||
import { isRussianLegalEntityQuestionary, isRussianIndividualEntityQuestionary } from '../../api';
|
||||
|
||||
export function getCompanyInfo(questionary: Questionary): { companyName: string; companyInn: string } {
|
||||
if (isRussianIndividualEntityQuestionary(questionary)) {
|
||||
return {
|
||||
companyName: get(questionary.data, ['shopInfo', 'details', 'name'], ''),
|
||||
companyInn: questionary.data.contractor.individualEntity.inn
|
||||
};
|
||||
} else if (isRussianLegalEntityQuestionary(questionary)) {
|
||||
return {
|
||||
companyName: get(questionary.data, ['shopInfo', 'details', 'name'], ''),
|
||||
companyInn: questionary.data.contractor.legalEntity.inn
|
||||
};
|
||||
}
|
||||
console.error('Unknown questionary');
|
||||
return null;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { ContactInfo } from '../../api-codegen/questionary';
|
||||
import { toOptional } from '../../../utils';
|
||||
|
||||
export function getContactInfo(contactInfo: ContactInfo): string {
|
||||
const { phoneNumber, email } = toOptional(contactInfo);
|
||||
return [phoneNumber, email].filter(i => i).join(', ');
|
||||
}
|
9
src/app/questionary-document/select-data/get-date.ts
Normal file
9
src/app/questionary-document/select-data/get-date.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export function getDate(date: string): string {
|
||||
return date
|
||||
? moment(date)
|
||||
.utc()
|
||||
.format('L')
|
||||
: null;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { RussianDomesticPassport, IdentityDocument } from '../../api-codegen/questionary';
|
||||
|
||||
function isRussianDomesticPassport(identityDocument: IdentityDocument): identityDocument is RussianDomesticPassport {
|
||||
return identityDocument && identityDocument.identityDocumentType === 'RussianDomesticPassport';
|
||||
}
|
||||
|
||||
export function getIdentityDocument(
|
||||
identityDocument: IdentityDocument
|
||||
): { name?: string; seriesNumber?: string; issuer?: string; issuedAt?: string } | null {
|
||||
if (isRussianDomesticPassport(identityDocument)) {
|
||||
const { seriesNumber, issuer, issuedAt, issuerCode } = identityDocument;
|
||||
return {
|
||||
name: 'Паспорт РФ',
|
||||
seriesNumber,
|
||||
issuer: [issuer, issuerCode].filter(i => !!i).join(', '),
|
||||
issuedAt
|
||||
};
|
||||
}
|
||||
console.error('Unknown identity document');
|
||||
return {};
|
||||
}
|
5
src/app/questionary-document/select-data/get-percent.ts
Normal file
5
src/app/questionary-document/select-data/get-percent.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
export function getPercent(value: string | number): string {
|
||||
return isEmpty(value) ? null : `${value}%`;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { ShopLocation, ShopLocationUrl } from '../../api-codegen/questionary';
|
||||
|
||||
function isShopLocationUrl(shopLocation: ShopLocation): shopLocation is ShopLocationUrl {
|
||||
return shopLocation && shopLocation.locationType === ShopLocation.LocationTypeEnum.ShopLocationUrl;
|
||||
}
|
||||
|
||||
export function getShopLocationURL(shopLocation: ShopLocation): string {
|
||||
if (isShopLocationUrl(shopLocation)) {
|
||||
return shopLocation.url;
|
||||
}
|
||||
return null;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { AccountantInfo } from '../../api-codegen/questionary';
|
||||
import { YesNo, toYesNo } from './yes-no';
|
||||
|
||||
export function hasChiefAccountant(accountantInfo: AccountantInfo): YesNo {
|
||||
return toYesNo(
|
||||
accountantInfo &&
|
||||
accountantInfo.accountantInfoType === AccountantInfo.AccountantInfoTypeEnum.WithChiefAccountant
|
||||
);
|
||||
}
|
10
src/app/questionary-document/select-data/index.ts
Normal file
10
src/app/questionary-document/select-data/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export * from './yes-no';
|
||||
export * from './has-chief-accountant';
|
||||
export * from './get-shop-location-url';
|
||||
export * from './get-contact-info';
|
||||
export * from './get-business-info';
|
||||
export * from './get-identity-document';
|
||||
export * from './get-company-info';
|
||||
export * from './get-date';
|
||||
export * from './get-percent';
|
||||
export * from './simple-yes-no';
|
@ -0,0 +1,3 @@
|
||||
import { YesNo } from './yes-no';
|
||||
|
||||
export const simpleYesNo: [YesNo, string][] = [[YesNo.yes, 'Да'], [YesNo.no, 'Нет']];
|
19
src/app/questionary-document/select-data/yes-no.spec.ts
Normal file
19
src/app/questionary-document/select-data/yes-no.spec.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { toYesNo, YesNo } from './yes-no';
|
||||
|
||||
describe('toYesNo', () => {
|
||||
it('null should return -1', () => {
|
||||
expect(toYesNo(null)).toBe(-1);
|
||||
});
|
||||
|
||||
it('undefined should return -1', () => {
|
||||
expect(toYesNo(undefined)).toBe(-1);
|
||||
});
|
||||
|
||||
it('true should return yes', () => {
|
||||
expect(toYesNo(true)).toBe(YesNo.yes);
|
||||
});
|
||||
|
||||
it('false should return no', () => {
|
||||
expect(toYesNo(false)).toBe(YesNo.no);
|
||||
});
|
||||
});
|
11
src/app/questionary-document/select-data/yes-no.ts
Normal file
11
src/app/questionary-document/select-data/yes-no.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export enum YesNo {
|
||||
yes,
|
||||
no
|
||||
}
|
||||
|
||||
export function toYesNo(value: boolean): YesNo {
|
||||
if (value === null || value === undefined) {
|
||||
return -1;
|
||||
}
|
||||
return value ? YesNo.yes : YesNo.no;
|
||||
}
|
@ -15,5 +15,20 @@
|
||||
*ngIf="isNotEmptyBankAccount$ | async"
|
||||
[bankAccount]="bankAccount$ | async"
|
||||
></dsh-bank-account-info>
|
||||
<div
|
||||
fxLayout="row wrap"
|
||||
fxLayoutGap="10px"
|
||||
style="float: left"
|
||||
*transloco="let c; scope: 'claim-modification-containers'; read: 'claimModificationContainers.download'"
|
||||
>
|
||||
<button dsh-button (click)="downloadDocument()">{{ c.questionaryDocument }}</button>
|
||||
<button
|
||||
dsh-button
|
||||
*ngFor="let beneficialOwnerDocument of beneficialOwnersDocuments$ | async; index as i"
|
||||
(click)="downloadBeneficialOwnerDocument(beneficialOwnerDocument)"
|
||||
>
|
||||
{{ c.beneficialOwnerDocument | translocoParams: { id: i + 1 } }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { pluck, map } from 'rxjs/operators';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import negate from 'lodash.negate';
|
||||
import { TCreatedPdf } from 'pdfmake/build/pdfmake';
|
||||
|
||||
import { DocumentModificationUnit } from '../../../../api-codegen/claim-management';
|
||||
import { DocumentModificationInfoService } from './document-modification-info.service';
|
||||
@ -14,7 +15,7 @@ import { DocumentModificationInfoService } from './document-modification-info.se
|
||||
export class DocumentModificationInfoComponent implements OnChanges {
|
||||
@Input() unit: DocumentModificationUnit;
|
||||
|
||||
questionary$ = this.documentModificationInfoService.questionary$;
|
||||
questionary$ = this.documentModificationInfoService.questionaryData$;
|
||||
isLoading$ = this.documentModificationInfoService.isLoading$;
|
||||
error$ = this.documentModificationInfoService.error$;
|
||||
contractor$ = this.questionary$.pipe(pluck('contractor'));
|
||||
@ -26,6 +27,8 @@ export class DocumentModificationInfoComponent implements OnChanges {
|
||||
bankAccount$ = this.questionary$.pipe(pluck('bankAccount'));
|
||||
isNotEmptyBankAccount$ = this.bankAccount$.pipe(map(negate(isEmpty)));
|
||||
|
||||
beneficialOwnersDocuments$ = this.documentModificationInfoService.beneficialOwnersDocuments$;
|
||||
|
||||
constructor(private documentModificationInfoService: DocumentModificationInfoService) {}
|
||||
|
||||
ngOnChanges({ unit }: SimpleChanges): void {
|
||||
@ -33,4 +36,12 @@ export class DocumentModificationInfoComponent implements OnChanges {
|
||||
this.documentModificationInfoService.receiveQuestionary(unit.currentValue.documentId);
|
||||
}
|
||||
}
|
||||
|
||||
downloadDocument() {
|
||||
this.documentModificationInfoService.document$.subscribe(doc => doc.download('russian-entity-questionary'));
|
||||
}
|
||||
|
||||
downloadBeneficialOwnerDocument(beneficialOwnerDocument: TCreatedPdf) {
|
||||
beneficialOwnerDocument.download('beneficial-owner-questionary');
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,20 @@ import {
|
||||
AuthorityConfirmingDocumentComponent
|
||||
} from './contractor-info';
|
||||
import { QuestionaryModule } from '../../../../api';
|
||||
import { QuestionaryDocumentModule } from '../../../../questionary-document';
|
||||
import { ButtonModule } from '../../../../button';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, FlexLayoutModule, TranslocoModule, QuestionaryModule, DetailsItemModule, MatDividerModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FlexLayoutModule,
|
||||
TranslocoModule,
|
||||
QuestionaryModule,
|
||||
DetailsItemModule,
|
||||
MatDividerModule,
|
||||
QuestionaryDocumentModule,
|
||||
ButtonModule
|
||||
],
|
||||
declarations: [
|
||||
DocumentModificationInfoComponent,
|
||||
ShopInfoComponent,
|
||||
|
@ -3,20 +3,33 @@ import { Observable, Subject } from 'rxjs';
|
||||
import { switchMap, pluck, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { QuestionaryService } from '../../../../api';
|
||||
import { QuestionaryData } from '../../../../api-codegen/questionary';
|
||||
import { QuestionaryData, Questionary } from '../../../../api-codegen/questionary';
|
||||
import { booleanDelay, takeError } from '../../../../custom-operators';
|
||||
import { QuestionaryDocumentService } from '../../../../questionary-document';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentModificationInfoService {
|
||||
private receiveQuestionary$: Subject<string> = new Subject();
|
||||
|
||||
questionary$: Observable<QuestionaryData> = this.receiveQuestionary$.pipe(
|
||||
questionary$: Observable<Questionary> = this.receiveQuestionary$.pipe(
|
||||
switchMap(documentId => this.questionaryService.getQuestionary(documentId)),
|
||||
pluck('questionary', 'data'),
|
||||
pluck('questionary'),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
isLoading$ = this.questionary$.pipe(
|
||||
questionaryData$: Observable<QuestionaryData> = this.questionary$.pipe(pluck('data'));
|
||||
|
||||
document$ = this.questionary$.pipe(
|
||||
switchMap(questionary => this.questionaryDocumentService.createDoc(questionary)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
beneficialOwnersDocuments$ = this.questionary$.pipe(
|
||||
switchMap(questionary => this.questionaryDocumentService.createBeneficialOwnerDocs(questionary)),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
isLoading$ = this.questionaryData$.pipe(
|
||||
booleanDelay(),
|
||||
shareReplay(1)
|
||||
);
|
||||
@ -26,8 +39,11 @@ export class DocumentModificationInfoService {
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
constructor(private questionaryService: QuestionaryService) {
|
||||
this.questionary$.subscribe();
|
||||
constructor(
|
||||
private questionaryService: QuestionaryService,
|
||||
private questionaryDocumentService: QuestionaryDocumentService
|
||||
) {
|
||||
this.questionaryData$.subscribe();
|
||||
}
|
||||
|
||||
receiveQuestionary(documentId: string) {
|
||||
|
@ -10,7 +10,7 @@ import { ButtonModule } from '../../button';
|
||||
import { DshTabsModule } from '../../layout/tabs';
|
||||
import { ClaimRoutingModule } from './claim-routing.module';
|
||||
import { ClaimComponent } from './claim.component';
|
||||
import { ClaimsModule } from '../../api/claims';
|
||||
import { ClaimsModule, QuestionaryModule } from '../../api';
|
||||
import { ConversationModule } from './conversation';
|
||||
import { StatusModule } from '../../status';
|
||||
import { SpinnerModule } from '../../spinner';
|
||||
@ -32,6 +32,7 @@ import { ConfirmActionDialogModule } from '../../confirm-action-dialog';
|
||||
DocumentsModule,
|
||||
TranslocoModule,
|
||||
SpinnerModule,
|
||||
QuestionaryModule,
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
|
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-BlackItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-BlackItalic.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-BoldItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-LightItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-LightItalic.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-MediumItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-MediumItalic.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-RegularItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-RegularItalic.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Executable file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-ThinItalic.ttf
Executable file
BIN
src/assets/fonts/Roboto/Roboto-ThinItalic.ttf
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user