From f6d11dd43da7825b3e67178221631b7e928f1fb9 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Tue, 16 Dec 2025 20:31:33 -0500
Subject: [PATCH 01/35] feat: add exit survey form schema
---
schemas/exit-survey.json | 65 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 schemas/exit-survey.json
diff --git a/schemas/exit-survey.json b/schemas/exit-survey.json
new file mode 100644
index 0000000..87bfc5b
--- /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.applicant.firstName}} {{formData.applicant.lastName}}",
+ "template": "exit-survey"
+ }
+ }
+ ]
+}
From 3b6a66eeac0e16f49f80948f52b8e1218b0d9edd Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 17 Dec 2025 08:23:26 -0500
Subject: [PATCH 02/35] fix: update validation rules for fields which are not
required
---
schemas/get-death-certificate.json | 576 ++++++++++++++---------------
1 file changed, 287 insertions(+), 289 deletions(-)
diff --git a/schemas/get-death-certificate.json b/schemas/get-death-certificate.json
index bbc8362..08a2649 100644
--- a/schemas/get-death-certificate.json
+++ b/schemas/get-death-certificate.json
@@ -1,292 +1,290 @@
{
- "id": "get-death-certificate",
- "name": "Get Death Certificate",
- "description": "Apply for a copy of your death certificate or someone else's death certificate",
- "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": "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": "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": "relationship",
- "type": "string",
- "label": "Tell us your relationship with the deceased",
- "required": true,
- "validations": {
- "max": 100
- }
- },
- {
- "name": "reasonForRequest",
- "type": "string",
- "label": "Tell us about why you need this certificate",
- "required": true,
- "validations": {
- "min": 1,
- "max": 500
- }
- },
- {
- "name": "deceased",
- "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": "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": "knownDateOfDeath",
- "type": "string",
- "label": "Do you know the date of death?",
- "required": true,
- "validations": {
- "regex": "^(yes|no)$",
- "message": "Must select an option"
- }
- },
- {
- "name": "dateOfDeath",
- "type": "date",
- "label": "Date of death",
- "required": false,
- "validations": {
- "regex": "^\\d{4}-\\d{2}-\\d{2}$",
- "message": "Date of death is required and must be in YYYY-MM-DD format"
- }
- },
- {
- "name": "estimatedDateOfDeath",
- "type": "string",
- "label": "Estimated date of death",
- "required": false,
- "validations": {
- "max": 100,
- "message": "Estimate is required"
- }
- },
- {
- "name": "idNumber",
- "type": "string",
- "label": "National Identification (ID) Number",
- "required": false,
- "validations": {
- "min": 2,
- "message": "ID Number must be at least 2 characters"
- }
- },
- {
- "name": "placeOfDeath",
- "type": "string",
- "label": "Place of death",
- "required": true,
- "validations": {
- "min": 2,
- "max": 200,
- "message": "Place of death must be at least 2 characters"
- }
- },
- {
- "name": "causeOfDeath",
- "type": "string",
- "label": "Cause of death",
- "required": true,
- "validations": {
- "min": 2,
- "max": 200,
- "message": "Cause of death must be at least 2 characters"
- }
- }
- ]
- },
+ "id": "get-death-certificate",
+ "name": "Get Death Certificate",
+ "description": "Apply for a copy of your death certificate or someone else's death certificate",
+ "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": "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": "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": "relationship",
+ "type": "string",
+ "label": "Tell us your relationship with the deceased",
+ "required": true,
+ "validations": {
+ "max": 100
+ }
+ },
+ {
+ "name": "reasonForRequest",
+ "type": "string",
+ "label": "Tell us about why you need this certificate",
+ "required": true,
+ "validations": {
+ "min": 1,
+ "max": 500
+ }
+ },
+ {
+ "name": "deceased",
+ "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": "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": "knownDateOfDeath",
+ "type": "string",
+ "label": "Do you know the date of death?",
+ "required": true,
+ "validations": {
+ "regex": "^(yes|no)$",
+ "message": "Must select an option"
+ }
+ },
+ {
+ "name": "dateOfDeath",
+ "type": "date",
+ "label": "Date of death",
+ "required": false,
+ "validations": {
+ "regex": "^\\d{4}-\\d{2}-\\d{2}$",
+ "message": "Date of death is required and must be in YYYY-MM-DD format"
+ }
+ },
+ {
+ "name": "estimatedDateOfDeath",
+ "type": "string",
+ "label": "Estimated date of death",
+ "required": false,
+ "validations": {
+ "max": 100,
+ "message": "Estimate is required"
+ }
+ },
+ {
+ "name": "idNumber",
+ "type": "string",
+ "label": "National Identification (ID) Number",
+ "required": false,
+ "validations": {
+ "message": "ID Number must be at least 2 characters"
+ }
+ },
+ {
+ "name": "placeOfDeath",
+ "type": "string",
+ "label": "Place of death",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 200,
+ "message": "Place of death must be at least 2 characters"
+ }
+ },
+ {
+ "name": "causeOfDeath",
+ "type": "string",
+ "label": "Cause of death",
+ "required": false,
+ "validations": {
+ "max": 200,
+ "message": "Cause of death must be at least 2 characters"
+ }
+ }
+ ]
+ },
- {
- "name": "order",
- "type": "object",
- "required": true,
- "fields": [
- {
- "name": "numberOfCopies",
- "type": "number",
- "label": "Number of copies",
- "required": true,
- "validations": {
- "min": 1,
- "max": 10,
- "message": "You must order at least 1 copy and maximum 10 copies"
- }
- }
- ]
- }
- ],
- "processors": [
- {
- "type": "payment",
- "config": {
- "provider": "ezpay",
- "paymentCode": "{{db:get-death-certificate:payment_code}}",
- "amount": "{{formData.order.numberOfCopies * db:get-death-certificate:payment_amount}}",
- "description": "Death Certificate Processing Fee (per copy)",
- "required": true,
- "timing": "after_validation",
- "responseData": {
- "include": ["order.numberOfCopies"]
- }
- }
- },
- {
- "type": "email",
- "config": {
- "to": "{{db:get-death-certificate:admin_email}}",
- "subject": "New Death Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}",
- "template": "death-certificate"
- }
- }
- ]
+ {
+ "name": "order",
+ "type": "object",
+ "required": true,
+ "fields": [
+ {
+ "name": "numberOfCopies",
+ "type": "number",
+ "label": "Number of copies",
+ "required": true,
+ "validations": {
+ "min": 1,
+ "max": 10,
+ "message": "You must order at least 1 copy and maximum 10 copies"
+ }
+ }
+ ]
+ }
+ ],
+ "processors": [
+ {
+ "type": "payment",
+ "config": {
+ "provider": "ezpay",
+ "paymentCode": "{{db:get-death-certificate:payment_code}}",
+ "amount": "{{formData.order.numberOfCopies * db:get-death-certificate:payment_amount}}",
+ "description": "Death Certificate Processing Fee (per copy)",
+ "required": true,
+ "timing": "after_validation",
+ "responseData": {
+ "include": ["order.numberOfCopies"]
+ }
+ }
+ },
+ {
+ "type": "email",
+ "config": {
+ "to": "{{db:get-death-certificate:admin_email}}",
+ "subject": "New Death Certificate Application - {{formData.applicant.firstName}} {{formData.applicant.lastName}}",
+ "template": "death-certificate"
+ }
+ }
+ ]
}
From 286feea6099d245f8208e0e2c85640e13a649462 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Thu, 18 Dec 2025 07:40:41 -0500
Subject: [PATCH 03/35] chore: update schema for textbook grant form
---
schemas/primary-school-textbook-grant.json | 650 +++++++++++++--------
1 file changed, 404 insertions(+), 246 deletions(-)
diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json
index a81cd0e..42bd6c3 100644
--- a/schemas/primary-school-textbook-grant.json
+++ b/schemas/primary-school-textbook-grant.json
@@ -1,248 +1,406 @@
{
- "id": "primary-school-textbook-grant",
- "name": "Primary School Textbook Grant Application",
- "description": "Apply for a textbook grant for primary school students",
- "fields": [
- {
- "name": "beneficiaries",
- "type": "object",
- "label": "Student Information",
- "required": true,
- "fields": [
- {
- "name": "firstName",
- "type": "string",
- "label": "Student First Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "lastName",
- "type": "string",
- "label": "Student Last Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "idNumber",
- "type": "string",
- "label": "Student ID Number",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "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",
- "type": "string",
- "label": "Title",
- "required": false,
- "validations": {
- "max": 10
- }
- },
- {
- "name": "firstName",
- "type": "string",
- "label": "Guardian First Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "middleName",
- "type": "string",
- "label": "Guardian Middle Name",
- "required": false,
- "validations": {
- "max": 50
- }
- },
- {
- "name": "lastName",
- "type": "string",
- "label": "Guardian Last Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "idNumber",
- "type": "string",
- "label": "Guardian ID Number",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "gender",
- "type": "string",
- "label": "Guardian Gender",
- "required": false
- },
- {
- "name": "relationship",
- "type": "string",
- "label": "Relationship to Student",
- "required": true,
- "validations": {
- "max": 50
- }
- },
- {
- "name": "email",
- "type": "email",
- "label": "Guardian Email Address",
- "required": false
- }
- ]
- },
- {
- "name": "contact",
- "type": "object",
- "label": "Contact Information",
- "required": true,
- "fields": [
- {
- "name": "addressLine1",
- "type": "string",
- "label": "Address Line 1",
- "required": true,
- "validations": {
- "min": 5,
- "max": 200
- }
- },
- {
- "name": "addressLine2",
- "type": "string",
- "label": "Address Line 2",
- "required": false,
- "validations": {
- "max": 200
- }
- },
- {
- "name": "parish",
- "type": "string",
- "label": "Parish",
- "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": "bankAccount",
- "type": "object",
- "label": "Bank Account Information",
- "required": true,
- "fields": [
- {
- "name": "bank",
- "type": "string",
- "label": "Bank Name",
- "required": true
- },
- {
- "name": "branch",
- "type": "string",
- "label": "Branch Location",
- "required": true,
- "validations": {
- "max": 100
- }
- },
- {
- "name": "accountType",
- "type": "string",
- "label": "Account Type",
- "required": true
- },
- {
- "name": "nameOnAccount",
- "type": "string",
- "label": "Name on Account",
- "required": true,
- "validations": {
- "min": 2,
- "max": 500
- }
- },
- {
- "name": "accountNumber",
- "type": "string",
- "label": "Account Number",
- "required": true,
- "validations": {
- "regex": "^[0-9]{6,20}$",
- "message": "Account number must be 6-20 digits"
- }
- }
- ]
- }
- ],
- "processors": [
- {
- "type": "email",
- "config": {
- "to": "{{db:primary-school-textbook-grant:admin_email}}",
- "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"
- }
- }
- ]
+ "id": "primary-school-textbook-grant",
+ "name": "Primary School Textbook Grant Application",
+ "description": "Apply for a textbook grant for primary school students",
+ "fields": [
+ {
+ "name": "beneficiaries",
+ "type": "object",
+ "label": "Student Information",
+ "required": true,
+ "fields": [
+ {
+ "name": "firstName",
+ "type": "string",
+ "label": "Student First Name",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "lastName",
+ "type": "string",
+ "label": "Student Last Name",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "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 is required"
+ }
+ },
+ {
+ "name": "class",
+ "type": "string",
+ "label": "What class are they currently in?",
+ "required": true,
+ "validations": {
+ "regex": "^[1-6]$",
+ "message": "Class must be between 1 and 6"
+ }
+ },
+ {
+ "name": "relationshipToChild",
+ "type": "string",
+ "label": "Relationship to Child",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "message": "Relationship to Child must be at least 2 characters"
+ }
+ }
+ ]
+ },
+ {
+ "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": "lastName",
+ "type": "string",
+ "label": "Last name",
+ "required": true,
+ "validations": {
+ "min": 1,
+ "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ },
+ {
+ "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": "tamisNumber",
+ "type": "string",
+ "label": "TAMIS Number",
+ "required": false,
+ "validations": {
+ "message": "Tamis must be 10-15 digits"
+ }
+ }
+ ]
+ },
+ {
+ "name": "guardianOrParentRelationship",
+ "type": "string",
+ "label": "Are you the parent or guardian?",
+ "required": true,
+ "validations": {
+ "regex": "^(yes|no)$",
+ "message": "Must select an option"
+ }
+ },
+ {
+ "name": "guardian",
+ "type": "object",
+ "label": "Guardian Information",
+ "required": true,
+ "fields": [
+ {
+ "name": "title",
+ "type": "string",
+ "label": "Title",
+ "required": false,
+ "validations": {
+ "max": 10
+ }
+ },
+ {
+ "name": "firstName",
+ "type": "string",
+ "label": "Guardian First Name",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "middleName",
+ "type": "string",
+ "label": "Guardian Middle Name",
+ "required": false,
+ "validations": {
+ "max": 50
+ }
+ },
+ {
+ "name": "lastName",
+ "type": "string",
+ "label": "Guardian Last Name",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "idNumber",
+ "type": "string",
+ "label": "Guardian ID Number",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "gender",
+ "type": "string",
+ "label": "Guardian Gender",
+ "required": false
+ },
+ {
+ "name": "relationship",
+ "type": "string",
+ "label": "Relationship to Student",
+ "required": true,
+ "validations": {
+ "max": 50
+ }
+ },
+ {
+ "name": "email",
+ "type": "email",
+ "label": "Guardian Email Address",
+ "required": false
+ }
+ ]
+ },
+ {
+ "name": "contact",
+ "type": "object",
+ "label": "Contact Information",
+ "required": true,
+ "fields": [
+ {
+ "name": "addressLine1",
+ "type": "string",
+ "label": "Address Line 1",
+ "required": true,
+ "validations": {
+ "min": 5,
+ "max": 200
+ }
+ },
+ {
+ "name": "addressLine2",
+ "type": "string",
+ "label": "Address Line 2",
+ "required": false,
+ "validations": {
+ "max": 200
+ }
+ },
+ {
+ "name": "parish",
+ "type": "string",
+ "label": "Parish",
+ "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": "bankAccount",
+ "type": "object",
+ "label": "Bank Account Information",
+ "required": true,
+ "fields": [
+ {
+ "name": "accountHolderName",
+ "type": "string",
+ "label": "Account Holder Name",
+ "required": true
+ },
+ {
+ "name": "bankName",
+ "type": "string",
+ "label": "Bank Name",
+ "required": true
+ },
+ {
+ "name": "accountNumber",
+ "type": "string",
+ "label": "Account Number",
+ "required": true,
+ "validations": {
+ "regex": "^[0-9]{6,20}$",
+ "message": "Account number must be 6-20 digits"
+ }
+ },
+ {
+ "name": "branchLocation",
+ "type": "string",
+ "label": "Branch Location",
+ "required": true,
+ "validations": {
+ "max": 100
+ }
+ },
+ {
+ "name": "accountType",
+ "type": "string",
+ "label": "Account Type",
+ "required": true,
+ "validations": {
+ "regex": "^(savings|chequing)$",
+ "message": "Must select an option"
+ }
+ }
+ ]
+ }
+ ],
+ "processors": [
+ {
+ "type": "payment",
+ "config": {
+ "provider": "ezpay",
+ "department": "education",
+ "paymentCode": "{{db:primary-school-textbook-grant:payment_code}}",
+ "amount": 25.0,
+ "description": "Processing Fee",
+ "required": true,
+ "timing": "after_validation"
+ }
+ },
+ {
+ "type": "email",
+ "config": {
+ "to": "{{db:primary-school-textbook-grant:admin_email}}",
+ "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"
+ }
+ }
+ ]
}
From ad0e5981178ff081683842bcd3f37785a40a0d79 Mon Sep 17 00:00:00 2001
From: Akinola Raphael <54055273+Ethical-Ralph@users.noreply.github.com>
Date: Thu, 18 Dec 2025 21:38:48 +0100
Subject: [PATCH 04/35] Remove payment processor from grant schema
Removed payment processor configuration from the grant schema.
---
schemas/primary-school-textbook-grant.json | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json
index 42bd6c3..bd40b52 100644
--- a/schemas/primary-school-textbook-grant.json
+++ b/schemas/primary-school-textbook-grant.json
@@ -374,18 +374,6 @@
}
],
"processors": [
- {
- "type": "payment",
- "config": {
- "provider": "ezpay",
- "department": "education",
- "paymentCode": "{{db:primary-school-textbook-grant:payment_code}}",
- "amount": 25.0,
- "description": "Processing Fee",
- "required": true,
- "timing": "after_validation"
- }
- },
{
"type": "email",
"config": {
From b3c052c0c78d8bc695235dec5b1c4f1a2c23f9a6 Mon Sep 17 00:00:00 2001
From: EthicalRalph
Date: Thu, 18 Dec 2025 21:43:07 +0100
Subject: [PATCH 05/35] feat: update applicant and guardian information fields
in grant templates
---
.../primary-school-textbook-grant-receipt.hbs | 10 +-
.../primary-school-textbook-grant.hbs | 109 +++++++++++++++---
2 files changed, 97 insertions(+), 22 deletions(-)
diff --git a/src/email/templates/primary-school-textbook-grant-receipt.hbs b/src/email/templates/primary-school-textbook-grant-receipt.hbs
index b0d7d55..37959a9 100644
--- a/src/email/templates/primary-school-textbook-grant-receipt.hbs
+++ b/src/email/templates/primary-school-textbook-grant-receipt.hbs
@@ -125,13 +125,15 @@
Class: Class {{beneficiaries.class}}
- Guardian:
- {{guardian.firstName}}
- {{guardian.lastName}}
+ Applicant:
+ {{applicant.title}}
+ {{applicant.firstName}}
+ {{applicant.lastName}}
Contact: {{contact.telephoneNumber}}
+ Email: {{applicant.email}}
{{#if guardian.email}}
- Email: {{guardian.email}}
+ Guardian Email: {{guardian.email}}
{{/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}}
From e26af1b1790b50702de5e6a84a9d1bd8f32169ca Mon Sep 17 00:00:00 2001
From: EthicalRalph
Date: Thu, 18 Dec 2025 21:54:00 +0100
Subject: [PATCH 06/35] feat: implement exit survey email template and update
survey submission subject
---
schemas/exit-survey.json | 126 +++++++++++++--------------
src/email/templates/exit-survey.hbs | 128 ++++++++++++++++++++++++++++
2 files changed, 191 insertions(+), 63 deletions(-)
create mode 100644 src/email/templates/exit-survey.hbs
diff --git a/schemas/exit-survey.json b/schemas/exit-survey.json
index 87bfc5b..7134886 100644
--- a/schemas/exit-survey.json
+++ b/schemas/exit-survey.json
@@ -1,65 +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.applicant.firstName}} {{formData.applicant.lastName}}",
- "template": "exit-survey"
- }
- }
- ]
+ "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/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 Submitted:
+ {{processedAt}}
+
+
+
+
+ Survey Responses
+
+
+
+ | 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}}
+ |
+
+ {{#if technicalProblemsDescription}}
+
+ | Problem Description: |
+ {{technicalProblemsDescription}} |
+
+ {{/if}}
+
+
+
+
+
+
+ Suggested Improvements
+
+
What could be improved:
+
{{areasForImprovement}}
+
+
+
+
+
+
+
+
\ No newline at end of file
From a1d0fc9e289a58514fa0ee10bf635dd181cabd85 Mon Sep 17 00:00:00 2001
From: EthicalRalph
Date: Fri, 19 Dec 2025 14:55:47 +0100
Subject: [PATCH 07/35] feat: enhance expression resolver to support multiple
embedded expressions and add comprehensive tests
---
src/forms/expression-resolver.service.spec.ts | 853 ++++++++++++++++++
src/forms/expression-resolver.service.ts | 87 +-
2 files changed, 930 insertions(+), 10 deletions(-)
create mode 100644 src/forms/expression-resolver.service.spec.ts
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));
From fecc2ad702ee477bde6126ec11804f38a24ef18c Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Mon, 22 Dec 2025 13:32:32 -0400
Subject: [PATCH 08/35] chore: update textbook grant schema
---
schemas/primary-school-textbook-grant.json | 200 +++++++--------------
1 file changed, 63 insertions(+), 137 deletions(-)
diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json
index bd40b52..360a20e 100644
--- a/schemas/primary-school-textbook-grant.json
+++ b/schemas/primary-school-textbook-grant.json
@@ -71,20 +71,66 @@
]
},
{
- "name": "applicant",
+ "name": "guardian",
"type": "object",
+ "label": "Guardian Information",
"required": true,
"fields": [
{
- "name": "title",
+ "name": "firstName",
+ "type": "string",
+ "label": "Guardian First Name",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "lastName",
"type": "string",
- "label": "Title",
+ "label": "Guardian Last Name",
"required": true,
"validations": {
- "regex": "^(mr|ms|mrs)$",
- "message": "Must select a valid title"
+ "min": 2,
+ "max": 50
}
},
+ {
+ "name": "idNumber",
+ "type": "string",
+ "label": "Guardian ID Number",
+ "required": true,
+ "validations": {
+ "min": 2,
+ "max": 50
+ }
+ },
+ {
+ "name": "passportNumber",
+ "type": "string",
+ "label": "Passport Number",
+ "required": false,
+ "validations": {
+ "message": "Passport number must be at least 6 characters"
+ }
+ },
+ {
+ "name": "tamisNumber",
+ "type": "string",
+ "label": "TAMIS Number",
+ "required": false,
+ "validations": {
+ "message": "Tamis must be 10-15 digits"
+ }
+ }
+ ]
+ },
+ {
+ "name": "applicant",
+ "type": "object",
+ "required": true,
+ "fields": [
{
"name": "firstName",
"type": "string",
@@ -193,136 +239,7 @@
}
]
},
- {
- "name": "guardianOrParentRelationship",
- "type": "string",
- "label": "Are you the parent or guardian?",
- "required": true,
- "validations": {
- "regex": "^(yes|no)$",
- "message": "Must select an option"
- }
- },
- {
- "name": "guardian",
- "type": "object",
- "label": "Guardian Information",
- "required": true,
- "fields": [
- {
- "name": "title",
- "type": "string",
- "label": "Title",
- "required": false,
- "validations": {
- "max": 10
- }
- },
- {
- "name": "firstName",
- "type": "string",
- "label": "Guardian First Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "middleName",
- "type": "string",
- "label": "Guardian Middle Name",
- "required": false,
- "validations": {
- "max": 50
- }
- },
- {
- "name": "lastName",
- "type": "string",
- "label": "Guardian Last Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "idNumber",
- "type": "string",
- "label": "Guardian ID Number",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "gender",
- "type": "string",
- "label": "Guardian Gender",
- "required": false
- },
- {
- "name": "relationship",
- "type": "string",
- "label": "Relationship to Student",
- "required": true,
- "validations": {
- "max": 50
- }
- },
- {
- "name": "email",
- "type": "email",
- "label": "Guardian Email Address",
- "required": false
- }
- ]
- },
- {
- "name": "contact",
- "type": "object",
- "label": "Contact Information",
- "required": true,
- "fields": [
- {
- "name": "addressLine1",
- "type": "string",
- "label": "Address Line 1",
- "required": true,
- "validations": {
- "min": 5,
- "max": 200
- }
- },
- {
- "name": "addressLine2",
- "type": "string",
- "label": "Address Line 2",
- "required": false,
- "validations": {
- "max": 200
- }
- },
- {
- "name": "parish",
- "type": "string",
- "label": "Parish",
- "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": "bankAccount",
"type": "object",
@@ -352,9 +269,18 @@
}
},
{
- "name": "branchLocation",
+ "name": "branchName",
+ "type": "string",
+ "label": "Branch Name",
+ "required": true,
+ "validations": {
+ "max": 100
+ }
+ },
+ {
+ "name": "branchCode",
"type": "string",
- "label": "Branch Location",
+ "label": "Branch Coe",
"required": true,
"validations": {
"max": 100
From ad1715d592e7d251ac351aa54f94d0617e0f83b3 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 24 Dec 2025 16:07:08 -0400
Subject: [PATCH 09/35] chore: set vscode project formatting options
---
.vscode/settings.json | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 .vscode/settings.json
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"
+ }
+}
From 281ccb68bd5342252109ac1721b02edfad25e163 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 24 Dec 2025 16:07:24 -0400
Subject: [PATCH 10/35] chore: allow local database connection
---
src/database/datasource.ts | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
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,
+ },
});
From e9c110afd4c6a1023a939049b61a3eea2a0396f9 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 24 Dec 2025 16:07:47 -0400
Subject: [PATCH 11/35] feat: modify form schema to accept array of objects for
beneficiaries
---
schemas/primary-school-textbook-grant.json | 529 +++++++++------------
1 file changed, 212 insertions(+), 317 deletions(-)
diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json
index 360a20e..e5619bf 100644
--- a/schemas/primary-school-textbook-grant.json
+++ b/schemas/primary-school-textbook-grant.json
@@ -1,320 +1,215 @@
{
- "id": "primary-school-textbook-grant",
- "name": "Primary School Textbook Grant Application",
- "description": "Apply for a textbook grant for primary school students",
- "fields": [
- {
- "name": "beneficiaries",
- "type": "object",
- "label": "Student Information",
- "required": true,
- "fields": [
- {
- "name": "firstName",
- "type": "string",
- "label": "Student First Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "lastName",
- "type": "string",
- "label": "Student Last Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "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 is required"
- }
- },
- {
- "name": "class",
- "type": "string",
- "label": "What class are they currently in?",
- "required": true,
- "validations": {
- "regex": "^[1-6]$",
- "message": "Class must be between 1 and 6"
- }
- },
- {
- "name": "relationshipToChild",
- "type": "string",
- "label": "Relationship to Child",
- "required": true,
- "validations": {
- "min": 2,
- "message": "Relationship to Child must be at least 2 characters"
- }
- }
- ]
- },
- {
- "name": "guardian",
- "type": "object",
- "label": "Guardian Information",
- "required": true,
- "fields": [
- {
- "name": "firstName",
- "type": "string",
- "label": "Guardian First Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "lastName",
- "type": "string",
- "label": "Guardian Last Name",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "idNumber",
- "type": "string",
- "label": "Guardian ID Number",
- "required": true,
- "validations": {
- "min": 2,
- "max": 50
- }
- },
- {
- "name": "passportNumber",
- "type": "string",
- "label": "Passport Number",
- "required": false,
- "validations": {
- "message": "Passport number must be at least 6 characters"
- }
- },
- {
- "name": "tamisNumber",
- "type": "string",
- "label": "TAMIS Number",
- "required": false,
- "validations": {
- "message": "Tamis must be 10-15 digits"
- }
- }
- ]
- },
- {
- "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": "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,
- "validations": {
- "regex": "^\\+?[0-9]{10,15}$",
- "message": "Telephone number must be 10-15 digits"
- }
- },
- {
- "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": "tamisNumber",
- "type": "string",
- "label": "TAMIS Number",
- "required": false,
- "validations": {
- "message": "Tamis must be 10-15 digits"
- }
- }
- ]
- },
+ "id": "primary-school-textbook-grant",
+ "name": "Primary School Textbook Grant Application",
+ "description": "Apply for a textbook grant for primary school students",
+ "fields": [
+ {
+ "name": "beneficiaries",
+ "type": "array",
+ "label": "Student Information",
+ "required": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "guardian": {
+ "type": "object"
+ }
+ }
+ }
+ },
- {
- "name": "bankAccount",
- "type": "object",
- "label": "Bank Account Information",
- "required": true,
- "fields": [
- {
- "name": "accountHolderName",
- "type": "string",
- "label": "Account Holder Name",
- "required": true
- },
- {
- "name": "bankName",
- "type": "string",
- "label": "Bank Name",
- "required": true
- },
- {
- "name": "accountNumber",
- "type": "string",
- "label": "Account Number",
- "required": true,
- "validations": {
- "regex": "^[0-9]{6,20}$",
- "message": "Account number must be 6-20 digits"
- }
- },
- {
- "name": "branchName",
- "type": "string",
- "label": "Branch Name",
- "required": true,
- "validations": {
- "max": 100
- }
- },
- {
- "name": "branchCode",
- "type": "string",
- "label": "Branch Coe",
- "required": true,
- "validations": {
- "max": 100
- }
- },
- {
- "name": "accountType",
- "type": "string",
- "label": "Account Type",
- "required": true,
- "validations": {
- "regex": "^(savings|chequing)$",
- "message": "Must select an option"
- }
- }
- ]
- }
- ],
- "processors": [
- {
- "type": "email",
- "config": {
- "to": "{{db:primary-school-textbook-grant:admin_email}}",
- "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"
- }
- }
- ]
+ {
+ "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": "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ },
+ {
+ "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": "tamisNumber",
+ "type": "string",
+ "label": "TAMIS Number",
+ "required": false,
+ "validations": {
+ "message": "Tamis must be 10-15 digits"
+ }
+ }
+ ]
+ },
+
+ {
+ "name": "bankAccount",
+ "type": "object",
+ "label": "Bank Account Information",
+ "required": true,
+ "fields": [
+ {
+ "name": "accountHolderName",
+ "type": "string",
+ "label": "Account Holder Name",
+ "required": true
+ },
+ {
+ "name": "bankName",
+ "type": "string",
+ "label": "Bank Name",
+ "required": true
+ },
+ {
+ "name": "accountNumber",
+ "type": "string",
+ "label": "Account Number",
+ "required": true,
+ "validations": {
+ "regex": "^[0-9]{6,20}$",
+ "message": "Account number must be 6-20 digits"
+ }
+ },
+ {
+ "name": "branchName",
+ "type": "string",
+ "label": "Branch Name",
+ "required": true,
+ "validations": {
+ "max": 100
+ }
+ },
+ {
+ "name": "branchCode",
+ "type": "string",
+ "label": "Branch Coe",
+ "required": true,
+ "validations": {
+ "max": 100
+ }
+ },
+ {
+ "name": "accountType",
+ "type": "string",
+ "label": "Account Type",
+ "required": true,
+ "validations": {
+ "regex": "^(savings|chequing)$",
+ "message": "Must select an option"
+ }
+ }
+ ]
+ }
+ ],
+ "processors": [
+ {
+ "type": "email",
+ "config": {
+ "to": "{{db:primary-school-textbook-grant:admin_email}}",
+ "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"
+ }
+ }
+ ]
}
From 4c52a2a9a312f235179adb003a6b21e267864301 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 24 Dec 2025 16:24:15 -0400
Subject: [PATCH 12/35] fix (primary-school-textbook-grant): remove email to
guardian
---
schemas/primary-school-textbook-grant.json | 8 --------
1 file changed, 8 deletions(-)
diff --git a/schemas/primary-school-textbook-grant.json b/schemas/primary-school-textbook-grant.json
index e5619bf..1539acf 100644
--- a/schemas/primary-school-textbook-grant.json
+++ b/schemas/primary-school-textbook-grant.json
@@ -202,14 +202,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"
- }
}
]
}
From 1f47b77602b41ad18f793ea51bd499704797ce18 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Tue, 30 Dec 2025 08:54:14 -0400
Subject: [PATCH 13/35] feat: add reserve-society-name to api
---
schemas/reserve-society-name.json | 157 ++++++++++++++++++++++++++++++
1 file changed, 157 insertions(+)
create mode 100644 schemas/reserve-society-name.json
diff --git a/schemas/reserve-society-name.json b/schemas/reserve-society-name.json
new file mode 100644
index 0000000..9718ad2
--- /dev/null
+++ b/schemas/reserve-society-name.json
@@ -0,0 +1,157 @@
+{
+ "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": {
+ "value": {
+ "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ }
+ ]
+ }
+ ],
+ "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"
+ }
+ }
+ ]
+}
From 15fd7d9bcdaccb6b5072dff6a1a0bb1465999ab7 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Tue, 30 Dec 2025 09:36:27 -0400
Subject: [PATCH 14/35] fix: add email template for reserve society name
---
src/email/templates/reserve-society-name.hbs | 122 +++++++++++++++++++
1 file changed, 122 insertions(+)
create mode 100644 src/email/templates/reserve-society-name.hbs
diff --git a/src/email/templates/reserve-society-name.hbs b/src/email/templates/reserve-society-name.hbs
new file mode 100644
index 0000000..6a79d94
--- /dev/null
+++ b/src/email/templates/reserve-society-name.hbs
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
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.value}}
+
+ {{/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
From 587631f990c4eb733ea31a69d808cabcde42ae7a Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Tue, 30 Dec 2025 10:39:18 -0400
Subject: [PATCH 15/35] fix: correct display proposed names in email for
reserve-society-name
---
schemas/reserve-society-name.json | 5 ++++-
src/email/templates/reserve-society-name.hbs | 6 +++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/schemas/reserve-society-name.json b/schemas/reserve-society-name.json
index 9718ad2..b17b442 100644
--- a/schemas/reserve-society-name.json
+++ b/schemas/reserve-society-name.json
@@ -38,7 +38,10 @@
"items": {
"type": "object",
"properties": {
- "value": {
+ "title": {
+ "type": "string"
+ },
+ "explanation": {
"type": "string"
}
}
diff --git a/src/email/templates/reserve-society-name.hbs b/src/email/templates/reserve-society-name.hbs
index 6a79d94..2be3b76 100644
--- a/src/email/templates/reserve-society-name.hbs
+++ b/src/email/templates/reserve-society-name.hbs
@@ -82,7 +82,11 @@
{{#each proposed-names}}
Option {{@index}}:
- {{this.value}}
+ {{this.title}}
+
+
+ Explanation:
+ {{this.explanation}}
{{/each}}
From 2f926e3f6779cc2c30e9f3fadeeb1acd14a805d6 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Wed, 31 Dec 2025 09:34:46 -0400
Subject: [PATCH 16/35] feat: add processing for 'reserve-company-name'
---
schemas/reserve-company-name.json | 185 +++++++++++++++++++
src/email/templates/reserve-company-name.hbs | 126 +++++++++++++
2 files changed, 311 insertions(+)
create mode 100644 schemas/reserve-company-name.json
create mode 100644 src/email/templates/reserve-company-name.hbs
diff --git a/schemas/reserve-company-name.json b/schemas/reserve-company-name.json
new file mode 100644
index 0000000..f3df679
--- /dev/null
+++ b/schemas/reserve-company-name.json
@@ -0,0 +1,185 @@
+{
+ "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ }
+ ]
+ }
+ ],
+ "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/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 @@
+
+
+
+
+
+
+
+
+
+
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
From 8df01cd90cd36a06343f2998d67c4736133817d0 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Fri, 2 Jan 2026 04:26:07 -0400
Subject: [PATCH 17/35] feat: add schemas for post office redirection notifce
forms for business,deceased and individual
---
schemas/post-office-redirection-business.json | 234 ++++++++++++++++++
schemas/post-office-redirection-deceased.json | 234 ++++++++++++++++++
...> post-office-redirection-individual.json} | 175 ++++++-------
3 files changed, 550 insertions(+), 93 deletions(-)
create mode 100644 schemas/post-office-redirection-business.json
create mode 100644 schemas/post-office-redirection-deceased.json
rename schemas/{post-office-redirection-notice.json => post-office-redirection-individual.json} (54%)
diff --git a/schemas/post-office-redirection-business.json b/schemas/post-office-redirection-business.json
new file mode 100644
index 0000000..69307c1
--- /dev/null
+++ b/schemas/post-office-redirection-business.json
@@ -0,0 +1,234 @@
+{
+ "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ }
+ ]
+ },
+ {
+ "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-business"
+ }
+ }
+ ]
+}
diff --git a/schemas/post-office-redirection-deceased.json b/schemas/post-office-redirection-deceased.json
new file mode 100644
index 0000000..eb7110b
--- /dev/null
+++ b/schemas/post-office-redirection-deceased.json
@@ -0,0 +1,234 @@
+{
+ "id": "post-office-redirection-deceased",
+ "name": "Post Office Redirection for the Deceased",
+ "description": "Change where your mail is sent (deceased)",
+ "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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
+ }
+ }
+ ]
+ },
+ {
+ "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": "uploadDocumentUrls",
+ "type": "array",
+ "label": "Uploaded Documents",
+ "required": true,
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "processors": [
+ {
+ "type": "email",
+ "config": {
+ "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-deceased"
+ }
+ }
+ ]
+}
diff --git a/schemas/post-office-redirection-notice.json b/schemas/post-office-redirection-individual.json
similarity index 54%
rename from schemas/post-office-redirection-notice.json
rename to schemas/post-office-redirection-individual.json
index 12c9d93..33fd9e4 100644
--- a/schemas/post-office-redirection-notice.json
+++ b/schemas/post-office-redirection-individual.json
@@ -1,61 +1,95 @@
{
- "id": "post-office-redirection-notice",
- "name": "Post Office Redirection Notice",
- "description": "Form to submit a post office redirection notice",
+ "id": "post-office-redirection-individual",
+ "name": "Post Office Redirection for an Individual",
+ "description": "Change where your mail is sent (individual)",
"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,
+ "validations": {
+ "regex": "^\\+?[0-9]{10,15}$",
+ "message": "Telephone number must be 10-15 digits"
}
}
]
},
{
- "name": "oldaddress",
+ "name": "oldAddress",
"type": "object",
- "label": "Old Address",
"required": true,
"fields": [
{
@@ -65,7 +99,7 @@
"required": true,
"validations": {
"min": 5,
- "max": 100,
+ "max": 200,
"message": "Address must be at least 5 characters"
}
},
@@ -75,7 +109,7 @@
"label": "Address Line 2",
"required": false,
"validations": {
- "max": 100
+ "max": 200
}
},
{
@@ -84,7 +118,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 +128,8 @@
"label": "Postal Code",
"required": false,
"validations": {
- "max": 10
+ "regex": "^BB\\d{5}$",
+ "message": "Enter a valid postal code (e.g., BB17004)"
}
}
]
@@ -101,7 +137,6 @@
{
"name": "newAddress",
"type": "object",
- "label": "New Address",
"required": true,
"fields": [
{
@@ -111,7 +146,7 @@
"required": true,
"validations": {
"min": 5,
- "max": 100,
+ "max": 200,
"message": "Address must be at least 5 characters"
}
},
@@ -121,7 +156,7 @@
"label": "Address Line 2",
"required": false,
"validations": {
- "max": 100
+ "max": 200
}
},
{
@@ -130,7 +165,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,85 +175,38 @@
"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"
- }
- },
- {
- "name": "idNumber",
- "type": "number",
- "label": "ID Number",
- "required": false,
- "validations": {
- "message": "ID Number must be a valid number"
+ "regex": "^(yes|no)$",
+ "message": "Must select an option"
}
},
{
- "name": "addAnother",
- "type": "string",
- "label": "Do you want to add another member?",
+ "name": "redirectionStartDate",
+ "type": "date",
+ "label": "Redirection Start Date",
"required": false,
"validations": {
- "regex": "^(yes|no)$",
- "message": "Must be 'yes' or 'no'"
- }
- }
- ]
- },
- {
- "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'"
+ "regex": "^\\d{4}-\\d{2}-\\d{2}$",
+ "message": "Redirection Start Date is required and must be in YYYY-MM-DD format"
}
},
{
- "name": "businessName",
- "type": "string",
- "label": "Business Name",
+ "name": "redirectionEndDate",
+ "type": "date",
+ "label": "Redirection End Date",
"required": false,
"validations": {
- "min": 2,
- "max": 100,
- "message": "Business name must be at least 2 characters"
+ "regex": "^\\d{4}-\\d{2}-\\d{2}$",
+ "message": "Redirection End Date is required and must be in YYYY-MM-DD format"
}
}
]
@@ -227,9 +216,9 @@
{
"type": "email",
"config": {
- "to": "{{db:post-office-redirection-notice:admin_email}}",
- "subject": "New Post Office Redirection Notice Request - {{formData.personal.firstName}} {{formData.personal.lastName}}",
- "template": "post-office-redirection-notice"
+ "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-individual"
}
}
]
From 983bbbe7f2b1a6aa77f9cb8d5040d6ef7ebc8626 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Fri, 2 Jan 2026 04:27:59 -0400
Subject: [PATCH 18/35] Update pull request template for testing checklist
Removed several testing checklist items and added manual, unit, and e2e tests.
---
.github/pull_request_template.md | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
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
From 645aa159d725784ae2a8565bb4ec5307c602beb8 Mon Sep 17 00:00:00 2001
From: Shannon Clarke
Date: Fri, 2 Jan 2026 05:05:50 -0400
Subject: [PATCH 19/35] fix: email template for post office redirection notice
---
schemas/post-office-redirection-business.json | 2 +-
schemas/post-office-redirection-deceased.json | 4 +-
.../post-office-redirection-individual.json | 4 +-
.../post-office-redirection-notice.hbs | 53 ++++++++++---------
4 files changed, 34 insertions(+), 29 deletions(-)
diff --git a/schemas/post-office-redirection-business.json b/schemas/post-office-redirection-business.json
index 69307c1..5631bb9 100644
--- a/schemas/post-office-redirection-business.json
+++ b/schemas/post-office-redirection-business.json
@@ -227,7 +227,7 @@
"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-business"
+ "template": "post-office-redirection-notice"
}
}
]
diff --git a/schemas/post-office-redirection-deceased.json b/schemas/post-office-redirection-deceased.json
index eb7110b..3ddb69c 100644
--- a/schemas/post-office-redirection-deceased.json
+++ b/schemas/post-office-redirection-deceased.json
@@ -226,8 +226,8 @@
"type": "email",
"config": {
"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-deceased"
+ "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
index 33fd9e4..cdcd15e 100644
--- a/schemas/post-office-redirection-individual.json
+++ b/schemas/post-office-redirection-individual.json
@@ -217,8 +217,8 @@
"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-individual"
+ "subject": "New Request to redirect mail for an individual - {{formData.applicant.firstName}} {{formData.applicant.lastName}}",
+ "template": "post-office-redirection-notice"
}
}
]
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}}
-