diff --git a/src/fn/delete-contacts.js b/src/fn/delete-contacts.js index 724c9f4f..ea09747e 100644 --- a/src/fn/delete-contacts.js +++ b/src/fn/delete-contacts.js @@ -56,7 +56,7 @@ ${bold('OPTIONS')} A comma delimited list of ids of contacts to be deleted. --disable-users - When flag is present, users at any deleted place will be permanently disabled. + When flag is present, users at any deleted place will be updated and may be permanently disabled. Supported by CHT Core 4.7 and above. --docDirectoryPath= Specifies the folder used to store the documents representing the changes in hierarchy. diff --git a/src/fn/merge-contacts.js b/src/fn/merge-contacts.js index 44f5f73d..65d35822 100644 --- a/src/fn/merge-contacts.js +++ b/src/fn/merge-contacts.js @@ -15,6 +15,7 @@ module.exports = { const options = { disableUsers: args.disableUsers, docDirectoryPath: args.docDirectoryPath, + mergePrimaryContacts: args.mergePrimaryContacts, force: args.force, }; return HierarchyOperations(db, options).merge(args.sourceIds, args.destinationId); @@ -44,6 +45,7 @@ const parseExtraArgs = (projectDir, extraArgs = []) => { sourceIds, disableUsers: !!args['disable-users'], docDirectoryPath: path.resolve(projectDir, args.docDirectoryPath || 'json_docs'), + mergePrimaryContacts: !!args['merge-primary-contacts'], force: !!args.force, }; }; @@ -68,6 +70,9 @@ ${bold('OPTIONS')} --disable-users When flag is present, users at any deleted place will be updated and may be permanently disabled. Supported by CHT Core 4.7 and above. +--merge-primary-contacts + When flag is present, the primary contacts for all the top-level places will also be merged into a single resulting contact. + --docDirectoryPath= Specifies the folder used to store the documents representing the changes in hierarchy. `); diff --git a/src/lib/hierarchy-operations/delete-hierarchy.js b/src/lib/hierarchy-operations/delete-hierarchy.js index 4d9f470c..c409749d 100644 --- a/src/lib/hierarchy-operations/delete-hierarchy.js +++ b/src/lib/hierarchy-operations/delete-hierarchy.js @@ -22,8 +22,7 @@ async function deleteHierarchy(db, options, sourceIds) { } const affectedContactCount = descendantsAndSelf.length; - - info(`Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount.length} contact(s) and ${affectedReportCount} report(s).`); + info(`Staged updates to delete ${prettyPrintDocument(sourceDoc)}. ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`); } } @@ -31,7 +30,7 @@ async function deleteReportsForContact(db, options, contact) { let skip = 0; let reportBatch; do { - reportBatch = await DataSource.getReportsForContacts(db, [], contact._id, skip); + reportBatch = await DataSource.getReportsForContacts(db, [], [contact._id], skip); for (const report of reportBatch) { JsDocs.deleteDoc(options, report); diff --git a/src/lib/hierarchy-operations/hierarchy-data-source.js b/src/lib/hierarchy-operations/hierarchy-data-source.js index da78ef7e..c5d15d1d 100644 --- a/src/lib/hierarchy-operations/hierarchy-data-source.js +++ b/src/lib/hierarchy-operations/hierarchy-data-source.js @@ -2,6 +2,7 @@ const lineageManipulation = require('./lineage-manipulation'); const HIERARCHY_ROOT = 'root'; const BATCH_SIZE = 10000; +const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid']; /* Fetches all of the documents associated with the "contactIds" and confirms they exist. @@ -54,14 +55,10 @@ async function getContactWithDescendants(db, contactId) { .filter(doc => doc && doc.type !== 'tombstone'); } -async function getReportsForContacts(db, createdByIds, createdAtId, skip) { +async function getReportsForContacts(db, createdByIds, createdAtIds, skip) { const createdByKeys = createdByIds.map(id => [`contact:${id}`]); - const createdAtKeys = createdAtId ? [ - [`patient_id:${createdAtId}`], - [`patient_uuid:${createdAtId}`], - [`place_id:${createdAtId}`], - [`place_uuid:${createdAtId}`] - ] : []; + const mapIdToSubjectKeys = id => SUBJECT_IDS.map(subjectId => [`${subjectId}:${id}`]); + const createdAtKeys = createdAtIds ? createdAtIds.map(mapIdToSubjectKeys).flat() : []; const reports = await db.query('medic-client/reports_by_freetext', { keys: [ @@ -95,6 +92,7 @@ async function getAncestorsOf(db, contactDoc) { module.exports = { BATCH_SIZE, HIERARCHY_ROOT, + SUBJECT_IDS, getAncestorsOf, getContactWithDescendants, getContact, diff --git a/src/lib/hierarchy-operations/index.js b/src/lib/hierarchy-operations/index.js index 1d40685d..edc71a47 100644 --- a/src/lib/hierarchy-operations/index.js +++ b/src/lib/hierarchy-operations/index.js @@ -19,24 +19,24 @@ async function moveHierarchy(db, options, sourceIds, destinationId) { for (const sourceId of sourceIds) { const sourceDoc = sourceDocs[sourceId]; const descendantsAndSelf = await DataSource.getContactWithDescendants(db, sourceId); + const moveContext = { sourceId, destinationId, descendantsAndSelf, replacementLineage, merge: !!options.merge, + disableUsers: !!options.disableUsers, + mergePrimaryContacts: !!options.mergePrimaryContacts, + sourcePrimaryContactId: getPrimaryContactId(sourceDoc), + destinationPrimaryContactId: getPrimaryContactId(destinationDoc), }; await constraints.assertNoPrimaryContactViolations(sourceDoc, destinationDoc, descendantsAndSelf); - if (options.merge) { - const toDeleteUsers = options.disableUsers && constraints.isPlace(sourceDoc); - JsDocs.deleteDoc(options, sourceDoc, toDeleteUsers); - } - const prettyPrintDocument = doc => `'${doc.name}' (${doc._id})`; - trace(`Considering lineage updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`); - const updatedDescendants = replaceLineageInContacts(moveContext); + trace(`Considering updates to ${descendantsAndSelf.length} descendant(s) of contact ${prettyPrintDocument(sourceDoc)}.`); + const updatedDescendants = updateContacts(moveContext, constraints); const ancestors = await DataSource.getAncestorsOf(db, sourceDoc); trace(`Considering primary contact updates to ${ancestors.length} ancestor(s) of contact ${prettyPrintDocument(sourceDoc)}.`); @@ -56,6 +56,10 @@ async function moveHierarchy(db, options, sourceIds, destinationId) { info(`Staged changes to lineage information for ${affectedContactCount} contact(s) and ${affectedReportCount} report(s).`); } +function getPrimaryContactId(doc) { + return typeof doc?.contact === 'string' ? doc.contact : doc?.contact?._id; +} + async function updateReports(db, options, moveContext) { const descendantIds = moveContext.descendantsAndSelf.map(contact => contact._id); @@ -63,8 +67,8 @@ async function updateReports(db, options, moveContext) { let reportDocsBatch; do { info(`Processing ${skip} to ${skip + DataSource.BATCH_SIZE} report docs`); - const createdAtId = options.merge && moveContext.sourceId; - reportDocsBatch = await DataSource.getReportsForContacts(db, descendantIds, createdAtId, skip); + const createdAtIds = getReportsCreatedAtIds(moveContext); + reportDocsBatch = await DataSource.getReportsForContacts(db, descendantIds, createdAtIds, skip); const lineageUpdates = replaceLineageOfReportCreator(reportDocsBatch, moveContext); const reassignUpdates = reassignReports(reportDocsBatch, moveContext); @@ -78,24 +82,47 @@ async function updateReports(db, options, moveContext) { return skip; } -function reassignReportSubjects(report, { sourceId, destinationId }) { - const SUBJECT_IDS = ['patient_id', 'patient_uuid', 'place_id', 'place_uuid']; +function getReportsCreatedAtIds(moveContext) { + const result = []; + if (moveContext.merge) { + result.push(moveContext.sourceId); + } + + if (moveContext.mergePrimaryContacts && moveContext.sourcePrimaryContactId) { + result.push(moveContext.sourcePrimaryContactId); + } + + return result; +} + +function reassignReportSubjects(report, moveContext) { let updated = false; - for (const subjectId of SUBJECT_IDS) { - if (report[subjectId] === sourceId) { - report[subjectId] = destinationId; - updated = true; - } + for (const subjectId of DataSource.SUBJECT_IDS) { + updated = updated || reassignSingleReport(report, subjectId, moveContext.sourceId, moveContext.destinationId); - if (report.fields[subjectId] === sourceId) { - report.fields[subjectId] = destinationId; - updated = true; + if (moveContext.mergePrimaryContacts && moveContext.sourcePrimaryContactId && moveContext.destinationPrimaryContactId) { + updated = updated || reassignSingleReport(report, subjectId, moveContext.sourcePrimaryContactId, moveContext.destinationPrimaryContactId); } } return updated; } +function reassignSingleReport(report, subjectId, matchId, resultingId) { + let result = false; + if (report[subjectId] === matchId) { + report[subjectId] = resultingId; + result = true; + } + + if (report.fields[subjectId] === matchId) { + report.fields[subjectId] = resultingId; + result = true; + } + + return result; +} + function reassignReports(reports, moveContext) { const updated = new Set(); if (!moveContext.merge) { @@ -167,9 +194,26 @@ function replaceLineageInSingleContact(doc, moveContext) { } } -function replaceLineageInContacts(moveContext) { +function updateContacts(moveContext, constraints) { return moveContext.descendantsAndSelf - .map(descendant => replaceLineageInSingleContact(descendant, moveContext)) + .map(descendant => { + const deleteSource = moveContext.merge && descendant._id === moveContext.sourceId; + const deletePrimaryContact = moveContext.mergePrimaryContacts + && descendant._id === moveContext.sourcePrimaryContactId + && moveContext.destinationPrimaryContactId; + + if (deleteSource || deletePrimaryContact) { + const toDeleteUsers = moveContext.disableUsers && constraints.isPlace(descendant); + return { + _id: descendant._id, + _rev: descendant._rev, + _deleted: true, + cht_disable_linked_users: !!toDeleteUsers, + }; + } + + return replaceLineageInSingleContact(descendant, moveContext); + }) .filter(Boolean); } diff --git a/src/lib/hierarchy-operations/lineage-constraints.js b/src/lib/hierarchy-operations/lineage-constraints.js index e2079504..4f3da28a 100644 --- a/src/lib/hierarchy-operations/lineage-constraints.js +++ b/src/lib/hierarchy-operations/lineage-constraints.js @@ -9,10 +9,8 @@ module.exports = async (db, options) => { return { assertNoPrimaryContactViolations: async (sourceDoc, destinationDoc, descendantDocs) => { - const invalidPrimaryContactDoc = await getPrimaryContactViolations(db, sourceDoc, destinationDoc, descendantDocs); - if (invalidPrimaryContactDoc) { - throw Error(`Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the hierarchy for which they are a primary contact.`); - } + await assertOnPrimaryContactRemoval(db, sourceDoc, destinationDoc, descendantDocs); + await assertSourcePrimaryContactType(db, contactTypeInfo, sourceDoc); }, assertNoHierarchyErrors: (sourceDocs, destinationDoc) => { @@ -43,13 +41,16 @@ module.exports = async (db, options) => { }); }, - isPlace: (contact) => { - const contactType = getContactType(contact); - return !contactTypeInfo[contactType]?.person; - }, + isPlace: (contact) => isPlace(contactTypeInfo, contact), }; }; +function isPlace(contactTypeInfo, contact) { + const contactType = getContactType(contact); + const isPerson = contactTypeInfo[contactType]?.person || false; + return !isPerson; +} + /* Enforce the list of allowed parents for each contact type Ensure we are not creating a circular hierarchy @@ -127,11 +128,11 @@ A place's primary contact must be a descendant of that place. 1. Check to see which part of the contact's lineage will be removed 2. For each removed part of the contact's lineage, confirm that place's primary contact isn't being removed. */ -const getPrimaryContactViolations = async (db, contactDoc, destinationDoc, descendantDocs) => { - const contactsLineageIds = lineageManipulation.pluckIdsFromLineage(contactDoc?.parent); - const parentsLineageIds = lineageManipulation.pluckIdsFromLineage(destinationDoc); +async function assertOnPrimaryContactRemoval(db, sourceDoc, destinationDoc, descendantDocs) { + const sourceLineageIds = lineageManipulation.pluckIdsFromLineage(sourceDoc?.parent); + const destinationLineageIds = lineageManipulation.pluckIdsFromLineage(destinationDoc); - const docIdsRemovedFromContactLineage = contactsLineageIds.filter(value => !parentsLineageIds.includes(value)); + const docIdsRemovedFromContactLineage = sourceLineageIds.filter(value => !destinationLineageIds.includes(value)); const docsRemovedFromContactLineage = await db.allDocs({ keys: docIdsRemovedFromContactLineage, include_docs: true, @@ -141,10 +142,32 @@ const getPrimaryContactViolations = async (db, contactDoc, destinationDoc, desce .map(row => row?.doc?.contact?._id) .filter(Boolean); - return descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId)); -}; + const invalidPrimaryContactDoc = descendantDocs.find(descendant => primaryContactIds.some(primaryId => descendant._id === primaryId)); + if (invalidPrimaryContactDoc) { + throw Error(`Cannot remove contact '${invalidPrimaryContactDoc?.name}' (${invalidPrimaryContactDoc?._id}) from the hierarchy for which they are a primary contact.`); + } +} -const getContactType = doc => doc?.type === 'contact' ? doc?.contact_type : doc?.type; +async function assertSourcePrimaryContactType(db, contactTypeInfo, sourceDoc) { + const sourcePrimaryContactId = getPrimaryContactId(sourceDoc); + if (!sourcePrimaryContactId) { + return; + } + + const sourcePrimaryContactDoc = await db.get(sourcePrimaryContactId); + const primaryContactIsPlace = isPlace(contactTypeInfo, sourcePrimaryContactDoc); + if (primaryContactIsPlace) { + throw Error(`Source "${sourceDoc._id}" has primary contact "${sourcePrimaryContactId}" which is of type place`); + } +} + +function getContactType(doc) { + return doc?.type === 'contact' ? doc?.contact_type : doc?.type; +} + +function getPrimaryContactId(doc) { + return typeof doc?.contact === 'string' ? doc.contact : doc?.contact?._id; +} async function fetchContactTypeInfo(db) { try { diff --git a/test/fn/merge-contacts.spec.js b/test/fn/merge-contacts.spec.js index 6770d846..8800bd33 100644 --- a/test/fn/merge-contacts.spec.js +++ b/test/fn/merge-contacts.spec.js @@ -19,6 +19,7 @@ describe('merge-contacts', () => { sourceIds: ['food', 'is', 'tasty'], destinationId: 'bar', disableUsers: false, + mergePrimaryContacts: false, force: true, docDirectoryPath: '/', }); diff --git a/test/fn/upload-docs.spec.js b/test/fn/upload-docs.spec.js index 2824996b..6bae3ddb 100644 --- a/test/fn/upload-docs.spec.js +++ b/test/fn/upload-docs.spec.js @@ -123,6 +123,7 @@ describe('upload-docs', function() { it('should throw if user denies the warning', async () => { userPrompt.__set__('readline', { keyInYN: () => false }); + await assertDbEmpty(); const actual = uploadDocs.execute(); await expect(actual).to.eventually.be.rejectedWith('User aborted execution.'); }); diff --git a/test/lib/hierarchy-operations/hierarchy-operations.spec.js b/test/lib/hierarchy-operations/hierarchy-operations.spec.js index 402771e9..3d2c7243 100644 --- a/test/lib/hierarchy-operations/hierarchy-operations.spec.js +++ b/test/lib/hierarchy-operations/hierarchy-operations.spec.js @@ -152,7 +152,10 @@ describe('hierarchy-operations', () => { it('move health_center_1 to root', async () => { sinon.spy(pouchDb, 'query'); - await updateHierarchyRules([{ id: 'health_center', parents: [] }]); + await updateHierarchyRules([ + { id: 'health_center', parents: [] }, + { id: 'person', parents: [], person: true }, + ]); await HierarchyOperations(pouchDb).move(['health_center_1'], 'root'); @@ -205,7 +208,10 @@ describe('hierarchy-operations', () => { }); it('move district_1 from root', async () => { - await updateHierarchyRules([{ id: 'district_hospital', parents: ['district_hospital'] }]); + await updateHierarchyRules([ + { id: 'district_hospital', parents: ['district_hospital'] }, + { id: 'person', parents: [], person: true }, + ]); await HierarchyOperations(pouchDb).move(['district_1'], 'district_2'); @@ -261,6 +267,7 @@ describe('hierarchy-operations', () => { await updateHierarchyRules([ { id: 'county', parents: [] }, { id: 'district_hospital', parents: ['county'] }, + { id: 'person', parents: [], person: true }, ]); await HierarchyOperations(pouchDb).move(['district_1'], 'county_1'); @@ -664,6 +671,14 @@ describe('hierarchy-operations', () => { }); describe('merge', () => { + beforeEach(async () => { + await mockReport(pouchDb, { + id: 'district_primary_contact_report', + creatorId: 'district_2_contact', + patientId: 'district_2_contact' + }); + }); + it('merge district_2 into district_1', async () => { // setup await mockReport(pouchDb, { @@ -692,7 +707,7 @@ describe('hierarchy-operations', () => { 'health_center_2', 'health_center_2_contact', 'clinic_2', 'clinic_2_contact', 'patient_2', - 'changing_subject_and_contact', 'changing_contact', 'changing_subject' + 'changing_subject_and_contact', 'changing_contact', 'changing_subject', 'district_primary_contact_report' ]); expect(getWrittenDoc('district_2')).to.deep.eq({ @@ -753,6 +768,16 @@ describe('hierarchy-operations', () => { patient_uuid: 'district_1' } }); + + expect(getWrittenDoc('district_primary_contact_report')).to.deep.eq({ + _id: 'district_primary_contact_report', + form: 'foo', + type: 'data_record', + contact: parentsToLineage('district_2_contact', 'district_1'), + fields: { + patient_uuid: 'district_2_contact' + } + }); }); it('merge two patients', async () => { @@ -791,6 +816,112 @@ describe('hierarchy-operations', () => { } }); }); + + it('--merge-primary-contacts results in merge of primary contacts and reports', async () => { + await HierarchyOperations(pouchDb, { mergePrimaryContacts: true }).merge(['district_2'], 'district_1'); + + expectWrittenDocs([ + 'district_2', 'district_2_contact', + 'health_center_2', 'health_center_2_contact', + 'clinic_2', 'clinic_2_contact', + 'patient_2', + 'district_primary_contact_report' + ]); + + expect(getWrittenDoc('district_2_contact')).to.deep.eq({ + _id: 'district_2_contact', + _deleted: true, + cht_disable_linked_users: false, + }); + + expect(getWrittenDoc('district_primary_contact_report')).to.deep.eq({ + _id: 'district_primary_contact_report', + form: 'foo', + type: 'data_record', + contact: parentsToLineage('district_2_contact', 'district_1'), + fields: { + patient_uuid: 'district_1_contact' + } + }); + }); + + it('--merge-primary-contacts when no primary contact on source', async () => { + await upsert('district_2', { + type: 'district_hospital', + }); + + await HierarchyOperations(pouchDb, { mergePrimaryContacts: true }).merge(['district_2'], 'district_1'); + + expectWrittenDocs([ + 'district_2', 'district_2_contact', + 'health_center_2', 'health_center_2_contact', + 'clinic_2', 'clinic_2_contact', + 'patient_2', + 'district_primary_contact_report' + ]); + + // not deleted + expect(getWrittenDoc('district_2_contact')).to.deep.eq({ + _id: 'district_2_contact', + type: 'person', + parent: parentsToLineage('district_1'), + }); + + // not reassigned + expect(getWrittenDoc('district_primary_contact_report')).to.deep.eq({ + _id: 'district_primary_contact_report', + form: 'foo', + type: 'data_record', + contact: parentsToLineage('district_2_contact', 'district_1'), + fields: { + patient_uuid: 'district_2_contact' + } + }); + }); + + it('--merge-primary-contacts when no primary contact on destination', async () => { + await upsert('district_1', { + type: 'district_hospital', + }); + + await HierarchyOperations(pouchDb, { mergePrimaryContacts: true }).merge(['district_2'], 'district_1'); + + expectWrittenDocs([ + 'district_2', 'district_2_contact', + 'health_center_2', 'health_center_2_contact', + 'clinic_2', 'clinic_2_contact', + 'patient_2', + 'district_primary_contact_report' + ]); + + // not deleted + expect(getWrittenDoc('district_2_contact')).to.deep.eq({ + _id: 'district_2_contact', + type: 'person', + parent: parentsToLineage('district_1'), + }); + + // not reassigned + expect(getWrittenDoc('district_primary_contact_report')).to.deep.eq({ + _id: 'district_primary_contact_report', + form: 'foo', + type: 'data_record', + contact: parentsToLineage('district_2_contact', 'district_1'), + fields: { + patient_uuid: 'district_2_contact' + } + }); + }); + + it('--merge-primary-contacts errors if primary contact is a place', async () => { + await upsert('district_2', { + type: 'district_hospital', + contact: 'health_center_2', + }); + + const actual = HierarchyOperations(pouchDb, { mergePrimaryContacts: true }).merge(['district_2'], 'district_1'); + await expect(actual).to.eventually.be.rejectedWith('"health_center_2" which is of type place'); + }); }); describe('delete', () => { diff --git a/test/lib/hierarchy-operations/lineage-constraints.spec.js b/test/lib/hierarchy-operations/lineage-constraints.spec.js index 85753992..624302be 100644 --- a/test/lib/hierarchy-operations/lineage-constraints.spec.js +++ b/test/lib/hierarchy-operations/lineage-constraints.spec.js @@ -100,8 +100,8 @@ describe('lineage constriants', () => { }); }); - describe('getPrimaryContactViolations', () => { - const assertNoHierarchyErrors = lineageConstraints.__get__('getPrimaryContactViolations'); + describe('assertOnPrimaryContactRemoval', () => { + const assertOnPrimaryContactRemoval = lineageConstraints.__get__('assertOnPrimaryContactRemoval'); describe('on memory pouchdb', async () => { let pouchDb, scenarioCount = 0; @@ -131,21 +131,21 @@ describe('lineage constriants', () => { const contactDoc = await pouchDb.get('clinic_1_contact'); const parentDoc = await pouchDb.get('clinic_2'); - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, parentDoc, [contactDoc]); - expect(doc).to.deep.include({ _id: 'clinic_1_contact' }); + const actual = assertOnPrimaryContactRemoval(pouchDb, contactDoc, parentDoc, [contactDoc]); + expect(actual).to.eventually.be.rejectedWith(`clinic_1_contact) from the hierarchy`); }); it('cannot move clinic_1_contact to root', async () => { const contactDoc = await pouchDb.get('clinic_1_contact'); - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, undefined, [contactDoc]); - expect(doc).to.deep.include({ _id: 'clinic_1_contact' }); + const actual = assertOnPrimaryContactRemoval(pouchDb, contactDoc, undefined, [contactDoc]); + expect(actual).to.eventually.be.rejectedWith(`clinic_1_contact) from the hierarchy`); }); it('can move clinic_1_contact to clinic_1', async () => { const contactDoc = await pouchDb.get('clinic_1_contact'); const parentDoc = await pouchDb.get('clinic_1'); - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, parentDoc, [contactDoc]); + const doc = await assertOnPrimaryContactRemoval(pouchDb, contactDoc, parentDoc, [contactDoc]); expect(doc).to.be.undefined; }); @@ -154,7 +154,7 @@ describe('lineage constriants', () => { const parentDoc = await pouchDb.get('district_1'); const descendants = await Promise.all(['health_center_2_contact', 'clinic_2', 'clinic_2_contact', 'patient_2'].map(id => pouchDb.get(id))); - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, parentDoc, descendants); + const doc = await assertOnPrimaryContactRemoval(pouchDb, contactDoc, parentDoc, descendants); expect(doc).to.be.undefined; }); @@ -167,8 +167,8 @@ describe('lineage constriants', () => { const parentDoc = await pouchDb.get('district_2'); const descendants = await Promise.all(['health_center_1_contact', 'clinic_1', 'clinic_1_contact', 'patient_1'].map(id => pouchDb.get(id))); - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, parentDoc, descendants); - expect(doc).to.deep.include({ _id: 'patient_1' }); + const actual = assertOnPrimaryContactRemoval(pouchDb, contactDoc, parentDoc, descendants); + expect(actual).to.eventually.be.rejectedWith(`patient_1) from the hierarchy`); }); // It is possible that one or more parents will not be found. Since these parents are being removed, do not throw @@ -178,7 +178,7 @@ describe('lineage constriants', () => { contactDoc.parent._id = 'dne'; - const doc = await assertNoHierarchyErrors(pouchDb, contactDoc, parentDoc, [contactDoc]); + const doc = await assertOnPrimaryContactRemoval(pouchDb, contactDoc, parentDoc, [contactDoc]); expect(doc).to.be.undefined; }); });