fix(reports): разрешить 10-значный рег.номер СФР наравне с XXX-XXX-XXXXXX
XSD-схема ЕФС-1 (ТипРегНомерОбщ) допускает регистрационный номер страхователя в двух форматах: действующий XXX-XXX-XXXXXX (12 цифр, 3-3-6) или прежний — 10 цифр без разделителей. Валидация на бэке и фронте (форма реквизитов кооператива + редактор нулевого отчёта ЕФС-1) принимала только 12-значный формат с тире, из-за чего кооперативы со старым 10-значным номером ФСС не могли его сохранить. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsOptional, IsString, IsIn, Matches, MaxLength } from 'class-validator';
|
||||
import { ReportType } from '../../domain/enums/report-type.enum';
|
||||
import { SFR_REG_NUMBER_PATTERN } from '../../domain/patterns';
|
||||
|
||||
export enum RequisiteSource {
|
||||
DATABASE = 'database',
|
||||
@@ -65,7 +66,7 @@ export class ReportRequisitesViewDTO {
|
||||
* ОКТМО — `\d{8}` или `\d{11}`
|
||||
* ОКПО — `\d{8}` или `\d{10}`
|
||||
* СНИЛС — `\d{3}-\d{3}-\d{3} \d{2}` (14 симв.) или 11 цифр подряд
|
||||
* РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире)
|
||||
* РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире) или 10 цифр подряд
|
||||
*/
|
||||
@InputType('UpdateReportRequisitesInput')
|
||||
export class UpdateReportRequisitesInputDTO {
|
||||
@@ -99,10 +100,10 @@ export class UpdateReportRequisitesInputDTO {
|
||||
@Matches(/^\d{8}(\d{2})?$/, { message: 'ОКПО — 8 или 10 цифр' })
|
||||
okpo?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX' })
|
||||
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{3}-\d{3}-\d{6}$/, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX (14 симв.)' })
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
sfrRegNumber?: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ export class ZeroReportSignerEditsInputDTO {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX' })
|
||||
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
|
||||
sfrRegNumber!: string | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
@@ -43,8 +43,9 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
|
||||
// СНИЛС: XXX-XXX-XXX YY (с пробелом/тире) или 11 цифр подряд.
|
||||
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
|
||||
|
||||
// Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6).
|
||||
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
|
||||
// Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр)
|
||||
// или прежний — 10 цифр без разделителей (ТипРегНомерОбщ в XSD ЕФС-1).
|
||||
export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/;
|
||||
|
||||
/** Дата в формате `DD.MM.YYYY` (XSD `xs:date`-совместимый ФНС-формат). */
|
||||
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
|
||||
|
||||
@@ -36,9 +36,10 @@ const props = defineProps<{
|
||||
mask?: string
|
||||
/** Подставлять плейсхолдеры маски при вводе */
|
||||
fillMask?: boolean
|
||||
/** Разрешить только цифры и точку (ОКВЭД: 94.99, 46.73.7). Блокирует
|
||||
* keypress/paste — буквы вообще не появляются в поле. */
|
||||
digitsDotsOnly?: boolean
|
||||
/** Разрешить только цифры + перечисленные доп. символы (напр. '.' для
|
||||
* ОКВЭД: 94.99, 46.73.7; '-' для Рег. номера СФР). Блокирует
|
||||
* keypress/paste — посторонние символы вообще не появляются в поле. */
|
||||
digitsExtraChars?: string
|
||||
/** HTML maxlength. */
|
||||
maxLength?: number
|
||||
/** Допустимые точные длины (ИЛИ). Напр. `[8, 11]` для ОКТМО. */
|
||||
@@ -60,12 +61,20 @@ const labelText = computed(() =>
|
||||
)
|
||||
|
||||
const inputModeComputed = computed<'numeric' | 'decimal' | 'text'>(() => {
|
||||
if (props.digitsDotsOnly) return 'decimal'
|
||||
// '.' — десятичная клавиатура мобильных устройств её показывает; для
|
||||
// прочих доп.символов (напр. '-') нужна полная клавиатура — 'text'.
|
||||
if (props.digitsExtraChars === '.') return 'decimal'
|
||||
if (props.digitsExtraChars) return 'text'
|
||||
// mask из одних `#` = чистая цифровая клавиатура на мобиле.
|
||||
if (props.mask && /^#+$/.test(props.mask)) return 'numeric'
|
||||
return 'text'
|
||||
})
|
||||
|
||||
/** Экранирует спецсимволы regex, чтобы вставить строку в символьный класс `[...]`. */
|
||||
function escapeForCharClass(chars: string): string {
|
||||
return chars.replace(/[\]\\^-]/g, '\\$&')
|
||||
}
|
||||
|
||||
const NAV_KEYS = new Set([
|
||||
'Backspace',
|
||||
'Delete',
|
||||
@@ -81,19 +90,21 @@ const NAV_KEYS = new Set([
|
||||
])
|
||||
|
||||
// Блокируем keypress не-допустимых символов — буквы не попадают в поле даже
|
||||
// на миллисекунды. Работает только для digitsDotsOnly (для чисто цифровых
|
||||
// на миллисекунды. Работает только для digitsExtraChars (для чисто цифровых
|
||||
// полей используем Quasar-mask — он нативно блокирует не-# символы).
|
||||
function onKeydown(e: KeyboardEvent): void {
|
||||
if (!props.digitsDotsOnly) return
|
||||
if (!props.digitsExtraChars) return
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return
|
||||
if (NAV_KEYS.has(e.key)) return
|
||||
if (!/^[\d.]$/.test(e.key)) e.preventDefault()
|
||||
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]$`)
|
||||
if (!re.test(e.key)) e.preventDefault()
|
||||
}
|
||||
|
||||
function onPaste(e: ClipboardEvent): void {
|
||||
if (!props.digitsDotsOnly) return
|
||||
if (!props.digitsExtraChars) return
|
||||
const text = e.clipboardData?.getData('text') ?? ''
|
||||
if (!/^[\d.]*$/.test(text)) e.preventDefault()
|
||||
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]*$`)
|
||||
if (!re.test(text)) e.preventDefault()
|
||||
}
|
||||
|
||||
const rules = computed(() => {
|
||||
@@ -128,7 +139,10 @@ function onInput(raw: string | number | null): void {
|
||||
// Фоллбек-фильтр на случай programmatic ввода (drop/autofill). Для ручного
|
||||
// набора актуальна либо mask (q-input сам режет), либо keydown-блокер.
|
||||
let s = String(raw ?? '')
|
||||
if (props.digitsDotsOnly) s = s.replace(/[^\d.]+/g, '')
|
||||
if (props.digitsExtraChars) {
|
||||
const re = new RegExp(`[^\\d${escapeForCharClass(props.digitsExtraChars)}]+`, 'g')
|
||||
s = s.replace(re, '')
|
||||
}
|
||||
if (props.maxLength && s.length > props.maxLength) s = s.slice(0, props.maxLength)
|
||||
emit('update:value', s)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
|
||||
:value='manualInput[f.key]'
|
||||
:placeholder='f.placeholder'
|
||||
:mask='f.mask'
|
||||
:digits-dots-only='f.digitsDotsOnly'
|
||||
:digits-extra-chars='f.digitsExtraChars'
|
||||
:max-length='f.maxLength'
|
||||
:exact-lengths='f.exactLengths'
|
||||
:pattern='f.pattern'
|
||||
@@ -60,10 +60,11 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
|
||||
id='field-sfrRegNumber'
|
||||
label='Рег. номер СФР'
|
||||
:value='manualInput.sfrRegNumber'
|
||||
placeholder='XXX-XXX-XXXXXX'
|
||||
mask='###-###-######'
|
||||
placeholder='XXX-XXX-XXXXXX или 10 цифр'
|
||||
digits-extra-chars='-'
|
||||
:max-length='14'
|
||||
:pattern='SFR_REG_PATTERN'
|
||||
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр)'
|
||||
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр) или 10 цифр без разделителей'
|
||||
required
|
||||
@update:value='v => (manualInput.sfrRegNumber = v)'
|
||||
)
|
||||
@@ -202,7 +203,7 @@ interface ClassifierField {
|
||||
label: string
|
||||
placeholder?: string
|
||||
mask?: string
|
||||
digitsDotsOnly?: boolean
|
||||
digitsExtraChars?: string
|
||||
maxLength?: number
|
||||
exactLengths?: number[]
|
||||
pattern?: RegExp
|
||||
@@ -212,13 +213,13 @@ interface ClassifierField {
|
||||
// Placeholder'ы — правило ввода (кол-во цифр), а не конкретные примеры.
|
||||
// Для цифровых полей фиксированной/короткой длины — используем Quasar-mask
|
||||
// (`#` = только цифра, буквы блокируются на keypress нативно).
|
||||
// Для ОКВЭД (цифры + точка переменной длины) — digitsDotsOnly с @keydown-блокером.
|
||||
// Для ОКВЭД (цифры + точка переменной длины) — digitsExtraChars='.' с @keydown-блокером.
|
||||
const classifierFields: ClassifierField[] = [
|
||||
{
|
||||
key: 'okved',
|
||||
label: 'ОКВЭД',
|
||||
placeholder: 'XX.XX или XX.XX.XX',
|
||||
digitsDotsOnly: true,
|
||||
digitsExtraChars: '.',
|
||||
maxLength: 8,
|
||||
pattern: /^\d{2}(\.\d{1,2}){0,2}$/,
|
||||
patternMessage: 'Формат: XX.XX или XX.XX.XX',
|
||||
@@ -265,7 +266,7 @@ const signerTypeOptions = [
|
||||
// пользователь прожал пару цифр и нажал «Сохранить»: mask не мешает
|
||||
// сохранить частичный ввод, паттерн-правило блокирует.
|
||||
const SNILS_PATTERN = /^\d{3}-\d{3}-\d{3} \d{2}$/
|
||||
const SFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/
|
||||
const SFR_REG_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/
|
||||
|
||||
function getValue(key: keyof IReportRequisitesView): string {
|
||||
const v = requisites.value?.[key] as any
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
:rules='[reportRules.sfrRegNumber()]'
|
||||
:error='errFor("signer.sfrRegNumber")'
|
||||
:error-message='msgFor("signer.sfrRegNumber")'
|
||||
mask='###-###-######'
|
||||
hint='XXX-XXX-XXXXXX — обязателен для ЕФС-1'
|
||||
maxlength='14'
|
||||
hint='XXX-XXX-XXXXXX или 10 цифр — обязателен для ЕФС-1'
|
||||
dense filled
|
||||
)
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
|
||||
/** СНИЛС: XXX-XXX-XXX YY или 11 цифр подряд. */
|
||||
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
|
||||
|
||||
/** Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6). */
|
||||
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
|
||||
/** Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр) или прежний — 10 цифр без разделителей. */
|
||||
export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/;
|
||||
|
||||
/** Дата в формате DD.MM.YYYY (ФНС). */
|
||||
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
|
||||
@@ -70,7 +70,7 @@ export const reportRules = {
|
||||
okopf: (message = 'ОКОПФ — 5 цифр') => (v: unknown) => OKOPF_PATTERN.test(String(v ?? '')) || message,
|
||||
okpo: (message = 'ОКПО — 8 или 10 цифр') => (v: unknown) => OKPO_PATTERN.test(String(v ?? '')) || message,
|
||||
snils: (message = 'СНИЛС — XXX-XXX-XXX YY или 11 цифр') => (v: unknown) => SNILS_PATTERN.test(String(v ?? '')) || message,
|
||||
sfrRegNumber: (message = 'Рег. номер СФР — XXX-XXX-XXXXXX') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message,
|
||||
sfrRegNumber: (message = 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message,
|
||||
dateDdMmYyyy: (message = 'Дата — DD.MM.YYYY') => (v: unknown) => DATE_DDMMYYYY_PATTERN.test(String(v ?? '')) || message,
|
||||
kbk: (message = 'КБК — 20 цифр') => (v: unknown) => KBK_PATTERN.test(String(v ?? '')) || message,
|
||||
/** Непустая строка длиной от `min` до `max` символов. */
|
||||
|
||||
Reference in New Issue
Block a user