@77sol-ui/form-schemas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/package.json +61 -0
- package/src/domains/financing/enums/banks.ts +22 -0
- package/src/domains/financing/enums/index.ts +2 -0
- package/src/domains/financing/enums/occupation.ts +2 -0
- package/src/domains/financing/formalization/extras.ts +21 -0
- package/src/domains/financing/formalization/index.ts +16 -0
- package/src/domains/financing/formalization/refinements.ts +70 -0
- package/src/domains/financing/formalization/registry.ts +114 -0
- package/src/domains/financing/formalization/schemas/AlfaPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/AlfaPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/BancoDoBrasilPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/BancoDoBrasilPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/BtgPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/BtgPJSchema.ts +77 -0
- package/src/domains/financing/formalization/schemas/BvPFSchema.ts +59 -0
- package/src/domains/financing/formalization/schemas/BvPJSchema.ts +84 -0
- package/src/domains/financing/formalization/schemas/CaixaEconomicaFederalPFSchema.ts +48 -0
- package/src/domains/financing/formalization/schemas/CaixaEconomicaFederalPJSchema.ts +67 -0
- package/src/domains/financing/formalization/schemas/CashMePFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/CashMePJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/Credito77PFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/Credito77PJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/EosPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/EosPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/HdtEnergyPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/HdtEnergyPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/LosangoPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/LosangoPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/SafraPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/SafraPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/SantanderPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/SantanderPJSchema.ts +65 -0
- package/src/domains/financing/formalization/schemas/SolAgoraPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/SolAgoraPJSchema.ts +75 -0
- package/src/domains/financing/formalization/schemas/SolfacilPFSchema.ts +46 -0
- package/src/domains/financing/formalization/schemas/SolfacilPJSchema.ts +70 -0
- package/src/domains/financing/formalization/uiMeta.ts +122 -0
- package/src/domains/financing/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/shared/enums/document-type.ts +4 -0
- package/src/shared/enums/index.ts +3 -0
- package/src/shared/enums/nationality.ts +2 -0
- package/src/shared/enums/sex.ts +4 -0
- package/src/shared/fields/index.ts +1 -0
- package/src/shared/fields/primitives.ts +32 -0
- package/src/shared/regex/index.ts +75 -0
- package/src/shared/regex/patterns.ts +15 -0
- package/src/shared/regex/validators.spec.ts +162 -0
- package/src/shared/regex/validators.ts +96 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { DocumentTypeEnum, SexEnum } from '../../../../shared/enums'
|
|
3
|
+
import { textField } from '../../../../shared/fields/primitives'
|
|
4
|
+
import {
|
|
5
|
+
birthDateSchema,
|
|
6
|
+
cepSchema,
|
|
7
|
+
cpfCnpjSchema,
|
|
8
|
+
cpfSchema,
|
|
9
|
+
currencySchema,
|
|
10
|
+
emailSchema,
|
|
11
|
+
issueDateSchema,
|
|
12
|
+
phoneBRSchema,
|
|
13
|
+
} from '../../../../shared/regex'
|
|
14
|
+
import { formalizationExtras } from '../extras'
|
|
15
|
+
import { applyPfRefinements } from '../refinements'
|
|
16
|
+
|
|
17
|
+
export const solfacilPfObject = z.object({
|
|
18
|
+
...formalizationExtras,
|
|
19
|
+
name: textField(),
|
|
20
|
+
cep: cepSchema,
|
|
21
|
+
cpf: cpfSchema,
|
|
22
|
+
address: textField(),
|
|
23
|
+
birth_date: birthDateSchema,
|
|
24
|
+
number: textField(),
|
|
25
|
+
sex: SexEnum,
|
|
26
|
+
complement: textField(15).optional(),
|
|
27
|
+
mother_name: textField(),
|
|
28
|
+
district: textField(),
|
|
29
|
+
energy_account_in_requester_name: textField(),
|
|
30
|
+
city: textField(),
|
|
31
|
+
energy_bill_owner_document: cpfCnpjSchema.optional(),
|
|
32
|
+
state: textField(2),
|
|
33
|
+
nationality: textField(),
|
|
34
|
+
cellphone: phoneBRSchema,
|
|
35
|
+
email: emailSchema,
|
|
36
|
+
monthly_income: currencySchema,
|
|
37
|
+
profession: textField(),
|
|
38
|
+
type_doc: DocumentTypeEnum,
|
|
39
|
+
doc: textField(15),
|
|
40
|
+
issuing_body: textField(6),
|
|
41
|
+
doc_issue_date: issueDateSchema,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const solfacilPfSchema = solfacilPfObject.strict().superRefine(applyPfRefinements)
|
|
45
|
+
|
|
46
|
+
export type SolfacilPfFicha = z.infer<typeof solfacilPfSchema>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { SexEnum } from '../../../../shared/enums'
|
|
3
|
+
import { textField } from '../../../../shared/fields/primitives'
|
|
4
|
+
import {
|
|
5
|
+
birthDateSchema,
|
|
6
|
+
cepSchema,
|
|
7
|
+
cnpjSchema,
|
|
8
|
+
cpfSchema,
|
|
9
|
+
currencySchema,
|
|
10
|
+
emailSchema,
|
|
11
|
+
issueDateSchema,
|
|
12
|
+
phoneBRSchema,
|
|
13
|
+
} from '../../../../shared/regex'
|
|
14
|
+
import { formalizationExtras } from '../extras'
|
|
15
|
+
import { applyPjRefinements } from '../refinements'
|
|
16
|
+
|
|
17
|
+
export const solfacilPjObject = z.object({
|
|
18
|
+
...formalizationExtras,
|
|
19
|
+
financing_company: z
|
|
20
|
+
.object({
|
|
21
|
+
corporate_name: textField(),
|
|
22
|
+
cep: cepSchema,
|
|
23
|
+
address: textField(),
|
|
24
|
+
foundation_date: issueDateSchema,
|
|
25
|
+
number: textField(),
|
|
26
|
+
cnpj: cnpjSchema,
|
|
27
|
+
email: emailSchema,
|
|
28
|
+
complement: textField(15).optional(),
|
|
29
|
+
phone: phoneBRSchema,
|
|
30
|
+
district: textField(),
|
|
31
|
+
legal_nature: textField(),
|
|
32
|
+
city: textField(),
|
|
33
|
+
state: textField(2),
|
|
34
|
+
})
|
|
35
|
+
.strict(),
|
|
36
|
+
financing_company_guarantor: z.array(
|
|
37
|
+
z
|
|
38
|
+
.object({
|
|
39
|
+
name: textField(),
|
|
40
|
+
cep: cepSchema,
|
|
41
|
+
cpf: cpfSchema,
|
|
42
|
+
address: textField(),
|
|
43
|
+
birth_date: birthDateSchema,
|
|
44
|
+
number: textField(),
|
|
45
|
+
complement: textField(15).optional(),
|
|
46
|
+
cellphone: phoneBRSchema,
|
|
47
|
+
phone: phoneBRSchema,
|
|
48
|
+
email: emailSchema,
|
|
49
|
+
district: textField(),
|
|
50
|
+
sex: SexEnum,
|
|
51
|
+
city: textField(),
|
|
52
|
+
state: textField(2),
|
|
53
|
+
civil_status: textField(),
|
|
54
|
+
office: textField(),
|
|
55
|
+
doc: textField(),
|
|
56
|
+
issuing_body: textField(),
|
|
57
|
+
uf_issuing_body: textField(2),
|
|
58
|
+
doc_issue_date: issueDateSchema,
|
|
59
|
+
mother_name: textField(),
|
|
60
|
+
monthly_income: currencySchema,
|
|
61
|
+
patrimony: currencySchema,
|
|
62
|
+
nationality: textField(),
|
|
63
|
+
})
|
|
64
|
+
.strict(),
|
|
65
|
+
),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const solfacilPjSchema = solfacilPjObject.strict().superRefine(applyPjRefinements)
|
|
69
|
+
|
|
70
|
+
export type SolfacilPjFicha = z.infer<typeof solfacilPjSchema>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export type FormalizationFieldKind =
|
|
2
|
+
| 'text'
|
|
3
|
+
| 'cpf'
|
|
4
|
+
| 'cnpj'
|
|
5
|
+
| 'cpfCnpj'
|
|
6
|
+
| 'cep'
|
|
7
|
+
| 'phone'
|
|
8
|
+
| 'email'
|
|
9
|
+
| 'date'
|
|
10
|
+
| 'currency'
|
|
11
|
+
| 'integer'
|
|
12
|
+
| 'number'
|
|
13
|
+
| 'select'
|
|
14
|
+
| 'file'
|
|
15
|
+
| 'toggle'
|
|
16
|
+
| 'group'
|
|
17
|
+
| 'array'
|
|
18
|
+
|
|
19
|
+
export interface FormalizationFieldMeta {
|
|
20
|
+
kind: FormalizationFieldKind
|
|
21
|
+
label: string
|
|
22
|
+
step: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const FORMALIZATION_FIELD_META: Record<string, FormalizationFieldMeta> = {
|
|
26
|
+
energy_bill_upload: { kind: 'file', label: 'Conta de energia', step: 1 },
|
|
27
|
+
account_third_party: {
|
|
28
|
+
kind: 'select',
|
|
29
|
+
label: 'Conta em nome de terceiro?',
|
|
30
|
+
step: 1,
|
|
31
|
+
},
|
|
32
|
+
bond_document_upload: {
|
|
33
|
+
kind: 'file',
|
|
34
|
+
label: 'Documento de vínculo com o titular',
|
|
35
|
+
step: 1,
|
|
36
|
+
},
|
|
37
|
+
document_front_upload: {
|
|
38
|
+
kind: 'file',
|
|
39
|
+
label: 'Documento de identidade (frente)',
|
|
40
|
+
step: 2,
|
|
41
|
+
},
|
|
42
|
+
document_back_upload: {
|
|
43
|
+
kind: 'file',
|
|
44
|
+
label: 'Documento de identidade (verso)',
|
|
45
|
+
step: 2,
|
|
46
|
+
},
|
|
47
|
+
rd_re_insurance_toggle: {
|
|
48
|
+
kind: 'toggle',
|
|
49
|
+
label: 'Seguro prestamista (RD/RE)',
|
|
50
|
+
step: 3,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
name: { kind: 'text', label: 'Nome', step: 3 },
|
|
54
|
+
cpf: { kind: 'cpf', label: 'CPF', step: 3 },
|
|
55
|
+
birth_date: { kind: 'date', label: 'Data de nascimento', step: 3 },
|
|
56
|
+
nationality: { kind: 'text', label: 'Nacionalidade', step: 3 },
|
|
57
|
+
type_doc: { kind: 'select', label: 'Tipo de documento', step: 3 },
|
|
58
|
+
doc: { kind: 'text', label: 'Número do documento', step: 3 },
|
|
59
|
+
issuing_body: { kind: 'text', label: 'Órgão emissor', step: 3 },
|
|
60
|
+
emitting_state: { kind: 'text', label: 'UF emissor', step: 3 },
|
|
61
|
+
doc_issue_date: { kind: 'date', label: 'Data de expedição', step: 3 },
|
|
62
|
+
email: { kind: 'email', label: 'E-mail', step: 3 },
|
|
63
|
+
cellphone: { kind: 'phone', label: 'Celular', step: 3 },
|
|
64
|
+
monthly_income: { kind: 'currency', label: 'Renda mensal', step: 3 },
|
|
65
|
+
equity_value: { kind: 'currency', label: 'Patrimônio', step: 3 },
|
|
66
|
+
mother_name: { kind: 'text', label: 'Nome da mãe', step: 3 },
|
|
67
|
+
civil_status: { kind: 'text', label: 'Estado civil', step: 3 },
|
|
68
|
+
naturalness: { kind: 'text', label: 'Naturalidade', step: 3 },
|
|
69
|
+
uf_naturalness: { kind: 'text', label: 'UF de naturalidade', step: 3 },
|
|
70
|
+
nature_of_occupation: {
|
|
71
|
+
kind: 'text',
|
|
72
|
+
label: 'Natureza da ocupação',
|
|
73
|
+
step: 3,
|
|
74
|
+
},
|
|
75
|
+
company_time: { kind: 'number', label: 'Tempo de empresa (meses)', step: 3 },
|
|
76
|
+
cnpj_proprietary: { kind: 'cnpj', label: 'CNPJ do proprietário', step: 3 },
|
|
77
|
+
pep_relationship: { kind: 'text', label: 'Relação com PEP', step: 3 },
|
|
78
|
+
residence_situation: {
|
|
79
|
+
kind: 'text',
|
|
80
|
+
label: 'Situação da residência',
|
|
81
|
+
step: 3,
|
|
82
|
+
},
|
|
83
|
+
months_in_residence: {
|
|
84
|
+
kind: 'integer',
|
|
85
|
+
label: 'Tempo de residência (meses)',
|
|
86
|
+
step: 3,
|
|
87
|
+
},
|
|
88
|
+
is_installation_at_requester: {
|
|
89
|
+
kind: 'text',
|
|
90
|
+
label: 'Instalação no endereço do solicitante',
|
|
91
|
+
step: 3,
|
|
92
|
+
},
|
|
93
|
+
energy_account_in_requester_name: {
|
|
94
|
+
kind: 'text',
|
|
95
|
+
label: 'Conta de energia no nome do solicitante',
|
|
96
|
+
step: 3,
|
|
97
|
+
},
|
|
98
|
+
energy_bill_owner_document: {
|
|
99
|
+
kind: 'cpfCnpj',
|
|
100
|
+
label: 'Documento do titular da conta de energia',
|
|
101
|
+
step: 3,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
cep: { kind: 'cep', label: 'CEP', step: 3 },
|
|
105
|
+
address: { kind: 'text', label: 'Endereço', step: 3 },
|
|
106
|
+
number: { kind: 'text', label: 'Número', step: 3 },
|
|
107
|
+
complement: { kind: 'text', label: 'Complemento', step: 3 },
|
|
108
|
+
district: { kind: 'text', label: 'Bairro', step: 3 },
|
|
109
|
+
state: { kind: 'text', label: 'Estado (UF)', step: 3 },
|
|
110
|
+
city: { kind: 'text', label: 'Cidade', step: 3 },
|
|
111
|
+
|
|
112
|
+
sex: { kind: 'select', label: 'Sexo', step: 4 },
|
|
113
|
+
profession: { kind: 'text', label: 'Profissão', step: 4 },
|
|
114
|
+
|
|
115
|
+
financing_company: { kind: 'group', label: 'Dados da empresa', step: 3 },
|
|
116
|
+
financing_company_guarantor: { kind: 'array', label: 'Avalistas', step: 3 },
|
|
117
|
+
financing_company_qsa: {
|
|
118
|
+
kind: 'array',
|
|
119
|
+
label: 'Quadro societário (QSA)',
|
|
120
|
+
step: 3,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './formalization'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './domains/financing'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './primitives'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export function textField(maxLength?: number) {
|
|
4
|
+
const base = z.string().trim().min(1, 'Campo obrigatório')
|
|
5
|
+
return maxLength === undefined ? base : base.max(maxLength, `Máximo ${maxLength} caracteres`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const numberField = z
|
|
9
|
+
.number({ invalid_type_error: 'Valor numérico inválido' })
|
|
10
|
+
.nonnegative('Valor não pode ser negativo')
|
|
11
|
+
|
|
12
|
+
export const integerField = z
|
|
13
|
+
.number({ invalid_type_error: 'Valor numérico inválido' })
|
|
14
|
+
.int('Deve ser um número inteiro')
|
|
15
|
+
.nonnegative('Valor não pode ser negativo')
|
|
16
|
+
|
|
17
|
+
export function selectField<T extends readonly [string, ...string[]]>(values: T) {
|
|
18
|
+
return z.enum(values)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Valida apenas os metadados do arquivo; o binário é tratado pelo serviço de upload.
|
|
22
|
+
export const MAX_UPLOAD_BYTES = 5 * 1024 * 1024
|
|
23
|
+
export const ACCEPTED_UPLOAD_MIME = ['image/jpeg', 'image/png', 'application/pdf'] as const
|
|
24
|
+
|
|
25
|
+
export const fileUploadField = z.object({
|
|
26
|
+
url: z.string().url().optional(),
|
|
27
|
+
mime: z.enum(ACCEPTED_UPLOAD_MIME, {
|
|
28
|
+
errorMap: () => ({ message: 'Formato deve ser JPEG, PNG ou PDF' }),
|
|
29
|
+
}),
|
|
30
|
+
sizeBytes: z.number().max(MAX_UPLOAD_BYTES, 'Arquivo deve ter no máximo 5MB'),
|
|
31
|
+
})
|
|
32
|
+
export type FileUpload = z.infer<typeof fileUploadField>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { PATTERNS } from './patterns'
|
|
3
|
+
import {
|
|
4
|
+
isAdultDMY,
|
|
5
|
+
isNotFutureDMY,
|
|
6
|
+
isValidCNPJ,
|
|
7
|
+
isValidCPF,
|
|
8
|
+
onlyDigits,
|
|
9
|
+
parseDMY,
|
|
10
|
+
} from './validators'
|
|
11
|
+
|
|
12
|
+
export * from './patterns'
|
|
13
|
+
export * from './validators'
|
|
14
|
+
|
|
15
|
+
export const cpfSchema = z
|
|
16
|
+
.string()
|
|
17
|
+
.transform(onlyDigits)
|
|
18
|
+
.refine((v) => PATTERNS.cpf.test(v), 'CPF deve ter 11 dígitos')
|
|
19
|
+
.refine(isValidCPF, 'CPF inválido')
|
|
20
|
+
|
|
21
|
+
export const cnpjSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.transform(onlyDigits)
|
|
24
|
+
.refine((v) => PATTERNS.cnpj.test(v), 'CNPJ deve ter 14 dígitos')
|
|
25
|
+
.refine(isValidCNPJ, 'CNPJ inválido')
|
|
26
|
+
|
|
27
|
+
export const cpfCnpjSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.transform(onlyDigits)
|
|
30
|
+
.refine(
|
|
31
|
+
(v) => PATTERNS.cpf.test(v) || PATTERNS.cnpj.test(v),
|
|
32
|
+
'Documento deve ter 11 (CPF) ou 14 (CNPJ) dígitos',
|
|
33
|
+
)
|
|
34
|
+
.refine((v) => (v.length === 11 ? isValidCPF(v) : isValidCNPJ(v)), 'Documento inválido')
|
|
35
|
+
|
|
36
|
+
export const cepSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.transform(onlyDigits)
|
|
39
|
+
.refine((v) => PATTERNS.cep.test(v), 'CEP deve ter 8 dígitos')
|
|
40
|
+
|
|
41
|
+
export const phoneBRSchema = z
|
|
42
|
+
.string()
|
|
43
|
+
.transform(onlyDigits)
|
|
44
|
+
.refine((v) => PATTERNS.phoneBR.test(v), 'Telefone deve ter 11 dígitos com DDD')
|
|
45
|
+
|
|
46
|
+
export const emailSchema = z
|
|
47
|
+
.string()
|
|
48
|
+
.trim()
|
|
49
|
+
.refine((v) => PATTERNS.email.test(v), 'E-mail inválido')
|
|
50
|
+
|
|
51
|
+
export const dateDMYSchema = z
|
|
52
|
+
.string()
|
|
53
|
+
.refine((v) => PATTERNS.dateDMY.test(v), 'Data deve estar no formato DD/MM/AAAA')
|
|
54
|
+
.refine((v) => parseDMY(v) !== null, 'Data inexistente no calendário')
|
|
55
|
+
|
|
56
|
+
export const birthDateSchema = dateDMYSchema.refine(
|
|
57
|
+
(v) => isAdultDMY(v),
|
|
58
|
+
'É necessário ser maior de idade (18+)',
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// RN-011 (prazo por tipo de documento) é aplicada no schema PF.
|
|
62
|
+
export const issueDateSchema = dateDMYSchema.refine(
|
|
63
|
+
(v) => isNotFutureDMY(v),
|
|
64
|
+
'Data de emissão não pode ser futura',
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const currencySchema = z
|
|
68
|
+
.number({ invalid_type_error: 'Valor monetário inválido' })
|
|
69
|
+
.positive('Valor deve ser positivo')
|
|
70
|
+
|
|
71
|
+
export const billDueDaySchema = z
|
|
72
|
+
.number()
|
|
73
|
+
.int('Deve ser um número inteiro')
|
|
74
|
+
.min(1, 'Mínimo 1')
|
|
75
|
+
.max(28, 'Máximo 28')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Regras de formato (validação), não máscara visual da UI. Assumem input já
|
|
2
|
+
// "limpo" (somente dígitos quando aplicável). Dígito verificador: validators.ts.
|
|
3
|
+
export const PATTERNS = {
|
|
4
|
+
cpf: /^\d{11}$/,
|
|
5
|
+
cnpj: /^\d{14}$/,
|
|
6
|
+
cep: /^\d{8}$/,
|
|
7
|
+
phoneBR: /^\d{11}$/,
|
|
8
|
+
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
9
|
+
dateDMY: /^\d{2}\/\d{2}\/\d{4}$/,
|
|
10
|
+
cnhNumber: /^\d{11}$/,
|
|
11
|
+
rneNumber: /^[A-Za-z0-9]+$/,
|
|
12
|
+
rgNumber: /^[A-Za-z0-9./-]+$/,
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export type PatternKey = keyof typeof PATTERNS
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
birthDateSchema,
|
|
4
|
+
cepSchema,
|
|
5
|
+
cnpjSchema,
|
|
6
|
+
cpfSchema,
|
|
7
|
+
emailSchema,
|
|
8
|
+
issueDateSchema,
|
|
9
|
+
phoneBRSchema,
|
|
10
|
+
} from './index'
|
|
11
|
+
import {
|
|
12
|
+
ageInYears,
|
|
13
|
+
isAdultDMY,
|
|
14
|
+
isNotFutureDMY,
|
|
15
|
+
isValidCNPJ,
|
|
16
|
+
isValidCPF,
|
|
17
|
+
onlyDigits,
|
|
18
|
+
parseDMY,
|
|
19
|
+
yearsSinceDMY,
|
|
20
|
+
} from './validators'
|
|
21
|
+
|
|
22
|
+
describe('CPF', () => {
|
|
23
|
+
it('aceita CPF com dígito verificador válido', () => {
|
|
24
|
+
expect(isValidCPF('52998224725')).toBe(true)
|
|
25
|
+
expect(cpfSchema.safeParse('529.982.247-25').success).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejeita dígito verificador inválido', () => {
|
|
29
|
+
expect(isValidCPF('52998224724')).toBe(false)
|
|
30
|
+
expect(cpfSchema.safeParse('52998224724').success).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejeita tamanho errado', () => {
|
|
34
|
+
expect(isValidCPF('1234567890')).toBe(false) // 10 dígitos
|
|
35
|
+
expect(cpfSchema.safeParse('123').success).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('rejeita repetidos (todos os dígitos iguais)', () => {
|
|
39
|
+
expect(isValidCPF('11111111111')).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('CNPJ', () => {
|
|
44
|
+
it('aceita CNPJ com dígito verificador válido', () => {
|
|
45
|
+
expect(isValidCNPJ('11222333000181')).toBe(true)
|
|
46
|
+
expect(cnpjSchema.safeParse('11.222.333/0001-81').success).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rejeita dígito verificador inválido', () => {
|
|
50
|
+
expect(isValidCNPJ('11222333000180')).toBe(false)
|
|
51
|
+
expect(cnpjSchema.safeParse('11222333000180').success).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('rejeita tamanho errado', () => {
|
|
55
|
+
expect(isValidCNPJ('112223330001')).toBe(false) // 12 dígitos
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('CEP', () => {
|
|
60
|
+
it('aceita 8 dígitos', () => {
|
|
61
|
+
expect(cepSchema.safeParse('01310930').success).toBe(true)
|
|
62
|
+
expect(cepSchema.safeParse('01310-930').success).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('rejeita tamanho diferente de 8', () => {
|
|
66
|
+
expect(cepSchema.safeParse('1234567').success).toBe(false)
|
|
67
|
+
expect(cepSchema.safeParse('123456789').success).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('Telefone', () => {
|
|
72
|
+
it('aceita 11 dígitos com DDD', () => {
|
|
73
|
+
expect(phoneBRSchema.safeParse('11999998888').success).toBe(true)
|
|
74
|
+
expect(phoneBRSchema.safeParse('(11) 99999-8888').success).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('rejeita menos de 11 dígitos', () => {
|
|
78
|
+
expect(phoneBRSchema.safeParse('1199998888').success).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('Email', () => {
|
|
83
|
+
it('aceita e-mail válido', () => {
|
|
84
|
+
expect(emailSchema.safeParse('cliente@exemplo.com').success).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('rejeita e-mail inválido', () => {
|
|
88
|
+
expect(emailSchema.safeParse('cliente@@exemplo').success).toBe(false)
|
|
89
|
+
expect(emailSchema.safeParse('sem-arroba.com').success).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('parseDMY', () => {
|
|
94
|
+
it('faz parse de data válida', () => {
|
|
95
|
+
const d = parseDMY('10/05/1990')
|
|
96
|
+
expect(d).not.toBeNull()
|
|
97
|
+
expect(d?.getFullYear()).toBe(1990)
|
|
98
|
+
expect(d?.getMonth()).toBe(4) // maio = índice 4
|
|
99
|
+
expect(d?.getDate()).toBe(10)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('rejeita data de calendário inexistente (31/02/2000)', () => {
|
|
103
|
+
expect(parseDMY('31/02/2000')).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('rejeita formato inválido', () => {
|
|
107
|
+
expect(parseDMY('1990-05-10')).toBeNull()
|
|
108
|
+
expect(parseDMY('5/5/1990')).toBeNull()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('onlyDigits', () => {
|
|
113
|
+
it('remove tudo que não for dígito', () => {
|
|
114
|
+
expect(onlyDigits('(11) 99999-8888')).toBe('11999998888')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('birthDate (idade mínima)', () => {
|
|
119
|
+
const reference = new Date(2026, 5, 1) // 01/06/2026
|
|
120
|
+
|
|
121
|
+
it('rejeita menor de 18 anos', () => {
|
|
122
|
+
expect(isAdultDMY('01/01/2010', reference)).toBe(false)
|
|
123
|
+
expect(birthDateSchema.safeParse('01/01/2015').success).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('aceita adulto', () => {
|
|
127
|
+
expect(isAdultDMY('01/01/1990', reference)).toBe(true)
|
|
128
|
+
expect(birthDateSchema.safeParse('10/05/1990').success).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('issueDate (não futura)', () => {
|
|
133
|
+
const reference = new Date(2026, 5, 1)
|
|
134
|
+
|
|
135
|
+
it('rejeita data futura', () => {
|
|
136
|
+
expect(isNotFutureDMY('01/01/2030', reference)).toBe(false)
|
|
137
|
+
expect(issueDateSchema.safeParse('01/01/2999').success).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('aceita data passada', () => {
|
|
141
|
+
expect(isNotFutureDMY('01/01/2020', reference)).toBe(true)
|
|
142
|
+
expect(issueDateSchema.safeParse('01/01/2020').success).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('yearsSinceDMY / ageInYears', () => {
|
|
147
|
+
const reference = new Date(2026, 5, 1) // 01/06/2026
|
|
148
|
+
|
|
149
|
+
it('calcula anos completos de forma coerente', () => {
|
|
150
|
+
expect(yearsSinceDMY('01/06/2016', reference)).toBe(10)
|
|
151
|
+
expect(yearsSinceDMY('02/06/2016', reference)).toBe(9) // aniversário ainda não chegou
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('retorna null para data inválida', () => {
|
|
155
|
+
expect(yearsSinceDMY('31/02/2000', reference)).toBeNull()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('ageInYears é consistente entre duas datas', () => {
|
|
159
|
+
expect(ageInYears(new Date(2000, 0, 1), new Date(2026, 0, 1))).toBe(26)
|
|
160
|
+
expect(ageInYears(new Date(2000, 0, 2), new Date(2026, 0, 1))).toBe(25)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export function onlyDigits(value: string): string {
|
|
2
|
+
return value.replace(/\D/g, '')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isValidCPF(value: string): boolean {
|
|
6
|
+
const cpf = onlyDigits(value)
|
|
7
|
+
if (cpf.length !== 11) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
if (/^(\d)\1{10}$/.test(cpf)) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const calcCheck = (slice: number): number => {
|
|
15
|
+
let sum = 0
|
|
16
|
+
for (let i = 0; i < slice; i++) {
|
|
17
|
+
sum += Number(cpf[i]) * (slice + 1 - i)
|
|
18
|
+
}
|
|
19
|
+
const rest = (sum * 10) % 11
|
|
20
|
+
return rest === 10 ? 0 : rest
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return calcCheck(9) === Number(cpf[9]) && calcCheck(10) === Number(cpf[10])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isValidCNPJ(value: string): boolean {
|
|
27
|
+
const cnpj = onlyDigits(value)
|
|
28
|
+
if (cnpj.length !== 14) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
if (/^(\d)\1{13}$/.test(cnpj)) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const calcCheck = (length: number): number => {
|
|
36
|
+
const weights =
|
|
37
|
+
length === 12 ? [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] : [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
38
|
+
let sum = 0
|
|
39
|
+
for (const [i, weight] of weights.entries()) {
|
|
40
|
+
sum += Number(cnpj[i]) * weight
|
|
41
|
+
}
|
|
42
|
+
const rest = sum % 11
|
|
43
|
+
return rest < 2 ? 0 : 11 - rest
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return calcCheck(12) === Number(cnpj[12]) && calcCheck(13) === Number(cnpj[13])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseDMY(value: string): Date | null {
|
|
50
|
+
const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value)
|
|
51
|
+
if (!match) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
const [, dd, mm, yyyy] = match
|
|
55
|
+
const day = Number(dd)
|
|
56
|
+
const month = Number(mm)
|
|
57
|
+
const year = Number(yyyy)
|
|
58
|
+
const date = new Date(year, month - 1, day)
|
|
59
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
return date
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ageInYears(birth: Date, reference: Date = new Date()): number {
|
|
66
|
+
let age = reference.getFullYear() - birth.getFullYear()
|
|
67
|
+
const monthDiff = reference.getMonth() - birth.getMonth()
|
|
68
|
+
if (monthDiff < 0 || (monthDiff === 0 && reference.getDate() < birth.getDate())) {
|
|
69
|
+
age--
|
|
70
|
+
}
|
|
71
|
+
return age
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isAdultDMY(value: string, reference?: Date): boolean {
|
|
75
|
+
const date = parseDMY(value)
|
|
76
|
+
if (!date) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
return ageInYears(date, reference) >= 18
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isNotFutureDMY(value: string, reference: Date = new Date()): boolean {
|
|
83
|
+
const date = parseDMY(value)
|
|
84
|
+
if (!date) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
return date.getTime() <= reference.getTime()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function yearsSinceDMY(value: string, reference: Date = new Date()): number | null {
|
|
91
|
+
const date = parseDMY(value)
|
|
92
|
+
if (!date) {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
return ageInYears(date, reference)
|
|
96
|
+
}
|