-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bulk import to create or reassign CHAs with multiple CHUs (#244)
* Unit tests passing for reassignment * Refactor to single "import-cha-users" script * Update place contacts * Round of testing
- Loading branch information
1 parent
eaeba1a
commit 0d3fa05
Showing
20 changed files
with
690 additions
and
252 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
Sub County,Affected CHU,CHA Name,CHA Phone | ||
Digital Payment,Hierarchy,CHA Name,0712344321 | ||
Digital Payment,Kenn,CHA Name,0712344321 | ||
Digital Payment,Replaced Holiday,CHA Name,0712344321 | ||
Narok East,Empaash,CHA Name,0712344321 | ||
Digital Payment,Kenn,Hierarchy Cha,0712344321 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import _ from 'lodash'; | ||
import { ChtApi } from '../../src/lib/cht-api'; | ||
import Place from '../../src/services/place'; | ||
import { MultiplaceUsers, PlaceReassignment } from '../../src/lib/multiplace-users'; | ||
import PrimaryContactDirectory from './primary-contact-directory'; | ||
|
||
export default class ChaReassignment { | ||
private readonly chtApi: ChtApi; | ||
|
||
constructor(chtApi: ChtApi) { | ||
this.chtApi = chtApi; | ||
} | ||
|
||
public async reassignUsersFromPlaces(places: Place[], usernames: PrimaryContactDirectory) { | ||
const reassignmentPromises = places.map(place => this.toReassignments(place, usernames)); | ||
const reassignments = await Promise.all(reassignmentPromises); | ||
const filteredReassignments = _.flatten(reassignments).filter(Boolean) as PlaceReassignment[]; | ||
|
||
await MultiplaceUsers.reassignPlaces(filteredReassignments, this.chtApi); | ||
} | ||
|
||
private async toReassignments(place: Place, usernames: PrimaryContactDirectory): Promise<PlaceReassignment[] | undefined> { | ||
const placeName = place.resolvedHierarchy[0]?.name.formatted; | ||
const placeId = place.resolvedHierarchy[0]?.id; | ||
const userInfos = await usernames.getUsersAtPlaceWithPC(place); | ||
|
||
if (!placeId) { | ||
throw Error('no placeId'); | ||
} | ||
|
||
if (!userInfos?.length) { | ||
throw Error(`Cannot reassign. No username for ${placeName}`); | ||
} | ||
|
||
console.log(`Reassigning: "${placeName}" (${placeId}) to user "${userInfos.map(info => info.username)}"`); | ||
return userInfos | ||
.map(userInfo => ({ | ||
placeId, | ||
toUsername: userInfo.username, | ||
deactivate: false, | ||
})); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import _ from 'lodash'; | ||
import yesno from 'yesno'; | ||
|
||
import { ChtApi } from '../../src/lib/cht-api'; | ||
import Place from '../../src/services/place'; | ||
import { UserPayload } from '../../src/services/user-payload'; | ||
|
||
export type CreatedUser = { | ||
subcounty: string; | ||
name: string; | ||
username: string; | ||
password: string; | ||
}; | ||
|
||
export default async function createMultiplaceUsers(places: Place[], chtApi: ChtApi): Promise<CreatedUser[]> { | ||
const chusByCha = _.groupBy(places, place => groupByKey(place)); | ||
const chaKeys = Object.keys(chusByCha); | ||
if (!chaKeys.length) { | ||
return []; | ||
} | ||
|
||
await promptToCreate(chaKeys); | ||
|
||
const results: CreatedUser[] = []; | ||
for (const chaKey of chaKeys) { | ||
const chus = chusByCha[chaKey]; | ||
const created = await createCha(chus, chtApi); | ||
results.push(created); | ||
} | ||
|
||
console.log(`Created users:`); | ||
console.table(results); | ||
return results; | ||
} | ||
|
||
function groupByKey(place: Place) { | ||
const subcounty = place.resolvedHierarchy[1]?.name.formatted; | ||
const contactName = place.contact.name; | ||
return `${contactName}@${subcounty}`; | ||
} | ||
|
||
async function createCha(chus: Place[], chtApi: ChtApi): Promise<CreatedUser> { | ||
const chaName = chus[0].contact.name; | ||
const chuNames = chus.map(chu => chu.resolvedHierarchy[0]?.name.formatted); | ||
console.log(`Creating CHA: ${chaName} with ${chus.length} CHUs: ${chuNames}`); | ||
|
||
const contactDocId = chus.find(chu => chu.resolvedHierarchy[0]?.contactId)?.resolvedHierarchy[0]?.contactId; | ||
if (!contactDocId) { | ||
throw Error(`Unresolved contact id`); | ||
} | ||
|
||
const chuIds = chus.map(chu => chu.resolvedHierarchy[0]?.id).filter(Boolean) as string[]; | ||
const userPayload = new UserPayload(chus[0], chuIds, contactDocId); | ||
const payload = await userPayload.create(chtApi); | ||
console.log(`Username: ${payload.username} Password: ${payload.password}`); | ||
|
||
return { | ||
subcounty: chus[0]?.resolvedHierarchy[1]?.name.formatted || '?', | ||
name: chaName, | ||
username: payload.username, | ||
password: payload.password, | ||
}; | ||
} | ||
|
||
async function promptToCreate(chaNames: string[]) { | ||
const proceed = await yesno({ | ||
question: `Create ${chaNames.length} new users: ${chaNames}?` | ||
}); | ||
if (!proceed) { | ||
console.error('Exiting...'); | ||
process.exit(-1); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import _ from 'lodash'; | ||
import { Config } from '../../src/config'; | ||
import { ChtApi, UserInfo } from '../../src/lib/cht-api'; | ||
import RemotePlaceCache from '../../src/lib/remote-place-cache'; | ||
import Place from '../../src/services/place'; | ||
|
||
// DirectoryData['subcounty']['cha'] = 'facility_id' | ||
type DirectoryData = { | ||
[key: string]: { | ||
[key: string]: string; | ||
}; | ||
}; | ||
|
||
export default class PrimaryContactDirectory { | ||
private readonly data: DirectoryData; | ||
private readonly chtApi: ChtApi; | ||
|
||
private constructor(chtApi: ChtApi, usernameDictionaryData: DirectoryData) { | ||
this.data = usernameDictionaryData; | ||
this.chtApi = chtApi; | ||
} | ||
|
||
public static async construct(chtApi: ChtApi): Promise<PrimaryContactDirectory> { | ||
const chuType = Config.getContactType('c_community_health_unit'); | ||
const chus = await RemotePlaceCache.getRemotePlaces(chtApi, chuType, chuType.hierarchy[1]); | ||
const chaIds = _.uniq(chus.map(chu => chu.contactId)); | ||
const chaDocs = await chtApi.getDocs(chaIds); | ||
|
||
const data: DirectoryData = {}; | ||
for (const chaDoc of chaDocs) { | ||
const chuId = chaDoc.parent?._id; | ||
const subcountyId = chaDoc.parent?.parent?._id; | ||
if (!data[subcountyId]) { | ||
data[subcountyId] = {}; | ||
} | ||
|
||
data[subcountyId][chaDoc.name] = chuId; | ||
} | ||
|
||
return new PrimaryContactDirectory(chtApi, data); | ||
} | ||
|
||
public primaryContactExists(place: Place): boolean { | ||
const subcountyId = place.resolvedHierarchy[1]?.id; | ||
if (!subcountyId) { | ||
return true; | ||
} | ||
|
||
const chaName = place.contact.name; | ||
return !!this.data[subcountyId]?.[chaName]; | ||
} | ||
|
||
public async getUsersAtPlaceWithPC(place: Place): Promise<UserInfo[] | undefined> { | ||
const subcountyId = place.resolvedHierarchy[1]?.id; | ||
if (!subcountyId) { | ||
throw Error('Place has no subcounty id'); | ||
} | ||
|
||
const chaName = place.contact.name; | ||
const chuId = this.data[subcountyId]?.[chaName]; | ||
if (!chuId) { | ||
const subcountyName = place.resolvedHierarchy[1]?.name.formatted; | ||
throw Error(`Place contact is not known within ${subcountyName}`); | ||
} | ||
|
||
return this.chtApi.getUsersAtPlace(chuId); | ||
} | ||
|
||
public print(): void { | ||
console.log(JSON.stringify(this.data, null, 2)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.