Skip to content

Commit 68bf314

Browse files
author
Brijesh
committed
Exemption implementation
Exemption implementation Exemption implementation feat: Implement exemption status updates for expired plans, enable filtering and rename script refactor: Move exemption processing to processAgreementExemptions and filter by agreement end date
1 parent 366077b commit 68bf314

28 files changed

Lines changed: 1629 additions & 167 deletions

.vscode/settings.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
22
"editor.tabSize": 2,
33
"editor.wordWrap": "on",
4-
"cSpell.words": ["all", "eagerload", "except", "many", "one", "plan", "to"],
4+
"cSpell.words": [
5+
"Agrologist",
6+
"all",
7+
"eagerload",
8+
"except",
9+
"many",
10+
"one",
11+
"plan",
12+
"to"
13+
],
514
"editor.formatOnSave": false
615
}

exemptionTemplate.docx

13.9 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"test:security": "nsp check",
8585
"test:lint": "eslint --env node --ext .js src __tests__ __testHelpers__ __mocks__",
8686
"import": "node build/scripts/import.js",
87-
"plan_extension": "node build/scripts/plan_extension.js",
87+
"plan_extension_exemption": "node build/scripts/plan_extension_exemption.js",
8888
"process_no_use": "node build/scripts/process_no_use.js",
8989
"initialize_docker": "npm run build && ./node_modules/.bin/knex migrate:latest && ./node_modules/.bin/knex seed:run && ./node_modules/.bin/babel-node scripts/import.js true",
9090
"initialize": "npm rum build && npx knex migrate:latest && npx knex seed:run && npx babel-node scripts/import.js true",
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import config from '../src/config';
2-
import { PLAN_EXTENSION_STATUS } from '../src/constants';
2+
import { PLAN_EXTENSION_STATUS, SYSTEM_USER_ID } from '../src/constants';
33
import DataManager from '../src/libs/db2';
44
import EmailTemplate from '../src/libs/db2/model/emailtemplate';
55
import { Mailer } from '../src/libs/mailer';
66
import { substituteFields } from '../src/libs/utils';
7+
import {
8+
updateAgreementExemptions,
9+
sendAgreementExemptionStatusEmails,
10+
} from '../src/router/helpers/AgreementExemptionHelper';
711

812
const dm = new DataManager(config);
913

10-
const { db, Plan, PlanExtensionRequests } = dm;
14+
const { db, Plan, PlanExtensionRequests, Agreement, User } = dm;
1115

1216
const sendEmailToAgreementHolders = async (db, expiringPlan) => {
1317
const template = await EmailTemplate.findOne(db, {
@@ -72,9 +76,14 @@ const processExpiredPlans = async (trx) => {
7276
};
7377

7478
const main = async () => {
75-
console.log('Starting plan extension batch process..');
7679
const trx = await db.transaction();
7780
try {
81+
// Fetch system user
82+
const systemUser = await User.findById(trx, SYSTEM_USER_ID);
83+
if (!systemUser) {
84+
throw new Error('System user not found. Please ensure a user with SYSTEM_USER_ID exists.');
85+
}
86+
7887
const currentDate = new Date();
7988
const oneYearLater = new Date();
8089
oneYearLater.setMonth(currentDate.getMonth() + 12);
@@ -110,9 +119,13 @@ const main = async () => {
110119
}
111120
await processExpiredPlans(trx);
112121
await activateReplacementPlans(trx);
122+
const updates = await updateAgreementExemptions(trx, systemUser); // Pass systemUser
123+
await sendAgreementExemptionStatusEmails(trx, updates);
113124
trx.commit();
125+
console.log('Script completed successfully.');
114126
} catch (err) {
115127
trx.rollback();
128+
console.error('Error in main function:', err); // Log the error
116129
process.exit(0);
117130
}
118131
process.exit(0);

src/constants.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,26 @@ export const PLAN_EXTENSION_STATUS = {
118118
};
119119

120120
export const NOT_PROVIDED = 'Not provided';
121+
122+
// Exemption workflow status codes
123+
export const EXEMPTION_STATUS = {
124+
DRAFT: 'Draft',
125+
PENDING_APPROVAL: 'Pending Approval',
126+
APPROVED: 'Approved',
127+
REJECTED: 'Rejected',
128+
DELETED: 'Deleted',
129+
CANCELLED: 'Cancelled',
130+
};
131+
132+
export const ATTACHMENT_TYPE = {
133+
PLAN_ATTACHMENT: 'Plan attachment',
134+
EXEMPTION_ATTACHMENT: 'Exemption attachment',
135+
};
136+
137+
export const AGREEMENT_EXEMPTION_STATUS = {
138+
NOT_EXEMPTED: 'NOT_EXEMPTED',
139+
ACTIVE: 'ACTIVE',
140+
SCHEDULED: 'SCHEDULED',
141+
};
142+
143+
export const SYSTEM_USER_ID = 1115;

src/libs/cdogs.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export default class Cdogs {
1717
this.enabled = enabled;
1818
}
1919

20-
async init() {
20+
async init(templateFile) {
2121
this.template = {
2222
encodingType: 'base64',
2323
fileType: 'docx',
24-
content: await Cdogs.readTemplate(),
24+
content: await Cdogs.readTemplate(templateFile),
2525
};
2626
this.options = {
2727
cacheReport: false,
@@ -48,14 +48,14 @@ export default class Cdogs {
4848
}
4949
}
5050

51-
async generatePDF(planData) {
51+
async generatePDF(data, pdfFileName = `${data.agreementId}.pdf`) {
5252
if (!this.enabled.toLowerCase() === 'true') return {};
5353
const serviceURL = `${this.serviceURL}/api/v2/template/render`;
5454
try {
5555
const token = await this.getBearerToken();
5656
const payload = {
57-
data: planData,
58-
options: { ...this.options, reportName: `${planData.agreementId}.pdf` },
57+
data: data,
58+
options: { ...this.options, reportName: pdfFileName },
5959
template: this.template,
6060
};
6161
const response = await axios.post(serviceURL, JSON.stringify(payload), {

src/libs/db2/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ import config from '../../config';
1717
import AdditionalRequirement from './model/additionalrequirement';
1818
import AdditionalRequirementCategory from './model/additionalrequirementcategory';
1919
import Agreement from './model/agreement';
20-
import AgreementExemptionStatus from './model/agreementexemptionstatus';
20+
import ExemptionStatusType from './model/exemptionstatustype';
2121
import AgreementType from './model/agreementtype';
2222
import AmendmentType from './model/amendmenttype';
2323
import Client from './model/client';
2424
import ClientAgreement from './model/ClientAgreement';
2525
import ClientType from './model/clienttype';
2626
import District from './model/district';
2727
import EmailTemplate from './model/emailtemplate';
28+
import Exemption from './model/exemption';
29+
import AgreementExemptionStatus from './model/agreementexemptionstatus';
30+
import ExemptionStatusHistory from './model/exemptionstatushistory';
31+
import ExemptionAttachment from './model/exemptionattachment';
2832
import Schedule from './model/grazingschedule';
2933
import GrazingScheduleEntry from './model/grazingscheduleentry';
3034
import HayCuttingScheduleEntry from './model/haycuttingscheduleentry';
@@ -94,7 +98,7 @@ export default class DataManager {
9498
this.db = k;
9599
this.config = config;
96100
this.Agreement = Agreement;
97-
this.AgreementExemptionStatus = AgreementExemptionStatus;
101+
this.ExemptionStatusType = ExemptionStatusType;
98102
this.AgreementType = AgreementType;
99103
this.Client = Client;
100104
this.ClientType = ClientType;
@@ -146,5 +150,9 @@ export default class DataManager {
146150
this.UserClientLink = UserClientLink;
147151
this.PlanFile = PlanFile;
148152
this.EmailTemplate = EmailTemplate;
153+
this.Exemption = Exemption;
154+
this.AgreementExemptionStatus = AgreementExemptionStatus;
155+
this.ExemptionStatusHistory = ExemptionStatusHistory;
156+
this.ExemptionAttachment = ExemptionAttachment;
149157
}
150158
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Unified migration: agreement_exemption_status, exemption, exemption_status_type, exemption_status_history, exemption_attachment
2+
3+
exports.up = async function (knex) {
4+
// 1. Create agreement_exemption_status table
5+
await knex.raw(`
6+
CREATE TABLE agreement_exemption_status (
7+
id SERIAL PRIMARY KEY,
8+
code VARCHAR(32) UNIQUE NOT NULL,
9+
description VARCHAR(255) NOT NULL,
10+
active BOOLEAN NOT NULL DEFAULT TRUE,
11+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
12+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
13+
);
14+
`);
15+
await knex.raw(`
16+
INSERT INTO agreement_exemption_status (id, code, description, active)
17+
VALUES
18+
(1, 'NOT_EXEMPTED', 'Not Exempted', TRUE),
19+
(2, 'ACTIVE', 'Active', TRUE),
20+
(3, 'SCHEDULED', 'Scheduled', TRUE)
21+
`);
22+
23+
// 2. Add exemption_status column to agreement table
24+
await knex.raw(`ALTER TABLE agreement ADD COLUMN exemption_status VARCHAR(32) NOT NULL DEFAULT 'NOT_EXEMPTED';`);
25+
await knex.raw(
26+
`ALTER TABLE agreement ADD CONSTRAINT agreement_exemption_status_fk FOREIGN KEY (exemption_status) REFERENCES agreement_exemption_status(code);`,
27+
);
28+
await knex.raw(`UPDATE agreement SET exemption_status = 'NOT_EXEMPTED';`);
29+
30+
// 4. Create exemption_status_type table
31+
await knex.raw(`
32+
CREATE TABLE exemption_status_type (
33+
id SERIAL PRIMARY KEY,
34+
code VARCHAR(32) UNIQUE NOT NULL,
35+
description TEXT NOT NULL,
36+
active BOOLEAN NOT NULL DEFAULT TRUE,
37+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
38+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
39+
);
40+
`);
41+
await knex.raw(`
42+
INSERT INTO exemption_status_type (code, description, active)
43+
VALUES
44+
('Draft', 'Draft - editable by Staff Agrologist', TRUE),
45+
('Pending Approval', 'Pending approval - submitted to Decision Maker', TRUE),
46+
('Approved', 'Approved - read only', TRUE),
47+
('Rejected', 'Rejected - editable by Staff Agrologist', TRUE),
48+
('Cancelled', 'Cancelled by Decision Maker', TRUE),
49+
('Deleted', 'Deleted', TRUE)
50+
`);
51+
52+
// 3. Create exemption table
53+
await knex.raw(`
54+
CREATE TABLE exemption (
55+
id SERIAL PRIMARY KEY,
56+
agreement_id VARCHAR(9) NOT NULL REFERENCES agreement(forest_file_id) ON DELETE CASCADE,
57+
start_date DATE NOT NULL,
58+
end_date DATE NOT NULL,
59+
reason TEXT,
60+
justification_text TEXT,
61+
status VARCHAR(32) NOT NULL DEFAULT 'Draft' REFERENCES exemption_status_type(code),
62+
user_id INTEGER NOT NULL REFERENCES user_account(id),
63+
approved_by INTEGER NULL REFERENCES user_account(id),
64+
approval_date TIMESTAMP WITH TIME ZONE NULL,
65+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
66+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
67+
);
68+
`);
69+
70+
// 5. Create exemption_status_history table
71+
await knex.raw(`
72+
CREATE TABLE exemption_status_history (
73+
id SERIAL PRIMARY KEY,
74+
exemption_id INTEGER NULL REFERENCES exemption(id) ON DELETE CASCADE,
75+
from_status VARCHAR(32) NULL REFERENCES exemption_status_type(code),
76+
to_status VARCHAR(32) NOT NULL REFERENCES exemption_status_type(code),
77+
note TEXT,
78+
user_id INTEGER NOT NULL REFERENCES user_account(id),
79+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
80+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
81+
);
82+
`);
83+
await knex.raw(`CREATE INDEX idx_exemption_status_history_exemption_id ON exemption_status_history(exemption_id);`);
84+
await knex.raw(`CREATE INDEX idx_exemption_status_history_to_status ON exemption_status_history(to_status);`);
85+
await knex.raw(
86+
`CREATE TRIGGER update_exemption_status_history_changetimestamp BEFORE UPDATE ON exemption_status_history FOR EACH ROW EXECUTE PROCEDURE update_changetimestamp_column();`,
87+
);
88+
89+
// 6. Create exemption_attachment table
90+
await knex.raw(`
91+
CREATE TABLE exemption_attachment (
92+
id SERIAL PRIMARY KEY,
93+
name VARCHAR NOT NULL,
94+
url VARCHAR NOT NULL,
95+
type VARCHAR NOT NULL,
96+
access VARCHAR NOT NULL DEFAULT 'staff_only',
97+
exemption_id INTEGER NOT NULL REFERENCES exemption(id) ON DELETE CASCADE,
98+
user_id INTEGER NOT NULL REFERENCES user_account(id),
99+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
100+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
101+
);
102+
`);
103+
await knex.raw(`CREATE INDEX idx_exemption_attachment_exemption_id ON exemption_attachment(exemption_id);`);
104+
await knex.raw(`CREATE INDEX idx_exemption_attachment_user_id ON exemption_attachment(user_id);`);
105+
await knex.raw(
106+
`CREATE TRIGGER update_exemption_attachment_changetimestamp BEFORE UPDATE ON exemption_attachment FOR EACH ROW EXECUTE PROCEDURE update_changetimestamp_column();`,
107+
);
108+
109+
// 7. Drop legacy tables/columns if they exist
110+
await knex.raw('DROP TABLE IF EXISTS ref_agreement_exemption_status CASCADE;');
111+
const hasOldCol = await knex.schema.hasColumn('agreement', 'agreement_exemption_status_id');
112+
if (hasOldCol) {
113+
await knex.schema.alterTable('agreement', (table) => {
114+
table.dropColumn('agreement_exemption_status_id');
115+
});
116+
}
117+
118+
await knex.raw(`
119+
INSERT INTO email_template (name, from_email, subject, body)
120+
SELECT 'Exemption Status Change', 'MyRangeBC@gov.bc.ca', 'Exemption Status Update', 'Exemption for agreement {agreementId} is {toStatus}.'
121+
WHERE NOT EXISTS (
122+
SELECT 1 FROM email_template WHERE name = 'Exemption Status Change'
123+
)
124+
`);
125+
await knex.raw(`
126+
INSERT INTO email_template (name, from_email, subject, body)
127+
SELECT 'Agreement Exemption Status Change', 'MyRangeBC@gov.bc.ca', 'Agreement Exemption Status Change', 'Exemption {toStatus} for agreement {agreement_id}.'
128+
WHERE NOT EXISTS (
129+
SELECT 1 FROM email_template WHERE name = 'Agreement Exemption Status Change'
130+
)
131+
`);
132+
};
133+
134+
exports.down = async function (knex) {
135+
await knex('email_template').where({ name: 'Agreement Exemption Status Change' }).del();
136+
await knex('email_template').where({ name: 'Exemption Status Change' }).del();
137+
await knex.raw('DROP TABLE IF EXISTS exemption_attachment CASCADE;');
138+
await knex.raw('DROP TABLE IF EXISTS exemption_status_history CASCADE;');
139+
await knex.raw('DROP TABLE IF EXISTS exemption_status_type CASCADE;');
140+
await knex.raw('DROP TABLE IF EXISTS exemption CASCADE;');
141+
await knex.raw('DROP TABLE IF EXISTS agreement_exemption_status CASCADE;');
142+
// Remove the new exemption_status column
143+
const hasNewCol = await knex.schema.hasColumn('agreement', 'exemption_status');
144+
if (hasNewCol) {
145+
await knex.schema.alterTable('agreement', (table) => {
146+
table.dropForeign('exemption_status');
147+
table.dropColumn('exemption_status');
148+
});
149+
}
150+
// Re-add the old foreign key column (no default value, must be handled manually)
151+
const hasOldCol = await knex.schema.hasColumn('agreement', 'agreement_exemption_status_id');
152+
if (!hasOldCol) {
153+
await knex.schema.alterTable('agreement', (table) => {
154+
table.integer('agreement_exemption_status_id').notNullable();
155+
});
156+
}
157+
};

0 commit comments

Comments
 (0)