Step 4: Employment Validation
Validating employment and income fields with conditional rules based on employment status.
What We're Validatingβ
Step 4 contains employment-related fields with conditional requirements:
| Field | Validation Rules |
|---|---|
employmentStatus | Required |
monthlyIncome | Required, min 10,000 |
additionalIncome | Optional, min 0 |
| For Employed | |
companyName | Required when employed |
companyAddress | Required when employed |
position | Required when employed |
workExperienceTotal | Optional, min 0 |
workExperienceCurrent | Required when employed, min 3 months |
| For Self-Employed | |
businessType | Required when self-employed |
businessInn | Required when self-employed, 10 or 12 digits |
businessAddress | Required when self-employed |
businessExperience | Required when self-employed, min 6 months |
Creating the Validator Fileβ
Create the validator file for Step 4:
touch src/schemas/validators/employment.ts
Implementationβ
Basic Employment Fieldsβ
Start with required fields that apply to all employment statuses:
import { required, min, pattern, applyWhen } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';
/**
* Validation for Step 4: Employment
*
* Validates:
* - Employment status (required for all)
* - Income fields (required for all)
* - Employment-specific fields (conditionally required)
* - Self-employment fields (conditionally required)
*/
export const employmentValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Basic Employment Fields
// ==========================================
// Employment status (always required)
required(path.employmentStatus, { message: 'Employment status is required' });
// Monthly income (always required, minimum threshold)
required(path.monthlyIncome, { message: 'Monthly income is required' });
min(path.monthlyIncome, 10000, {
message: 'Minimum monthly income: 10,000',
});
// Additional income (optional, but must be non-negative if provided)
min(path.additionalIncome, 0, {
message: 'Additional income cannot be negative',
});
};
Conditional Validation: Employedβ
Add validation for employed individuals:
export const employmentValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
// ... previous validation ...
// ==========================================
// Conditional: Employed Fields
// ==========================================
applyWhen(
path.employmentStatus,
(status) => status === 'employed',
(p) => {
required(p.companyName, { message: 'Company name is required' });
required(p.companyAddress, { message: 'Company address is required' });
required(p.position, { message: 'Position is required' });
required(p.workExperienceCurrent, { message: 'Work experience at current job is required' });
min(p.workExperienceCurrent, 3, {
message: 'Minimum 3 months of experience at current job required',
});
min(p.workExperienceTotal, 0, {
message: 'Total work experience cannot be negative',
});
}
);
};
Conditional Validation: Self-Employedβ
Add validation for self-employed individuals:
export const employmentValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
// ... previous validation ...
// ==========================================
// Conditional: Self-Employed Fields
// ==========================================
applyWhen(
path.employmentStatus,
(status) => status === 'selfEmployed',
(p) => {
required(p.businessType, { message: 'Business type is required' });
required(p.businessInn, { message: 'Business INN is required' });
}
);
pattern(path.businessInn, /^\d{10}$|^\d{12}$/, {
message: 'Business INN must be 10 or 12 digits',
});
};
Complete Codeβ
Here's the complete validator for Step 4:
import { required, min, pattern, applyWhen } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '@/types';
/**
* Validation for Step 4: Employment
*
* Validates:
* - Employment status (required for all)
* - Income fields (required for all)
* - Employment-specific fields (conditionally required)
* - Self-employment fields (conditionally required)
*/
export const employmentValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// ==========================================
// Basic Employment Fields
// ==========================================
required(path.employmentStatus, { message: 'Employment status is required' });
required(path.monthlyIncome, { message: 'Monthly income is required' });
min(path.monthlyIncome, 10000, {
message: 'Minimum monthly income: 10,000',
});
min(path.additionalIncome, 0, {
message: 'Additional income cannot be negative',
});
// ==========================================
// Conditional: Employed Fields
// ==========================================
applyWhen(
path.employmentStatus,
(status) => status === 'employed',
(p) => {
required(p.companyName, { message: 'Company name is required' });
required(p.companyAddress, { message: 'Company address is required' });
required(p.position, { message: 'Position is required' });
required(p.workExperienceCurrent, { message: 'Work experience at current job is required' });
min(p.workExperienceCurrent, 3, {
message: 'Minimum 3 months of experience at current job required',
});
min(p.workExperienceTotal, 0, {
message: 'Total work experience cannot be negative',
});
}
);
// ==========================================
// Conditional: Self-Employed Fields
// ==========================================
applyWhen(
path.employmentStatus,
(status) => status === 'selfEmployed',
(p) => {
required(p.businessType, { message: 'Business type is required' });
required(p.businessInn, { message: 'Business INN is required' });
}
);
pattern(path.businessInn, /^\d{10}$|^\d{12}$/, {
message: 'Business INN must be 10 or 12 digits',
});
};
How It Worksβ
Always Required Fieldsβ
These fields are required regardless of employment status:
required(path.employmentStatus, { message: 'Employment status is required' });
required(path.monthlyIncome, { message: 'Monthly income is required' });
min(path.monthlyIncome, 10000, { message: 'Minimum monthly income: 10,000' });
Conditionally Required Fieldsβ
These fields are only required for specific employment statuses:
// Required only when employed
applyWhen(
path.employmentStatus,
(status) => status === 'employed',
(p) => {
required(p.companyName, { message: 'Company name is required' });
min(p.workExperienceCurrent, 3, {
message: 'Minimum 3 months of experience at current job required',
});
}
);
// Required only when self-employed
applyWhen(
path.employmentStatus,
(status) => status === 'selfEmployed',
(p) => {
required(p.businessType, { message: 'Business type is required' });
}
);
Integration with Behaviorsβ
From the Behaviors section, we have:
// Behavior: Show company fields only when employed
enableWhen(path.companyName, path.employmentStatus, (status) => status === 'employed');
enableWhen(path.companyAddress, path.employmentStatus, (status) => status === 'employed');
// Validation: Require company fields only when employed
applyWhen(
path.employmentStatus,
(status) => status === 'employed',
(p) => {
required(p.companyName, { message: 'Company name is required' });
}
);
Perfect alignment! Fields are hidden/shown and required/optional in sync.
Testing the Validationβ
Test these scenarios:
Basic Fields (All Statuses)β
- Leave employment status empty β Error shown
- Leave monthly income empty β Error shown
- Enter monthly income < 10,000 β Error shown
- Enter monthly income >= 10,000 β No error
- Enter negative additional income β Error shown
- Leave additional income empty β No error (optional)
Employed Statusβ
- Select "employed" β Company fields become required
- Leave company name empty β Error shown
- Leave company address empty β Error shown
- Leave position empty β Error shown
- Leave work experience empty β Error shown
- Enter work experience < 3 months β Error shown
- Enter work experience >= 3 months β No error
Self-Employed Statusβ
- Select "self-employed" β Business fields become required
- Leave business type empty β Error shown
- Leave business INN empty β Error shown
- Enter business INN with 9 digits β Error shown
- Enter business INN with 10 digits β No error
- Enter business INN with 12 digits β No error
- Leave business address empty β Error shown
- Leave business experience empty β Error shown
- Enter business experience < 6 months β Error shown
- Enter business experience >= 6 months β No error
Unemployed/Other Statusβ
- Select "unemployed" β Only basic fields required
- Company fields not required
- Business fields not required
- Monthly income still required
Switching Employment Statusβ
- Fill employed fields β Switch to "self-employed" β Employed errors disappear
- Fill business fields β Switch to "employed" β Business errors disappear
- Switch to "unemployed" β All conditional errors disappear
Employment Status Valuesβ
Typical employment status values:
type EmploymentStatus =
| 'employed' // Full-time employment
| 'selfEmployed' // Self-employed / entrepreneur
| 'unemployed' // Unemployed
| 'retired' // Retired
| 'student'; // Student
Each status may have different validation requirements.
Key Takeawaysβ
- Always Required - Some fields required regardless of status
- Conditionally Required - Use
applyWhen()for status-specific fields - Works with Behaviors - Hidden fields skip validation
- Business Rules - Different minimum thresholds (3 months employed, 6 months business)
Common Patternsβ
Required for Specific Statusβ
applyWhen(
path.employmentStatus,
(status) => status === 'employed',
(p) => {
required(p.field, { message: 'Field is required' });
min(p.field, minimumValue, { message: 'Minimum value not met' });
}
);
Non-Negative Optional Fieldβ
// No required(), just min(0) to prevent negatives
min(path.additionalIncome, 0, {
message: 'Cannot be negative',
});
What's Next?β
In the next section, we'll add validation for Step 5: Additional Information, including:
- Array validation (properties, existing loans, co-borrowers)
- Array length constraints (min/max)
- Validating individual array elements
- Nested object validation within arrays
- Conditional array requirements
This will demonstrate the powerful array validation capabilities of ReFormer!