Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions __tests__/api_v1/plan.restore-version.transaction.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import passport from 'passport';
import createApp from '../../src';
import DataManager from '../../src/libs/db2';
import config from '../../src/config';
import Plan from '../../src/libs/db2/model/plan';

const dm = new DataManager(config);
const { db } = dm;
Expand Down Expand Up @@ -46,6 +47,10 @@ describe('PlanVersionController.restoreVersion() transaction', () => {
await db.schema.raw(truncate('plan_file'));
await db.schema.raw(truncate('plan_status_history'));
await db.schema.raw(truncate('plan_confirmation'));
await db.schema.raw(truncate('grazing_schedule_entry'));
await db.schema.raw(truncate('haycutting_schedule_entry'));
await db.schema.raw(truncate('grazing_schedule'));
await db.schema.raw(truncate('ref_livestock'));
await db.schema.raw(truncate('plan_snapshot'));
await db.schema.raw(truncate('plan'));
await db.schema.raw(truncate('agreement'));
Expand All @@ -56,6 +61,7 @@ describe('PlanVersionController.restoreVersion() transaction', () => {
await db('user_account').insert([seed.user]);
await db('ref_district').insert([seed.district]);
await db('ref_zone').insert([seed.zone]);
await db('ref_livestock').insert([{ id: 1, name: 'Test Livestock', au_factor: 1, active: true }]);
await db('agreement').insert([seed.agreement]);
});

Expand Down Expand Up @@ -114,6 +120,142 @@ describe('PlanVersionController.restoreVersion() transaction', () => {
await request(app).post(`${baseUrl}/1/version/1/restore`).expect(200);
});

test('remaps restored hierarchy children to their new parents', async () => {
await db('plan').insert([
{
id: 1,
agreement_id: 'RAN076843',
range_name: 'Original Plan',
plan_start_date: '2026-01-01T08:00:00.000Z',
plan_end_date: '2030-12-31T08:00:00.000Z',
status_id: 9,
creator_id: 1,
uploaded: true,
},
]);

const snapshot = {
id: 1,
range_name: 'Restored Plan',
rangeName: 'Restored Plan',
agreement_id: 'RAN076843',
agreementId: 'RAN076843',
status_id: 9,
statusId: 9,
creator_id: 1,
creatorId: 1,
uploaded: true,
pastures: [
{
id: 10,
name: 'Restored Pasture',
allowable_aum: 100,
plan_id: 999,
plantCommunities: [
{
name: 'Restored Community',
communityTypeId: 1,
purposeOfAction: 'maintain',
pasture_id: 999,
indicatorPlants: [{ name: 'Restored Indicator', criteria: 'rangereadiness', plant_community_id: 999 }],
monitoringAreas: [
{
name: 'Restored Area',
plant_community_id: 999,
purposes: [{ purpose_type_id: 1, monitoring_area_id: 999 }],
},
],
plantCommunityActions: [{ name: 'Restored Action', plant_community_id: 999 }],
},
],
},
],
schedules: [
{
id: 10,
year: 2026,
plan_id: 999,
scheduleEntries: [
{
id: 20,
pastureId: 10,
livestockTypeId: 1,
dateIn: '2026-06-01',
dateOut: '2026-06-10',
livestockCount: 10,
graceDays: 0,
},
],
},
],
additionalRequirements: [],
ministerIssues: [],
managementConsiderations: [],
confirmations: [],
planStatusHistory: [],
files: [],
agreement: { agreementTypeId: 1, agreement_type_id: 1 },
};

await db('plan_snapshot').insert([
{
plan_id: 1,
version: 1,
status_id: 9,
user_id: 1,
snapshot: JSON.stringify(snapshot),
},
]);

await Plan.restoreVersion(db, 1, 1);

const pasture = await db.selectFrom('pasture').selectAll().where('plan_id', '=', 1).executeTakeFirstOrThrow();
const community = await db
.selectFrom('plant_community')
.selectAll()
.where('pasture_id', '=', pasture.id)
.executeTakeFirstOrThrow();
const indicator = await db
.selectFrom('indicator_plant')
.selectAll()
.where('plant_community_id', '=', community.id)
.executeTakeFirstOrThrow();
const area = await db
.selectFrom('monitoring_area')
.selectAll()
.where('plant_community_id', '=', community.id)
.executeTakeFirstOrThrow();
const purpose = await db
.selectFrom('monitoring_area_purpose')
.selectAll()
.where('monitoring_area_id', '=', area.id)
.executeTakeFirstOrThrow();
const action = await db
.selectFrom('plant_community_action')
.selectAll()
.where('plant_community_id', '=', community.id)
.executeTakeFirstOrThrow();
const schedule = await db
.selectFrom('grazing_schedule')
.selectAll()
.where('plan_id', '=', 1)
.executeTakeFirstOrThrow();
const scheduleEntry = await db
.selectFrom('grazing_schedule_entry')
.selectAll()
.where('grazing_schedule_id', '=', schedule.id)
.executeTakeFirstOrThrow();

expect(pasture.plan_id).toBe(1);
expect(community.pasture_id).toBe(pasture.id);
expect(indicator.plant_community_id).toBe(community.id);
expect(area.plant_community_id).toBe(community.id);
expect(purpose.monitoring_area_id).toBe(area.id);
expect(action.plant_community_id).toBe(community.id);
expect(scheduleEntry.grazing_schedule_id).toBe(schedule.id);
expect(scheduleEntry.pasture_id).toBe(pasture.id);
});

test('rolls back all writes when restoreVersion fails', async () => {
await db('plan').insert([
{
Expand Down
57 changes: 40 additions & 17 deletions src/libs/db2/model/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,29 +318,44 @@ export default class Plan extends KyselyModel {
await Plan.update(db, { id: planId }, { ...snapshot, isRestored: true });
await Pasture.remove(db, { plan_id: planId });
const pasturePromises = snapshot.pastures.map(async (pasture: any) => {
const newPasture = await Pasture.create(db, pasture);
const newPasture = await Pasture.create(db, { ...pasture, planId });
await PlantCommunity.remove(db, { pasture_id: pasture.id });
const plantCommunityPromises = pasture.plantCommunities.map(async (plantCommunity: any) => {
const newPlantCommunity = await PlantCommunity.create(db, plantCommunity);
const newPlantCommunity = await PlantCommunity.create(db, {
...plantCommunity,
pastureId: newPasture.id,
});
await IndicatorPlant.remove(db, { plant_community_id: plantCommunity.id });
const indicatorPlantPromises = plantCommunity.indicatorPlants.map(async (indicatorPlant: any) => {
return IndicatorPlant.create(db, indicatorPlant);
return IndicatorPlant.create(db, {
...indicatorPlant,
plantCommunityId: newPlantCommunity.id,
});
});
const newIndicatorPlants = await Promise.all(indicatorPlantPromises);
await MonitoringArea.remove(db, { plant_community_id: plantCommunity.id });
const monitoringAreaPromises = plantCommunity.monitoringAreas.map(async (monitoringArea: any) => {
const newMonitoringArea = await MonitoringArea.create(db, monitoringArea);
const newMonitoringArea = await MonitoringArea.create(db, {
...monitoringArea,
plantCommunityId: newPlantCommunity.id,
});
await MonitoringAreaPurpose.remove(db, { monitoring_area_id: monitoringArea.id });
const purposePromises = monitoringArea.purposes.map(async (purpose: any) => {
return MonitoringAreaPurpose.create(db, purpose);
return MonitoringAreaPurpose.create(db, {
...purpose,
monitoringAreaId: newMonitoringArea.id,
});
});
const newPurposes = await Promise.all(purposePromises);
return { ...newMonitoringArea, monitoringAreaPurposes: newPurposes };
});
const newMonitoringAreas = await Promise.all(monitoringAreaPromises);
await PlantCommunityAction.remove(db, { plant_community_id: plantCommunity.id });
const actionPromises = plantCommunity.plantCommunityActions.map(async ({ ...action }: any) => {
return PlantCommunityAction.create(db, action);
return PlantCommunityAction.create(db, {
...action,
plantCommunityId: newPlantCommunity.id,
});
});
const newActions = await Promise.all(actionPromises);
return {
Expand All @@ -357,7 +372,7 @@ export default class Plan extends KyselyModel {
await Schedule.remove(db, { plan_id: planId });
await Promise.all(
snapshot.schedules.map(async (schedule: any) => {
const newSchedule = await Schedule.create(db, schedule);
const newSchedule = await Schedule.create(db, { ...schedule, planId });
await Promise.all([
GrazingScheduleEntry.remove(db, { grazing_schedule_id: schedule.id }),
HayCuttingScheduleEntry.remove(db, { haycutting_schedule_id: schedule.id }),
Expand All @@ -367,10 +382,18 @@ export default class Plan extends KyselyModel {
(schedule.scheduleEntries || [])
.map((entry: any) => {
if (!scheduleEntryCreator) return null;
const originalPastureId = entry.pastureId ?? entry.pasture_id;
const newPasture = newPastures.find((pasture) => pasture.original.id === originalPastureId);
if (!newPasture) {
throw errorWithCode(
`Could not find restored pasture for schedule entry pasture ID ${originalPastureId}.`,
400,
);
}
const entryWithScheduleId =
scheduleEntryCreator === GrazingScheduleEntry
? { ...entry, grazing_schedule_id: newSchedule.id }
: { ...entry, haycutting_schedule_id: newSchedule.id };
? { ...entry, grazingScheduleId: newSchedule.id, pastureId: newPasture.id }
: { ...entry, haycuttingScheduleId: newSchedule.id, pastureId: newPasture.id };
Comment thread
brijesh-amin marked this conversation as resolved.
return scheduleEntryCreator.create(db, entryWithScheduleId);
})
.filter(Boolean),
Expand All @@ -380,15 +403,15 @@ export default class Plan extends KyselyModel {
);
await AdditionalRequirement.remove(db, { plan_id: planId });
const additionalRequirementPromises = snapshot.additionalRequirements.map(async (requirement: any) => {
return AdditionalRequirement.create(db, requirement);
return AdditionalRequirement.create(db, { ...requirement, planId });
});
await Promise.all(additionalRequirementPromises);
await MinisterIssue.remove(db, { plan_id: planId });
const ministerIssuePromises = snapshot.ministerIssues.map(async (issue: any) => {
const newIssue = await MinisterIssue.create(db, issue);
const newIssue = await MinisterIssue.create(db, { ...issue, planId });
await MinisterIssueAction.remove(db, { issue_id: issue.id });
const actionPromises = issue.ministerIssueActions.map(async (action: any) => {
return MinisterIssueAction.create(db, action);
return MinisterIssueAction.create(db, { ...action, issueId: newIssue.id });
});
const newActions = await Promise.all(actionPromises);
await MinisterIssuePasture.remove(db, { minister_issue_id: issue.id });
Expand All @@ -402,28 +425,28 @@ export default class Plan extends KyselyModel {
await Promise.all(ministerIssuePromises);
await ManagementConsideration.remove(db, { plan_id: planId });
const managementConsiderationPromises = snapshot.managementConsiderations.map(async (consideration: any) => {
return ManagementConsideration.create(db, consideration);
return ManagementConsideration.create(db, { ...consideration, planId });
});
await Promise.all(managementConsiderationPromises);
await PlanConfirmation.remove(db, { plan_id: planId });
const confirmationPromises = snapshot.confirmations.map(async (confirmation: any) => {
return PlanConfirmation.create(db, confirmation);
return PlanConfirmation.create(db, { ...confirmation, planId });
});
await Promise.all(confirmationPromises);
if (snapshot.invasivePlantChecklist && snapshot.invasivePlantChecklist.planId) {
await InvasivePlantChecklist.remove(db, { plan_id: planId });
await InvasivePlantChecklist.create(db, snapshot.invasivePlantChecklist);
await InvasivePlantChecklist.create(db, { ...snapshot.invasivePlantChecklist, planId });
}
if (!preserverHistory) {
await PlanStatusHistory.remove(db, { plan_id: planId });
const newStatusHistoryPromises = snapshot.planStatusHistory.map(async ({ ...history }: any) => {
return PlanStatusHistory.create(db, history);
return PlanStatusHistory.create(db, { ...history, planId });
});
await Promise.all(newStatusHistoryPromises);
}
await PlanFile.remove(db, { plan_id: planId });
const filePromises = snapshot.files.map(async (file: any) => {
return PlanFile.create(db, file);
return PlanFile.create(db, { ...file, planId });
});
await Promise.all(filePromises);
}
Expand Down
Loading