diff --git a/.env.example b/.env.example index baae955..9f9cdad 100644 --- a/.env.example +++ b/.env.example @@ -43,3 +43,6 @@ EZPAY_TRANSPORT_API_KEY=transport_api_key_here # Form-specific secrets (example) PRIMARY_SCHOOL_TEXTBOOK_GRANT_PAYMENT_CODE=EDU001 PRIMARY_SCHOOL_TEXTBOOK_GRANT_ADMIN_EMAIL=education@gov.bb + +AWS_SES_ENDPOINT=PASTE_VALUE_HERE +AWS_SES_FROM_EMAIL=PASTE_VALUE_HERE \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4b25bea..f45487a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,12 +15,9 @@ ## Testing -- [ ] Visual regression tests added for all device sizes -- [ ] Verify all pages render correctly -- [ ] Test responsive layout across device sizes (snapshots created) -- [ ] Check accessibility standards are met -- [ ] Validate markdown formatting renders properly -- [ ] Test navigation and breadcrumbs +- [ ] Manual tests completed +- [ ] Added unit tests +- [ ] Added e2e tests ## Related Github Issue(s)/Trello Ticket(s) @@ -29,5 +26,4 @@ ## Checklist - [ ] Code follows project style guidelines - [ ] Self-review completed -- [ ] Tests added/updated (visual regression snapshots) - [ ] Documentation updated diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9cd0e03 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } +} diff --git a/README.md b/README.md index 0fae6b2..3a338e6 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,22 @@ npm run test:e2e npm run test:cov ``` +### Testing Emails (Locally) + +Start mock server + +``` +npx aws-ses-v2-local@latest --port 8005 +``` + +Update .env file + +``` +NODE_ENV=development +AWS_SES_ENDPOINT=http://localhost:8005 +AWS_SES_FROM_EMAIL=test@example.com +``` + ## 🚀 Deployment ### Build for Production diff --git a/schemas/conductor-licence.json b/schemas/conductor-licence.json new file mode 100644 index 0000000..e4a7c28 --- /dev/null +++ b/schemas/conductor-licence.json @@ -0,0 +1,243 @@ +{ + "id": "apply-for-conductor-licence", + "name": "Conductor Licence Application", + "description": "Conductor Licence Application", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "middleName", + "type": "string", + "label": "Middle name", + "required": false, + "validations": { + "max": 100, + "message": "Middle name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "dateOfBirth", + "type": "date", + "label": "Date of birth", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of birth is required and must be in YYYY-MM-DD format" + } + } + ] + }, + { + "name": "contactDetails", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + }, + { + "name": "licenceHistory", + "type": "object", + "required": true, + "fields": [ + { + "name": "hasPreviousLicence", + "type": "string", + "label": "Do you have any previous licence(s)?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "licenceNumber", + "type": "string", + "label": "Provide your licence number", + "required": false + }, + { + "name": "dateOfIssue", + "type": "date", + "label": "Date of issue", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of issue is required and must be in YYYY-MM-DD format" + } + } + ] + }, + { + "name": "hasEndorsements", + "type": "string", + "label": "Do you have any endorsements?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "endorsementDetails", + "type": "array", + "label": "Your endorsements", + "required": false, + "items": { + "type": "object", + "properties": { + "typeOfEndorsement": { + "type": "string" + }, + "dateOfEndorsement": { + "type": "date" + }, + "duration": { + "type": "string" + } + } + } + }, + { + "name": "disqualifications", + "type": "object", + "required": true, + "fields": [ + { + "name": "hasDisqualifications", + "type": "string", + "label": "Have you ever been disqualified?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "courtName", + "type": "string", + "label": "Court name", + "required": false + }, + { + "name": "dateOfDisqualification", + "type": "string", + "label": "Date of disqualification", + "required": false + }, + { + "name": "lengthOfDisqualification", + "type": "string", + "label": "Length of disqualification", + "required": false + } + ] + }, + { + "name": "hasCriminalConvictions", + "type": "string", + "label": "Have you ever had any criminal convictions?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:apply-for-conductor-licence:admin_email}}", + "subject": "Apply for conductor licence - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "apply-for-conductor-licence" + } + } + ] +} diff --git a/schemas/exit-survey.json b/schemas/exit-survey.json new file mode 100644 index 0000000..7134886 --- /dev/null +++ b/schemas/exit-survey.json @@ -0,0 +1,65 @@ +{ + "id": "exit-survey", + "name": "Exit Survey", + "description": "Provide feedback on your experience", + "fields": [ + { + "name": "difficultyRating", + "type": "string", + "label": "How easy or difficult was it to complete this form?", + "required": true, + "validations": { + "regex": "^(very-easy|easy|neither|difficult|very-difficult)$", + "message": "Must select an option" + } + }, + { + "name": "clarityRating", + "type": "string", + "label": "How clear was the information provided?", + "required": true, + "validations": { + "regex": "^(very-clear|clear|neither|unclear|very-unclear)$", + "message": "Must select an option" + } + }, + { + "name": "technicalProblems", + "type": "string", + "label": "Did you experience any technical problems?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "technicalProblemsDescription", + "type": "string", + "label": "Please briefly describe the problem you experienced", + "required": false, + "validations": { + "max": 100 + } + }, + { + "name": "areasForImprovement", + "type": "string", + "label": "What is one thing we could do to improve this form?", + "required": true, + "validations": { + "max": 500 + } + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:exit-survey:admin_email}}", + "subject": "New Exit Survey Submission - {{formData.difficultyRating}} Experience", + "template": "exit-survey" + } + } + ] +} diff --git a/schemas/fire-service-inspection.json b/schemas/fire-service-inspection.json new file mode 100644 index 0000000..d74cfb0 --- /dev/null +++ b/schemas/fire-service-inspection.json @@ -0,0 +1,126 @@ +{ + "id": "request-a-fire-service-inspection", + "name": "Request a fire service inspection", + "description": "Request a fire service inspection", + "fields": [ + { + "name": "premises", + "type": "object", + "required": true, + "fields": [ + { + "name": "typeOfPremises", + "type": "string", + "label": "Type of premises", + "required": true, + "validations": { + "regex": "^(hotel|daycare|placeOfEntertainment)$", + "message": "Must select a valid title" + } + }, + { + "name": "nameOfPremises", + "type": "string", + "label": "Name of Premises", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Name of Premises is required" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + } + ] + }, + { + "name": "purposeOfCertificate", + "type": "string", + "label": "Who is the certificate for?", + "required": true, + "validations": { + "regex": "^(barbados-tourism-authority|child-care-board|treasury)$", + "message": "Must select an option" + } + }, + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:request-a-fire-service-inspection:admin_email}}", + "subject": "New Birth Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "request-a-fire-service-inspection" + } + } + ] +} diff --git a/schemas/get-death-certificate.json b/schemas/get-death-certificate.json index 4428422..7c345b8 100644 --- a/schemas/get-death-certificate.json +++ b/schemas/get-death-certificate.json @@ -209,7 +209,6 @@ "label": "National Identification (ID) Number", "required": false, "validations": { - "min": 2, "message": "ID Number must be at least 2 characters" } }, diff --git a/schemas/jobstart-plus-programme.json b/schemas/jobstart-plus-programme.json new file mode 100644 index 0000000..cd6a9fd --- /dev/null +++ b/schemas/jobstart-plus-programme.json @@ -0,0 +1,503 @@ +{ + "id": "jobstart-plus-programme", + "name": "Apply to JobStart Plus Programme", + "description": "Registering for JobStart Plus Programme", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "middleName", + "type": "string", + "label": "Middle name", + "required": false, + "validations": { + "max": 100 + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "dateOfBirth", + "type": "date", + "label": "Date of birth", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of birth is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "sex", + "type": "string", + "label": "Sex", + "required": true, + "validations": { + "regex": "^(male|female)$", + "message": "Must select a valid sex" + } + }, + { + "name": "sex", + "type": "string", + "label": "Sex", + "required": true, + "validations": { + "regex": "^(male|female)$", + "message": "Must select a valid sex" + } + }, + { + "name": "maritalStatus", + "type": "string", + "label": "Marital Status", + "required": true, + "validations": { + "regex": "^(single|married|divorced)$", + "message": "Must select a valid marital status" + } + }, + { + "name": "idNumber", + "type": "string", + "label": "National Identification (ID) Number", + "required": false, + "validations": { + "min": 2, + "message": "ID Number must be at least 2 characters" + } + }, + { + "name": "passportNumber", + "type": "string", + "label": "Passport Number", + "required": false, + "validations": { + "message": "Passport number must be at least 6 characters" + } + }, + { + "name": "hasNisNumber", + "type": "string", + "label": "Do you have a National Insurance number (NIS)?", + "required": false, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "nisNumber", + "type": "string", + "label": "Provide your National insurance number (NIS)", + "required": false, + "validations": { + "message": "National insurance number must be at least 6 characters" + } + }, + { + "name": "hasDisability", + "type": "string", + "label": "Do you have a disability?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "disabilityDetails", + "type": "string", + "label": "What is your disability?", + "required": false + } + ] + }, + { + "name": "contactDetails", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + }, + { + "name": "emergency", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "relationship", + "type": "string", + "label": "Relationship", + "required": true, + "validations": { + "regex": "^(mother|father|grandmother|grandfather|aunt|uncle|legal_guardian|other)$", + "message": "Relationship is required" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + }, + { + "name": "primaryEducation", + "type": "object", + "required": true, + "fields": [ + { + "name": "schoolName", + "type": "string", + "label": "Name of primary school", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "School name must be at least 5 characters" + } + }, + { + "name": "startYear", + "type": "string", + "label": "Start year", + "required": true, + "validations": { + "min": 4, + "max": 4, + "message": "Start year must be 4 characters" + } + }, + { + "name": "endYear", + "type": "string", + "label": "End year", + "required": true, + "validations": { + "min": 4, + "max": 4, + "message": "End year must be 4 characters" + } + } + ] + }, + { + "name": "secondaryEducation", + "type": "object", + "required": true, + "fields": [ + { + "name": "schoolName", + "type": "string", + "label": "Name of secondary school", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "School name must be at least 5 characters" + } + }, + { + "name": "startYear", + "type": "string", + "label": "Start year", + "required": true, + "validations": { + "min": 4, + "max": 4, + "message": "Start year must be 4 characters" + } + }, + { + "name": "endYear", + "type": "string", + "label": "End year", + "required": true, + "validations": { + "min": 4, + "max": 4, + "message": "End year must be 4 characters" + } + } + ] + }, + { + "name": "postSecondaryEducation", + "type": "array", + "label": "Post-secondary and tertiary training", + "required": false, + "items": { + "type": "object", + "properties": { + "institutionName": { + "type": "string" + }, + "qualificationsObtained": { + "type": "string" + }, + "coursesOrSubjects": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "endDate": { + "type": "string" + } + } + } + }, + { + "name": "hasPreviousPaidJob", + "type": "string", + "label": "Have you had a paid job?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "employmentHistory", + "type": "array", + "label": "Tell us about your previous job", + "required": false, + "items": { + "type": "object", + "properties": { + "employerName": { + "type": "string" + }, + "occupation": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "endDate": { + "type": "string" + }, + "currentlyWorkingHere": { + "type": "string" + }, + "mainTasks": { + "type": "string" + } + } + } + }, + { + "name": "eligibility", + "type": "object", + "required": false, + "fields": [ + { + "name": "interests", + "type": "string", + "label": "What types of jobs or trades are you interested in?", + "required": true, + "validations": { + "min": 5, + "max": 500, + "message": "This must be at least 5 characters" + } + }, + { + "name": "areYouOver18", + "type": "string", + "label": "Are you 18 and over?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "shortTermGoals", + "type": "string", + "label": "Tell us about your short-term goals", + "required": true, + "validations": { + "min": 5, + "max": 500, + "message": "This must be at least 5 characters" + } + } + ] + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:jobstart-plus-programme:admin_email}}", + "subject": "New JobStart Plus Programme Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "jobstart-plus-programme" + } + } + ] +} diff --git a/schemas/permission-to-remove-tree.json b/schemas/permission-to-remove-tree.json new file mode 100644 index 0000000..c889fd0 --- /dev/null +++ b/schemas/permission-to-remove-tree.json @@ -0,0 +1,150 @@ +{ + "id": "permission-to-remove-tree", + "name": "Apply for permission to remove a protected tree", + "description": "Apply for permission to remove a protected tree", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "middleName", + "type": "string", + "label": "Middle name", + "required": false, + "validations": { + "max": 100, + "message": "Middle name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "mobileNumber", + "type": "string", + "label": "Mobile number", + "required": true + }, + { + "name": "homeNumber", + "type": "string", + "label": "Home number", + "required": true + } + ] + }, + { + "name": "treesToRemove", + "type": "array", + "label": "Tell us about the protected tree you want to remove", + "required": false, + "items": { + "type": "object", + "properties": { + "typeOfProtectedTree": { + "type": "string" + }, + "reasonForRemovingTree": { + "type": "string" + }, + "addressLine1": { + "type": "string" + }, + "addressLine2": { + "type": "string" + }, + "parish": { + "type": "string" + } + } + } + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:permission-to-remove-tree:admin_email}}", + "subject": "New Request to remove protected tree - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "permission-to-remove-tree" + } + } + ] +} diff --git a/schemas/post-office-redirection-business.json b/schemas/post-office-redirection-business.json new file mode 100644 index 0000000..7f0b3e4 --- /dev/null +++ b/schemas/post-office-redirection-business.json @@ -0,0 +1,230 @@ +{ + "id": "post-office-redirection-business", + "name": "Post Office Redirection for a Business", + "description": "Change where your mail is sent (business)", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "dateOfBirth", + "type": "date", + "label": "Date of birth", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of birth is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "idNumber", + "type": "string", + "label": "National Identification (ID) Number", + "required": false, + "validations": { + "min": 2, + "message": "ID Number must be at least 2 characters" + } + }, + { + "name": "passportNumber", + "type": "string", + "label": "Passport Number", + "required": false, + "validations": { + "message": "Passport number must be at least 6 characters" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + }, + { + "name": "oldBusinessAddress", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + } + ] + }, + { + "name": "newBusinessAddress", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "isMovingPermanent", + "type": "string", + "label": "Are you moving permanently?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "redirectionStartDate", + "type": "date", + "label": "Redirection Start Date", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection Start Date is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "redirectionEndDate", + "type": "date", + "label": "Redirection End Date", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection End Date is required and must be in YYYY-MM-DD format" + } + } + ] + }, + { + "name": "uploadDocumentUrls", + "type": "array", + "label": "Uploaded Documents", + "required": true, + "items": { + "type": "string" + } + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:post-office-redirection-business:admin_email}}", + "subject": "New Request to Redirect Mail for a Business - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "post-office-redirection-notice" + } + } + ] +} diff --git a/schemas/post-office-redirection-notice.json b/schemas/post-office-redirection-deceased.json similarity index 54% rename from schemas/post-office-redirection-notice.json rename to schemas/post-office-redirection-deceased.json index 12c9d93..c8e9c8d 100644 --- a/schemas/post-office-redirection-notice.json +++ b/schemas/post-office-redirection-deceased.json @@ -1,61 +1,91 @@ { - "id": "post-office-redirection-notice", - "name": "Post Office Redirection Notice", - "description": "Form to submit a post office redirection notice", + "id": "post-office-redirection-deceased", + "name": "Post Office Redirection for the Deceased", + "description": "Change where your mail is sent (deceased)", "fields": [ { - "name": "personal", + "name": "applicant", "type": "object", - "label": "Application Details", "required": true, "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, { "name": "firstName", "type": "string", - "label": "First Name", + "label": "First name", "required": true, "validations": { "min": 2, - "max": 50, - "message": "First name must be at least 2 characters" + "max": 100, + "message": "First name is required" } }, { "name": "lastName", "type": "string", - "label": "Last Name", + "label": "Last name", "required": true, "validations": { "min": 2, - "max": 50, - "message": "Last name must be at least 2 characters" + "max": 100, + "message": "Last name is required" } }, { "name": "dateOfBirth", "type": "date", - "label": "Date of Birth", - "required": true, + "label": "Date of birth", + "required": false, "validations": { "regex": "^\\d{4}-\\d{2}-\\d{2}$", - "message": "Date must be in YYYY-MM-DD format" + "message": "Date of birth is required and must be in YYYY-MM-DD format" } }, { "name": "idNumber", - "type": "number", - "label": "ID Number", + "type": "string", + "label": "National Identification (ID) Number", + "required": false, + "validations": { + "min": 2, + "message": "ID Number must be at least 2 characters" + } + }, + { + "name": "passportNumber", + "type": "string", + "label": "Passport Number", "required": false, "validations": { - "message": "ID Number must be a valid number" + "message": "Passport number must be at least 6 characters" } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true } ] }, { - "name": "oldaddress", + "name": "oldAddress", "type": "object", - "label": "Old Address", "required": true, "fields": [ { @@ -65,7 +95,7 @@ "required": true, "validations": { "min": 5, - "max": 100, + "max": 200, "message": "Address must be at least 5 characters" } }, @@ -75,7 +105,7 @@ "label": "Address Line 2", "required": false, "validations": { - "max": 100 + "max": 200 } }, { @@ -84,7 +114,8 @@ "label": "Parish", "required": true, "validations": { - "message": "Please select a valid parish" + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" } }, { @@ -93,7 +124,8 @@ "label": "Postal Code", "required": false, "validations": { - "max": 10 + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" } } ] @@ -101,7 +133,6 @@ { "name": "newAddress", "type": "object", - "label": "New Address", "required": true, "fields": [ { @@ -111,7 +142,7 @@ "required": true, "validations": { "min": 5, - "max": 100, + "max": 200, "message": "Address must be at least 5 characters" } }, @@ -121,7 +152,7 @@ "label": "Address Line 2", "required": false, "validations": { - "max": 100 + "max": 200 } }, { @@ -130,7 +161,8 @@ "label": "Parish", "required": true, "validations": { - "message": "Please select a valid parish" + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" } }, { @@ -139,96 +171,58 @@ "label": "Postal Code", "required": false, "validations": { - "max": 10 - } - } - ] - }, - { - "name": "houseMembers", - "type": "object", - "label": "House Member Details", - "required": true, - "fields": [ - { - "name": "firstName", - "type": "string", - "label": "First Name", - "required": true, - "validations": { - "min": 2, - "max": 50, - "message": "First name must be at least 2 characters" + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" } }, { - "name": "lastName", + "name": "isMovingPermanent", "type": "string", - "label": "Last Name", + "label": "Are you moving permanently?", "required": true, "validations": { - "min": 2, - "max": 50, - "message": "Last name must be at least 2 characters" + "regex": "^(yes|no)$", + "message": "Must select an option" } }, { - "name": "idNumber", - "type": "number", - "label": "ID Number", + "name": "redirectionStartDate", + "type": "date", + "label": "Redirection Start Date", "required": false, "validations": { - "message": "ID Number must be a valid number" + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection Start Date is required and must be in YYYY-MM-DD format" } }, { - "name": "addAnother", - "type": "string", - "label": "Do you want to add another member?", + "name": "redirectionEndDate", + "type": "date", + "label": "Redirection End Date", "required": false, "validations": { - "regex": "^(yes|no)$", - "message": "Must be 'yes' or 'no'" + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection End Date is required and must be in YYYY-MM-DD format" } } ] }, { - "name": "businessInformation", - "type": "object", - "label": "Business Information", - "required": false, - "fields": [ - { - "name": "belongsToBusiness", - "type": "string", - "label": "Is this for a business/company?", - "required": true, - "validations": { - "regex": "^(true|false)$", - "message": "Must be 'true' or 'false'" - } - }, - { - "name": "businessName", - "type": "string", - "label": "Business Name", - "required": false, - "validations": { - "min": 2, - "max": 100, - "message": "Business name must be at least 2 characters" - } - } - ] + "name": "uploadDocumentUrls", + "type": "array", + "label": "Uploaded Documents", + "required": true, + "items": { + "type": "string" + } } ], "processors": [ { "type": "email", "config": { - "to": "{{db:post-office-redirection-notice:admin_email}}", - "subject": "New Post Office Redirection Notice Request - {{formData.personal.firstName}} {{formData.personal.lastName}}", + "to": "{{db:post-office-redirection-deceased:admin_email}}", + "subject": "New Request to redirect mail for a Deceased Person - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", "template": "post-office-redirection-notice" } } diff --git a/schemas/post-office-redirection-individual.json b/schemas/post-office-redirection-individual.json new file mode 100644 index 0000000..f440c7c --- /dev/null +++ b/schemas/post-office-redirection-individual.json @@ -0,0 +1,248 @@ +{ + "id": "post-office-redirection-individual", + "name": "Post Office Redirection for an Individual", + "description": "Change where your mail is sent (individual)", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "dateOfBirth", + "type": "date", + "label": "Date of birth", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of birth is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "idNumber", + "type": "string", + "label": "National Identification (ID) Number", + "required": false, + "validations": { + "min": 2, + "message": "ID Number must be at least 2 characters" + } + }, + { + "name": "passportNumber", + "type": "string", + "label": "Passport Number", + "required": false, + "validations": { + "message": "Passport number must be at least 6 characters" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + }, + { + "name": "oldAddress", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + } + ] + }, + { + "name": "newAddress", + "type": "object", + "required": true, + "fields": [ + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "isMovingPermanent", + "type": "string", + "label": "Are you moving permanently?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "redirectionStartDate", + "type": "date", + "label": "Redirection Start Date", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection Start Date is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "redirectionEndDate", + "type": "date", + "label": "Redirection End Date", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Redirection End Date is required and must be in YYYY-MM-DD format" + } + } + ] + }, + { + "name": "anyMinorDependents", + "type": "string", + "label": "Are there any minor dependents?", + "required": true, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + }, + { + "name": "minorDetails", + "type": "array", + "label": "Tell us about the minor dependents that needs their email sent to the new address", + "required": false, + "items": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + } + } + } + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:post-office-redirection-individual:admin_email}}", + "subject": "New Request to redirect mail for an individual - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "post-office-redirection-notice" + } + } + ] +} diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json index 4c13f83..b1e1a52 100644 --- a/schemas/primary-school-textbook-grant.json +++ b/schemas/primary-school-textbook-grant.json @@ -5,171 +5,127 @@ "fields": [ { "name": "beneficiaries", - "type": "object", + "type": "array", "label": "Student Information", "required": true, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "guardian": { + "type": "object" + } + } + } + }, + { + "name": "applicant", + "type": "object", + "required": true, "fields": [ { "name": "firstName", "type": "string", - "label": "Student First Name", + "label": "First name", "required": true, "validations": { - "min": 2, - "max": 50 + "min": 1, + "max": 100, + "message": "First name is required" } }, { "name": "lastName", "type": "string", - "label": "Student Last Name", + "label": "Last name", "required": true, "validations": { - "min": 2, - "max": 50 + "min": 1, + "max": 100, + "message": "Last name is required" } }, { - "name": "idNumber", + "name": "addressLine1", "type": "string", - "label": "Student ID Number", + "label": "Address Line 1", "required": true, "validations": { - "min": 2, - "max": 50 + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" } }, { - "name": "gender", - "type": "string", - "label": "Student Gender", - "required": true - }, - { - "name": "class", - "type": "string", - "label": "Student Class/Grade", - "required": true, - "validations": { - "regex": "^[1-6]$", - "message": "Class must be between 1 and 6" - } - } - ] - }, - { - "name": "guardian", - "type": "object", - "label": "Guardian Information", - "required": true, - "fields": [ - { - "name": "title", + "name": "addressLine2", "type": "string", - "label": "Title", + "label": "Address Line 2", "required": false, "validations": { - "max": 10 + "max": 200 } }, { - "name": "firstName", + "name": "parish", "type": "string", - "label": "Guardian First Name", + "label": "Parish", "required": true, "validations": { - "min": 2, - "max": 50 + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" } }, { - "name": "middleName", + "name": "postalCode", "type": "string", - "label": "Guardian Middle Name", + "label": "Postal Code", "required": false, "validations": { - "max": 50 - } - }, - { - "name": "lastName", - "type": "string", - "label": "Guardian Last Name", - "required": true, - "validations": { - "min": 2, - "max": 50 + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" } }, { - "name": "idNumber", - "type": "string", - "label": "Guardian ID Number", - "required": true, - "validations": { - "min": 2, - "max": 50 - } + "name": "email", + "type": "email", + "label": "Email Address", + "required": true }, { - "name": "gender", + "name": "telephoneNumber", "type": "string", - "label": "Guardian Gender", - "required": false + "label": "Telephone Number", + "required": true }, { - "name": "relationship", + "name": "idNumber", "type": "string", - "label": "Relationship to Student", - "required": true, + "label": "National Identification (ID) Number", + "required": false, "validations": { - "max": 50 + "min": 2, + "message": "ID Number must be at least 2 characters" } }, { - "name": "email", - "type": "email", - "label": "Guardian Email Address", - "required": false - } - ] - }, - { - "name": "contact", - "type": "object", - "label": "Contact Information", - "required": true, - "fields": [ - { - "name": "addressLine1", + "name": "passportNumber", "type": "string", - "label": "Address Line 1", - "required": true, + "label": "Passport Number", + "required": false, "validations": { - "min": 5, - "max": 200 + "message": "Passport number must be at least 6 characters" } }, { - "name": "addressLine2", + "name": "tamisNumber", "type": "string", - "label": "Address Line 2", + "label": "TAMIS Number", "required": false, "validations": { - "max": 200 + "message": "Tamis must be 10-15 digits" } - }, - { - "name": "parish", - "type": "string", - "label": "Parish", - "required": true - }, - { - "name": "telephoneNumber", - "type": "string", - "label": "Telephone Number", - "required": true } ] }, @@ -180,44 +136,53 @@ "required": true, "fields": [ { - "name": "bank", + "name": "accountHolderName", + "type": "string", + "label": "Account Holder Name", + "required": true + }, + { + "name": "bankName", "type": "string", "label": "Bank Name", "required": true }, { - "name": "branch", + "name": "accountNumber", "type": "string", - "label": "Branch Location", + "label": "Account Number", "required": true, "validations": { - "max": 100 + "regex": "^[0-9]{6,20}$", + "message": "Account number must be 6-20 digits" } }, { - "name": "accountType", + "name": "branchName", "type": "string", - "label": "Account Type", - "required": true + "label": "Branch Name", + "required": true, + "validations": { + "max": 100 + } }, { - "name": "nameOnAccount", + "name": "branchCode", "type": "string", - "label": "Name on Account", + "label": "Branch Coe", "required": true, "validations": { - "min": 2, - "max": 500 + "max": 100 } }, { - "name": "accountNumber", + "name": "accountType", "type": "string", - "label": "Account Number", + "label": "Account Type", "required": true, "validations": { - "regex": "^[0-9]{6,20}$", - "message": "Account number must be 6-20 digits" + "regex": "^(savings|chequing)$", + "message": "Must select an option" } } ] @@ -231,14 +196,6 @@ "subject": "New Textbook Grant Application - {{formData.beneficiaries.firstName}} {{formData.beneficiaries.lastName}}", "template": "primary-school-textbook-grant" } - }, - { - "type": "email", - "config": { - "to": "{{formData.guardian.email}}", - "subject": "Textbook Grant Application Received - Government of Barbados", - "template": "primary-school-textbook-grant-receipt" - } } ] } diff --git a/schemas/reserve-company-name.json b/schemas/reserve-company-name.json new file mode 100644 index 0000000..73f80f8 --- /dev/null +++ b/schemas/reserve-company-name.json @@ -0,0 +1,181 @@ +{ + "id": "reserve-company-name", + "name": "Reserve Company Name Application", + "description": "Request to reserve company name", + "fields": [ + { + "name": "purposeOfNewCompanyName", + "type": "string", + "label": "What will this name be used for?", + "required": true, + "validations": { + "regex": "^(newCompany|nameChange|merger)$", + "message": "Must select an option" + } + }, + { + "name": "companyPresentName", + "type": "string", + "label": "What is the present name of the company?", + "required": false, + "validations": { + "max": 100 + } + }, + { + "name": "companyName", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstChoice", + "type": "string", + "label": "First choice", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First choice is required" + } + }, + { + "name": "secondChoice", + "type": "string", + "label": "Second choice", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Second choice is required" + } + }, + { + "name": "thirdChoice", + "type": "string", + "label": "Third choice", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Third choice is required" + } + }, + { + "name": "reserveFirstAvailableName", + "type": "string", + "label": "Reserve First Available Name", + "required": false, + "validations": { + "regex": "^(yes|no)$", + "message": "Must select an option" + } + } + ] + }, + { + "name": "businessActivity", + "type": "array", + "label": "What type of business will use this name?", + "required": true, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + }, + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:reserve-company-name:admin_email}}", + "subject": "New Request to Reserve Society Name - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "reserve-company-name" + } + } + ] +} diff --git a/schemas/reserve-society-name.json b/schemas/reserve-society-name.json new file mode 100644 index 0000000..8a24435 --- /dev/null +++ b/schemas/reserve-society-name.json @@ -0,0 +1,156 @@ +{ + "id": "reserve-society-name", + "name": "Reserve Society Name Application", + "description": "Request to reserve society name", + "fields": [ + { + "name": "request", + "type": "object", + "required": true, + "fields": [ + { + "name": "purpose", + "type": "string", + "label": "What do you want to do?", + "required": true, + "validations": { + "regex": "^(request-search|reserve-name|change-name)$", + "message": "Must select an option" + } + }, + { + "name": "currentSocietyName", + "type": "string", + "label": "What is the current name of the society?", + "required": false, + "validations": { + "max": 100, + "message": "Current name of society is required" + } + } + ] + }, + { + "name": "proposed-names", + "type": "array", + "label": "What is the proposed society name?", + "required": true, + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "explanation": { + "type": "string" + } + } + } + }, + { + "name": "activities", + "type": "array", + "label": "What does the society do?", + "required": true, + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + }, + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true + } + ] + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:reserve-society-name:admin_email}}", + "subject": "New Request to Reserve Society Name - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "reserve-society-name" + } + } + ] +} diff --git a/schemas/sell-goods-services-beach-park.json b/schemas/sell-goods-services-beach-park.json new file mode 100644 index 0000000..4081786 --- /dev/null +++ b/schemas/sell-goods-services-beach-park.json @@ -0,0 +1,387 @@ +{ + "id": "sell-goods-services-beach-park", + "name": "Apply for a licence to sell goods or services at a beach or park", + "description": "Apply for a licence to sell goods or services at a beach or park", + "fields": [ + { + "name": "applicant", + "type": "object", + "required": true, + "fields": [ + { + "name": "title", + "type": "string", + "label": "Title", + "required": true, + "validations": { + "regex": "^(mr|ms|mrs)$", + "message": "Must select a valid title" + } + }, + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "middleName", + "type": "string", + "label": "Middle name", + "required": false, + "validations": { + "max": 100, + "message": "Middle name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "dateOfBirth", + "type": "date", + "label": "Date of birth", + "required": false, + "validations": { + "regex": "^\\d{4}-\\d{2}-\\d{2}$", + "message": "Date of birth is required and must be in YYYY-MM-DD format" + } + }, + { + "name": "idNumber", + "type": "string", + "label": "National Identification (ID) Number", + "required": false, + "validations": { + "min": 2, + "message": "ID Number must be at least 2 characters" + } + }, + { + "name": "passportNumber", + "type": "string", + "label": "Passport Number", + "required": false, + "validations": { + "message": "Passport number must be at least 6 characters" + } + }, + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone Number", + "required": true, + "validations": { + "regex": "^\\+?[0-9]{10,15}$", + "message": "Telephone number must be 10-15 digits" + } + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address Line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address Line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postalCode", + "type": "string", + "label": "Postal Code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + } + ] + }, + { + "name": "selling", + "type": "object", + "required": true, + "fields": [ + { + "name": "goodsOrServices", + "type": "string", + "required": true, + "validations": { + "regex": "^(goods|services)$", + "message": "Must be 'goods' or 'services'" + } + }, + { + "name": "manufacturingLocation", + "type": "string", + "required": false, + "validations": { + "max": 100, + "message": "Location is required" + } + } + ] + }, + { + "name": "business", + "type": "object", + "required": true, + "fields": [ + { + "name": "descriptionOfGoodsOrServices", + "type": "string", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Description is required" + } + }, + { + "name": "intendedPlaceOfDoingBusiness", + "type": "string", + "required": true, + "validations": { + "min": 2, + "max": 100, + "message": "Place of doing business is required" + } + } + ] + }, + { + "name": "professionalReferee", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "relationship", + "type": "string", + "label": "Relationship", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Relationship is required" + } + }, + { + "name": "email", + "type": "email", + "label": "Email address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone number", + "required": true + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postcode", + "type": "string", + "label": "Post code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + } + ] + }, + { + "name": "personalReferee", + "type": "object", + "required": true, + "fields": [ + { + "name": "firstName", + "type": "string", + "label": "First name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "First name is required" + } + }, + { + "name": "lastName", + "type": "string", + "label": "Last name", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Last name is required" + } + }, + { + "name": "relationship", + "type": "string", + "label": "Relationship", + "required": true, + "validations": { + "min": 1, + "max": 100, + "message": "Relationship is required" + } + }, + { + "name": "email", + "type": "email", + "label": "Email address", + "required": true + }, + { + "name": "telephoneNumber", + "type": "string", + "label": "Telephone number", + "required": true + }, + { + "name": "addressLine1", + "type": "string", + "label": "Address line 1", + "required": true, + "validations": { + "min": 5, + "max": 200, + "message": "Address must be at least 5 characters" + } + }, + { + "name": "addressLine2", + "type": "string", + "label": "Address line 2", + "required": false, + "validations": { + "max": 200 + } + }, + { + "name": "parish", + "type": "string", + "label": "Parish", + "required": true, + "validations": { + "regex": "^(christ-church|st-andrew|st-george|st-james|st-john|st-joseph|st-lucy|st-michael|st-peter|st-philip|st-thomas)$", + "message": "Must select a valid parish" + } + }, + { + "name": "postcode", + "type": "string", + "label": "Post code", + "required": false, + "validations": { + "regex": "^BB\\d{5}$", + "message": "Enter a valid postal code (e.g., BB17004)" + } + } + ] + } + ], + "processors": [ + { + "type": "email", + "config": { + "to": "{{db:sell-goods-services-beach-park:admin_email}}", + "subject": "New Request to sell good or services at a beach or park - {{formData.applicant.firstName}} {{formData.applicant.lastName}}", + "template": "sell-goods-services-beach-park-notice" + } + } + ] +} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 221ede6..30e21a0 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -21,6 +21,7 @@ export default () => ({ configurationSet: process.env.SES_CONFIGURATION_SET, tagKey: process.env.SES_TAG_KEY || 'ses:configuration-set', tagValue: process.env.SES_TAG_VALUE || 'prod', + endpoint: process.env.AWS_SES_ENDPOINT, // Custom endpoint for local development (e.g., aws-ses-v2-local) }, s3: { bucketName: process.env.BUCKET_NAME, diff --git a/src/database/datasource.ts b/src/database/datasource.ts index 8fa8009..efd0c25 100644 --- a/src/database/datasource.ts +++ b/src/database/datasource.ts @@ -5,9 +5,12 @@ import { config } from 'dotenv'; // Load environment variables config(); +const dbHost = process.env.DB_HOST || 'localhost'; +const isLocalDatabase = dbHost === 'localhost' || dbHost === '127.0.0.1'; + export const dataSource = new DataSource({ type: 'postgres', - host: process.env.DB_HOST || 'localhost', + host: dbHost, port: parseInt(process.env.DB_PORT, 10) || 5432, username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', @@ -16,7 +19,10 @@ export const dataSource = new DataSource({ migrations: [path.join(__dirname, './migrations/*{.ts,.js}')], synchronize: false, logging: process.env.DB_LOGGING === 'true', - ssl: { - rejectUnauthorized: false, - }, + // Automatically disable SSL for localhost, enable for remote databases + ssl: isLocalDatabase + ? false + : { + rejectUnauthorized: false, + }, }); diff --git a/src/email/email.service.ts b/src/email/email.service.ts index 86297b6..4571c9d 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -28,10 +28,12 @@ export class EmailService { constructor(private readonly configService: ConfigService) { const region = this.configService.get('aws.region', 'us-east-1'); + const sesEndpoint = this.configService.get('aws.ses.endpoint'); - // Initialize AWS SES v2 client + // Initialize AWS SES v2 client (with optional custom endpoint for local development) this.sesClient = new SESv2Client({ region, + ...(sesEndpoint && { endpoint: sesEndpoint }), }); this.defaultFromEmail = this.configService.get('aws.ses.fromEmail'); @@ -44,7 +46,13 @@ export class EmailService { // Register Handlebars helpers this.registerHandlebarsHelpers(); - this.logger.log('EmailService initialized with AWS SES v2'); + if (sesEndpoint) { + this.logger.log( + `EmailService initialized with AWS SES v2 (custom endpoint: ${sesEndpoint})`, + ); + } else { + this.logger.log('EmailService initialized with AWS SES v2'); + } } private registerHandlebarsHelpers(): void { @@ -102,11 +110,6 @@ export class EmailService { async sendEmail(options: EmailOptions): Promise { try { - if (this.configService.get('app.nodeEnv') === 'local') { - this.logger.log('Local env detected, email will not be sent', options); - return; - } - const from = options.from || this.defaultFromEmail; const to = Array.isArray(options.to) ? options.to : [options.to]; diff --git a/src/email/templates/apply-for-conductor-licence.hbs b/src/email/templates/apply-for-conductor-licence.hbs new file mode 100644 index 0000000..e6842ce --- /dev/null +++ b/src/email/templates/apply-for-conductor-licence.hbs @@ -0,0 +1,133 @@ + + + + + + +
+

📮 Post Office Redirection Notice Request

+
+ +
+

A new post office redirection notice request has been submitted.

+ +
+
Personal Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Date of Birth: + {{applicant.dateOfBirth}} +
+
+ ID Number: + {{applicant.idNumber}} +
+ {{#if applicant.passportNumber}} +
+ Passport Number: + {{applicant.passportNumber}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + +
+ +
+
Contact Details
+
+
+
+ Address Line 1: + {{contactDetails.addressLine1}} +
+ {{#if contactDetails.addressLine2}} +
+ Address Line 2: + {{contactDetails.addressLine2}} +
+ {{/if}} +
+ Parish: + {{contactDetails.parish}} +
+ {{#if contactDetails.postalCode}} +
+ Postal Code: + {{contactDetails.postalCode}} +
+ {{/if}} + + {{#if contactDetails.email}} +
+ Email: + {{contactDetails.email}} +
+ {{/if}} + {{#if contactDetails.telephoneNumber}} +
+ Phone Number: + {{contactDetails.telephoneNumber}} +
+ {{/if}} +
+
+
+ +
+
House Member Information
+
+ Member Name: + {{houseMembers.firstName}} + {{houseMembers.lastName}} +
+
+ Member ID Number: + {{houseMembers.idNumber}} +
+ {{#if houseMembers.addAnother}} +
+ Add Another Member: + {{houseMembers.addAnother}} +
+ {{/if}} +
+ + +
+ + \ No newline at end of file diff --git a/src/email/templates/death-certificate.hbs b/src/email/templates/death-certificate.hbs index 07e22a6..435fefb 100644 --- a/src/email/templates/death-certificate.hbs +++ b/src/email/templates/death-certificate.hbs @@ -27,12 +27,8 @@ @@ -74,10 +70,10 @@ {{applicant.email}} {{/if}} - {{#if applicant.phoneNumber}} + {{#if applicant.telephoneNumber}} - Phone Number: - {{applicant.phoneNumber}} + Telephone Number: + {{applicant.telephoneNumber}} {{/if}} @@ -116,20 +112,38 @@

Identification

- {{#if applicant.usePassportInstead}} - {{#if applicant.passportNumber}} - - - - - {{/if}} - {{else}} - {{#if applicant.idNumber}} - - - - - {{/if}} + {{#if applicant.idNumber}} + + + + + {{/if}} + {{#if applicant.passportNumber}} + + + + + {{/if}} + +
Passport Number:{{applicant.passportNumber}}
National ID Number:{{applicant.idNumber}}
National ID Number:{{applicant.idNumber}}
Passport Number:{{applicant.passportNumber}}
+ + + +
+

Application Details

+ + + {{#if relationship}} + + + + + {{/if}} + {{#if reasonForRequest}} + + + + {{/if}}
Relationship to Deceased:{{relationship}}
Reason for Request:{{reasonForRequest}}
@@ -158,10 +172,30 @@ {{deceased.lastName}} {{/if}} - {{#if deceased.dateOfBirth}} + {{#if deceased.idNumber}} + + National ID Number: + {{deceased.idNumber}} + + {{/if}} + {{#if deceased.knownDateOfDeath}} + + Known Date of Death: + {{#if + (eq deceased.knownDateOfDeath 'yes') + }}Yes{{else}}No{{/if}} + + {{/if}} + {{#if deceased.dateOfDeath}} + + Date of Death: + {{deceased.dateOfDeath}} + + {{/if}} + {{#if deceased.estimatedDateOfDeath}} - Date of Birth: - {{deceased.dateOfBirth}} + Estimated Date of Death: + {{deceased.estimatedDateOfDeath}} {{/if}} {{#if deceased.placeOfDeath}} @@ -194,13 +228,12 @@

This is an automated email from the Government of Barbados Forms Processing System.
- Birth Certificate applications are processed during regular business + Death Certificate applications are processed during regular business hours.
Please allow 5-10 business days for processing.

Important: - If this application is for someone other than yourself, additional - documentation may be required for verification. + Additional documentation may be required for verification purposes.

diff --git a/src/email/templates/exit-survey.hbs b/src/email/templates/exit-survey.hbs new file mode 100644 index 0000000..3f569ee --- /dev/null +++ b/src/email/templates/exit-survey.hbs @@ -0,0 +1,128 @@ + + + + + + +
+

📋 New Exit Survey Submission

+ + + + +
+

Survey Responses

+ + + + + + + + + + + + + + + {{#if technicalProblemsDescription}} + + + + + {{/if}} + +
Difficulty Rating: + {{#if (eq difficultyRating 'very-easy')}} + Very Easy + {{else if (eq difficultyRating 'easy')}} + Easy + {{else if (eq difficultyRating 'neither')}} + Neither Easy nor + Difficult + {{else if (eq difficultyRating 'difficult')}} + Difficult + {{else if (eq difficultyRating 'very-difficult')}} + Very + Difficult + {{else}} + {{difficultyRating}} + {{/if}} +
Clarity Rating: + {{#if (eq clarityRating 'very-clear')}} + Very Clear + {{else if (eq clarityRating 'clear')}} + Clear + {{else if (eq clarityRating 'neither')}} + Neither Clear nor + Unclear + {{else if (eq clarityRating 'unclear')}} + Unclear + {{else if (eq clarityRating 'very-unclear')}} + Very Unclear + {{else}} + {{clarityRating}} + {{/if}} +
Technical Problems: + {{#if (eq technicalProblems 'yes')}} + Yes + {{else if (eq technicalProblems 'no')}} + No + {{else}} + {{technicalProblems}} + {{/if}} +
Problem Description:{{technicalProblemsDescription}}
+
+ + +
+

Suggested Improvements

+
+ What could be improved: +

{{areasForImprovement}}

+
+
+ + + +
+ + \ No newline at end of file diff --git a/src/email/templates/jobstart-plus-programme.hbs b/src/email/templates/jobstart-plus-programme.hbs new file mode 100644 index 0000000..53aeca5 --- /dev/null +++ b/src/email/templates/jobstart-plus-programme.hbs @@ -0,0 +1,114 @@ + + + + + + +
+

New JobStart Plus Application

+
+ +
+

A new application to the JobStart Plus programme has been submitted.

+ +
+
Personal Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Date of Birth: + {{applicant.dateOfBirth}} +
+
+ ID Number: + {{applicant.idNumber}} +
+ {{#if applicant.passportNumber}} +
+ Passport Number: + {{applicant.passportNumber}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + +
+ +
+
Contact Details
+
+
+
+ Address Line 1: + {{contactDetails.addressLine1}} +
+ {{#if contactDetails.addressLine2}} +
+ Address Line 2: + {{contactDetails.addressLine2}} +
+ {{/if}} +
+ Parish: + {{contactDetails.parish}} +
+ {{#if contactDetails.postalCode}} +
+ Postal Code: + {{contactDetails.postalCode}} +
+ {{/if}} + + {{#if contactDetails.email}} +
+ Email: + {{contactDetails.email}} +
+ {{/if}} + {{#if contactDetails.telephoneNumber}} +
+ Phone Number: + {{contactDetails.telephoneNumber}} +
+ {{/if}} +
+
+
+ + +
+ + \ No newline at end of file diff --git a/src/email/templates/permission-to-remove-tree.hbs b/src/email/templates/permission-to-remove-tree.hbs new file mode 100644 index 0000000..ff3b442 --- /dev/null +++ b/src/email/templates/permission-to-remove-tree.hbs @@ -0,0 +1,119 @@ + + + + + + +
+

Request for Permission to Remove Protected Tree(s)

+
+ +
+

A new request for permission to remove protected tree has been + submitted.

+ +
+
Personal Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address Line 1: + {{applicant.addressLine1}} +
+ {{#if applicant.addressLine2}} +
+ Address Line 2: + {{applicant.addressLine2}} +
+ {{/if}} +
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + {{#if applicant.mobileNumber}} +
+ Mobile Number: + {{applicant.mobileNumber}} +
+ {{/if}} + {{#if applicant.homeNumber}} +
+ Home Number: + {{applicant.homeNumber}} +
+ {{/if}} +
+ + {{#if treesToRemove}} +
+
Trees to remove
+ {{#each treesToRemove}} +
+
+ Type of Protected Tree: + {{this.typeOfProtectedTree}} +
+
+ Reason for Removing Tree: + {{this.reasonForRemovingTree}} +
+
+ Address Line 1: + {{this.addressLine1}} +
+
+ Address Line 2: + {{this.addressLine2}} +
+
+ Parish: + {{this.parish}} +
+
+ {{/each}} +
+ {{/if}} + + +
+ + \ No newline at end of file diff --git a/src/email/templates/post-office-redirection-notice.hbs b/src/email/templates/post-office-redirection-notice.hbs index 4d5fc02..5346102 100644 --- a/src/email/templates/post-office-redirection-notice.hbs +++ b/src/email/templates/post-office-redirection-notice.hbs @@ -33,17 +33,42 @@
Personal Information
Name: - {{personal.firstName}} - {{personal.lastName}} + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}}
Date of Birth: - {{personal.dateOfBirth}} + {{applicant.dateOfBirth}}
ID Number: - {{personal.idNumber}} + {{applicant.idNumber}}
+ {{#if applicant.passportNumber}} +
+ Passport Number: + {{applicant.passportNumber}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + {{#if applicant.telephoneNumber}} +
+ Phone Number: + {{applicant.telephoneNumber}} +
+ {{/if}}
@@ -118,26 +143,6 @@ {{/if}}
- {{#if businessInformation}} -
-
Business Information
-
- For Business/Company: - {{#if - (eq businessInformation.belongsToBusiness 'true') - }}Yes{{else}}No{{/if}} -
- {{#if businessInformation.businessName}} -
- Business Name: - {{businessInformation.businessName}} -
- {{/if}} -
- {{/if}} - diff --git a/src/email/templates/primary-school-textbook-grant.hbs b/src/email/templates/primary-school-textbook-grant.hbs index d4c1726..bc39dd3 100644 --- a/src/email/templates/primary-school-textbook-grant.hbs +++ b/src/email/templates/primary-school-textbook-grant.hbs @@ -35,26 +35,99 @@ {{beneficiaries.firstName}} {{beneficiaries.lastName}} + {{#if beneficiaries.idNumber}} +
+ Student ID: + {{beneficiaries.idNumber}} +
+ {{/if}} + {{#if beneficiaries.passportNumber}} +
+ Passport Number: + {{beneficiaries.passportNumber}} +
+ {{/if}}
- Student ID: - {{beneficiaries.idNumber}} + Class/Grade: + Class {{beneficiaries.class}}
- Gender: - {{beneficiaries.gender}} + Relationship to Child: + {{beneficiaries.relationshipToChild}}
+ + +
+
Applicant Information
- Class/Grade: - Class {{beneficiaries.class}} + Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} +
+ Email: + {{applicant.email}} +
+
+ Phone: + {{applicant.telephoneNumber}} +
+ {{#if applicant.idNumber}} +
+ ID Number: + {{applicant.idNumber}} +
+ {{/if}} + {{#if applicant.passportNumber}} +
+ Passport Number: + {{applicant.passportNumber}} +
+ {{/if}} + {{#if applicant.tamisNumber}} +
+ TAMIS Number: + {{applicant.tamisNumber}} +
+ {{/if}} +
+ +
+
Parent/Guardian Relationship
+
+ Are you the parent or guardian? + {{guardianOrParentRelationship}}
Guardian Information
+ {{#if guardian.title}} +
+ Title: + {{guardian.title}} +
+ {{/if}}
Guardian Name: - {{guardian.title}} - {{guardian.firstName}} + {{guardian.firstName}} {{#if guardian.middleName}}{{guardian.middleName}} {{/if}}{{guardian.lastName}}
@@ -101,24 +174,24 @@
Bank Account Information
- Bank: - {{bankAccount.bank}} + Account Holder Name: + {{bankAccount.accountHolderName}}
- Branch: - {{bankAccount.branch}} + Bank Name: + {{bankAccount.bankName}}
- Account Type: - {{bankAccount.accountType}} + Account Number: + {{bankAccount.accountNumber}}
- Name on Account: - {{bankAccount.nameOnAccount}} + Branch Location: + {{bankAccount.branchLocation}}
- Account Number: - {{bankAccount.accountNumber}} + Account Type: + {{bankAccount.accountType}}
diff --git a/src/email/templates/reserve-company-name.hbs b/src/email/templates/reserve-company-name.hbs new file mode 100644 index 0000000..a798735 --- /dev/null +++ b/src/email/templates/reserve-company-name.hbs @@ -0,0 +1,126 @@ + + + + + + +
+

Request to Reserve Company Name Application

+
+ +
+

A new request to reserve a company name has been submitted.

+ +
+
Purpose of New Company Name
+
+ What will this name be used for? + {{purposeOfNewCompanyName}} +
+ {{#if companyPresentName}} +
+ What is the present name of the company? + {{companyPresentName}} +
+ {{/if}} +
+ +
+
Business Activities
+ {{#each businessActivity}} +
+ • {{this.value}} +
+ {{/each}} +
+ +
+
Applicant Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} +
+ Email: + {{applicant.email}} +
+
+ Phone: + {{applicant.telephoneNumber}} +
+ +
+ +
+
Request Details
+
+ Purpose: + {{request.purpose}} +
+ {{#if request.currentSocietyName}} +
+ Current Society Name: + {{request.currentSocietyName}} +
+ {{/if}} +
+ +
+
Contact Information
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ +
+ + +
+ + \ No newline at end of file diff --git a/src/email/templates/reserve-society-name.hbs b/src/email/templates/reserve-society-name.hbs new file mode 100644 index 0000000..2be3b76 --- /dev/null +++ b/src/email/templates/reserve-society-name.hbs @@ -0,0 +1,126 @@ + + + + + + +
+

Request to Reserve Society Name Application

+
+ +
+

A new request to reserve a society name has been submitted.

+ +
+
Applicant Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} +
+ Email: + {{applicant.email}} +
+
+ Phone: + {{applicant.telephoneNumber}} +
+ +
+ +
+
Request Details
+
+ Purpose: + {{request.purpose}} +
+ {{#if request.currentSocietyName}} +
+ Current Society Name: + {{request.currentSocietyName}} +
+ {{/if}} +
+ +
+
Proposed Society Names
+ {{#each proposed-names}} +
+ Option {{@index}}: + {{this.title}} +
+
+ Explanation: + {{this.explanation}} +
+ {{/each}} +
+ +
+
Society Activities
+ {{#each activities}} +
+ • {{this.value}} +
+ {{/each}} +
+ +
+
Contact Information
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ +
+ + +
+ + \ No newline at end of file diff --git a/src/email/templates/sell-goods-services-beach-park-notice.hbs b/src/email/templates/sell-goods-services-beach-park-notice.hbs new file mode 100644 index 0000000..87f623e --- /dev/null +++ b/src/email/templates/sell-goods-services-beach-park-notice.hbs @@ -0,0 +1,82 @@ + + + + + + +
+

Request to sell goods or services at a beach or park

+
+ +
+

A new request to sell goods or services at a beach or park has been + submitted.

+ +
+
Personal Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Date of Birth: + {{applicant.dateOfBirth}} +
+
+ ID Number: + {{applicant.idNumber}} +
+ {{#if applicant.passportNumber}} +
+ Passport Number: + {{applicant.passportNumber}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + {{#if applicant.email}} +
+ Email: + {{applicant.email}} +
+ {{/if}} + {{#if applicant.telephoneNumber}} +
+ Phone Number: + {{applicant.telephoneNumber}} +
+ {{/if}} +
+ + +
+ + \ No newline at end of file diff --git a/src/forms/expression-resolver.service.spec.ts b/src/forms/expression-resolver.service.spec.ts new file mode 100644 index 0000000..acb142e --- /dev/null +++ b/src/forms/expression-resolver.service.spec.ts @@ -0,0 +1,853 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { Repository } from 'typeorm'; +import { + ExpressionResolverService, + ExpressionContext, +} from './expression-resolver.service'; +import { FormConfig } from '../database/entities'; + +describe('ExpressionResolverService', () => { + let service: ExpressionResolverService; + let mockConfigRepository: jest.Mocked>; + + // Helper function to create mock FormConfig + const createMockFormConfig = ( + id: string, + formId: string, + key: string, + value: string, + description: string | null = null, + ): FormConfig => ({ + id, + formId, + key, + value, + description, + createdAt: new Date(), + updatedAt: new Date(), + }); + + beforeEach(async () => { + // Mock ConfigService + const mockConfigService = { + get: jest.fn((key: string, defaultValue?: any) => { + const config: Record = { + TEST_FORM_ADMIN_EMAIL: 'admin@test.com', + TEST_FORM_PAYMENT_CODE: 'PAY123', + }; + return config[key] || defaultValue; + }), + }; + + // Mock FormConfig Repository + mockConfigRepository = { + findOne: jest.fn(), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ExpressionResolverService, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + service = module.get(ExpressionResolverService); + }); + + describe('resolveExpression - Simple Cases', () => { + it('should return non-string values as-is', async () => { + const context: ExpressionContext = { + formId: 'test-form', + }; + + expect(await service.resolveExpression(123, context)).toBe(123); + expect(await service.resolveExpression(0, context)).toBe(0); + expect(await service.resolveExpression(null as any, context)).toBe(null); + }); + + it('should return strings without expressions as-is', async () => { + const context: ExpressionContext = { + formId: 'test-form', + }; + + expect(await service.resolveExpression('plain text', context)).toBe( + 'plain text', + ); + expect(await service.resolveExpression('no brackets here', context)).toBe( + 'no brackets here', + ); + }); + }); + + describe('resolveExpression - Single formData References', () => { + it('should resolve single formData field reference', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + firstName: 'John', + }, + }; + + const result = await service.resolveExpression( + '{{formData.firstName}}', + context, + ); + expect(result).toBe('John'); + }); + + it('should resolve nested formData field reference', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + applicant: { + firstName: 'Jane', + lastName: 'Doe', + }, + }, + }; + + expect( + await service.resolveExpression( + '{{formData.applicant.firstName}}', + context, + ), + ).toBe('Jane'); + expect( + await service.resolveExpression( + '{{formData.applicant.lastName}}', + context, + ), + ).toBe('Doe'); + }); + + it('should resolve deeply nested formData references', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + personal: { + contact: { + address: { + street: '123 Main St', + }, + }, + }, + }, + }; + + const result = await service.resolveExpression( + '{{formData.personal.contact.address.street}}', + context, + ); + expect(result).toBe('123 Main St'); + }); + + it('should handle missing formData fields gracefully', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + firstName: 'John', + }, + }; + + const result = await service.resolveExpression( + '{{formData.lastName}}', + context, + ); + expect(result).toBe('0'); // Default fallback for missing fields + }); + }); + + describe('resolveExpression - Multiple Embedded Expressions', () => { + it('should resolve multiple formData expressions in a string', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + applicant: { + firstName: 'John', + lastName: 'Smith', + }, + }, + }; + + const result = await service.resolveExpression( + 'New Death Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}', + context, + ); + expect(result).toBe('New Death Certificate Application - John Smith'); + }); + + it('should resolve multiple expressions with text in between', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + personal: { + firstName: 'Alice', + lastName: 'Johnson', + }, + age: 30, + }, + }; + + const result = await service.resolveExpression( + 'Hello {{formData.personal.firstName}} {{formData.personal.lastName}}, you are {{formData.age}} years old', + context, + ); + expect(result).toBe('Hello Alice Johnson, you are 30 years old'); + }); + + it('should resolve birth registration subject line', async () => { + const context: ExpressionContext = { + formId: 'register-birth-form', + formData: { + child: { + firstNames: 'Emma Grace', + lastName: 'Williams', + }, + }, + }; + + const result = await service.resolveExpression( + 'New Birth Registration - {{formData.child.firstNames}} {{formData.child.lastName}}', + context, + ); + expect(result).toBe('New Birth Registration - Emma Grace Williams'); + }); + + it('should resolve marriage certificate subject line', async () => { + const context: ExpressionContext = { + formId: 'get-marriage-certificate', + formData: { + applicant: { + firstName: 'Michael', + lastName: 'Brown', + }, + }, + }; + + const result = await service.resolveExpression( + 'New Marriage Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}', + context, + ); + expect(result).toBe( + 'New Marriage Certificate Application - Michael Brown', + ); + }); + + it('should resolve exit survey subject with single field', async () => { + const context: ExpressionContext = { + formId: 'exit-survey', + formData: { + difficultyRating: 'Easy', + }, + }; + + const result = await service.resolveExpression( + 'New Exit Survey Submission - {{formData.difficultyRating}} Experience', + context, + ); + expect(result).toBe('New Exit Survey Submission - Easy Experience'); + }); + + it('should handle expressions at the start and end of string', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + greeting: 'Hello', + name: 'World', + }, + }; + + const result = await service.resolveExpression( + '{{formData.greeting}} there, {{formData.name}}', + context, + ); + expect(result).toBe('Hello there, World'); + }); + + it('should handle three or more expressions in a string', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + title: 'Mr', + firstName: 'John', + middleName: 'Michael', + lastName: 'Doe', + }, + }; + + const result = await service.resolveExpression( + '{{formData.title}}. {{formData.firstName}} {{formData.middleName}} {{formData.lastName}}', + context, + ); + expect(result).toBe('Mr. John Michael Doe'); + }); + }); + + describe('resolveExpression - Database References', () => { + it('should resolve database reference with form-id:key format', async () => { + mockConfigRepository.findOne.mockResolvedValue( + createMockFormConfig( + '1', + 'test-form', + 'admin_email', + 'admin@example.com', + ), + ); + + const context: ExpressionContext = { + formId: 'test-form', + configRepository: mockConfigRepository, + }; + + const result = await service.resolveExpression( + '{{db:test-form:admin_email}}', + context, + ); + expect(result).toBe('admin@example.com'); + }); + + it('should resolve database reference with key-only format (uses current formId)', async () => { + mockConfigRepository.findOne.mockResolvedValue( + createMockFormConfig('1', 'test-form', 'payment_code', 'PAY456'), + ); + + const context: ExpressionContext = { + formId: 'test-form', + configRepository: mockConfigRepository, + }; + + const result = await service.resolveExpression( + '{{db:payment_code}}', + context, + ); + expect(result).toBe('PAY456'); + }); + + it('should fallback to environment variable if database lookup fails', async () => { + mockConfigRepository.findOne.mockResolvedValue(null); + + const context: ExpressionContext = { + formId: 'test-form', + configRepository: mockConfigRepository, + }; + + const result = await service.resolveExpression( + '{{db:test-form:admin_email}}', + context, + ); + expect(result).toBe('admin@test.com'); + }); + + it('should resolve database reference in embedded string', async () => { + mockConfigRepository.findOne.mockResolvedValue( + createMockFormConfig( + '1', + 'test-form', + 'department_name', + 'Revenue Authority', + ), + ); + + const context: ExpressionContext = { + formId: 'test-form', + configRepository: mockConfigRepository, + formData: { + applicant: { + firstName: 'John', + }, + }, + }; + + const result = await service.resolveExpression( + 'Application from {{formData.applicant.firstName}} to {{db:department_name}}', + context, + ); + expect(result).toBe('Application from John to Revenue Authority'); + }); + }); + + describe('resolveExpression - Mathematical Expressions', () => { + it('should evaluate simple multiplication', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + order: { + numberOfCopies: 3, + }, + }, + }; + + const result = await service.resolveExpression( + '{{formData.order.numberOfCopies * 25}}', + context, + ); + expect(result).toBe(75); + }); + + it('should evaluate formData field multiplied by database value', async () => { + mockConfigRepository.findOne.mockResolvedValue( + createMockFormConfig( + '1', + 'get-death-certificate', + 'payment_amount', + '50', + ), + ); + + const context: ExpressionContext = { + formId: 'get-death-certificate', + configRepository: mockConfigRepository, + formData: { + order: { + numberOfCopies: 2, + }, + }, + }; + + const result = await service.resolveExpression( + '{{formData.order.numberOfCopies * db:get-death-certificate:payment_amount}}', + context, + ); + expect(result).toBe(100); + }); + + it('should evaluate addition', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + basePrice: 100, + tax: 15, + }, + }; + + const result = await service.resolveExpression( + '{{formData.basePrice + formData.tax}}', + context, + ); + expect(result).toBe(115); + }); + + it('should evaluate complex mathematical expression', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + quantity: 5, + price: 20, + discount: 10, + }, + }; + + const result = await service.resolveExpression( + '{{(formData.quantity * formData.price) - formData.discount}}', + context, + ); + expect(result).toBe(90); + }); + + it('should handle division', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + total: 100, + people: 4, + }, + }; + + const result = await service.resolveExpression( + '{{formData.total / formData.people}}', + context, + ); + expect(result).toBe(25); + }); + + it('should handle subtraction', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + budget: 1000, + spent: 350, + }, + }; + + const result = await service.resolveExpression( + '{{formData.budget - formData.spent}}', + context, + ); + expect(result).toBe(650); + }); + }); + + describe('resolveObjectExpressions - Complex Objects', () => { + it('should resolve expressions in nested objects', async () => { + mockConfigRepository.findOne.mockResolvedValue( + createMockFormConfig( + '1', + 'test-form', + 'admin_email', + 'admin@example.com', + ), + ); + + const context: ExpressionContext = { + formId: 'test-form', + configRepository: mockConfigRepository, + formData: { + applicant: { + firstName: 'John', + lastName: 'Doe', + }, + }, + }; + + const input = { + email: { + to: '{{db:admin_email}}', + subject: + 'Application from {{formData.applicant.firstName}} {{formData.applicant.lastName}}', + }, + }; + + const result = await service.resolveObjectExpressions(input, context); + expect(result).toEqual({ + email: { + to: 'admin@example.com', + subject: 'Application from John Doe', + }, + }); + }); + + it('should resolve expressions in arrays', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + name: 'John', + email: 'john@example.com', + }, + }; + + const input = [ + 'Hello {{formData.name}}', + 'Your email is {{formData.email}}', + ]; + + const result = await service.resolveObjectExpressions(input, context); + expect(result).toEqual(['Hello John', 'Your email is john@example.com']); + }); + + it('should resolve complete processor config', async () => { + // Mock based on the key being requested + mockConfigRepository.findOne.mockImplementation(async (options: any) => { + const key = options.where.key; + if (key === 'admin_email') { + return createMockFormConfig( + '1', + 'get-death-certificate', + 'admin_email', + 'death-certs@gov.bb', + ); + } else if (key === 'payment_code') { + return createMockFormConfig( + '2', + 'get-death-certificate', + 'payment_code', + 'DEATH_CERT_001', + ); + } else if (key === 'payment_amount') { + return createMockFormConfig( + '3', + 'get-death-certificate', + 'payment_amount', + '75', + ); + } + return null; + }); + + const context: ExpressionContext = { + formId: 'get-death-certificate', + configRepository: mockConfigRepository, + formData: { + applicant: { + firstName: 'Sarah', + lastName: 'Johnson', + }, + order: { + numberOfCopies: 2, + }, + }, + }; + + const processorConfig = { + payment: { + provider: 'ezpay', + department: 'revenue_authority', + paymentCode: '{{db:payment_code}}', + amount: '{{formData.order.numberOfCopies * db:payment_amount}}', + description: 'Death Certificate Processing Fee (per copy)', + }, + email: { + to: '{{db:admin_email}}', + subject: + 'New Death Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}', + template: 'death-certificate', + }, + }; + + const result = await service.resolveObjectExpressions( + processorConfig, + context, + ); + expect(result).toEqual({ + payment: { + provider: 'ezpay', + department: 'revenue_authority', + paymentCode: 'DEATH_CERT_001', + amount: 150, + description: 'Death Certificate Processing Fee (per copy)', + }, + email: { + to: 'death-certs@gov.bb', + subject: 'New Death Certificate Application - Sarah Johnson', + template: 'death-certificate', + }, + }); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty formData', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: {}, + }; + + const result = await service.resolveExpression( + '{{formData.name}}', + context, + ); + expect(result).toBe('0'); + }); + + it('should handle undefined formData', async () => { + const context: ExpressionContext = { + formId: 'test-form', + }; + + const result = await service.resolveExpression( + '{{formData.name}}', + context, + ); + expect(result).toBe('0'); + }); + + it('should handle expressions with special characters in text', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + name: 'John', + }, + }; + + const result = await service.resolveExpression( + 'Email: {{formData.name}}@example.com', + context, + ); + expect(result).toBe('Email: John@example.com'); + }); + + it('should handle consecutive expressions without space', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + firstName: 'John', + lastName: 'Doe', + }, + }; + + const result = await service.resolveExpression( + '{{formData.firstName}}{{formData.lastName}}', + context, + ); + expect(result).toBe('JohnDoe'); + }); + + it('should handle numeric formData values', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + age: 25, + score: 98.5, + }, + }; + + expect( + await service.resolveExpression('Age: {{formData.age}}', context), + ).toBe('Age: 25'); + expect( + await service.resolveExpression('Score: {{formData.score}}', context), + ).toBe('Score: 98.5'); + }); + + it('should handle boolean formData values', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + isActive: true, + isDeleted: false, + }, + }; + + expect( + await service.resolveExpression( + 'Active: {{formData.isActive}}', + context, + ), + ).toBe('Active: true'); + expect( + await service.resolveExpression( + 'Deleted: {{formData.isDeleted}}', + context, + ), + ).toBe('Deleted: false'); + }); + + it('should handle null values in formData', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + middleName: null, + }, + }; + + const result = await service.resolveExpression( + 'Middle: {{formData.middleName}}', + context, + ); + expect(result).toBe('Middle: 0'); + }); + + it('should not resolve malformed expressions', async () => { + const context: ExpressionContext = { + formId: 'test-form', + formData: { + name: 'John', + }, + }; + + // Missing closing braces + expect(await service.resolveExpression('{{formData.name', context)).toBe( + '{{formData.name', + ); + + // Missing opening braces + expect(await service.resolveExpression('formData.name}}', context)).toBe( + 'formData.name}}', + ); + + // Single braces + expect(await service.resolveExpression('{formData.name}', context)).toBe( + '{formData.name}', + ); + }); + }); + + describe('Real-World Form Scenarios', () => { + it('should resolve complete death certificate form submission', async () => { + mockConfigRepository.findOne.mockResolvedValueOnce( + createMockFormConfig( + '1', + 'get-death-certificate', + 'admin_email', + 'registrar@gov.bb', + ), + ); + + const context: ExpressionContext = { + formId: 'get-death-certificate', + configRepository: mockConfigRepository, + formData: { + applicant: { + title: 'Mrs', + firstName: 'Mary', + lastName: 'Thompson', + email: 'mary.thompson@example.com', + telephoneNumber: '+12465551234', + }, + deceased: { + firstName: 'Robert', + lastName: 'Thompson', + dateOfDeath: '2024-01-15', + }, + relationship: 'Spouse', + order: { + numberOfCopies: 1, + }, + }, + }; + + const emailConfig = { + to: '{{db:admin_email}}', + subject: + 'New Death Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}', + template: 'death-certificate', + }; + + const result = await service.resolveObjectExpressions( + emailConfig, + context, + ); + expect(result).toEqual({ + to: 'registrar@gov.bb', + subject: 'New Death Certificate Application - Mary Thompson', + template: 'death-certificate', + }); + }); + + it('should resolve birth registration form with multiple names', async () => { + const context: ExpressionContext = { + formId: 'register-birth-form', + formData: { + child: { + firstNames: 'Emily Rose', + middleName: 'Grace', + lastName: 'Anderson', + }, + mother: { + firstName: 'Jennifer', + lastName: 'Anderson', + }, + }, + }; + + const subject = + 'New Birth Registration - {{formData.child.firstNames}} {{formData.child.lastName}}'; + const result = await service.resolveExpression(subject, context); + expect(result).toBe('New Birth Registration - Emily Rose Anderson'); + }); + + it('should resolve project protege mentor registration', async () => { + const context: ExpressionContext = { + formId: 'project-protege-mentor', + formData: { + personal: { + firstName: 'David', + lastName: 'Martinez', + email: 'david.martinez@example.com', + }, + expertise: 'Software Engineering', + }, + }; + + const subject = + 'New Project Protege Mentor Registration - {{formData.personal.firstName}} {{formData.personal.lastName}}'; + const result = await service.resolveExpression(subject, context); + expect(result).toBe( + 'New Project Protege Mentor Registration - David Martinez', + ); + }); + }); +}); diff --git a/src/forms/expression-resolver.service.ts b/src/forms/expression-resolver.service.ts index 869177d..c48698a 100644 --- a/src/forms/expression-resolver.service.ts +++ b/src/forms/expression-resolver.service.ts @@ -22,6 +22,7 @@ export class ExpressionResolverService { * - Form data references: {{formData.path}} * - Mathematical expressions: {{formData.field * db:form-id:amount}} * - Simple values: direct strings or numbers + * - Multiple expressions in a string: "Hello {{formData.name}} from {{formData.city}}" */ async resolveExpression( expression: string | number, @@ -37,13 +38,21 @@ export class ExpressionResolverService { return expression; } - // Handle complex expressions (mathematical operations) - if (this.isMathematicalExpression(expression)) { - return await this.resolveMathematicalExpression(expression, context); + // Check if the entire string is a single expression (no braces in the middle) + const isSingleExpression = /^\{\{[^{}]+\}\}$/.test(expression); + + if (isSingleExpression) { + // Handle complex expressions (mathematical operations) + if (this.isMathematicalExpression(expression)) { + return await this.resolveMathematicalExpression(expression, context); + } + + // Handle simple variable replacement + return await this.resolveSimpleExpression(expression, context); } - // Handle simple variable replacement - return await this.resolveSimpleExpression(expression, context); + // Handle strings with multiple embedded expressions + return await this.resolveEmbeddedExpressions(expression, context); } /** @@ -127,6 +136,64 @@ export class ExpressionResolverService { return innerExpression; // Return the resolved inner content without {{}} } + /** + * Resolve strings with multiple embedded expressions + * Example: "Hello {{formData.name}} from {{formData.city}}" + */ + private async resolveEmbeddedExpressions( + expression: string, + context: ExpressionContext, + ): Promise { + let result = expression; + + // Find all {{...}} patterns in the string (non-greedy match until }}) + const expressionPattern = /\{\{(.*?)\}\}/g; + const matches = [...expression.matchAll(expressionPattern)]; + + // Create a map of replacements to avoid issues with overlapping matches + const replacements: Map = new Map(); + + // Process each expression found + for (const match of matches) { + const [fullMatch, innerExpression] = match; + + // Skip if already processed + if (replacements.has(fullMatch)) { + continue; + } + + try { + // Resolve database references + let resolved = await this.replaceDatabaseReferences( + innerExpression, + context, + ); + + // Resolve form data references + resolved = this.replaceFormDataReferences(resolved, context); + + replacements.set(fullMatch, resolved); + } catch (error) { + this.logger.warn( + `Failed to resolve embedded expression ${fullMatch}:`, + error.message, + ); + // Leave the expression as-is if it fails to resolve + } + } + + // Apply all replacements + for (const [pattern, value] of replacements) { + // Escape special regex characters in the pattern + const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Create a global regex to replace all occurrences + const regex = new RegExp(escapedPattern, 'g'); + result = result.replace(regex, value); + } + + return result; + } + /** * Replace database references in expression */ @@ -181,10 +248,6 @@ export class ExpressionResolverService { expression: string, context: ExpressionContext, ): string { - if (!context.formData) { - return expression; - } - // Match patterns like formData.field.path const formDataPattern = /formData\.([a-zA-Z0-9._]+)/g; let result = expression; @@ -192,7 +255,11 @@ export class ExpressionResolverService { const matches = [...expression.matchAll(formDataPattern)]; for (const match of matches) { const [fullMatch, fieldPath] = match; - const value = this.getNestedValue(context.formData, fieldPath); + + // Get the value, returns undefined if formData doesn't exist or field not found + const value = context.formData + ? this.getNestedValue(context.formData, fieldPath) + : undefined; if (value !== undefined && value !== null) { result = result.replace(fullMatch, String(value));