diff --git a/__tests__/api_v1/plan.restore-version.transaction.spec.js b/__tests__/api_v1/plan.restore-version.transaction.spec.js index c48a9d1b..be7bb1f4 100644 --- a/__tests__/api_v1/plan.restore-version.transaction.spec.js +++ b/__tests__/api_v1/plan.restore-version.transaction.spec.js @@ -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; @@ -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')); @@ -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]); }); @@ -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([ { diff --git a/src/libs/db2/model/plan.ts b/src/libs/db2/model/plan.ts index 650344a5..d2a12908 100644 --- a/src/libs/db2/model/plan.ts +++ b/src/libs/db2/model/plan.ts @@ -318,21 +318,33 @@ 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 }; @@ -340,7 +352,10 @@ export default class Plan extends KyselyModel { 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 { @@ -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 }), @@ -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 }; return scheduleEntryCreator.create(db, entryWithScheduleId); }) .filter(Boolean), @@ -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 }); @@ -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); }