diff --git a/.forceignore b/.forceignore
new file mode 100644
index 0000000..c267c60
--- /dev/null
+++ b/.forceignore
@@ -0,0 +1,11 @@
+# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
+# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
+#
+
+
+# LWC configuration files
+**/jsconfig.json
+**/.eslintrc.json
+
+# LWC Jest
+**/__tests__/**
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f5f33eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,45 @@
+# This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
+# If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
+# For useful gitignore templates see: https://github.com/github/gitignore
+
+# Salesforce cache
+.sf/
+.sfdx/
+.localdevserver/
+deploy-options.json
+
+# LWC VSCode autocomplete
+**/lwc/jsconfig.json
+
+# LWC Jest coverage reports
+coverage/
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Dependency directories
+node_modules/
+
+# Eslint cache
+.eslintcache
+
+# MacOS system files
+.DS_Store
+
+# Windows system files
+Thumbs.db
+ehthumbs.db
+[Dd]esktop.ini
+$RECYCLE.BIN/
+
+# Local environment variables
+.env
+
+# Python Salesforce Functions
+**/__pycache__/
+**/.venv/
+**/venv/
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..feac116
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm run precommit
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..8cccc6e
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,11 @@
+# List files or directories below to ignore them when running prettier
+# More information: https://prettier.io/docs/en/ignore.html
+#
+
+**/staticresources/**
+.localdevserver
+.sfdx
+.sf
+.vscode
+
+coverage/
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..18039a0
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,17 @@
+{
+ "trailingComma": "none",
+ "plugins": [
+ "prettier-plugin-apex",
+ "@prettier/plugin-xml"
+ ],
+ "overrides": [
+ {
+ "files": "**/lwc/**/*.html",
+ "options": { "parser": "lwc" }
+ },
+ {
+ "files": "*.{cmp,page,component}",
+ "options": { "parser": "html" }
+ }
+ ]
+}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..7e6cb10
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,9 @@
+{
+ "recommendations": [
+ "salesforce.salesforcedx-vscode",
+ "redhat.vscode-xml",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "financialforce.lana"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..e07e391
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Apex Replay Debugger",
+ "type": "apex-replay",
+ "request": "launch",
+ "logFile": "${command:AskForLogFileName}",
+ "stopOnEntry": true,
+ "trace": true
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..76decfb
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "search.exclude": {
+ "**/node_modules": true,
+ "**/bower_components": true,
+ "**/.sfdx": true
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..afcda4a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# Salesforce DX Project: Next Steps
+
+Now that you’ve created a Salesforce DX project, what’s next? Here are some documentation resources to get you started.
+
+## How Do You Plan to Deploy Your Changes?
+
+Do you want to deploy a set of changes, or create a self-contained application? Choose a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models).
+
+## Configure Your Salesforce DX Project
+
+The `sfdx-project.json` file contains useful configuration information for your project. See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) in the _Salesforce DX Developer Guide_ for details about this file.
+
+## Read All About It
+
+- [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/)
+- [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm)
+- [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm)
+- [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
diff --git a/config/project-scratch-def.json b/config/project-scratch-def.json
new file mode 100644
index 0000000..bb72192
--- /dev/null
+++ b/config/project-scratch-def.json
@@ -0,0 +1,13 @@
+{
+ "orgName": "Demo company",
+ "edition": "Developer",
+ "features": ["EnableSetPasswordInApi"],
+ "settings": {
+ "lightningExperienceSettings": {
+ "enableS1DesktopEnabled": true
+ },
+ "mobileSettings": {
+ "enableS1EncryptedStoragePref2": false
+ }
+ }
+}
diff --git a/force-app/main/default/appMenus/AppSwitcher.appMenu-meta.xml b/force-app/main/default/appMenus/AppSwitcher.appMenu-meta.xml
new file mode 100644
index 0000000..bf441cb
--- /dev/null
+++ b/force-app/main/default/appMenus/AppSwitcher.appMenu-meta.xml
@@ -0,0 +1,103 @@
+
+
+
+ Job_Application_Tracker
+ CustomApplication
+
+
+ standard__Platform
+ CustomApplication
+
+
+ standard__Sales
+ CustomApplication
+
+
+ standard__Service
+ CustomApplication
+
+
+ standard__Marketing
+ CustomApplication
+
+
+ standard__ServiceConsole
+ CustomApplication
+
+
+ standard__AppLauncher
+ CustomApplication
+
+
+ standard__Community
+ CustomApplication
+
+
+ standard__Sites
+ CustomApplication
+
+
+ standard__Chatter
+ CustomApplication
+
+
+ standard__Content
+ CustomApplication
+
+
+ standard__Insights
+ CustomApplication
+
+
+ standard__LightningSalesConsole
+ CustomApplication
+
+
+ standard__LightningService
+ CustomApplication
+
+
+ standard__LightningSales
+ CustomApplication
+
+
+ standard__LightningInstrumentation
+ CustomApplication
+
+
+ standard__SalesforceCMS
+ CustomApplication
+
+
+ standard__AllTabSet
+ CustomApplication
+
+
+ standard__QueueManagement
+ CustomApplication
+
+
+ standard__DataManager
+ CustomApplication
+
+
+ standard__RevenueCloudConsole
+ CustomApplication
+
+
+ standard__LightningScheduler
+ CustomApplication
+
+
+ standard__LightningBolt
+ CustomApplication
+
+
+ standard__FlowsApp
+ CustomApplication
+
+
+ standard__ExpressionSetConsole
+ CustomApplication
+
+
diff --git a/force-app/main/default/appMenus/Salesforce1.appMenu-meta.xml b/force-app/main/default/appMenus/Salesforce1.appMenu-meta.xml
new file mode 100644
index 0000000..d623590
--- /dev/null
+++ b/force-app/main/default/appMenus/Salesforce1.appMenu-meta.xml
@@ -0,0 +1,71 @@
+
+
+
+ EinsteinInsights
+ StandardAppMenuItem
+
+
+ Feed
+ StandardAppMenuItem
+
+
+ MyDay
+ StandardAppMenuItem
+
+
+ Dashboards
+ StandardAppMenuItem
+
+
+ Tasks
+ StandardAppMenuItem
+
+
+ Search
+ StandardAppMenuItem
+
+
+ People
+ StandardAppMenuItem
+
+
+ Groups
+ StandardAppMenuItem
+
+
+ Reports
+ StandardAppMenuItem
+
+
+ Events
+ StandardAppMenuItem
+
+
+ ProcessInstanceWorkitem
+ StandardAppMenuItem
+
+
+ PendingInterviews
+ StandardAppMenuItem
+
+
+ LightningInstrumentation
+ StandardAppMenuItem
+
+
+ LightningSchedulerSetupAssistant
+ StandardAppMenuItem
+
+
+ ProductCatalog
+ StandardAppMenuItem
+
+
+ ProductCategory
+ StandardAppMenuItem
+
+
+ MobileHome
+ StandardAppMenuItem
+
+
diff --git a/force-app/main/default/applications/Job_Application_Tracker.app-meta.xml b/force-app/main/default/applications/Job_Application_Tracker.app-meta.xml
new file mode 100644
index 0000000..5c1ef0b
--- /dev/null
+++ b/force-app/main/default/applications/Job_Application_Tracker.app-meta.xml
@@ -0,0 +1,64 @@
+
+
+
+ View
+ Action override created by Lightning App Builder during activation.
+ Job_Application_Record_Page
+ Large
+ false
+ Flexipage
+ Job_Application__c
+
+
+ View
+ Action override created by Lightning App Builder during activation.
+ Job_Application_Record_Page
+ Small
+ false
+ Flexipage
+ Job_Application__c
+
+
+ #0070D2
+ false
+
+ Small
+ Large
+ false
+ false
+ false
+
+ Console
+ Job_Application__c
+ standard-Event
+ standard-Account
+ standard-Contact
+ standard-report
+ standard-Dashboard
+ Jooble_Job_Board_Import
+ Lightning
+ Job_Application_Tracker_UtilityBar
+
+
+ Job_Application__c
+
+
+ Jooble_Job_Board_Import
+
+
+ standard-Account
+
+
+ standard-Contact
+
+
+ standard-Dashboard
+
+
+ standard-Event
+
+
+ standard-report
+
+
+
diff --git a/force-app/main/default/aura/.eslintrc.json b/force-app/main/default/aura/.eslintrc.json
new file mode 100644
index 0000000..226a5a2
--- /dev/null
+++ b/force-app/main/default/aura/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "plugins": ["@salesforce/eslint-plugin-aura"],
+ "extends": ["plugin:@salesforce/eslint-plugin-aura/recommended"],
+ "rules": {
+ "vars-on-top": "off",
+ "no-unused-expressions": "off"
+ }
+}
diff --git a/force-app/main/default/classes/CleanUpStaleJobApplications.cls b/force-app/main/default/classes/CleanUpStaleJobApplications.cls
new file mode 100644
index 0000000..2c15086
--- /dev/null
+++ b/force-app/main/default/classes/CleanUpStaleJobApplications.cls
@@ -0,0 +1,31 @@
+global class CleanUpStaleJobApplications implements Database.Batchable, Database.Stateful {
+
+ global Database.QueryLocator start(Database.BatchableContext BC) {
+ // Query to find stale job applications
+ return Database.getQueryLocator([
+ SELECT Id, Job_Application_Status__c, Follow_up_date__c, Notes__c
+ FROM Job_Application__c
+ WHERE Job_Application_Status__c != 'Closed'
+ AND Job_Application_Status__c != 'Accepted'
+ AND Follow_up_date__c <= :System.today().addDays(-30)
+ ]);
+ }
+
+ global void execute(Database.BatchableContext BC, List scope) {
+ List applicationsToUpdate = new List();
+
+ for (Job_Application__c application : scope) {
+ application.Job_Application_Status__c = 'Closed'; // Update status to Closed
+ application.Notes__c = 'Closed by automated process'; // Update notes
+ applicationsToUpdate.add(application);
+ }
+
+ if (!applicationsToUpdate.isEmpty()) {
+ update applicationsToUpdate; // Perform the update
+ }
+ }
+
+ global void finish(Database.BatchableContext BC) {
+ System.debug('CleanUpStaleJobApplications batch job completed successfully.');
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/classes/CleanUpStaleJobApplications.cls-meta.xml b/force-app/main/default/classes/CleanUpStaleJobApplications.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/CleanUpStaleJobApplications.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls b/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls
new file mode 100644
index 0000000..dab4a55
--- /dev/null
+++ b/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls
@@ -0,0 +1,41 @@
+@isTest
+private class CleanUpStaleJobApplicationsTest {
+ @isTest
+ static void testBatchJob() {
+ // Create test job applications
+ List applications = new List();
+ for (Integer i = 0; i < 5; i++) {
+ applications.add(new Job_Application__c(
+ Job_Application_Status__c = 'Saved', // Change if necessary
+ Follow_up_date__c = System.today().addDays(-31) // Stale application
+ ));
+ }
+ insert applications;
+
+ // Start the batch job
+ Test.startTest();
+ CleanUpStaleJobApplications batchJob = new CleanUpStaleJobApplications();
+ Database.executeBatch(batchJob);
+ Test.stopTest();
+
+ // Verify that the applications were updated
+ List updatedApps = [SELECT Id, Job_Application_Status__c, Notes__c FROM Job_Application__c];
+ for (Job_Application__c app : updatedApps) {
+ System.assertEquals('Closed', app.Job_Application_Status__c, 'The status should be updated to Closed for stale applications.');
+ System.assertEquals('Closed by automated process', app.Notes__c, 'Notes should indicate closure by the automated process.');
+ System.debug('Updated Application: ' + app.Id + ' Status: ' + app.Job_Application_Status__c);
+ }
+ }
+
+ @isTest
+ static void testScheduler() {
+ // Schedule the job
+ Test.startTest();
+ String jobId = System.schedule('Test Schedule Clean Up', '0 0 0 ? * MON-FRI', new ScheduleCleanUpStaleJobApplications());
+ Test.stopTest();
+
+ // Check if the job has been scheduled
+ List triggers = [SELECT Id, NextFireTime FROM CronTrigger WHERE Id = :jobId];
+ System.assert(!triggers.isEmpty(), 'The job should be scheduled.');
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls-meta.xml b/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/CleanUpStaleJobApplicationsTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/CustomException.cls b/force-app/main/default/classes/CustomException.cls
new file mode 100644
index 0000000..4bbbb7f
--- /dev/null
+++ b/force-app/main/default/classes/CustomException.cls
@@ -0,0 +1 @@
+public class CustomException extends Exception {}
diff --git a/force-app/main/default/classes/CustomException.cls-meta.xml b/force-app/main/default/classes/CustomException.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/CustomException.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/EventHelper.cls b/force-app/main/default/classes/EventHelper.cls
new file mode 100644
index 0000000..f5fb054
--- /dev/null
+++ b/force-app/main/default/classes/EventHelper.cls
@@ -0,0 +1,126 @@
+public class EventHelper {
+
+ public static void validateNoOverlapAndFetchJobPosition(List newMeetings) {
+ Map jobPositionMap = new Map();
+ Map companyNameMap = new Map();
+ Set jobAppIds = new Set();
+
+ // Gather Job Application IDs from new meetings
+ for (Event meeting : newMeetings) {
+ if (meeting.WhatId != null) {
+ jobAppIds.add(meeting.WhatId);
+ }
+ }
+
+ // Query Job Applications related to those IDs
+ Map jobApplications = new Map(
+ [SELECT Id, Job_Position__c, Company_Name__c FROM Job_Application__c WHERE Id IN :jobAppIds]
+ );
+
+ // Map job position and company name to each event
+ for (Event meeting : newMeetings) {
+ if (meeting.WhatId != null && jobApplications.containsKey(meeting.WhatId)) {
+ jobPositionMap.put(meeting.Id, jobApplications.get(meeting.WhatId).Job_Position__c);
+ companyNameMap.put(meeting.Id, jobApplications.get(meeting.WhatId).Company_Name__c);
+ }
+ }
+
+ // Prepare new meeting times in minutes
+ Map> newMeetingTimesInMinutes = new Map>();
+ for (Event meeting : newMeetings) {
+ Map times = new Map();
+ times.put('start', convertToMinutes(meeting.StartDateTime));
+ times.put('end', convertToMinutes(meeting.EndDateTime));
+ newMeetingTimesInMinutes.put(meeting.Id, times);
+ }
+
+ // Fetch existing meetings
+ List existingMeetings = [SELECT Id, StartDateTime, EndDateTime, Type
+ FROM Event
+ WHERE StartDateTime < :Datetime.now().addDays(30)
+ AND (Type = 'Phone Screen' OR Type = 'Interview')];
+
+ // Check for overlaps
+ for (Event newMeeting : newMeetings) {
+ Long newStart = newMeetingTimesInMinutes.get(newMeeting.Id).get('start');
+ Long newEnd = newMeetingTimesInMinutes.get(newMeeting.Id).get('end');
+
+ for (Event existingMeeting : existingMeetings) {
+ // Skip the same event
+ if (existingMeeting.Id == newMeeting.Id) {
+ continue;
+ }
+
+ Long existingStart = convertToMinutes(existingMeeting.StartDateTime);
+ Long existingEnd = convertToMinutes(existingMeeting.EndDateTime);
+
+ // Check for overlap condition
+ if ((newStart < existingEnd) && (newEnd > existingStart)) {
+ // Prepare the error message
+ String jobPosition = jobPositionMap.containsKey(newMeeting.Id) ? jobPositionMap.get(newMeeting.Id) : 'N/A';
+ newMeeting.addError('This interview for the position ' + jobPosition +
+ ' overlaps with an existing meeting. Please check your Calendar!');
+ break; // Exit the loop after adding the error
+ }
+ }
+ }
+ }
+
+
+ public static void sendUpcomingEventEmails(List newMeetings) {
+ List emails = new List();
+
+ // Prepare emails for events happening within 24 hours
+ for (Event meeting : newMeetings) {
+ if (meeting.StartDateTime != null && meeting.StartDateTime < Datetime.now().addHours(24)) {
+
+ // Retrieve job details if available
+ String jobPosition = 'N/A';
+ String accountName = 'N/A';
+
+ if (meeting.WhatId != null) {
+ List jobApps = [
+ SELECT Job_Position__c, Company_Name__c, Company_Name__r.Name
+ FROM Job_Application__c
+ WHERE Id = :meeting.WhatId
+ ];
+
+ if (!jobApps.isEmpty()) {
+ Job_Application__c jobApp = jobApps[0];
+ jobPosition = jobApp.Job_Position__c != null ? jobApp.Job_Position__c : 'N/A';
+ accountName = jobApp.Company_Name__r != null ? jobApp.Company_Name__r.Name : 'N/A';
+ }
+ }
+
+
+ List users = [SELECT Email FROM User WHERE Id = :meeting.OwnerId];
+
+ String recipientEmail = 'default@example.com'; // Use a default email in case the user is not found
+ if (!users.isEmpty()) {
+ recipientEmail = users[0].Email;
+ }
+
+ String eventType = meeting.Type;
+
+ Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
+ email.setToAddresses(new String[] { recipientEmail });
+ email.setSubject('Upcoming Interview Reminder');
+ email.setPlainTextBody(
+ 'This is a reminder for your upcoming ' + eventType + ' for the ' + jobPosition +
+ ' position with ' + accountName + ', scheduled on ' + meeting.StartDateTime.format() + '.'
+ );
+
+ emails.add(email);
+ }
+ }
+
+ if (!emails.isEmpty()) {
+ Messaging.sendEmail(emails);
+ }
+}
+
+ public static Long convertToMinutes(Datetime dt) {
+ Long epochMillis = dt.getTime();
+ return epochMillis / 60000;
+ }
+}
diff --git a/force-app/main/default/classes/EventHelper.cls-meta.xml b/force-app/main/default/classes/EventHelper.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/EventHelper.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/EventTriggerHelperTest.cls b/force-app/main/default/classes/EventTriggerHelperTest.cls
new file mode 100644
index 0000000..f597fd4
--- /dev/null
+++ b/force-app/main/default/classes/EventTriggerHelperTest.cls
@@ -0,0 +1,150 @@
+@isTest
+private class EventTriggerHelperTest {
+
+ @isTest
+ static void testNoOverlap() {
+ // Create test data
+ Event existingMeeting = new Event(
+ Subject = 'Existing Meeting',
+ StartDateTime = Datetime.now().addHours(1),
+ EndDateTime = Datetime.now().addHours(2)
+ );
+ insert existingMeeting;
+
+ Event newMeeting = new Event(
+ Subject = 'New Meeting',
+ StartDateTime = Datetime.now().addHours(3),
+ EndDateTime = Datetime.now().addHours(4)
+ );
+
+ Test.startTest();
+ try {
+ EventHelper.validateNoOverlapAndFetchJobPosition(new List{newMeeting});
+ insert newMeeting;
+ Assert.areEqual(true, true, 'No overlap detected as expected');
+ } catch(Exception e) {
+ Assert.areEqual(false, true, 'Unexpected error occurred: ' + e.getMessage());
+ }
+ Test.stopTest();
+ }
+
+ /*@isTest
+static void testWithOverlap() {
+ // Create test data
+ Event existingMeeting = new Event(
+ Subject = 'Existing Meeting',
+ StartDateTime = Datetime.now().addHours(1),
+ EndDateTime = Datetime.now().addHours(2)
+ );
+ insert existingMeeting;
+
+ Event overlappingMeeting = new Event(
+ Subject = 'Overlapping Meeting',
+ StartDateTime = Datetime.now().addHours(1).addMinutes(30),
+ EndDateTime = Datetime.now().addHours(2).addMinutes(30)
+ );
+
+ Test.startTest();
+ try {
+ // This line should throw an exception if there is an overlap
+ EventHelper.validateNoOverlapAndFetchJobPosition(new List{overlappingMeeting});
+ // If we reach this point, it means there was no overlap detected, which is unexpected
+ Assert.fail('Expected an error due to overlap, but none was thrown');
+ } catch(DmlException e) {
+ // Validate the overlap error message
+ Assert.isTrue(e.getMessage().contains('overlaps with an existing meeting'),
+ 'Expected overlap error message: ' + e.getMessage());
+ }
+ Test.stopTest();
+}*/
+
+
+
+ @isTest
+ static void testMultipleEvents() {
+ // Create test data
+ Event existingMeeting = new Event(
+ Subject = 'Existing Meeting',
+ StartDateTime = Datetime.now().addHours(1),
+ EndDateTime = Datetime.now().addHours(2)
+ );
+ insert existingMeeting;
+
+ Event newMeeting1 = new Event(
+ Subject = 'New Meeting 1',
+ StartDateTime = Datetime.now().addHours(3),
+ EndDateTime = Datetime.now().addHours(4)
+ );
+
+ Event newMeeting2 = new Event(
+ Subject = 'New Meeting 2',
+ StartDateTime = Datetime.now().addHours(1).addMinutes(30),
+ EndDateTime = Datetime.now().addHours(2).addMinutes(30)
+ );
+
+ Test.startTest();
+ try {
+ // Validate no overlap
+ EventHelper.validateNoOverlapAndFetchJobPosition(new List{newMeeting1, newMeeting2});
+
+ // If there is no overlap, this means the code execution should continue
+ insert new List{newMeeting1, newMeeting2};
+
+ // If we reach this point, it means there was no overlap, so assert that the expected outcome was achieved
+ Assert.isTrue(true, 'No overlap detected as expected.');
+ } catch(DmlException e) {
+ // If an exception is thrown, it indicates there was an overlap
+ Assert.isTrue(e.getMessage().contains('overlaps with an existing meeting'),
+ 'Expected overlap error message: ' + e.getMessage());
+ }
+ Test.stopTest();
+ }
+
+
+ @isTest
+ static void testConvertToMinutes() {
+ Datetime testDate = Datetime.newInstance(2023, 1, 1, 12, 0, 0);
+ Long minutes = EventHelper.convertToMinutes(testDate);
+ Assert.areNotEqual(null, minutes, 'Minutes should not be null');
+ Assert.isTrue(minutes > 0, 'Minutes should be positive');
+ }
+
+ /*@isTest
+static void testSendUpcomingEventEmails() {
+ // Create a test Job Application
+ Job_Application__c jobApp = new Job_Application__c(
+ Job_Position__c = 'Software Engineer',
+ Company_Name__c = 'Acme Corp'
+ );
+ insert jobApp;
+
+ // Create a test User
+ User testUser = new User(
+ Username = 'testuser@example.com',
+ Email = 'testuser@example.com',
+ Alias = 'tuser',
+ ProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id,
+ LastName = 'User',
+ FirstName = 'Test',
+ IsActive = true
+ );
+ insert testUser;
+
+ // Create a test Event linked to the Job Application and the User
+ Event meeting = new Event(
+ WhatId = jobApp.Id,
+ OwnerId = testUser.Id,
+ StartDateTime = Datetime.now().addHours(1),
+ Type = 'Interview'
+ );
+ insert meeting;
+
+ // Now call the method you're testing
+ EventHelper.sendUpcomingEventEmails(new List { meeting });
+
+ // Add assertions here to verify expected email sent, etc.
+}*/
+
+
+
+}
diff --git a/force-app/main/default/classes/EventTriggerHelperTest.cls-meta.xml b/force-app/main/default/classes/EventTriggerHelperTest.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/EventTriggerHelperTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/JobApplicationHelper.cls b/force-app/main/default/classes/JobApplicationHelper.cls
new file mode 100644
index 0000000..fa2df1e
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationHelper.cls
@@ -0,0 +1,158 @@
+public with sharing class JobApplicationHelper {
+ public static void processJobApplications(List newJobAppList, Map oldJobAppMap) {
+ List tasksToInsert = new List();
+ Set processedJobAppIds = new Set();
+
+ // Fetch existing tasks related to job applications
+ Set existingTaskSubjects = new Set();
+ for (Task existingTask : [SELECT Id, WhatId, Subject, Type, Application_Status_Snapshot__c FROM Task WHERE WhatId IN :newJobAppList]) {
+ existingTaskSubjects.add(existingTask.Subject + '|' + existingTask.Application_Status_Snapshot__c);
+ processedJobAppIds.add(existingTask.WhatId);
+ }
+
+ List subjectList = new List{
+ 'Check if the job description aligns with your interests and values',
+ 'Review the highlighted skills to see if the role is a good fit',
+ 'Research the company or role and mark your excitement level',
+ 'Find and research someone who works at the company and add them as a contact',
+ 'Set up an informational interview to learn more about the role/company',
+ 'Identify potential referrals to help get your application on the top of the pile',
+ 'Customize your work achievements using the job description keywords',
+ 'Submit your application on the company website if possible',
+ 'Reach out to the hiring manager or recruiter',
+ 'Follow up on your application via email weekly',
+ 'Continue identifying and saving similar job opportunities',
+ 'Set up weekly networking calls to explore similar companies/roles',
+ 'Prepare your blurb or “tell me about yourself” response',
+ 'Practice answering behavioral interview questions',
+ 'Research the company and your interviewers',
+ 'Set up your virtual interview space and test your tech',
+ 'Send thank you emails within 24 hours',
+ 'Research your market value and know your numbers',
+ 'Prepare your negotiation scripts',
+ 'Evaluate your offer and decline or accept',
+ 'Plan your resignation if applicable',
+ 'Take some time to relax and recharge',
+ 'Prepare for your first day of onboarding',
+ 'Send a follow-up email thanking the interviewer and asking for feedback',
+ 'Review your notes and reflect on areas of improvement'
+ };
+
+
+ for (Job_Application__c newApplication : newJobAppList) {
+ Job_Application__c oldApp = oldJobAppMap != null ? oldJobAppMap.get(newApplication.Id) : null;
+
+ Boolean isStatusChanged = oldApp == null || newApplication.Job_Application_Status__c != oldApp.Job_Application_Status__c;
+
+ // If the application has been processed before and the status didn't change, skip it
+ if (!isStatusChanged && processedJobAppIds.contains(newApplication.Id)) {
+ continue;
+ }
+
+ if (isStatusChanged || !processedJobAppIds.contains(newApplication.Id)) {
+ switch on newApplication.Job_Application_Status__c {
+ when 'Saved' {
+ if (!processedJobAppIds.contains(newApplication.Id)) {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Research', 'Normal', subjectList[0]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Research', 'Normal', subjectList[1]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Research', 'Low', subjectList[2]);
+
+ }
+ }
+ when 'Applying' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Networking', 'High', subjectList[3]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Networking', 'High', subjectList[4]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Networking', 'High', subjectList[5]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Application Process', 'Normal', subjectList[6]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Application Process', 'High', subjectList[7]);
+ }
+ when 'Applied' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Application Process', 'High', subjectList[8]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Application Process', 'Normal', subjectList[9]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Research', 'Low', subjectList[10]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Networking', 'Normal', subjectList[11]);
+ }
+ when 'Interviewing' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Interview Preparation', 'High', subjectList[12]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Interview Preparation', 'Normal', subjectList[13]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Interview Preparation', 'Normal', subjectList[14]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Interview Preparation', 'High', subjectList[15]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Interview Preparation', 'High', subjectList[16]);
+ }
+ when 'Negotiating' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Offer Negotiation', 'Normal', subjectList[17]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Offer Negotiation', 'Normal', subjectList[18]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Offer Negotiation', 'High', subjectList[19]);
+ }
+ when 'Accepted' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Onboarding & Reflection', 'Normal', subjectList[20]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Onboarding & Reflection', 'Low', subjectList[21]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Onboarding & Reflection', 'Normal', subjectList[22]);
+ }
+ when 'Closed' {
+ processedJobAppIds.add(newApplication.Id);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Email', 'Normal', subjectList[23]);
+ addTaskIfNotExists(tasksToInsert, existingTaskSubjects, newApplication, 'Onboarding & Reflection', 'Low', subjectList[24]);
+ }
+ when else {
+ System.debug('No tasks created for unrecognized status: ' + newApplication.Job_Application_Status__c);
+ throw new CustomException('Unrecognized job application status: ' + newApplication.Job_Application_Status__c);
+ }
+ }
+ }
+ }
+
+ TaskManager.createTasksForApplication(tasksToInsert);
+ }
+
+ private static void addTaskIfNotExists(List tasksToInsert, Set existingTaskSubjects, Job_Application__c application, String taskType, String priority, String subject) {
+ String key = subject + '|' + application.Job_Application_Status__c;
+ // Check if a task with the same subject and Application Status Snapshot already exists
+ if (!existingTaskSubjects.contains(key)) {
+ tasksToInsert.add(TaskManager.prepareTask(application, taskType, priority, subject));
+ }
+ }
+
+ public static void setPrimaryContact(List jobAppList) {
+ Set accountIds = new Set();
+
+ for (Job_Application__c jobApp : jobAppList) {
+ if (jobApp.Primary_Contact__c == null && jobApp.Company_Name__c != null) {
+ accountIds.add(jobApp.Company_Name__c);
+ }
+ }
+
+ if (accountIds.isEmpty()) {
+ return;
+ }
+
+ Map accountIdToContact = new Map([
+ SELECT Id, FirstName, AccountId
+ FROM Contact
+ WHERE AccountId IN :accountIds
+ ORDER BY FirstName ASC // Or any other relevant field
+ ]);
+
+ Map contactsByAccount = new Map();
+ for (Contact contact : accountIdToContact.values()) {
+ if (!contactsByAccount.containsKey(contact.AccountId)) {
+ contactsByAccount.put(contact.AccountId, contact);
+ }
+ }
+
+ for (Job_Application__c jobApp : jobAppList) {
+ if (jobApp.Primary_Contact__c == null) {
+ Contact primaryContact = contactsByAccount.get(jobApp.Company_Name__c);
+ if (primaryContact != null) {
+ jobApp.Primary_Contact__c = primaryContact.Id;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/classes/JobApplicationHelper.cls-meta.xml b/force-app/main/default/classes/JobApplicationHelper.cls-meta.xml
new file mode 100644
index 0000000..7a51829
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationHelper.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 58.0
+ Active
+
diff --git a/force-app/main/default/classes/JobApplicationHelperTest.cls b/force-app/main/default/classes/JobApplicationHelperTest.cls
new file mode 100644
index 0000000..4788237
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationHelperTest.cls
@@ -0,0 +1,631 @@
+@isTest
+private class JobApplicationHelperTest {
+ @TestSetup
+ static void setup(){
+ TestDataFactory.createJobApplications(400, 'Saved', 2);
+ }
+
+ @isTest
+ static void testProcessJobApplications_Saved() {
+ // Retrieve the updated job applications
+ List updatedAppList = [SELECT Id, Job_Application_Status__c, OwnerId FROM Job_Application__c];
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(updatedAppList, null);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(3, tasksForApp.size(), 'There should be 3 tasks created for each job application with the "Saved" status');
+
+ // Check the details of each task
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Research' && task.Priority == 'Normal' && task.Subject == 'Check if the job description aligns with your interests and values') {
+ check1 = true;
+ } else if (task.Type == 'Research' && task.Priority == 'Normal' && task.Subject == 'Review the highlighted skills to see if the role is a good fit') {
+ check2 = true;
+ } else if (task.Type == 'Research' && task.Priority == 'Low' && task.Subject == 'Research the company or role and mark your excitement level') {
+ check3 = true;
+ }
+ }
+
+ // Assert that all three tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Check if the job description aligns with your interests and values) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Review the highlighted skills to see if the role is a good fit) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Research the company or role and mark your excitement level) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Applying() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Applying' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Applying');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(8, tasksForApp.size(), 'There should be 8 tasks created for each job application after updating to "Applying" status');
+
+ // Separate tasks into "Saved" and "Applying" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer applyingTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Applying" status
+ applyingTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 5 tasks from 'Applying' status and check their details
+ Assert.areEqual(5, applyingTaskCount, 'There should be 5 new tasks from the "Applying" status');
+
+ // Check the details of the "Applying" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+ Boolean check4 = false;
+ Boolean check5 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Networking' && task.Priority == 'High' && task.Subject == 'Find and research someone who works at the company and add them as a contact') {
+ check1 = true;
+ } else if (task.Type == 'Networking' && task.Priority == 'High' && task.Subject == 'Set up an informational interview to learn more about the role/company') {
+ check2 = true;
+ } else if (task.Type == 'Networking' && task.Priority == 'High' && task.Subject == 'Identify potential referrals to help get your application on the top of the pile') {
+ check3 = true;
+ } else if (task.Type == 'Application Process' && task.Priority == 'Normal' && task.Subject == 'Customize your work achievements using the job description keywords') {
+ check4 = true;
+ } else if (task.Type == 'Application Process' && task.Priority == 'High' && task.Subject == 'Submit your application on the company website if possible') {
+ check5 = true;
+ }
+ }
+
+ // Assert that all five "Applying" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Find and research someone who works at the company and add them as a contact) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Set up an informational interview to learn more about the role/company) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Identify potential referrals to help get your application on the top of the pile) should be created.');
+ Assert.isTrue(check4, 'Task 4 (Customize your work achievements using the job description keywords) should be created.');
+ Assert.isTrue(check5, 'Task 5 (Submit your application on the company website if possible) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Applied() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Applied' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Applied');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(7, tasksForApp.size(), 'There should be 7 tasks created for each job application after updating to "Applied" status');
+
+ // Separate tasks into "Saved" and "Applying" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer appliedTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Applied" Status
+ appliedTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 4 tasks from 'Applied' status and check their details
+ Assert.areEqual(4, appliedTaskCount, 'There should be 4 new tasks from the "Applied" status');
+
+ // Check the details of the "Applying" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+ Boolean check4 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Application Process' && task.Priority == 'High' && task.Subject == 'Reach out to the hiring manager or recruiter') {
+ check1 = true;
+ } else if (task.Type == 'Application Process' && task.Priority == 'Normal' && task.Subject == 'Follow up on your application via email weekly') {
+ check2 = true;
+ } else if (task.Type == 'Research' && task.Priority == 'Low' && task.Subject == 'Continue identifying and saving similar job opportunities') {
+ check3 = true;
+ } else if (task.Type == 'Networking' && task.Priority == 'Normal' && task.Subject == 'Set up weekly networking calls to explore similar companies/roles') {
+ check4 = true;
+ }
+ }
+
+ // Assert that all five "Applying" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Reach out to the hiring manager or recruiter) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Follow up on your application via email weekly) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Continue identifying and saving similar job opportunities) should be created.');
+ Assert.isTrue(check4, 'Task 4 (Set up weekly networking calls to explore similar companies/roles) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Interviewing() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Interviewing' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Interviewing');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(8, tasksForApp.size(), 'There should be 8 tasks created for each job application after updating to "Interviewing" status');
+
+ // Separate tasks into "Saved" and "Interviewing" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer interviewingTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Interviewing" Status
+ interviewingTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 5 tasks from 'Interviewing' status and check their details
+ Assert.areEqual(5, interviewingTaskCount, 'There should be 5 new tasks from the "Interviewing" status');
+
+ // Check the details of the "Interviewing" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+ Boolean check4 = false;
+ Boolean check5 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Interview Preparation' && task.Priority == 'High' && task.Subject == 'Prepare your blurb or “tell me about yourself” response') {
+ check1 = true;
+ } else if (task.Type == 'Interview Preparation' && task.Priority == 'Normal' && task.Subject == 'Practice answering behavioral interview questions') {
+ check2 = true;
+ } else if (task.Type == 'Interview Preparation' && task.Priority == 'Normal' && task.Subject == 'Research the company and your interviewers') {
+ check3 = true;
+ } else if (task.Type == 'Interview Preparation' && task.Priority == 'High' && task.Subject == 'Set up your virtual interview space and test your tech') {
+ check4 = true;
+ } else if (task.Type == 'Interview Preparation' && task.Priority == 'High' && task.Subject == 'Send thank you emails within 24 hours') {
+ check4 = true;
+ }
+ }
+
+ // Assert that all four "Interviewing" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Prepare your blurb or “tell me about yourself” response) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Practice answering behavioral interview questions) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Research the company and your interviewers) should be created.');
+ Assert.isTrue(check4, 'Task 4 (Send thank you emails within 24 hours) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Negotiating() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Negotiating' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Negotiating');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(6, tasksForApp.size(), 'There should be 6 tasks created for each job application after updating to "Negotiating" status');
+
+ // Separate tasks into "Saved" and "Negotiating" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer negotiatingTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Negotiating" Status
+ negotiatingTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 3 tasks from 'Negotiating' status and check their details
+ Assert.areEqual(3, negotiatingTaskCount, 'There should be 3 new tasks from the "Negotiating" status');
+
+ // Check the details of the "Negotiating" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Offer Negotiation' && task.Priority == 'Normal' && task.Subject == 'Research your market value and know your numbers') {
+ check1 = true;
+ } else if (task.Type == 'Offer Negotiation' && task.Priority == 'Normal' && task.Subject == 'Prepare your negotiation scripts') {
+ check2 = true;
+ } else if (task.Type == 'Offer Negotiation' && task.Priority == 'High' && task.Subject == 'Evaluate your offer and decline or accept') {
+ check3 = true;
+ }
+ }
+
+ // Assert that all three "Negotiating" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Research your market value and know your numbers) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Prepare your negotiation scripts) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Evaluate your offer and decline or accept) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Accepted() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Accepted' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Accepted');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(6, tasksForApp.size(), 'There should be 6 tasks created for each job application after updating to "Accepted" status');
+
+ // Separate tasks into "Saved" and "Accepted" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer acceptedTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Accepted" Status
+ acceptedTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 3 tasks from 'Accepted' status and check their details
+ Assert.areEqual(3, acceptedTaskCount, 'There should be 3 new tasks from the "Accepted" status');
+
+ // Check the details of the "Accepted" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+ Boolean check3 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Onboarding & Reflection' && task.Priority == 'Normal' && task.Subject == 'Plan your resignation if applicable') {
+ check1 = true;
+ } else if (task.Type == 'Onboarding & Reflection' && task.Priority == 'Low' && task.Subject == 'Take some time to relax and recharge') {
+ check2 = true;
+ } else if (task.Type == 'Onboarding & Reflection' && task.Priority == 'Normal' && task.Subject == 'Prepare for your first day of onboarding') {
+ check3 = true;
+ }
+ }
+
+ // Assert that all three "Accepted" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Plan your resignation if applicable) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Take some time to relax and recharge) should be created.');
+ Assert.isTrue(check3, 'Task 3 (Prepare for your first day of onboarding) should be created.');
+ }
+ }
+
+ @isTest
+ static void testProcessJobApplications_Closed() {
+ // Retrieve Job Applications from TestSetup
+ List oldAppList = [SELECT Id, Job_Application_Status__c FROM Job_Application__c];
+
+ //Prepare map of old job applications with original status
+ Map oldJobAppMap = new Map(oldAppList);
+
+ // Use TestDataFactory to update job applications to 'Closed' status
+ TestDataFactory.updateJobApplicationStatus(oldAppList, 'Closed');
+
+ // Call the method to be tested
+ Test.startTest();
+ JobApplicationHelper.processJobApplications(oldAppList, oldJobAppMap);
+ Test.stopTest();
+
+ // Retrieve the created tasks
+ List createdTasks = [SELECT Id, Type, ActivityDate, Priority, OwnerId, WhatId, Status, Subject FROM Task];
+
+ // Assertions
+ // Ensure that each job application has three related tasks
+ Map> tasksByJobApp = new Map>();
+ for (Task task : createdTasks) {
+ if (!tasksByJobApp.containsKey(task.WhatId)) {
+ tasksByJobApp.put(task.WhatId, new List());
+ }
+ tasksByJobApp.get(task.WhatId).add(task);
+ }
+
+ for (Id appId : tasksByJobApp.keySet()) {
+ List tasksForApp = tasksByJobApp.get(appId);
+
+ // Check that there are exactly 3 tasks for each job application with the "Saved" status
+ Assert.areEqual(5, tasksForApp.size(), 'There should be 5 tasks created for each job application after updating to "Closed" status');
+
+ // Separate tasks into "Saved" and "Closed" groups based on their task subjects
+ Integer savedTaskCount = 0;
+ Integer closedTaskCount = 0;
+
+ for (Task task : tasksForApp) {
+ if (task.Subject.contains('aligns with your interests') ||
+ task.Subject.contains('skills to see if the role is a good fit') ||
+ task.Subject.contains('mark your excitement level')) {
+ // Count tasks from the "Saved" status
+ savedTaskCount++;
+ } else {
+ // Count tasks from the "Closed" Status
+ closedTaskCount++;
+ }
+ }
+
+ // Ensure that there are 3 tasks from 'Saved' status (but don't recheck their details)
+ Assert.areEqual(3, savedTaskCount, 'There should still be 3 tasks from the "Saved" status');
+
+ // Ensure that there are 2 tasks from 'Closed' status and check their details
+ Assert.areEqual(2, closedTaskCount, 'There should be 2 new tasks from the "Closed" status');
+
+ // Check the details of the "Closed" tasks
+ Boolean check1 = false;
+ Boolean check2 = false;
+
+ for (Task task : tasksForApp) {
+ if (task.Type == 'Email' && task.Priority == 'Normal' && task.Subject == 'Send a follow-up email thanking the interviewer and asking for feedback') {
+ check1 = true;
+ } else if (task.Type == 'Onboarding & Reflection' && task.Priority == 'Low' && task.Subject == 'Review your notes and reflect on areas of improvement') {
+ check2 = true;
+ }
+ }
+
+ // Assert that all two "Closed" tasks were created correctly
+ Assert.isTrue(check1, 'Task 1 (Send a follow-up email thanking the interviewer and asking for feedback) should be created.');
+ Assert.isTrue(check2, 'Task 2 (Review your notes and reflect on areas of improvement) should be created.');
+ }
+ }
+
+
+ @isTest
+ static void testWhenElseCondition_ExceptionThrown() {
+ // Arrange: Create a list of new job applications with an unrecognized status.
+ List newJobAppList = new List();
+ Job_Application__c jobApp = new Job_Application__c(Job_Application_Status__c = 'UnrecognizedStatus');
+ newJobAppList.add(jobApp);
+
+ // Empty map for old job applications.
+ Map oldJobAppMap = new Map();
+
+ // Act and Assert: Expect the exception to be thrown.
+ Test.startTest();
+ try {
+ JobApplicationHelper.processJobApplications(newJobAppList, oldJobAppMap);
+ Assert.fail('Expected a CustomException to be thrown'); // Fail the test if no exception is thrown.
+ } catch (CustomException e) {
+ Assert.areEqual('Unrecognized job application status: UnrecognizedStatus', e.getMessage(), 'The exception message should match the expected message.');
+ }
+ Test.stopTest();
+ }
+
+ @isTest
+ static void testSetPrimaryContact_PostitiveCase() {
+ List jobApps = TestDataFactory.createJobApplications(200, 'Saved', 2);
+
+ for (Job_Application__c jobApp : jobApps) {
+ jobApp.Id = null;
+ }
+
+ Test.startTest();
+ insert jobApps;
+ Test.stopTest();
+
+ jobApps = [SELECT Id, Primary_Contact__c FROM Job_Application__c WHERE Id IN :jobApps];
+ for (Job_Application__c jobApp : jobApps) {
+ Assert.areNotEqual(null, jobApp.Primary_Contact__c, 'Primary Contact should be set for all job applications.');
+ }
+ }
+
+ @isTest
+ static void testSetPrimaryContact_NegativeNoContacts() {
+ // Insert an account with no related contacts
+ Account acc = new Account(Name = 'No Contact Account');
+ insert acc;
+
+ // Insert a job application with that account
+ Job_Application__c jobApp = new Job_Application__c(
+ Company_Name__c = acc.Id,
+ OwnerId = UserInfo.getUserId(),
+ Job_Application_Status__c = 'Saved'
+ );
+
+ Test.startTest();
+ insert jobApp;
+ Test.stopTest();
+
+ // Reload the job application after insert to check the Primary Contact field
+ jobApp = [SELECT Primary_Contact__c FROM Job_Application__c WHERE Id = :jobApp.Id];
+
+ // Assert that the primary contact is still null
+ Assert.areEqual(null, jobApp.Primary_Contact__c, 'Primary Contact should remain null for an account with no contacts.');
+ }
+
+ @isTest
+ static void testSetPrimaryContact_NegativeNoAccount() {
+ // Insert a job application with no related account
+ Job_Application__c jobApp = new Job_Application__c(
+ Company_Name__c = null,
+ OwnerId = UserInfo.getUserId(),
+ Job_Application_Status__c = 'Saved'
+ );
+
+ Test.startTest();
+ insert jobApp;
+ Test.stopTest();
+
+ // Reload the job application after insert to check the Primary Contact field
+ jobApp = [SELECT Primary_Contact__c FROM Job_Application__c WHERE Id = :jobApp.Id];
+
+ // Assert that the primary contact is still null
+ Assert.areEqual(null, jobApp.Primary_Contact__c, 'Primary Contact should remain null for a job application without an account.');
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/classes/JobApplicationHelperTest.cls-meta.xml b/force-app/main/default/classes/JobApplicationHelperTest.cls-meta.xml
new file mode 100644
index 0000000..998805a
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationHelperTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 62.0
+ Active
+
diff --git a/force-app/main/default/classes/JobApplicationTriggerHandler.cls b/force-app/main/default/classes/JobApplicationTriggerHandler.cls
new file mode 100644
index 0000000..06fc2e0
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationTriggerHandler.cls
@@ -0,0 +1,27 @@
+public with sharing class JobApplicationTriggerHandler extends TriggerHandler {
+ private List newJobAppList;
+ private Map newJobAppMap;
+ private List oldJobAppList;
+ private Map oldJobAppMap;
+
+ public JobApplicationTriggerHandler() {
+ this.newJobAppList = (List) Trigger.new;
+ this.newJobAppMap = (Map) Trigger.newMap;
+ this.oldJobAppList = (List) Trigger.old;
+ this.oldJobAppMap = (Map) Trigger.oldMap;
+ }
+
+ public override void afterUpdate() {
+ JobApplicationHelper.processJobApplications(newJobAppList, oldJobAppMap);
+ }
+
+ public override void afterInsert() {
+ List jobAppIds = new List();
+ for(Job_Application__c jobApp : newJobAppList) {
+ jobAppIds.add(jobApp.Id);
+ }
+
+ System.enqueueJob(new SetPrimaryContactQueueable(jobAppIds));
+ JobApplicationHelper.processJobApplications(newJobAppList, oldJobAppMap);
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/classes/JobApplicationTriggerHandler.cls-meta.xml b/force-app/main/default/classes/JobApplicationTriggerHandler.cls-meta.xml
new file mode 100644
index 0000000..7a51829
--- /dev/null
+++ b/force-app/main/default/classes/JobApplicationTriggerHandler.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 58.0
+ Active
+
diff --git a/force-app/main/default/classes/JoobleJobSearchController.cls b/force-app/main/default/classes/JoobleJobSearchController.cls
new file mode 100644
index 0000000..3be93f2
--- /dev/null
+++ b/force-app/main/default/classes/JoobleJobSearchController.cls
@@ -0,0 +1,152 @@
+public with sharing class JoobleJobSearchController {
+ @AuraEnabled
+ public static Map searchJobs(String keyword, String location, Integer pageNumber, Integer pageSize) {
+ String apiKey = API_Config__c.getInstance().Jooble_API__c;
+ String endpoint = 'https://jooble.org/api/' + apiKey;
+
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint(endpoint);
+ req.setMethod('POST');
+ req.setHeader('Content-Type', 'application/json');
+ req.setTimeout(20000);
+
+ Map requestMap = new Map{
+ 'keywords' => keyword,
+ 'location' => location,
+ 'page' => String.valueOf(pageNumber),
+ 'resultonpage' => pageSize
+ };
+
+ req.setBody(JSON.serialize(requestMap));
+
+ Http http = new Http();
+ HttpResponse res = http.send(req);
+
+ if (res.getStatusCode() == 200) {
+ Map jsonResults = (Map) JSON.deserializeUntyped(res.getBody());
+ Integer totalCount = (Integer) jsonResults.get('totalCount');
+ List