Projo FHIR Documentation
0.1.0 - ci-build
Projo FHIR Documentation - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
Projo must reattach each inbound WhatsApp message to a FHIR conversational identity before it can be answered. This page documents the two symmetric scans the sidecar performs, the identity types they cover, the reverse-CR matrix that surfaces unresolved senders, and the downstream triggered-plan selection that gives new (or returning) patients a CarePlan.
scanSenderIdentities({ phone, bsuid })Runs over IDENTITY_LOOKUP_SENDER_TYPES (default: Patient, RelatedPerson, Practitioner, Organization). Execution model: parallel-and-aggregate — each resource type is queried concurrently and the results are merged into a single identity set.
scanReceiverIdentities(businessPhoneNumber)Runs over IDENTITY_LOOKUP_RECEIVER_TYPES (default: Organization, Practitioner, Patient, RelatedPerson). Execution model: sequential-with-short-circuit — types are tried in order; the first type to yield a match wins and subsequent types are skipped. Receiver resolution is expected to terminate at Organization in the overwhelming majority of cases.
HAPI ships indexed telecom SearchParameters for Patient, Practitioner, RelatedPerson, and Person. Organization has no indexed telecom SearchParameter, so Organization receiver lookups fall back to paginated in-memory matching: _count=500 per page, with the deployment sized for an expected 20–200 tenant Organizations.
| Type | Role | Conversational? |
|---|---|---|
Patient |
Default conversational identity — DEFAULT_IDENTITY_RESOURCE_TYPE |
Yes |
Practitioner |
Alternative conversational identity (e.g. telemedicine line owner) | Yes |
RelatedPerson |
Alternative conversational identity | Yes |
Organization |
Receiver identity only; the tenant whose WABA received the message | No |
Person |
Walked via Person.link for the non-conversational-only reverse-CR reason |
No |
On this deployment, Person and RelatedPerson are not granted to the Device client role used by the sidecar. The sidecar's scanIdentities receives a 403 for those types; the resulting UnauthorizedFhirError is swallowed per type and the scan continues with the remaining types. The default config still lists them so the same code path works on a deployment that does grant them.
When the sidecar cannot reattach an inbound message, it files a reverse CommunicationRequest with category=identity-resolution-required. Projo's identity-resolution pipeline inspects the sender and receiver scan results and selects one of the following reasons:
| Reason | Meaning | Projo response |
|---|---|---|
no-match |
No existing identity matches the sender's phone or BSUID | Create identity (Patient by default) + apply triggered plan (onboarding) |
multiple-matches |
More than one identity matches | Refuse; mark the reverse CR entered-in-error |
non-conversational-only |
Matches exist but none of a conversational type | Walk Person.link; if none, create |
no-active-cr |
Identity matched but no active CommunicationRequest |
Reattach + apply triggered plan (default / re-engagement); skip if an active CarePlan exists |
unresolved-receiver |
Receiver Organization unknown |
Idempotent stub Organization upsert; then create identity + apply plan |
On a cold inbound (a brand-new sender or a no-active-cr reattachment), Projo selects a PlanDefinition to $apply for the patient. The plan-slot extension on the receiver Organization drives selection, with the following precedence on cold inbound:
onboarding (highest)re-engagementdefault (lowest)The trigger-aware selector applyTriggeredPlanIfNeeded only fires when both of the following hold:
Organization, andPatient was just created by this resolution, or (b) the no-active-cr reason is reattaching a returning patient.If neither condition holds, no triggered plan is applied and the inbound is answered against whatever existing context (active CarePlan, etc.) the patient already has.
upsertOrganizationFromInboundBusinessPhone creates a stub Organization from the business phone number that received the unroutable message. The stub carries:
telecom[] = { system: phone, value: <E.164> }wa-phone-number-id extension (resolved from Meta)waba-account-id extension (resolved from Meta)The upsert uses createOrganization unconditionally — no If-None-Exist header — because HAPI does not index Organization.telecom, so a conditional create on telecom would not deduplicate. Idempotency is instead provided by the reverse-CR pipeline: the unresolved-receiver reason is only emitted once per receiver phone, and subsequent inbounds from that receiver find the stub via the receiver scan.
When the resolution outcome is to create a new identity (default type Patient), buildPatient constructs the resource with:
telecom[] = [{ system: phone, use: mobile, value: <E.164> }]identifier[] = [{ system: whatsapp-bsuid, value: <BSUID> }]name[] = HumanName parsed from the WhatsApp profile namemanagingOrganization = the resolved receiver Organizationcommunication[] = [{ language, preferred: true }] (from the inbound's detected language)The new Patient, the applied CarePlan (from the triggered-plan selection), and the now-attached inbound Communication are written together in a single FHIR transaction so the patient never exists in a half-attached state.