diff --git a/src/applications/education/22-1990n/app-entry.jsx b/src/applications/education/22-1990n/app-entry.jsx
new file mode 100644
index 000000000000..67d00e595c2a
--- /dev/null
+++ b/src/applications/education/22-1990n/app-entry.jsx
@@ -0,0 +1,15 @@
+import 'platform/polyfills';
+import './sass/22-1990n.scss';
+
+import startApp from 'platform/startup';
+
+import routes from './routes';
+import reducer from './reducers';
+import manifest from './manifest.json';
+
+startApp({
+ entryName: manifest.entryName,
+ url: manifest.rootUrl,
+ reducer,
+ routes,
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/activeDutyStatus.js b/src/applications/education/22-1990n/config/chapters/activeDutyStatus.js
new file mode 100644
index 000000000000..3079715e214b
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/activeDutyStatus.js
@@ -0,0 +1,40 @@
+import {
+ yesNoUI,
+ yesNoSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const activeDutyStatusUiSchema = {
+ serviceInformation: {
+ 'ui:title': 'Active duty status',
+ isActiveDuty: yesNoUI({
+ title: 'Are you now on active duty?',
+ hint: 'Answer based on your status today.',
+ errorMessages: {
+ required: 'Please indicate whether you are on active duty.',
+ },
+ }),
+ isOnTerminalLeave: yesNoUI({
+ title: 'Are you now on terminal leave just before discharge?',
+ hint: 'Terminal leave is leave taken immediately before your separation from active duty.',
+ errorMessages: {
+ required:
+ 'Please indicate whether you are on terminal leave.',
+ },
+ }),
+ },
+};
+
+export const activeDutyStatusSchema = {
+ type: 'object',
+ required: ['serviceInformation'],
+ properties: {
+ serviceInformation: {
+ type: 'object',
+ required: ['isActiveDuty', 'isOnTerminalLeave'],
+ properties: {
+ isActiveDuty: yesNoSchema,
+ isOnTerminalLeave: yesNoSchema,
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/activeDutyStatus.unit.spec.js b/src/applications/education/22-1990n/config/chapters/activeDutyStatus.unit.spec.js
new file mode 100644
index 000000000000..9c5eb5be79e2
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/activeDutyStatus.unit.spec.js
@@ -0,0 +1,34 @@
+import { expect } from 'chai';
+import {
+ activeDutyStatusUiSchema,
+ activeDutyStatusSchema,
+} from './activeDutyStatus';
+
+describe('config/chapters/activeDutyStatus', () => {
+ it('uiSchema has serviceInformation.isActiveDuty', () => {
+ expect(activeDutyStatusUiSchema).to.have.property('serviceInformation');
+ expect(
+ activeDutyStatusUiSchema.serviceInformation,
+ ).to.have.property('isActiveDuty');
+ });
+
+ it('uiSchema has serviceInformation.isOnTerminalLeave', () => {
+ expect(
+ activeDutyStatusUiSchema.serviceInformation,
+ ).to.have.property('isOnTerminalLeave');
+ });
+
+ it('schema has boolean types for both fields', () => {
+ const props =
+ activeDutyStatusSchema.properties.serviceInformation.properties;
+ expect(props.isActiveDuty.type).to.equal('boolean');
+ expect(props.isOnTerminalLeave.type).to.equal('boolean');
+ });
+
+ it('schema requires both isActiveDuty and isOnTerminalLeave', () => {
+ const required =
+ activeDutyStatusSchema.properties.serviceInformation.required;
+ expect(required).to.include('isActiveDuty');
+ expect(required).to.include('isOnTerminalLeave');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/additionalAssistance.js b/src/applications/education/22-1990n/config/chapters/additionalAssistance.js
new file mode 100644
index 000000000000..e929e3323871
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/additionalAssistance.js
@@ -0,0 +1,93 @@
+import {
+ yesNoUI,
+ yesNoSchema,
+ textUI,
+ textSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const additionalAssistanceUiSchema = {
+ additionalAssistance: {
+ 'ui:title': 'Additional assistance',
+ isSeniorROTCScholar: yesNoUI({
+ title:
+ 'Are you currently participating in a Senior ROTC scholarship program which pays for your tuition, fees, books and supplies under Section 2107, Title 10, U.S. Code?',
+ hint: 'This includes ROTC scholarship programs that cover the costs of college attendance.',
+ errorMessages: {
+ required:
+ 'Please indicate whether you are a Senior ROTC scholar.',
+ },
+ }),
+ receivingFederalTuitionAssist: {
+ ...yesNoUI({
+ title:
+ 'Are you receiving or do you anticipate receiving any money (including but not limited to Federal Tuition Assistance) from the Armed Forces or Public Health Service for the course for which you have applied to the VA for education benefits?',
+ hint: 'If you receive such benefits during any part of your training, select Yes. For active duty claimants only.',
+ errorMessages: {
+ required:
+ 'Please indicate whether you are receiving Federal Tuition Assistance.',
+ },
+ }),
+ 'ui:options': {
+ hideIf: formData =>
+ formData?.serviceInformation?.isActiveDuty !== true,
+ },
+ },
+ isCivilianGovEmployee: yesNoUI({
+ title: 'Are you a civilian employee of the U.S. Government?',
+ hint: 'This determines whether additional questions about agency funds apply to you.',
+ errorMessages: {
+ required:
+ 'Please indicate whether you are a civilian government employee.',
+ },
+ }),
+ receivingAgencyFunds: {
+ ...yesNoUI({
+ title:
+ 'Do you expect to receive funds from your agency or department for the same course(s) for which you expect to receive VA education assistance?',
+ hint: 'This applies if you work as a civilian employee for the U.S. Government.',
+ errorMessages: {
+ required:
+ 'Please indicate whether you expect to receive agency funds.',
+ },
+ }),
+ 'ui:options': {
+ hideIf: formData =>
+ formData?.additionalAssistance?.isCivilianGovEmployee !== true,
+ },
+ },
+ agencyFundsSource: {
+ ...textUI({
+ title: 'Source of funds',
+ hint: 'Show the source of these funds. Example: Department of Defense Tuition Assistance Program',
+ errorMessages: {
+ required: 'Please enter the source of agency funds.',
+ },
+ }),
+ 'ui:options': {
+ hideIf: formData =>
+ formData?.additionalAssistance?.receivingAgencyFunds !== true,
+ },
+ },
+ },
+};
+
+export const additionalAssistanceSchema = {
+ type: 'object',
+ required: ['additionalAssistance'],
+ properties: {
+ additionalAssistance: {
+ type: 'object',
+ required: ['isSeniorROTCScholar'],
+ properties: {
+ isSeniorROTCScholar: yesNoSchema,
+ receivingFederalTuitionAssist: yesNoSchema,
+ isCivilianGovEmployee: yesNoSchema,
+ receivingAgencyFunds: yesNoSchema,
+ agencyFundsSource: {
+ type: 'string',
+ maxLength: 100,
+ },
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/additionalAssistance.unit.spec.js b/src/applications/education/22-1990n/config/chapters/additionalAssistance.unit.spec.js
new file mode 100644
index 000000000000..3f84128844df
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/additionalAssistance.unit.spec.js
@@ -0,0 +1,61 @@
+import { expect } from 'chai';
+import {
+ additionalAssistanceUiSchema,
+ additionalAssistanceSchema,
+} from './additionalAssistance';
+
+describe('config/chapters/additionalAssistance', () => {
+ it('uiSchema has additionalAssistance with isSeniorROTCScholar', () => {
+ expect(additionalAssistanceUiSchema).to.have.property(
+ 'additionalAssistance',
+ );
+ expect(
+ additionalAssistanceUiSchema.additionalAssistance,
+ ).to.have.property('isSeniorROTCScholar');
+ });
+
+ it('uiSchema has receivingFederalTuitionAssist with hideIf', () => {
+ const field =
+ additionalAssistanceUiSchema.additionalAssistance
+ .receivingFederalTuitionAssist;
+ expect(field['ui:options']).to.have.property('hideIf');
+ });
+
+ it('receivingFederalTuitionAssist hideIf returns true when not active duty', () => {
+ const hideIf =
+ additionalAssistanceUiSchema.additionalAssistance
+ .receivingFederalTuitionAssist['ui:options'].hideIf;
+ expect(
+ hideIf({ serviceInformation: { isActiveDuty: false } }),
+ ).to.equal(true);
+ expect(
+ hideIf({ serviceInformation: { isActiveDuty: true } }),
+ ).to.equal(false);
+ });
+
+ it('agencyFundsSource hideIf hides when receivingAgencyFunds is not true', () => {
+ const hideIf =
+ additionalAssistanceUiSchema.additionalAssistance.agencyFundsSource[
+ 'ui:options'
+ ].hideIf;
+ expect(
+ hideIf({ additionalAssistance: { receivingAgencyFunds: false } }),
+ ).to.equal(true);
+ expect(
+ hideIf({ additionalAssistance: { receivingAgencyFunds: true } }),
+ ).to.equal(false);
+ });
+
+ it('schema requires isSeniorROTCScholar', () => {
+ const required =
+ additionalAssistanceSchema.properties.additionalAssistance.required;
+ expect(required).to.include('isSeniorROTCScholar');
+ });
+
+ it('agencyFundsSource schema has maxLength of 100', () => {
+ const schema =
+ additionalAssistanceSchema.properties.additionalAssistance.properties
+ .agencyFundsSource;
+ expect(schema.maxLength).to.equal(100);
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/contactInformation.js b/src/applications/education/22-1990n/config/chapters/contactInformation.js
new file mode 100644
index 000000000000..39f8ffa4f8a4
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/contactInformation.js
@@ -0,0 +1,172 @@
+import {
+ textUI,
+ textSchema,
+ emailUI,
+ emailSchema,
+ phoneUI,
+ phoneSchema,
+ selectUI,
+ selectSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+const stateLabels = {
+ AL: 'Alabama',
+ AK: 'Alaska',
+ AZ: 'Arizona',
+ AR: 'Arkansas',
+ CA: 'California',
+ CO: 'Colorado',
+ CT: 'Connecticut',
+ DC: 'District of Columbia',
+ DE: 'Delaware',
+ FL: 'Florida',
+ GA: 'Georgia',
+ HI: 'Hawaii',
+ ID: 'Idaho',
+ IL: 'Illinois',
+ IN: 'Indiana',
+ IA: 'Iowa',
+ KS: 'Kansas',
+ KY: 'Kentucky',
+ LA: 'Louisiana',
+ ME: 'Maine',
+ MD: 'Maryland',
+ MA: 'Massachusetts',
+ MI: 'Michigan',
+ MN: 'Minnesota',
+ MS: 'Mississippi',
+ MO: 'Missouri',
+ MT: 'Montana',
+ NE: 'Nebraska',
+ NV: 'Nevada',
+ NH: 'New Hampshire',
+ NJ: 'New Jersey',
+ NM: 'New Mexico',
+ NY: 'New York',
+ NC: 'North Carolina',
+ ND: 'North Dakota',
+ OH: 'Ohio',
+ OK: 'Oklahoma',
+ OR: 'Oregon',
+ PA: 'Pennsylvania',
+ RI: 'Rhode Island',
+ SC: 'South Carolina',
+ SD: 'South Dakota',
+ TN: 'Tennessee',
+ TX: 'Texas',
+ UT: 'Utah',
+ VT: 'Vermont',
+ VA: 'Virginia',
+ WA: 'Washington',
+ WV: 'West Virginia',
+ WI: 'Wisconsin',
+ WY: 'Wyoming',
+ PR: 'Puerto Rico',
+ GU: 'Guam',
+ VI: 'U.S. Virgin Islands',
+ AS: 'American Samoa',
+ MP: 'Northern Mariana Islands',
+ AA: 'Armed Forces Americas (AA)',
+ AE: 'Armed Forces Europe (AE)',
+ AP: 'Armed Forces Pacific (AP)',
+};
+
+const STATE_KEYS = Object.keys(stateLabels);
+
+export const contactInformationUiSchema = {
+ contactInformation: {
+ 'ui:title': 'Contact information',
+ mailingAddress: {
+ 'ui:title': 'Mailing address',
+ street: textUI({
+ title: 'Street address (number and street)',
+ hint: 'Enter the address where you want VA to send mail about your application.',
+ autocomplete: 'street-address',
+ errorMessages: {
+ required: 'Please enter your street address.',
+ },
+ }),
+ street2: textUI({
+ title: 'Apartment or unit number',
+ autocomplete: 'address-line2',
+ }),
+ city: textUI({
+ title: 'City',
+ autocomplete: 'address-level2',
+ errorMessages: {
+ required: 'Please enter your city.',
+ },
+ }),
+ state: selectUI({
+ title: 'State',
+ labels: stateLabels,
+ errorMessages: {
+ required: 'Please select a state.',
+ },
+ }),
+ postalCode: textUI({
+ title: 'ZIP code',
+ autocomplete: 'postal-code',
+ errorMessages: {
+ required: 'Please enter your ZIP code.',
+ pattern: 'Please enter a valid 5- or 9-digit ZIP code.',
+ },
+ }),
+ },
+ homePhone: phoneUI({
+ title: 'Home phone number (include area code)',
+ hint: 'Enter your 10-digit phone number including area code.',
+ }),
+ mobilePhone: phoneUI({
+ title: 'Mobile phone number (include area code)',
+ hint: 'Optional. Enter your 10-digit mobile phone number including area code.',
+ }),
+ email: emailUI({
+ title: 'Email address',
+ hint: "We'll use this email address to send you a confirmation when your application is submitted.",
+ }),
+ },
+};
+
+export const contactInformationSchema = {
+ type: 'object',
+ required: ['contactInformation'],
+ properties: {
+ contactInformation: {
+ type: 'object',
+ required: ['mailingAddress'],
+ properties: {
+ mailingAddress: {
+ type: 'object',
+ required: ['street', 'city', 'state', 'postalCode'],
+ properties: {
+ street: {
+ type: 'string',
+ maxLength: 50,
+ minLength: 1,
+ },
+ street2: {
+ type: 'string',
+ maxLength: 20,
+ },
+ city: {
+ type: 'string',
+ maxLength: 30,
+ minLength: 1,
+ },
+ state: selectSchema(STATE_KEYS),
+ postalCode: {
+ type: 'string',
+ pattern: '^\\d{5}(-\\d{4})?$',
+ minLength: 5,
+ maxLength: 10,
+ },
+ },
+ },
+ homePhone: phoneSchema,
+ mobilePhone: phoneSchema,
+ email: emailSchema,
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/contactInformation.unit.spec.js b/src/applications/education/22-1990n/config/chapters/contactInformation.unit.spec.js
new file mode 100644
index 000000000000..89e0f8bf87ec
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/contactInformation.unit.spec.js
@@ -0,0 +1,50 @@
+import { expect } from 'chai';
+import {
+ contactInformationUiSchema,
+ contactInformationSchema,
+} from './contactInformation';
+
+describe('config/chapters/contactInformation', () => {
+ it('uiSchema has mailingAddress fields', () => {
+ const ci = contactInformationUiSchema.contactInformation;
+ expect(ci).to.have.property('mailingAddress');
+ expect(ci.mailingAddress).to.have.property('street');
+ expect(ci.mailingAddress).to.have.property('city');
+ expect(ci.mailingAddress).to.have.property('state');
+ expect(ci.mailingAddress).to.have.property('postalCode');
+ });
+
+ it('uiSchema has phone and email fields', () => {
+ const ci = contactInformationUiSchema.contactInformation;
+ expect(ci).to.have.property('homePhone');
+ expect(ci).to.have.property('mobilePhone');
+ expect(ci).to.have.property('email');
+ });
+
+ it('schema requires mailingAddress', () => {
+ const required =
+ contactInformationSchema.properties.contactInformation.required;
+ expect(required).to.include('mailingAddress');
+ });
+
+ it('mailingAddress schema requires street, city, state, postalCode', () => {
+ const required =
+ contactInformationSchema.properties.contactInformation.properties
+ .mailingAddress.required;
+ expect(required).to.include('street');
+ expect(required).to.include('city');
+ expect(required).to.include('state');
+ expect(required).to.include('postalCode');
+ });
+
+ it('state schema includes all 50 states', () => {
+ const stateEnum =
+ contactInformationSchema.properties.contactInformation.properties
+ .mailingAddress.properties.state.enum;
+ expect(stateEnum).to.include('CA');
+ expect(stateEnum).to.include('NY');
+ expect(stateEnum).to.include('TX');
+ expect(stateEnum).to.include('AP');
+ expect(stateEnum).to.include('PR');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/directDeposit.js b/src/applications/education/22-1990n/config/chapters/directDeposit.js
new file mode 100644
index 000000000000..8c8eafeb9b81
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/directDeposit.js
@@ -0,0 +1,84 @@
+import {
+ radioUI,
+ radioSchema,
+ textUI,
+ textSchema,
+ yesNoUI,
+ yesNoSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const directDepositUiSchema = {
+ directDepositEnrolling: yesNoUI({
+ title: 'Would you like to enroll in direct deposit?',
+ hint: 'VA will deposit your education benefit payments directly into your bank account.',
+ labels: {
+ Y: 'Yes, enroll in direct deposit',
+ N: 'No, I will receive a paper check',
+ },
+ errorMessages: {
+ required: 'Please indicate whether you want to enroll in direct deposit.',
+ },
+ }),
+ directDeposit: {
+ 'ui:title': 'Bank account information',
+ 'ui:options': {
+ expandUnder: 'directDepositEnrolling',
+ expandUnderCondition: true,
+ },
+ accountType: radioUI({
+ title: 'Account type',
+ hint: "Select the type of bank account where you'd like VA to deposit your education benefits.",
+ labels: {
+ CHECKING: 'Checking',
+ SAVINGS: 'Savings',
+ },
+ required: formData => formData?.directDepositEnrolling === true,
+ errorMessages: {
+ required: 'Please select your account type.',
+ },
+ }),
+ routingNumber: textUI({
+ title: 'Routing or transit number',
+ hint: 'Your 9-digit routing number is printed at the bottom left of your checks or on your deposit slip.',
+ inputType: 'text',
+ errorMessages: {
+ required: 'Please enter your routing number.',
+ pattern: 'Please enter a valid 9-digit routing number.',
+ },
+ }),
+ accountNumber: textUI({
+ title: 'Account number',
+ hint: 'Your account number is printed at the bottom of your checks, after the routing number. Do not include the check number.',
+ inputType: 'text',
+ errorMessages: {
+ required: 'Please enter your account number.',
+ pattern: 'Please enter a valid account number (up to 17 digits).',
+ },
+ }),
+ },
+};
+
+export const directDepositSchema = {
+ type: 'object',
+ properties: {
+ directDepositEnrolling: yesNoSchema,
+ directDeposit: {
+ type: 'object',
+ properties: {
+ accountType: radioSchema(['CHECKING', 'SAVINGS']),
+ routingNumber: {
+ type: 'string',
+ pattern: '^\\d{9}$',
+ minLength: 9,
+ maxLength: 9,
+ },
+ accountNumber: {
+ type: 'string',
+ pattern: '^\\d{1,17}$',
+ minLength: 1,
+ maxLength: 17,
+ },
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/directDeposit.unit.spec.js b/src/applications/education/22-1990n/config/chapters/directDeposit.unit.spec.js
new file mode 100644
index 000000000000..02094e249bee
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/directDeposit.unit.spec.js
@@ -0,0 +1,55 @@
+import { expect } from 'chai';
+import {
+ directDepositUiSchema,
+ directDepositSchema,
+} from './directDeposit';
+
+describe('config/chapters/directDeposit', () => {
+ it('uiSchema has directDepositEnrolling field', () => {
+ expect(directDepositUiSchema).to.have.property('directDepositEnrolling');
+ });
+
+ it('uiSchema has directDeposit nested fields', () => {
+ expect(directDepositUiSchema).to.have.property('directDeposit');
+ expect(directDepositUiSchema.directDeposit).to.have.property(
+ 'accountType',
+ );
+ expect(directDepositUiSchema.directDeposit).to.have.property(
+ 'routingNumber',
+ );
+ expect(directDepositUiSchema.directDeposit).to.have.property(
+ 'accountNumber',
+ );
+ });
+
+ it('schema has directDepositEnrolling as boolean', () => {
+ expect(
+ directDepositSchema.properties.directDepositEnrolling,
+ ).to.deep.equal({ type: 'boolean' });
+ });
+
+ it('accountType schema has CHECKING and SAVINGS enum', () => {
+ const enumValues =
+ directDepositSchema.properties.directDeposit.properties.accountType
+ .enum;
+ expect(enumValues).to.include('CHECKING');
+ expect(enumValues).to.include('SAVINGS');
+ });
+
+ it('routingNumber schema has 9-digit pattern', () => {
+ const rn =
+ directDepositSchema.properties.directDeposit.properties
+ .routingNumber;
+ expect(rn.pattern).to.equal('^\\d{9}$');
+ expect(rn.maxLength).to.equal(9);
+ });
+
+ it('directDeposit accountType required function returns true when enrolling', () => {
+ const reqFn =
+ directDepositUiSchema.directDeposit.accountType['ui:required'];
+ if (reqFn) {
+ expect(reqFn({ directDepositEnrolling: true })).to.equal(true);
+ expect(reqFn({ directDepositEnrolling: false })).to.equal(false);
+ }
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/educationObjective.js b/src/applications/education/22-1990n/config/chapters/educationObjective.js
new file mode 100644
index 000000000000..8c9db1f841a5
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/educationObjective.js
@@ -0,0 +1,46 @@
+import {
+ textareaUI,
+ textareaSchema,
+ checkboxGroupUI,
+ checkboxGroupSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+const AUTHORIZATION_LABELS = {
+ highestRateAuthorization:
+ 'If during the review made by VA I am found to be eligible for more than one benefit, I authorize VA to pay the benefit with the highest monthly rate.',
+};
+
+const AUTHORIZATION_KEYS = Object.keys(AUTHORIZATION_LABELS);
+
+export const educationObjectiveUiSchema = {
+ trainingProgram: {
+ educationObjective: textareaUI({
+ title: 'Educational or career objective',
+ hint: 'Describe your educational or career goal. For example: Bachelor of Arts in Accounting, welding certificate, police officer.',
+ charcount: true,
+ }),
+ highestRateAuthorizationGroup: checkboxGroupUI({
+ title: 'Authorization for highest benefit rate',
+ hint: 'Check this box if you want VA to automatically pay you at the highest rate if you qualify for more than one benefit.',
+ required: false,
+ labels: AUTHORIZATION_LABELS,
+ }),
+ },
+};
+
+export const educationObjectiveSchema = {
+ type: 'object',
+ required: ['trainingProgram'],
+ properties: {
+ trainingProgram: {
+ type: 'object',
+ properties: {
+ educationObjective: {
+ type: 'string',
+ maxLength: 500,
+ },
+ highestRateAuthorizationGroup: checkboxGroupSchema(AUTHORIZATION_KEYS),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/educationObjective.unit.spec.js b/src/applications/education/22-1990n/config/chapters/educationObjective.unit.spec.js
new file mode 100644
index 000000000000..7204d7d7b8ca
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/educationObjective.unit.spec.js
@@ -0,0 +1,44 @@
+import { expect } from 'chai';
+import {
+ educationObjectiveUiSchema,
+ educationObjectiveSchema,
+} from './educationObjective';
+
+describe('config/chapters/educationObjective', () => {
+ it('uiSchema has trainingProgram.educationObjective', () => {
+ expect(educationObjectiveUiSchema).to.have.property('trainingProgram');
+ expect(educationObjectiveUiSchema.trainingProgram).to.have.property(
+ 'educationObjective',
+ );
+ });
+
+ it('uiSchema has highestRateAuthorizationGroup', () => {
+ expect(
+ educationObjectiveUiSchema.trainingProgram,
+ ).to.have.property('highestRateAuthorizationGroup');
+ });
+
+ it('schema has educationObjective with maxLength 500', () => {
+ const eoSchema =
+ educationObjectiveSchema.properties.trainingProgram.properties
+ .educationObjective;
+ expect(eoSchema.maxLength).to.equal(500);
+ });
+
+ it('educationObjective is not required', () => {
+ const required =
+ educationObjectiveSchema.properties.trainingProgram.required;
+ expect(required || []).not.to.include('educationObjective');
+ });
+
+ it('highestRateAuthorizationGroup schema has the authorization key', () => {
+ const hraSchema =
+ educationObjectiveSchema.properties.trainingProgram.properties
+ .highestRateAuthorizationGroup;
+ expect(hraSchema).to.exist;
+ expect(hraSchema.type).to.equal('object');
+ expect(hraSchema.properties).to.have.property(
+ 'highestRateAuthorization',
+ );
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/eligibilityScreener.js b/src/applications/education/22-1990n/config/chapters/eligibilityScreener.js
new file mode 100644
index 000000000000..968d042e45e0
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/eligibilityScreener.js
@@ -0,0 +1,68 @@
+import {
+ radioUI,
+ radioSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const eligibilityScreenerUiSchema = {
+ eligibilityScreener: {
+ 'ui:title': 'Check your eligibility',
+ enteredServiceOnOrAfterOct2003: radioUI({
+ title: 'Did you first enter military service on or after October 1, 2003?',
+ hint: 'This is the date you first reported to your branch of service for initial active duty training.',
+ labels: {
+ Y: 'Yes',
+ N: 'No',
+ },
+ errorMessages: {
+ required: 'Please select yes or no.',
+ },
+ }),
+ signedNCSContract: radioUI({
+ title:
+ 'Did you sign an enlistment contract with the Department of Defense (DoD) specifically under the National Call to Service (NCS) program?',
+ hint:
+ 'The NCS program is authorized under Section 510, Title 10, U.S. Code. Your contract would have been for a short-term enlistment (typically 15 months of active duty) under a specific NCS recruitment option. If you are not sure, check your enlistment paperwork or DD Form 2863.',
+ labels: {
+ Y: 'Yes',
+ N: 'No',
+ },
+ errorMessages: {
+ required: 'Please select yes or no.',
+ },
+ }),
+ electedEducationIncentive: radioUI({
+ title:
+ 'Did you elect one of the education incentives on your DD Form 2863 (National Call to Service Election of Options)?',
+ hint:
+ 'DD Form 2863 is the form you completed when you chose your NCS incentive option. If you elected a cash bonus rather than an education incentive, VA Form 22-1990n is not the correct form for you.',
+ labels: {
+ YES: 'Yes',
+ NO: 'No',
+ NOT_SURE: "I'm not sure",
+ },
+ errorMessages: {
+ required: 'Please select an answer.',
+ },
+ }),
+ },
+};
+
+export const eligibilityScreenerSchema = {
+ type: 'object',
+ required: ['eligibilityScreener'],
+ properties: {
+ eligibilityScreener: {
+ type: 'object',
+ required: [
+ 'enteredServiceOnOrAfterOct2003',
+ 'signedNCSContract',
+ 'electedEducationIncentive',
+ ],
+ properties: {
+ enteredServiceOnOrAfterOct2003: radioSchema(['Y', 'N']),
+ signedNCSContract: radioSchema(['Y', 'N']),
+ electedEducationIncentive: radioSchema(['YES', 'NO', 'NOT_SURE']),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/eligibilityScreener.unit.spec.js b/src/applications/education/22-1990n/config/chapters/eligibilityScreener.unit.spec.js
new file mode 100644
index 000000000000..9c42bf5152a2
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/eligibilityScreener.unit.spec.js
@@ -0,0 +1,46 @@
+import { expect } from 'chai';
+import {
+ eligibilityScreenerUiSchema,
+ eligibilityScreenerSchema,
+} from './eligibilityScreener';
+
+describe('config/chapters/eligibilityScreener', () => {
+ it('uiSchema has eligibilityScreener nested keys', () => {
+ expect(eligibilityScreenerUiSchema).to.have.property(
+ 'eligibilityScreener',
+ );
+ expect(
+ eligibilityScreenerUiSchema.eligibilityScreener,
+ ).to.have.property('enteredServiceOnOrAfterOct2003');
+ expect(
+ eligibilityScreenerUiSchema.eligibilityScreener,
+ ).to.have.property('signedNCSContract');
+ expect(
+ eligibilityScreenerUiSchema.eligibilityScreener,
+ ).to.have.property('electedEducationIncentive');
+ });
+
+ it('schema has correct structure', () => {
+ expect(eligibilityScreenerSchema.type).to.equal('object');
+ expect(eligibilityScreenerSchema.properties).to.have.property(
+ 'eligibilityScreener',
+ );
+ });
+
+ it('electedEducationIncentive schema includes NOT_SURE option', () => {
+ const enumValues =
+ eligibilityScreenerSchema.properties.eligibilityScreener.properties
+ .electedEducationIncentive.enum;
+ expect(enumValues).to.include('NOT_SURE');
+ expect(enumValues).to.include('YES');
+ expect(enumValues).to.include('NO');
+ });
+
+ it('required fields are listed in schema', () => {
+ const required =
+ eligibilityScreenerSchema.properties.eligibilityScreener.required;
+ expect(required).to.include('enteredServiceOnOrAfterOct2003');
+ expect(required).to.include('signedNCSContract');
+ expect(required).to.include('electedEducationIncentive');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.js b/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.js
new file mode 100644
index 000000000000..124623cf0153
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.js
@@ -0,0 +1,47 @@
+import {
+ radioUI,
+ radioSchema,
+ yesNoUI,
+ yesNoSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const flightTrainingRequirementsUiSchema = {
+ flightTraining: {
+ 'ui:title': 'Flight training requirements',
+ hasPrivatePilotLicense: yesNoUI({
+ title: 'Do you currently hold a private pilot\'s license?',
+ hint: "Per VA regulations, you must already have a private pilot's license to receive vocational flight training benefits.",
+ errorMessages: {
+ required:
+ "Please indicate whether you hold a private pilot's license.",
+ },
+ }),
+ isATPCourse: radioUI({
+ title: 'What type of flight training course are you pursuing?',
+ hint: 'Your answer determines the class of medical certificate required when you enter training.',
+ labels: {
+ Y: 'Airline Transport Pilot (ATP) course',
+ N: 'Other flight training course',
+ },
+ required: formData =>
+ formData?.flightTraining?.hasPrivatePilotLicense === true,
+ errorMessages: {
+ required: 'Please select your course type.',
+ },
+ }),
+ },
+};
+
+export const flightTrainingRequirementsSchema = {
+ type: 'object',
+ properties: {
+ flightTraining: {
+ type: 'object',
+ required: ['hasPrivatePilotLicense'],
+ properties: {
+ hasPrivatePilotLicense: yesNoSchema,
+ isATPCourse: radioSchema(['Y', 'N']),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.unit.spec.js b/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.unit.spec.js
new file mode 100644
index 000000000000..e03755ae6061
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/flightTrainingRequirements.unit.spec.js
@@ -0,0 +1,61 @@
+import { expect } from 'chai';
+import {
+ flightTrainingRequirementsUiSchema,
+ flightTrainingRequirementsSchema,
+} from './flightTrainingRequirements';
+
+describe('config/chapters/flightTrainingRequirements', () => {
+ it('uiSchema has flightTraining with hasPrivatePilotLicense', () => {
+ expect(flightTrainingRequirementsUiSchema).to.have.property(
+ 'flightTraining',
+ );
+ expect(
+ flightTrainingRequirementsUiSchema.flightTraining,
+ ).to.have.property('hasPrivatePilotLicense');
+ });
+
+ it('uiSchema has isATPCourse field', () => {
+ expect(
+ flightTrainingRequirementsUiSchema.flightTraining,
+ ).to.have.property('isATPCourse');
+ });
+
+ it('schema has flightTraining with hasPrivatePilotLicense', () => {
+ expect(flightTrainingRequirementsSchema.properties).to.have.property(
+ 'flightTraining',
+ );
+ expect(
+ flightTrainingRequirementsSchema.properties.flightTraining.properties,
+ ).to.have.property('hasPrivatePilotLicense');
+ });
+
+ it('hasPrivatePilotLicense schema is boolean', () => {
+ const schema =
+ flightTrainingRequirementsSchema.properties.flightTraining.properties
+ .hasPrivatePilotLicense;
+ expect(schema.type).to.equal('boolean');
+ });
+
+ it('isATPCourse schema has enum with Y and N', () => {
+ const schema =
+ flightTrainingRequirementsSchema.properties.flightTraining.properties
+ .isATPCourse;
+ expect(schema.enum).to.include('Y');
+ expect(schema.enum).to.include('N');
+ });
+
+ it('isATPCourse required function checks hasPrivatePilotLicense', () => {
+ const reqFn =
+ flightTrainingRequirementsUiSchema.flightTraining.isATPCourse[
+ 'ui:required'
+ ];
+ if (reqFn) {
+ expect(
+ reqFn({ flightTraining: { hasPrivatePilotLicense: true } }),
+ ).to.equal(true);
+ expect(
+ reqFn({ flightTraining: { hasPrivatePilotLicense: false } }),
+ ).to.equal(false);
+ }
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/personalInformation.js b/src/applications/education/22-1990n/config/chapters/personalInformation.js
new file mode 100644
index 000000000000..0dc9c1d05c01
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/personalInformation.js
@@ -0,0 +1,99 @@
+import {
+ textUI,
+ textSchema,
+ radioUI,
+ radioSchema,
+ currentOrPastDateUI,
+ currentOrPastDateSchema,
+ ssnUI,
+ ssnSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const personalInformationUiSchema = {
+ personalInformation: {
+ 'ui:title': 'Personal information',
+ applicantSSN: {
+ ...ssnUI(),
+ 'ui:title': 'Social Security number',
+ 'ui:options': {
+ hint: 'We use your Social Security number to match your application to your VA records.',
+ widgetClassNames: 'field-medium',
+ },
+ },
+ applicantSex: radioUI({
+ title: 'Sex',
+ hint: 'This information is used for VA records processing.',
+ labels: {
+ F: 'Female',
+ M: 'Male',
+ },
+ errorMessages: {
+ required: 'Please select your sex.',
+ },
+ }),
+ applicantDOB: currentOrPastDateUI({
+ title: 'Date of birth',
+ hint: 'Enter your date of birth as shown on your government-issued ID.',
+ errorMessages: {
+ required: 'Please enter your date of birth.',
+ futureDate: 'Date of birth must be in the past.',
+ },
+ }),
+ applicantFirstName: textUI({
+ title: 'First name',
+ hint: 'Enter your name exactly as it appears on your military service records.',
+ autocomplete: 'given-name',
+ errorMessages: {
+ required: 'Please enter your first name.',
+ },
+ }),
+ applicantMiddleInitial: textUI({
+ title: 'Middle initial',
+ autocomplete: 'additional-name',
+ }),
+ applicantLastName: textUI({
+ title: 'Last name',
+ hint: 'Enter your name exactly as it appears on your military service records.',
+ autocomplete: 'family-name',
+ errorMessages: {
+ required: 'Please enter your last name.',
+ },
+ }),
+ },
+};
+
+export const personalInformationSchema = {
+ type: 'object',
+ required: ['personalInformation'],
+ properties: {
+ personalInformation: {
+ type: 'object',
+ required: [
+ 'applicantSSN',
+ 'applicantSex',
+ 'applicantDOB',
+ 'applicantFirstName',
+ 'applicantLastName',
+ ],
+ properties: {
+ applicantSSN: ssnSchema,
+ applicantSex: radioSchema(['F', 'M']),
+ applicantDOB: currentOrPastDateSchema,
+ applicantFirstName: {
+ type: 'string',
+ maxLength: 30,
+ minLength: 1,
+ },
+ applicantMiddleInitial: {
+ type: 'string',
+ maxLength: 1,
+ },
+ applicantLastName: {
+ type: 'string',
+ maxLength: 35,
+ minLength: 1,
+ },
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/personalInformation.unit.spec.js b/src/applications/education/22-1990n/config/chapters/personalInformation.unit.spec.js
new file mode 100644
index 000000000000..c3e2833255cc
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/personalInformation.unit.spec.js
@@ -0,0 +1,44 @@
+import { expect } from 'chai';
+import {
+ personalInformationUiSchema,
+ personalInformationSchema,
+} from './personalInformation';
+
+describe('config/chapters/personalInformation', () => {
+ it('uiSchema has required fields', () => {
+ const pi = personalInformationUiSchema.personalInformation;
+ expect(pi).to.have.property('applicantSSN');
+ expect(pi).to.have.property('applicantSex');
+ expect(pi).to.have.property('applicantDOB');
+ expect(pi).to.have.property('applicantFirstName');
+ expect(pi).to.have.property('applicantLastName');
+ });
+
+ it('schema required array includes applicantSSN', () => {
+ const required =
+ personalInformationSchema.properties.personalInformation.required;
+ expect(required).to.include('applicantSSN');
+ });
+
+ it('applicantSex schema has enum with F and M', () => {
+ const sexEnum =
+ personalInformationSchema.properties.personalInformation.properties
+ .applicantSex.enum;
+ expect(sexEnum).to.include('F');
+ expect(sexEnum).to.include('M');
+ expect(sexEnum).to.have.lengthOf(2);
+ });
+
+ it('applicantFirstName schema has maxLength of 30', () => {
+ const firstNameSchema =
+ personalInformationSchema.properties.personalInformation.properties
+ .applicantFirstName;
+ expect(firstNameSchema.maxLength).to.equal(30);
+ });
+
+ it('applicantMiddleInitial is not in required array', () => {
+ const required =
+ personalInformationSchema.properties.personalInformation.required;
+ expect(required).not.to.include('applicantMiddleInitial');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/schoolInformation.js b/src/applications/education/22-1990n/config/chapters/schoolInformation.js
new file mode 100644
index 000000000000..bfa4dae8b765
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/schoolInformation.js
@@ -0,0 +1,134 @@
+import {
+ textUI,
+ textSchema,
+ selectUI,
+ selectSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+const stateLabels = {
+ AL: 'Alabama',
+ AK: 'Alaska',
+ AZ: 'Arizona',
+ AR: 'Arkansas',
+ CA: 'California',
+ CO: 'Colorado',
+ CT: 'Connecticut',
+ DC: 'District of Columbia',
+ DE: 'Delaware',
+ FL: 'Florida',
+ GA: 'Georgia',
+ HI: 'Hawaii',
+ ID: 'Idaho',
+ IL: 'Illinois',
+ IN: 'Indiana',
+ IA: 'Iowa',
+ KS: 'Kansas',
+ KY: 'Kentucky',
+ LA: 'Louisiana',
+ ME: 'Maine',
+ MD: 'Maryland',
+ MA: 'Massachusetts',
+ MI: 'Michigan',
+ MN: 'Minnesota',
+ MS: 'Mississippi',
+ MO: 'Missouri',
+ MT: 'Montana',
+ NE: 'Nebraska',
+ NV: 'Nevada',
+ NH: 'New Hampshire',
+ NJ: 'New Jersey',
+ NM: 'New Mexico',
+ NY: 'New York',
+ NC: 'North Carolina',
+ ND: 'North Dakota',
+ OH: 'Ohio',
+ OK: 'Oklahoma',
+ OR: 'Oregon',
+ PA: 'Pennsylvania',
+ RI: 'Rhode Island',
+ SC: 'South Carolina',
+ SD: 'South Dakota',
+ TN: 'Tennessee',
+ TX: 'Texas',
+ UT: 'Utah',
+ VT: 'Vermont',
+ VA: 'Virginia',
+ WA: 'Washington',
+ WV: 'West Virginia',
+ WI: 'Wisconsin',
+ WY: 'Wyoming',
+ PR: 'Puerto Rico',
+ GU: 'Guam',
+ VI: 'U.S. Virgin Islands',
+ AS: 'American Samoa',
+ MP: 'Northern Mariana Islands',
+ AA: 'Armed Forces Americas (AA)',
+ AE: 'Armed Forces Europe (AE)',
+ AP: 'Armed Forces Pacific (AP)',
+};
+
+const STATE_KEYS = Object.keys(stateLabels);
+
+export const schoolInformationUiSchema = {
+ trainingProgram: {
+ schoolName: textUI({
+ title: 'Name of school or training establishment',
+ hint: 'If you have already selected a school or training program, enter its full name. Your school\'s address will be used to route your application to the correct VA Regional Processing Office.',
+ }),
+ schoolAddress: {
+ 'ui:title': 'School address',
+ street: textUI({
+ title: 'Street address',
+ hint: "Entering your school's address helps VA route your application to the correct processing office.",
+ }),
+ city: textUI({
+ title: 'City',
+ }),
+ state: selectUI({
+ title: 'State',
+ labels: stateLabels,
+ }),
+ postalCode: textUI({
+ title: 'ZIP code',
+ errorMessages: {
+ pattern: 'Please enter a valid 5- or 9-digit ZIP code.',
+ },
+ }),
+ },
+ },
+};
+
+export const schoolInformationSchema = {
+ type: 'object',
+ required: ['trainingProgram'],
+ properties: {
+ trainingProgram: {
+ type: 'object',
+ properties: {
+ schoolName: {
+ type: 'string',
+ maxLength: 100,
+ },
+ schoolAddress: {
+ type: 'object',
+ properties: {
+ street: {
+ type: 'string',
+ maxLength: 50,
+ },
+ city: {
+ type: 'string',
+ maxLength: 30,
+ },
+ state: selectSchema(STATE_KEYS),
+ postalCode: {
+ type: 'string',
+ pattern: '^\\d{5}(-\\d{4})?$',
+ maxLength: 10,
+ },
+ },
+ },
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/schoolInformation.unit.spec.js b/src/applications/education/22-1990n/config/chapters/schoolInformation.unit.spec.js
new file mode 100644
index 000000000000..0c9f0bf601ef
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/schoolInformation.unit.spec.js
@@ -0,0 +1,44 @@
+import { expect } from 'chai';
+import {
+ schoolInformationUiSchema,
+ schoolInformationSchema,
+} from './schoolInformation';
+
+describe('config/chapters/schoolInformation', () => {
+ it('uiSchema has trainingProgram.schoolName', () => {
+ expect(schoolInformationUiSchema).to.have.property('trainingProgram');
+ expect(schoolInformationUiSchema.trainingProgram).to.have.property(
+ 'schoolName',
+ );
+ });
+
+ it('uiSchema has trainingProgram.schoolAddress', () => {
+ expect(schoolInformationUiSchema.trainingProgram).to.have.property(
+ 'schoolAddress',
+ );
+ });
+
+ it('schema has trainingProgram with optional schoolName', () => {
+ const props =
+ schoolInformationSchema.properties.trainingProgram.properties;
+ expect(props).to.have.property('schoolName');
+ expect(props).to.have.property('schoolAddress');
+ });
+
+ it('schoolName is not required in schema (optional field)', () => {
+ const required =
+ schoolInformationSchema.properties.trainingProgram.required;
+ // schoolName should NOT be in required since it is optional per PDF
+ expect(required || []).not.to.include('schoolName');
+ });
+
+ it('schoolAddress has street, city, state, postalCode properties', () => {
+ const addrProps =
+ schoolInformationSchema.properties.trainingProgram.properties
+ .schoolAddress.properties;
+ expect(addrProps).to.have.property('street');
+ expect(addrProps).to.have.property('city');
+ expect(addrProps).to.have.property('state');
+ expect(addrProps).to.have.property('postalCode');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/servicePeriods.js b/src/applications/education/22-1990n/config/chapters/servicePeriods.js
new file mode 100644
index 000000000000..09c4e41242e4
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/servicePeriods.js
@@ -0,0 +1,122 @@
+import React from 'react';
+import {
+ currentOrPastDateUI,
+ currentOrPastDateSchema,
+ selectUI,
+ selectSchema,
+ textUI,
+} from 'platform/forms-system/src/js/web-component-patterns';
+import VaMemorableDateField from 'platform/forms-system/src/js/web-component-fields/VaMemorableDateField';
+
+const SERVICE_COMPONENT_LABELS = {
+ USA: 'U.S. Army (USA)',
+ USN: 'U.S. Navy (USN)',
+ USAF: 'U.S. Air Force (USAF)',
+ USMC: 'U.S. Marine Corps (USMC)',
+ USCG: 'U.S. Coast Guard (USCG)',
+ USAR: 'U.S. Army Reserve (USAR)',
+ ARNG: 'U.S. Army National Guard (ARNG)',
+ USNR: 'U.S. Naval Reserve (USNR)',
+ AFRES: 'U.S. Air Force Reserve (AFRES)',
+ ANG: 'U.S. Air National Guard (ANG)',
+ USMCR: 'U.S. Marine Corps Reserve (USMCR)',
+ CGRES: 'U.S. Coast Guard Reserve (CGRES)',
+};
+
+const SERVICE_COMPONENT_KEYS = Object.keys(SERVICE_COMPONENT_LABELS);
+
+function ServicePeriodViewField({ formData }) {
+ const { serviceComponent, dateEnteredService } = formData || {};
+ return (
+
+
+ {SERVICE_COMPONENT_LABELS[serviceComponent] || serviceComponent}
+
+ {dateEnteredService &&
Entered: {dateEnteredService}
}
+
+ );
+}
+
+export const servicePeriodsUiSchema = {
+ serviceInformation: {
+ servicePeriods: {
+ 'ui:title': 'Service periods',
+ 'ui:options': {
+ itemName: 'Service period',
+ viewField: ServicePeriodViewField,
+ addAnotherLabel: 'Add another service period',
+ keepInPageOnReview: true,
+ },
+ items: {
+ dateEnteredService: currentOrPastDateUI({
+ title: 'Date entered service',
+ hint: 'Enter the date you began your period of military service.',
+ errorMessages: {
+ required: 'Please enter the date you entered service.',
+ },
+ }),
+ dateSeparated: {
+ 'ui:title': 'Date separated from service',
+ 'ui:webComponentField': VaMemorableDateField,
+ 'ui:options': {
+ hint: 'Leave blank if you are still on active duty.',
+ },
+ },
+ serviceComponent: selectUI({
+ title: 'Service component',
+ hint: 'Select the branch and component you served in during this period.',
+ labels: SERVICE_COMPONENT_LABELS,
+ errorMessages: {
+ required: 'Please select your service component.',
+ },
+ }),
+ serviceStatus: textUI({
+ title: 'Service status',
+ hint: 'Describe your service status during this period. Examples: Active duty, Drilling reservist, IRR',
+ errorMessages: {
+ required: 'Please enter your service status.',
+ },
+ }),
+ },
+ },
+ },
+};
+
+export const servicePeriodsSchema = {
+ type: 'object',
+ required: ['serviceInformation'],
+ properties: {
+ serviceInformation: {
+ type: 'object',
+ required: ['servicePeriods'],
+ properties: {
+ servicePeriods: {
+ type: 'array',
+ minItems: 1,
+ maxItems: 10,
+ items: {
+ type: 'object',
+ required: [
+ 'dateEnteredService',
+ 'serviceComponent',
+ 'serviceStatus',
+ ],
+ properties: {
+ dateEnteredService: currentOrPastDateSchema,
+ dateSeparated: {
+ type: 'string',
+ pattern: '^\\d{4}-\\d{2}-\\d{2}$',
+ },
+ serviceComponent: selectSchema(SERVICE_COMPONENT_KEYS),
+ serviceStatus: {
+ type: 'string',
+ maxLength: 50,
+ minLength: 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/servicePeriods.unit.spec.js b/src/applications/education/22-1990n/config/chapters/servicePeriods.unit.spec.js
new file mode 100644
index 000000000000..89f8e70f6d92
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/servicePeriods.unit.spec.js
@@ -0,0 +1,50 @@
+import { expect } from 'chai';
+import {
+ servicePeriodsUiSchema,
+ servicePeriodsSchema,
+} from './servicePeriods';
+
+describe('config/chapters/servicePeriods', () => {
+ it('uiSchema has serviceInformation.servicePeriods', () => {
+ expect(servicePeriodsUiSchema).to.have.property('serviceInformation');
+ expect(servicePeriodsUiSchema.serviceInformation).to.have.property(
+ 'servicePeriods',
+ );
+ });
+
+ it('uiSchema servicePeriods has items with required fields', () => {
+ const items = servicePeriodsUiSchema.serviceInformation.servicePeriods.items;
+ expect(items).to.have.property('dateEnteredService');
+ expect(items).to.have.property('dateSeparated');
+ expect(items).to.have.property('serviceComponent');
+ expect(items).to.have.property('serviceStatus');
+ });
+
+ it('schema has servicePeriods as array', () => {
+ const spSchema =
+ servicePeriodsSchema.properties.serviceInformation.properties
+ .servicePeriods;
+ expect(spSchema.type).to.equal('array');
+ expect(spSchema.minItems).to.equal(1);
+ expect(spSchema.maxItems).to.equal(10);
+ });
+
+ it('servicePeriods items require dateEnteredService', () => {
+ const required =
+ servicePeriodsSchema.properties.serviceInformation.properties
+ .servicePeriods.items.required;
+ expect(required).to.include('dateEnteredService');
+ expect(required).to.include('serviceComponent');
+ expect(required).to.include('serviceStatus');
+ });
+
+ it('serviceComponent schema has valid enum values', () => {
+ const enumValues =
+ servicePeriodsSchema.properties.serviceInformation.properties
+ .servicePeriods.items.properties.serviceComponent.enum;
+ expect(enumValues).to.include('USA');
+ expect(enumValues).to.include('USAR');
+ expect(enumValues).to.include('ARNG');
+ expect(enumValues).to.include('USNR');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/trainingType.js b/src/applications/education/22-1990n/config/chapters/trainingType.js
new file mode 100644
index 000000000000..7ffabe5db610
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/trainingType.js
@@ -0,0 +1,45 @@
+import {
+ checkboxGroupUI,
+ checkboxGroupSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+const TRAINING_TYPE_LABELS = {
+ collegeOrSchool: 'College or other school (including online courses)',
+ apprenticeshipOJT: 'Apprenticeship or on-the-job training',
+ vocationalFlightTraining: 'Vocational flight training',
+ correspondence: 'Correspondence',
+ nationalTestReimbursement:
+ 'National test reimbursement (SAT, CLEP, etc.)',
+ licensingCertificationTest:
+ 'Licensing or certification test reimbursement (MCSE, CCNA, EMT, NCLEX, etc.)',
+};
+
+const TRAINING_TYPE_KEYS = Object.keys(TRAINING_TYPE_LABELS);
+
+export const trainingTypeUiSchema = {
+ trainingProgram: {
+ trainingType: checkboxGroupUI({
+ title: 'Type of education or training',
+ hint: 'Select all that apply.',
+ required: true,
+ labels: TRAINING_TYPE_LABELS,
+ errorMessages: {
+ required: 'Please select at least one type of education or training.',
+ },
+ }),
+ },
+};
+
+export const trainingTypeSchema = {
+ type: 'object',
+ required: ['trainingProgram'],
+ properties: {
+ trainingProgram: {
+ type: 'object',
+ required: ['trainingType'],
+ properties: {
+ trainingType: checkboxGroupSchema(TRAINING_TYPE_KEYS),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/trainingType.unit.spec.js b/src/applications/education/22-1990n/config/chapters/trainingType.unit.spec.js
new file mode 100644
index 000000000000..9146448ad2bb
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/trainingType.unit.spec.js
@@ -0,0 +1,40 @@
+import { expect } from 'chai';
+import {
+ trainingTypeUiSchema,
+ trainingTypeSchema,
+} from './trainingType';
+
+describe('config/chapters/trainingType', () => {
+ it('uiSchema has trainingProgram.trainingType', () => {
+ expect(trainingTypeUiSchema).to.have.property('trainingProgram');
+ expect(trainingTypeUiSchema.trainingProgram).to.have.property(
+ 'trainingType',
+ );
+ });
+
+ it('schema has trainingProgram with trainingType', () => {
+ expect(trainingTypeSchema.properties).to.have.property(
+ 'trainingProgram',
+ );
+ expect(
+ trainingTypeSchema.properties.trainingProgram.properties,
+ ).to.have.property('trainingType');
+ });
+
+ it('trainingType schema is an object type for checkboxGroup', () => {
+ const tt =
+ trainingTypeSchema.properties.trainingProgram.properties.trainingType;
+ expect(tt.type).to.equal('object');
+ });
+
+ it('trainingType schema has vocationalFlightTraining property', () => {
+ const props =
+ trainingTypeSchema.properties.trainingProgram.properties.trainingType
+ .properties;
+ expect(props).to.have.property('vocationalFlightTraining');
+ });
+
+ it('trainingProgram is in schema required array', () => {
+ expect(trainingTypeSchema.required).to.include('trainingProgram');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadDD214.js b/src/applications/education/22-1990n/config/chapters/uploadDD214.js
new file mode 100644
index 000000000000..817f6e67e1bc
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadDD214.js
@@ -0,0 +1,33 @@
+import {
+ fileInputMultipleUI,
+ fileInputMultipleSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const uploadDD214UiSchema = {
+ supportingDocuments: {
+ dd214Upload: fileInputMultipleUI({
+ title:
+ 'Upload your DD Form 214 (Member 4 copy) for all periods of active duty service',
+ hint: 'You must upload the Member 4 copy — not Member 1, 2, or 3. If you served multiple periods of active duty, upload a DD Form 214 for each period. Accepted file types: PDF, JPG, PNG.',
+ required: formData =>
+ formData?.serviceInformation?.isOnTerminalLeave !== true,
+ errorMessages: {
+ required:
+ 'Please upload the Member 4 copy of your DD Form 214. If you served multiple periods of active duty, upload one for each period.',
+ },
+ }),
+ },
+};
+
+export const uploadDD214Schema = {
+ type: 'object',
+ required: ['supportingDocuments'],
+ properties: {
+ supportingDocuments: {
+ type: 'object',
+ properties: {
+ dd214Upload: fileInputMultipleSchema(),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadDD214.unit.spec.js b/src/applications/education/22-1990n/config/chapters/uploadDD214.unit.spec.js
new file mode 100644
index 000000000000..47ff92e8b759
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadDD214.unit.spec.js
@@ -0,0 +1,40 @@
+import { expect } from 'chai';
+import {
+ uploadDD214UiSchema,
+ uploadDD214Schema,
+} from './uploadDD214';
+
+describe('config/chapters/uploadDD214', () => {
+ it('uiSchema has supportingDocuments.dd214Upload', () => {
+ expect(uploadDD214UiSchema).to.have.property('supportingDocuments');
+ expect(uploadDD214UiSchema.supportingDocuments).to.have.property(
+ 'dd214Upload',
+ );
+ });
+
+ it('dd214Upload uiSchema has a title mentioning DD Form 214', () => {
+ const field = uploadDD214UiSchema.supportingDocuments.dd214Upload;
+ expect(field['ui:title']).to.be.a('string');
+ expect(field['ui:title']).to.include('DD Form 214');
+ });
+
+ it('dd214Upload required function returns false when on terminal leave', () => {
+ const reqFn =
+ uploadDD214UiSchema.supportingDocuments.dd214Upload['ui:required'];
+ if (reqFn) {
+ expect(
+ reqFn({ serviceInformation: { isOnTerminalLeave: true } }),
+ ).to.equal(false);
+ expect(
+ reqFn({ serviceInformation: { isOnTerminalLeave: false } }),
+ ).to.equal(true);
+ }
+ });
+
+ it('schema has dd214Upload property', () => {
+ const schema =
+ uploadDD214Schema.properties.supportingDocuments.properties
+ .dd214Upload;
+ expect(schema).to.exist;
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadDD2863.js b/src/applications/education/22-1990n/config/chapters/uploadDD2863.js
new file mode 100644
index 000000000000..fa9fa94c675a
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadDD2863.js
@@ -0,0 +1,33 @@
+import {
+ fileInputUI,
+ fileInputSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const uploadDD2863UiSchema = {
+ supportingDocuments: {
+ dd2863Upload: fileInputUI({
+ title:
+ 'Upload your DD Form 2863 (National Call to Service Election of Options)',
+ hint: 'This document proves you elected an education incentive under the NCS program. Accepted file types: PDF, JPG, PNG. Maximum file size: 50 MB.',
+ required: true,
+ errorMessages: {
+ required:
+ 'Please upload a copy of your DD Form 2863. This document is required to process your application.',
+ },
+ }),
+ },
+};
+
+export const uploadDD2863Schema = {
+ type: 'object',
+ required: ['supportingDocuments'],
+ properties: {
+ supportingDocuments: {
+ type: 'object',
+ required: ['dd2863Upload'],
+ properties: {
+ dd2863Upload: fileInputSchema(),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadDD2863.unit.spec.js b/src/applications/education/22-1990n/config/chapters/uploadDD2863.unit.spec.js
new file mode 100644
index 000000000000..164054b01dcc
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadDD2863.unit.spec.js
@@ -0,0 +1,34 @@
+import { expect } from 'chai';
+import {
+ uploadDD2863UiSchema,
+ uploadDD2863Schema,
+} from './uploadDD2863';
+
+describe('config/chapters/uploadDD2863', () => {
+ it('uiSchema has supportingDocuments.dd2863Upload', () => {
+ expect(uploadDD2863UiSchema).to.have.property('supportingDocuments');
+ expect(
+ uploadDD2863UiSchema.supportingDocuments,
+ ).to.have.property('dd2863Upload');
+ });
+
+ it('dd2863Upload uiSchema has a title', () => {
+ const field = uploadDD2863UiSchema.supportingDocuments.dd2863Upload;
+ expect(field['ui:title']).to.be.a('string');
+ expect(field['ui:title']).to.include('DD Form 2863');
+ });
+
+ it('schema requires dd2863Upload', () => {
+ const required =
+ uploadDD2863Schema.properties.supportingDocuments.required;
+ expect(required).to.include('dd2863Upload');
+ });
+
+ it('dd2863Upload schema is an object with the expected shape', () => {
+ const schema =
+ uploadDD2863Schema.properties.supportingDocuments.properties
+ .dd2863Upload;
+ expect(schema).to.exist;
+ expect(schema.type).to.be.a('string');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.js b/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.js
new file mode 100644
index 000000000000..5968592b0c82
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.js
@@ -0,0 +1,32 @@
+import {
+ fileInputUI,
+ fileInputSchema,
+} from 'platform/forms-system/src/js/web-component-patterns';
+
+export const uploadVoidedCheckUiSchema = {
+ supportingDocuments: {
+ voidedCheckUpload: fileInputUI({
+ title:
+ 'Upload a voided check or deposit slip to verify your bank account information',
+ hint: 'Attach a voided personal check or a deposit slip that matches the routing number and account number you provided. Accepted file types: PDF, JPG, PNG.',
+ required: true,
+ errorMessages: {
+ required:
+ 'Please upload a voided check or deposit slip that matches your routing and account numbers.',
+ },
+ }),
+ },
+};
+
+export const uploadVoidedCheckSchema = {
+ type: 'object',
+ required: ['supportingDocuments'],
+ properties: {
+ supportingDocuments: {
+ type: 'object',
+ properties: {
+ voidedCheckUpload: fileInputSchema(),
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.unit.spec.js b/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.unit.spec.js
new file mode 100644
index 000000000000..1fb5ff4e78cc
--- /dev/null
+++ b/src/applications/education/22-1990n/config/chapters/uploadVoidedCheck.unit.spec.js
@@ -0,0 +1,30 @@
+import { expect } from 'chai';
+import {
+ uploadVoidedCheckUiSchema,
+ uploadVoidedCheckSchema,
+} from './uploadVoidedCheck';
+
+describe('config/chapters/uploadVoidedCheck', () => {
+ it('uiSchema has supportingDocuments.voidedCheckUpload', () => {
+ expect(uploadVoidedCheckUiSchema).to.have.property(
+ 'supportingDocuments',
+ );
+ expect(
+ uploadVoidedCheckUiSchema.supportingDocuments,
+ ).to.have.property('voidedCheckUpload');
+ });
+
+ it('voidedCheckUpload uiSchema has a title mentioning voided check', () => {
+ const field =
+ uploadVoidedCheckUiSchema.supportingDocuments.voidedCheckUpload;
+ expect(field['ui:title']).to.be.a('string');
+ expect(field['ui:title'].toLowerCase()).to.include('voided');
+ });
+
+ it('schema has voidedCheckUpload property', () => {
+ const schema =
+ uploadVoidedCheckSchema.properties.supportingDocuments.properties
+ .voidedCheckUpload;
+ expect(schema).to.exist;
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/form.js b/src/applications/education/22-1990n/config/form.js
new file mode 100644
index 000000000000..06d7f93f1468
--- /dev/null
+++ b/src/applications/education/22-1990n/config/form.js
@@ -0,0 +1,226 @@
+import environment from 'platform/utilities/environment';
+import footerContent from 'platform/forms/components/FormFooter';
+import { VA_FORM_IDS } from 'platform/forms/constants';
+
+import manifest from '../manifest.json';
+import IntroductionPage from '../containers/IntroductionPage';
+import ConfirmationPage from '../containers/ConfirmationPage';
+
+import {
+ eligibilityScreenerUiSchema,
+ eligibilityScreenerSchema,
+} from './chapters/eligibilityScreener';
+import {
+ personalInformationUiSchema,
+ personalInformationSchema,
+} from './chapters/personalInformation';
+import {
+ contactInformationUiSchema,
+ contactInformationSchema,
+} from './chapters/contactInformation';
+import {
+ directDepositUiSchema,
+ directDepositSchema,
+} from './chapters/directDeposit';
+import {
+ trainingTypeUiSchema,
+ trainingTypeSchema,
+} from './chapters/trainingType';
+import {
+ flightTrainingRequirementsUiSchema,
+ flightTrainingRequirementsSchema,
+} from './chapters/flightTrainingRequirements';
+import {
+ schoolInformationUiSchema,
+ schoolInformationSchema,
+} from './chapters/schoolInformation';
+import {
+ educationObjectiveUiSchema,
+ educationObjectiveSchema,
+} from './chapters/educationObjective';
+import {
+ activeDutyStatusUiSchema,
+ activeDutyStatusSchema,
+} from './chapters/activeDutyStatus';
+import {
+ servicePeriodsUiSchema,
+ servicePeriodsSchema,
+} from './chapters/servicePeriods';
+import {
+ additionalAssistanceUiSchema,
+ additionalAssistanceSchema,
+} from './chapters/additionalAssistance';
+import {
+ uploadDD2863UiSchema,
+ uploadDD2863Schema,
+} from './chapters/uploadDD2863';
+import {
+ uploadDD214UiSchema,
+ uploadDD214Schema,
+} from './chapters/uploadDD214';
+import {
+ uploadVoidedCheckUiSchema,
+ uploadVoidedCheckSchema,
+} from './chapters/uploadVoidedCheck';
+
+/** @type {FormConfig} */
+const formConfig = {
+ rootUrl: manifest.rootUrl,
+ urlPrefix: '/',
+ submitUrl: `${environment.API_URL}/v0/education_benefits_claims/1990n`,
+ trackingPrefix: 'edu-1990n-',
+ v3SegmentedProgressBar: true,
+ introduction: IntroductionPage,
+ confirmation: ConfirmationPage,
+ footerContent,
+ formId: VA_FORM_IDS.FORM_22_1990N || '22-1990N',
+ saveInProgress: {
+ messages: {
+ inProgress:
+ 'Your VA education benefits application (22-1990n) is in progress.',
+ expired:
+ 'Your saved application (22-1990n) has expired. Start a new application.',
+ saved: 'Your application has been saved.',
+ },
+ },
+ version: 0,
+ prefillEnabled: true,
+ savedFormMessages: {
+ notFound:
+ 'Start your application to apply for NCS education benefits.',
+ noAuth: 'Sign in to check your application status.',
+ },
+ hideUnauthedStartLink: true,
+ title:
+ 'Apply for VA Education Benefits Under the National Call to Service Program',
+ subTitle: 'VA Form 22-1990n',
+ defaultDefinitions: {},
+ dev: {
+ showNavLinks: true,
+ collapsibleNavLinks: true,
+ },
+ chapters: {
+ eligibilityChapter: {
+ title: 'Eligibility',
+ pages: {
+ eligibilityScreener: {
+ path: 'eligibility',
+ title: 'Check your eligibility',
+ uiSchema: eligibilityScreenerUiSchema,
+ schema: eligibilityScreenerSchema,
+ },
+ },
+ },
+ applicantInformationChapter: {
+ title: 'Applicant Information',
+ pages: {
+ personalInformation: {
+ path: 'applicant-information/personal-information',
+ title: 'Personal information',
+ uiSchema: personalInformationUiSchema,
+ schema: personalInformationSchema,
+ },
+ contactInformation: {
+ path: 'applicant-information/contact-information',
+ title: 'Contact information',
+ uiSchema: contactInformationUiSchema,
+ schema: contactInformationSchema,
+ },
+ directDeposit: {
+ path: 'applicant-information/direct-deposit',
+ title: 'Direct deposit',
+ uiSchema: directDepositUiSchema,
+ schema: directDepositSchema,
+ },
+ },
+ },
+ trainingProgramChapter: {
+ title: 'Training Program',
+ pages: {
+ trainingType: {
+ path: 'training-program/training-type',
+ title: 'Type of education or training',
+ uiSchema: trainingTypeUiSchema,
+ schema: trainingTypeSchema,
+ },
+ flightTrainingRequirements: {
+ path: 'training-program/flight-training-requirements',
+ title: 'Flight training requirements',
+ uiSchema: flightTrainingRequirementsUiSchema,
+ schema: flightTrainingRequirementsSchema,
+ depends: formData =>
+ formData?.trainingProgram?.trainingType
+ ?.vocationalFlightTraining === true,
+ },
+ schoolInformation: {
+ path: 'training-program/school-information',
+ title: 'School or training establishment',
+ uiSchema: schoolInformationUiSchema,
+ schema: schoolInformationSchema,
+ },
+ educationObjective: {
+ path: 'training-program/education-objective',
+ title: 'Educational or career objective',
+ uiSchema: educationObjectiveUiSchema,
+ schema: educationObjectiveSchema,
+ },
+ },
+ },
+ serviceInformationChapter: {
+ title: 'Service Information',
+ pages: {
+ activeDutyStatus: {
+ path: 'service-information/active-duty-status',
+ title: 'Active duty status',
+ uiSchema: activeDutyStatusUiSchema,
+ schema: activeDutyStatusSchema,
+ },
+ servicePeriods: {
+ path: 'service-information/service-periods',
+ title: 'Service periods',
+ uiSchema: servicePeriodsUiSchema,
+ schema: servicePeriodsSchema,
+ },
+ },
+ },
+ additionalAssistanceChapter: {
+ title: 'Additional Assistance',
+ pages: {
+ additionalAssistance: {
+ path: 'additional-assistance/additional-assistance',
+ title: 'Entitlement to additional types of assistance',
+ uiSchema: additionalAssistanceUiSchema,
+ schema: additionalAssistanceSchema,
+ },
+ },
+ },
+ supportingDocumentsChapter: {
+ title: 'Supporting Documents',
+ pages: {
+ uploadDD2863: {
+ path: 'supporting-documents/upload-dd-2863',
+ title: 'Upload DD Form 2863',
+ uiSchema: uploadDD2863UiSchema,
+ schema: uploadDD2863Schema,
+ },
+ uploadDD214: {
+ path: 'supporting-documents/upload-dd-214',
+ title: 'Upload DD Form 214',
+ uiSchema: uploadDD214UiSchema,
+ schema: uploadDD214Schema,
+ },
+ uploadVoidedCheck: {
+ path: 'supporting-documents/upload-voided-check',
+ title: 'Upload voided check or deposit slip',
+ uiSchema: uploadVoidedCheckUiSchema,
+ schema: uploadVoidedCheckSchema,
+ depends: formData =>
+ formData?.directDepositEnrolling === true,
+ },
+ },
+ },
+ },
+};
+
+export default formConfig;
+export { formConfig };
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/config/form.unit.spec.js b/src/applications/education/22-1990n/config/form.unit.spec.js
new file mode 100644
index 000000000000..d60db8c4b34c
--- /dev/null
+++ b/src/applications/education/22-1990n/config/form.unit.spec.js
@@ -0,0 +1,114 @@
+import { expect } from 'chai';
+import formConfig from './form';
+
+describe('config/form', () => {
+ it('has required top-level properties', () => {
+ expect(formConfig).to.have.property('formId');
+ expect(formConfig).to.have.property('title');
+ expect(formConfig).to.have.property('chapters');
+ expect(formConfig).to.have.property('introduction');
+ expect(formConfig).to.have.property('confirmation');
+ expect(formConfig).to.have.property('saveInProgress');
+ expect(formConfig).to.have.property('trackingPrefix');
+ });
+
+ it('has saveInProgress messages', () => {
+ expect(formConfig.saveInProgress.messages).to.have.property(
+ 'inProgress',
+ );
+ expect(formConfig.saveInProgress.messages).to.have.property('expired');
+ expect(formConfig.saveInProgress.messages).to.have.property('saved');
+ });
+
+ it('has trackingPrefix starting with "edu-1990n"', () => {
+ expect(formConfig.trackingPrefix).to.include('edu-1990n');
+ });
+
+ it('has prefillEnabled set to true', () => {
+ expect(formConfig.prefillEnabled).to.equal(true);
+ });
+
+ it('has expected chapters', () => {
+ const chapterKeys = Object.keys(formConfig.chapters);
+ expect(chapterKeys).to.include('eligibilityChapter');
+ expect(chapterKeys).to.include('applicantInformationChapter');
+ expect(chapterKeys).to.include('trainingProgramChapter');
+ expect(chapterKeys).to.include('serviceInformationChapter');
+ expect(chapterKeys).to.include('additionalAssistanceChapter');
+ expect(chapterKeys).to.include('supportingDocumentsChapter');
+ });
+
+ it('every page has required path, title, uiSchema, schema', () => {
+ Object.entries(formConfig.chapters).forEach(([, chapter]) => {
+ Object.entries(chapter.pages).forEach(([, page]) => {
+ expect(page, `page ${page.path}`).to.have.property('path');
+ expect(page, `page ${page.path}`).to.have.property('title');
+ expect(page, `page ${page.path}`).to.have.property('uiSchema');
+ expect(page, `page ${page.path}`).to.have.property('schema');
+ });
+ });
+ });
+
+ it('flightTrainingRequirements page has a depends function', () => {
+ const page =
+ formConfig.chapters.trainingProgramChapter.pages
+ .flightTrainingRequirements;
+ expect(page.depends).to.be.a('function');
+ });
+
+ it('flightTrainingRequirements depends returns true when vocationalFlightTraining is true', () => {
+ const { depends } =
+ formConfig.chapters.trainingProgramChapter.pages
+ .flightTrainingRequirements;
+ const result = depends({
+ trainingProgram: { trainingType: { vocationalFlightTraining: true } },
+ });
+ expect(result).to.equal(true);
+ });
+
+ it('flightTrainingRequirements depends returns false when vocationalFlightTraining is false', () => {
+ const { depends } =
+ formConfig.chapters.trainingProgramChapter.pages
+ .flightTrainingRequirements;
+ const result = depends({
+ trainingProgram: { trainingType: { vocationalFlightTraining: false } },
+ });
+ expect(result).to.equal(false);
+ });
+
+ it('flightTrainingRequirements depends returns false when formData is null', () => {
+ const { depends } =
+ formConfig.chapters.trainingProgramChapter.pages
+ .flightTrainingRequirements;
+ expect(() => depends(null)).to.not.throw();
+ expect(depends(null)).to.equal(false);
+ });
+
+ it('uploadVoidedCheck page has a depends function', () => {
+ const page =
+ formConfig.chapters.supportingDocumentsChapter.pages.uploadVoidedCheck;
+ expect(page.depends).to.be.a('function');
+ });
+
+ it('uploadVoidedCheck depends returns true when directDepositEnrolling is true', () => {
+ const { depends } =
+ formConfig.chapters.supportingDocumentsChapter.pages.uploadVoidedCheck;
+ expect(depends({ directDepositEnrolling: true })).to.equal(true);
+ });
+
+ it('uploadVoidedCheck depends returns false when directDepositEnrolling is false', () => {
+ const { depends } =
+ formConfig.chapters.supportingDocumentsChapter.pages.uploadVoidedCheck;
+ expect(depends({ directDepositEnrolling: false })).to.equal(false);
+ });
+
+ it('uploadVoidedCheck depends handles null input without throwing', () => {
+ const { depends } =
+ formConfig.chapters.supportingDocumentsChapter.pages.uploadVoidedCheck;
+ expect(() => depends(null)).to.not.throw();
+ });
+
+ it('submitUrl points to the 1990n endpoint', () => {
+ expect(formConfig.submitUrl).to.include('1990n');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/App.jsx b/src/applications/education/22-1990n/containers/App.jsx
new file mode 100644
index 000000000000..ce0ad43c961d
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/App.jsx
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import RoutedSavableApp from 'platform/forms/save-in-progress/RoutedSavableApp';
+import formConfig from '../config/form';
+
+export default function App({ location, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+App.propTypes = {
+ children: PropTypes.node,
+ location: PropTypes.object,
+};
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/App.unit.spec.jsx b/src/applications/education/22-1990n/containers/App.unit.spec.jsx
new file mode 100644
index 000000000000..6db6ccd38e7c
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/App.unit.spec.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { expect } from 'chai';
+import { render } from '@testing-library/react';
+import App from './App';
+import formConfig from '../config/form';
+
+const createMockStore = (overrides = {}) => ({
+ getState: () => ({
+ user: {
+ login: { currentlyLoggedIn: false },
+ profile: {
+ savedForms: [],
+ prefillsAvailable: [],
+ loa: { current: 3, highest: 3 },
+ verified: true,
+ dob: '1990-01-01',
+ claims: { appeals: false },
+ ...overrides.user?.profile,
+ },
+ ...overrides.user,
+ },
+ form: {
+ formId: formConfig.formId,
+ loadedStatus: 'success',
+ savedStatus: '',
+ loadedData: { metadata: {} },
+ data: {},
+ ...overrides.form,
+ },
+ scheduledDowntime: {
+ globalDowntime: null,
+ isReady: true,
+ isPending: false,
+ serviceMap: { get() {} },
+ dismissedDowntimeWarnings: [],
+ },
+ ...overrides,
+ }),
+ subscribe: () => {},
+ dispatch: () => {},
+});
+
+describe('containers/App', () => {
+ it('renders without crashing', () => {
+ const store = createMockStore();
+ const location = { pathname: '/introduction' };
+ // RoutedSavableApp requires a store context; we verify the component
+ // itself doesn't throw during construction
+ expect(() => {
+ // shallow render check — just verify the component function exists
+ const element = App({ location, children: null });
+ expect(element).to.exist;
+ }).to.not.throw();
+ });
+
+ it('is a function (functional component)', () => {
+ expect(App).to.be.a('function');
+ });
+
+ it('has expected propTypes', () => {
+ expect(App.propTypes).to.have.property('children');
+ expect(App.propTypes).to.have.property('location');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/ConfirmationPage.jsx b/src/applications/education/22-1990n/containers/ConfirmationPage.jsx
new file mode 100644
index 000000000000..2b027ccec499
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/ConfirmationPage.jsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect, useSelector } from 'react-redux';
+
+import { ConfirmationView } from 'platform/forms-system/src/js/components/ConfirmationView';
+
+export const ConfirmationPage = ({ route }) => {
+ const form = useSelector(state => state.form || {});
+ const submission = form?.submission || {};
+ const submitDate = submission?.timestamp || '';
+ const confirmationNumber =
+ submission?.response?.confirmationNumber ||
+ submission?.response?.attributes?.confirmationNumber ||
+ '';
+
+ const firstName =
+ form?.data?.personalInformation?.applicantFirstName || '';
+ const lastName =
+ form?.data?.personalInformation?.applicantLastName || '';
+
+ const submitterName =
+ firstName && lastName ? { first: firstName, last: lastName } : undefined;
+
+ const submissionAlertContent = (
+ <>
+
+ We've received your application for education benefits under the
+ National Call to Service program. We'll review your application and
+ notify you of our decision.
+
+ {confirmationNumber && (
+
+ Your confirmation number is{' '}
+ {confirmationNumber}. Save or print this page
+ for your records.
+
+ )}
+ >
+ );
+
+ return (
+
+ }
+ />
+
+
+
+ }
+ item2Header="VA will notify you of its decision"
+ item2Content="VA will notify you of its decision concerning your eligibility for education benefits."
+ item2Actions={}
+ item3Header="If approved, VA will issue a Certificate of Eligibility"
+ item3Content="If you are found eligible, VA will issue a Certificate of Eligibility (COE). Provide this to the veterans certifying official at your school or training establishment."
+ item3Actions={}
+ />
+
+
+
+
+
+ );
+};
+
+ConfirmationPage.propTypes = {
+ route: PropTypes.shape({
+ formConfig: PropTypes.object,
+ }),
+};
+
+function mapStateToProps(state) {
+ return {
+ form: state.form,
+ };
+}
+
+export default connect(mapStateToProps)(ConfirmationPage);
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/ConfirmationPage.unit.spec.jsx b/src/applications/education/22-1990n/containers/ConfirmationPage.unit.spec.jsx
new file mode 100644
index 000000000000..04d7c746e3ad
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/ConfirmationPage.unit.spec.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import { expect } from 'chai';
+import { render } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import { ConfirmationPage } from './ConfirmationPage';
+import formConfig from '../config/form';
+
+const createMockStore = (overrides = {}) => ({
+ getState: () => ({
+ user: {
+ login: { currentlyLoggedIn: true },
+ profile: {
+ savedForms: [],
+ prefillsAvailable: [],
+ loa: { current: 3, highest: 3 },
+ verified: true,
+ dob: '1990-01-01',
+ claims: { appeals: false },
+ ...overrides.user?.profile,
+ },
+ ...overrides.user,
+ },
+ form: {
+ formId: formConfig.formId,
+ loadedStatus: 'success',
+ savedStatus: '',
+ loadedData: { metadata: {} },
+ data: {
+ personalInformation: {
+ applicantFirstName: 'Jane',
+ applicantLastName: 'Smith',
+ },
+ },
+ submission: {
+ response: {
+ confirmationNumber: '1234567890',
+ },
+ timestamp: new Date('2024-01-15'),
+ },
+ ...overrides.form,
+ },
+ scheduledDowntime: {
+ globalDowntime: null,
+ isReady: true,
+ isPending: false,
+ serviceMap: { get() {} },
+ dismissedDowntimeWarnings: [],
+ },
+ ...overrides,
+ }),
+ subscribe: () => {},
+ dispatch: () => {},
+});
+
+const defaultRoute = { formConfig };
+
+describe('containers/ConfirmationPage', () => {
+ it('renders without crashing', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container).to.exist;
+ });
+
+ it('displays the confirmation number', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.textContent).to.include('1234567890');
+ });
+
+ it('renders submission confirmation content', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.textContent).to.include(
+ "We've received your application",
+ );
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/IntroductionPage.jsx b/src/applications/education/22-1990n/containers/IntroductionPage.jsx
new file mode 100644
index 000000000000..4a28faba0982
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/IntroductionPage.jsx
@@ -0,0 +1,181 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import FormTitle from 'platform/forms-system/src/js/components/FormTitle';
+import SaveInProgressIntro from 'platform/forms/save-in-progress/SaveInProgressIntro';
+import { isLOA3, isLoggedIn } from 'platform/user/selectors';
+import { focusElement, scrollToTop } from 'platform/utilities/ui';
+
+const OMB_RES_BURDEN = 15;
+const OMB_NUMBER = '2900-0154';
+const OMB_EXP_DATE = '03/31/2026';
+
+export const IntroductionPage = ({ route, userIdVerified, userLoggedIn }) => {
+ const { formConfig, pageList } = route;
+
+ useEffect(() => {
+ scrollToTop();
+ focusElement('h1');
+ }, []);
+
+ return (
+
+
+
+
+ Use this form to apply for education benefits under the National Call to
+ Service (NCS) program. The NCS program is authorized under Section 510,
+ Title 10, U.S. Code.
+
+
+ Eligibility requirements
+ To use this form, you must meet all of the following criteria:
+
+ -
+ You first entered military service on or after October 1, 2003
+
+ -
+ You signed an enlistment contract with the Department of Defense (DoD)
+ specifically under the National Call to Service program
+
+ -
+ You elected one of the education incentives on your DD Form 2863
+ (National Call to Service Election of Options)
+
+
+
+
+ What you'll need to apply
+
+
Please have the following documents ready before you start:
+
+ -
+ DD Form 2863 (National Call to Service Election of Options) —
+ required
+
+ -
+ DD Form 214 (Member 4 copy) for all periods of active duty service
+ — required unless you are on terminal leave pending issuance
+
+ -
+ Voided personal check or deposit slip — required if you want to
+ enroll in direct deposit
+
+ -
+ Your Social Security number and date of birth
+
+ -
+ Your military service dates and branch of service
+
+
+
+
+
+ How to apply
+
+
+
+ Answer a few questions to confirm you meet the NCS program
+ requirements before starting your application.
+
+
+ Locate your DD Form 2863, DD Form 214 (Member 4), and banking
+ information if you want direct deposit.
+
+
+ Fill out all required sections. The form takes approximately 15
+ minutes to complete.
+
+
+ Review your information and submit. You will receive a confirmation
+ number after submission.
+
+
+
+
+
+ {userLoggedIn && !userIdVerified && (
+
+ Verify your identity to apply
+
+ You need to verify your identity before you can apply for education
+ benefits. Verifying your identity helps us protect your personal
+ information.
+
+
+ )}
+
+
+
+
+ If you are approved for education benefits under the NCS program,
+ VA will pay benefits directly to you via direct deposit or check.
+ Benefits are paid based on your enrollment certification submitted
+ by your school or training establishment using VA Form 22-1999.
+
+
+
+
+ The information you provide is covered under the Privacy Act of
+ 1974 (38 U.S.C. § 3471, 38 U.S.C. § 5701, 38 CFR § 1.526).
+ Information is maintained in the System of Records 58VA21/22/28,
+ Compensation, Pension, Education and Veteran Readiness and
+ Employment Records — VA.
+
+
+
+
+
+ OMB Control Number: {OMB_NUMBER}
+
+ Expiration Date: {OMB_EXP_DATE}
+
+ Estimated Response Time: {OMB_RES_BURDEN} minutes
+
+
+
+
+ );
+};
+
+IntroductionPage.propTypes = {
+ route: PropTypes.shape({
+ formConfig: PropTypes.shape({
+ prefillEnabled: PropTypes.bool,
+ saveInProgress: PropTypes.shape({
+ messages: PropTypes.shape({}),
+ }),
+ }),
+ pageList: PropTypes.array,
+ }),
+ userIdVerified: PropTypes.bool,
+ userLoggedIn: PropTypes.bool,
+};
+
+const mapStateToProps = state => ({
+ userIdVerified: isLOA3(state),
+ userLoggedIn: isLoggedIn(state),
+});
+
+export default connect(mapStateToProps)(IntroductionPage);
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/containers/IntroductionPage.unit.spec.jsx b/src/applications/education/22-1990n/containers/IntroductionPage.unit.spec.jsx
new file mode 100644
index 000000000000..563d73040cd9
--- /dev/null
+++ b/src/applications/education/22-1990n/containers/IntroductionPage.unit.spec.jsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon';
+import { render } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import { IntroductionPage } from './IntroductionPage';
+import formConfig from '../config/form';
+
+// Stub platform/utilities/ui
+let scrollStub;
+let focusStub;
+
+const createMockStore = (overrides = {}) => ({
+ getState: () => ({
+ user: {
+ login: { currentlyLoggedIn: false },
+ profile: {
+ savedForms: [],
+ prefillsAvailable: [],
+ loa: { current: 3, highest: 3 },
+ verified: true,
+ dob: '1990-01-01',
+ claims: { appeals: false },
+ ...overrides.user?.profile,
+ },
+ ...overrides.user,
+ },
+ form: {
+ formId: formConfig.formId,
+ loadedStatus: 'success',
+ savedStatus: '',
+ loadedData: { metadata: {} },
+ data: {},
+ ...overrides.form,
+ },
+ scheduledDowntime: {
+ globalDowntime: null,
+ isReady: true,
+ isPending: false,
+ serviceMap: { get() {} },
+ dismissedDowntimeWarnings: [],
+ },
+ ...overrides,
+ }),
+ subscribe: () => {},
+ dispatch: () => {},
+});
+
+const defaultRoute = {
+ formConfig,
+ pageList: [{ path: '/introduction' }, { path: '/eligibility' }],
+};
+
+describe('containers/IntroductionPage', () => {
+ beforeEach(() => {
+ // SaveInProgressIntro may call scrollToTop / focusElement
+ scrollStub = sinon.stub();
+ focusStub = sinon.stub();
+ });
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it('renders the form title', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ // FormTitle renders title text
+ expect(container.textContent).to.include(
+ 'Apply for VA Education Benefits Under the National Call to Service Program',
+ );
+ });
+
+ it('renders eligibility requirements section', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.textContent).to.include('Eligibility requirements');
+ });
+
+ it('renders OMB control number', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.textContent).to.include('2900-0154');
+ });
+
+ it('does not render identity verification alert when user is verified', () => {
+ const store = createMockStore();
+ const { queryByText } = render(
+
+
+ ,
+ );
+ expect(queryByText('Verify your identity to apply')).to.be.null;
+ });
+
+ it('renders identity verification alert when user is logged in but not verified', () => {
+ const store = createMockStore();
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.textContent).to.include('Verify your identity to apply');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/manifest.json b/src/applications/education/22-1990n/manifest.json
new file mode 100644
index 000000000000..8bc9bb45c4b1
--- /dev/null
+++ b/src/applications/education/22-1990n/manifest.json
@@ -0,0 +1,12 @@
+{
+ "appName": "Apply for VA Education Benefits Under the National Call to Service Program",
+ "entryFile": "./app-entry.jsx",
+ "entryName": "edu-benefits-1990n",
+ "rootUrl": "/education/apply-for-education-benefits/application/1990n",
+ "productId": "edu-benefits-1990n",
+ "template": {
+ "vagovprod": false,
+ "layout": "page-react.html",
+ "includeBreadcrumbs": true
+ }
+}
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/reducers/index.js b/src/applications/education/22-1990n/reducers/index.js
new file mode 100644
index 000000000000..89023ae95ba9
--- /dev/null
+++ b/src/applications/education/22-1990n/reducers/index.js
@@ -0,0 +1,4 @@
+import { createSaveInProgressFormReducer } from 'platform/forms/save-in-progress/reducers';
+import formConfig from '../config/form';
+
+export default { form: createSaveInProgressFormReducer(formConfig) };
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/reducers/index.unit.spec.js b/src/applications/education/22-1990n/reducers/index.unit.spec.js
new file mode 100644
index 000000000000..b92789378ee8
--- /dev/null
+++ b/src/applications/education/22-1990n/reducers/index.unit.spec.js
@@ -0,0 +1,20 @@
+import { expect } from 'chai';
+import reducers from './index';
+
+describe('reducers', () => {
+ it('exports an object with a form slice', () => {
+ expect(reducers).to.be.an('object');
+ expect(reducers.form).to.be.a('function');
+ });
+
+ it('returns a non-null state on @@INIT', () => {
+ const result = reducers.form(undefined, { type: '@@INIT' });
+ expect(result).to.not.be.null;
+ expect(result).to.be.an('object');
+ });
+
+ it('initializes with expected form state shape', () => {
+ const result = reducers.form(undefined, { type: '@@INIT' });
+ expect(result).to.have.property('data');
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/routes.js b/src/applications/education/22-1990n/routes.js
new file mode 100644
index 000000000000..b1b536446621
--- /dev/null
+++ b/src/applications/education/22-1990n/routes.js
@@ -0,0 +1,12 @@
+import { createRoutesWithSaveInProgress } from 'platform/forms/save-in-progress/helpers';
+import formConfig from './config/form';
+import App from './containers/App';
+
+const route = {
+ path: '/',
+ component: App,
+ indexRoute: { onEnter: (_nextState, replace) => replace('/introduction') },
+ childRoutes: createRoutesWithSaveInProgress(formConfig),
+};
+
+export default route;
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/routes.unit.spec.js b/src/applications/education/22-1990n/routes.unit.spec.js
new file mode 100644
index 000000000000..6e2e42f53db9
--- /dev/null
+++ b/src/applications/education/22-1990n/routes.unit.spec.js
@@ -0,0 +1,32 @@
+import { expect } from 'chai';
+import routes from './routes';
+
+describe('routes', () => {
+ it('exports a route object, not an array', () => {
+ expect(routes).to.be.an('object');
+ expect(routes).not.to.be.an('array');
+ });
+
+ it('has path set to "/"', () => {
+ expect(routes.path).to.equal('/');
+ });
+
+ it('has a component property', () => {
+ expect(routes.component).to.exist;
+ });
+
+ it('has an indexRoute with onEnter that redirects to /introduction', () => {
+ expect(routes.indexRoute).to.exist;
+ expect(routes.indexRoute.onEnter).to.be.a('function');
+ let replaced = null;
+ routes.indexRoute.onEnter({}, path => {
+ replaced = path;
+ });
+ expect(replaced).to.equal('/introduction');
+ });
+
+ it('has childRoutes array', () => {
+ expect(routes.childRoutes).to.be.an('array');
+ expect(routes.childRoutes.length).to.be.greaterThan(0);
+ });
+});
\ No newline at end of file
diff --git a/src/applications/education/22-1990n/sass/22-1990n.scss b/src/applications/education/22-1990n/sass/22-1990n.scss
new file mode 100644
index 000000000000..3faeca4a9389
--- /dev/null
+++ b/src/applications/education/22-1990n/sass/22-1990n.scss
@@ -0,0 +1,3 @@
+@import "~@department-of-veterans-affairs/css-library/dist/stylesheets/modules/m-form-process";
+@import "../../../../platform/forms/sass/m-schemaform";
+@import "../../../../platform/forms/sass/m-form-confirmation";
\ No newline at end of file