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 Responses

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

Suggested Improvements

+
+ What could be improved: +

{{areasForImprovement}}

+
+
+ + + +
+ + \ No newline at end of file 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 @@ + + + + + + +
+

Request to Reserve Society Name Application

+
+ +
+

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

+ +
+
Applicant Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} +
+ Email: + {{applicant.email}} +
+
+ Phone: + {{applicant.telephoneNumber}} +
+ +
+ +
+
Request Details
+
+ Purpose: + {{request.purpose}} +
+ {{#if request.currentSocietyName}} +
+ Current Society Name: + {{request.currentSocietyName}} +
+ {{/if}} +
+ +
+
Proposed Society Names
+ {{#each proposed-names}} +
+ Option {{@index}}: + {{this.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 @@ + + + + + + +
+

Request to Reserve Company Name Application

+
+ +
+

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

+ +
+
Purpose of New Company Name
+
+ What will this name be used for? + {{purposeOfNewCompanyName}} +
+ {{#if companyPresentName}} +
+ What is the present name of the company? + {{companyPresentName}} +
+ {{/if}} +
+ +
+
Business Activities
+ {{#each businessActivity}} +
+ • {{this.value}} +
+ {{/each}} +
+ +
+
Applicant Information
+
+ Name: + {{applicant.title}} + {{applicant.firstName}} + {{applicant.lastName}} +
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ {{#if applicant.postalCode}} +
+ Postal Code: + {{applicant.postalCode}} +
+ {{/if}} +
+ Email: + {{applicant.email}} +
+
+ Phone: + {{applicant.telephoneNumber}} +
+ +
+ +
+
Request Details
+
+ Purpose: + {{request.purpose}} +
+ {{#if request.currentSocietyName}} +
+ Current Society Name: + {{request.currentSocietyName}} +
+ {{/if}} +
+ +
+
Contact Information
+
+ Address: + {{applicant.addressLine1}}{{#if + applicant.addressLine2 + }}, {{applicant.addressLine2}}{{/if}} +
+
+ Parish: + {{applicant.parish}} +
+ +
+ + +
+ + \ No newline at end of file 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}} -