Skip to content

CCN beckn referral - #16676

Draft
amjithtitus09 wants to merge 47 commits into
developfrom
amjithtitus09-ccn-beckn-referral
Draft

CCN beckn referral#16676
amjithtitus09 wants to merge 47 commits into
developfrom
amjithtitus09-ccn-beckn-referral

Conversation

@amjithtitus09

@amjithtitus09 amjithtitus09 commented Aug 12, 2026

Copy link
Copy Markdown
Member

DO NOT REVIEW/MERGE

This is used only to expose a branch preview for testing CCN flow

Tagging: @ohcnetwork/care-fe-code-reviewers

Merge Checklist

  • Add specs that demonstrate the bug or test the new feature.
  • Update product documentation.
  • Ensure that UI text is placed in I18n files.
  • Prepare a screenshot or demo video for the changelog entry and attach it to the issue.
  • Request peer reviews.
  • Complete QA on mobile devices.
  • Complete QA on desktop devices.
  • Add or update Playwright tests for related changes

Summary by CodeRabbit

  • New Features

    • Added Beckn-powered consultation and appointment booking for resource requests.
    • Added provider and appointment-slot selection with confirmation dialogs and status updates.
    • Added a facility-level care coordination console for reviewing and approving incoming referrals.
    • Added navigation access to the care coordination console.
    • Added localized text for appointment, referral, and resource-request workflows.
  • Updates

    • Simplified resource-request creation to focus on status, category, and title.
    • Updated the resource request category label to “Diagnostic.”

abhimanyurajeesh and others added 30 commits July 30, 2026 16:32
Entire-Checkpoint: bc6a6d40b35a
… a scale

Replaced 90 arbitrary font sizes (53 of them sub-pixel: 12.5px, 13.5px,
10.5px, 14.5px, 11.5px, 9.5px) with Tailwind scale classes. A single
screen rendered 8-11 distinct sizes with no ratio between them.

Scale is now 10 / xs(12) / sm(14) / base(16) / lg(18) plus three
display sizes. Max visual change is 1.5px.

Entire-Checkpoint: ee2aa8184c79
The patient portal is mobile-first but several primary controls were
well under the 44px tap minimum: 'See all' at 20px (below even the
WCAG 2.2 AA 24px floor), Records/Visits header tabs at 34px, filter
chips at 33px, the patient switcher chip at 42px, and the logo home
link at 28px.

All now min-h-11. Filter chips lose their vertical padding in favour
of the min-height so the pill keeps its proportions.

Entire-Checkpoint: fd291f30c9fd
View Details, Reschedule and Cancel Appointment used Button size="sm"
(h-8, 32px). These are the primary actions on the upcoming appointment
card, so they get a min-h-11 floor. Same for the cancel dialog's
confirm/dismiss pair.

Entire-Checkpoint: 007b8d19f32c
Entire-Checkpoint: c316d7900a98
Entire-Checkpoint: 40edbd0d886e
Entire-Checkpoint: ed0463f3ed2f

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy Review 🔥

71 files, 6,884 added lines, and a description that says DO NOT REVIEW/MERGE. So naturally, here I am. Reviewing it.

Credit where it is grudgingly due: the Beckn parsers are genuinely defensive, the phase state machine is readable, and someone actually wrote comments explaining why rather than restating the code. That is rarer than it should be.

What bothers me, roughly in order of how much:

  1. Unbounded slice polling — a 1.2s interval with no deadline and no awaiting gate. One silent BPP and the browser politely DDoSes your own API forever.
  2. Sticky slice cache — first payload per action wins permanently, so retries render stale catalogs and slots.
  3. Index-aligned offer↔resource pairing with a fallback to resources[0]. In an appointment booking flow. Guessing is not a matching strategy.
  4. A round-trip verification that fabricates its own value when the real one is missing. That is not verification, that is optimism with extra steps.
  5. Assorted smaller sins: unpaginated limit: 100, an i18n key masquerading as a user-facing string, crypto.randomUUID() with no secure-context guard.

Also: .env now points at develop-api. I assume that is deliberate for the preview branch, but do not let it ride to develop.

None of this is unfixable, and for a WIP preview branch it is a lot better than I expected. Which I resent slightly.> Generated by Grumpy PR Reviewer for #16676 · opus50 · 86.5 AIC · ⌖ 2.17 AIC · ⊞ 8.7K


// Stop polling once the awaited callback (matching `ON_*`) or a terminal error
// arrives — it's now the FE's turn (or the flow has failed).
useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slice poller has no off switch. refetchInterval fires every 1.2s forever while ready is false, and unlike the status query it ignores awaiting — so a BPP that never sends the slice payload leaves the browser hammering the API until the tab is closed. Add a max attempt/deadline, or gate it on awaiting like the status query.

Comment thread src/hooks/useBecknTransaction.ts Outdated
const data = sliceQuery.data;
if (data?.ready && sliceAction) {
setSlices((prev) =>
prev[sliceAction] ? prev : { ...prev, [sliceAction]: data },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First slice wins, forever. prev[sliceAction] ? prev : ... means a re-run of the same action (start over, re-select, retry) keeps the stale payload and silently renders yesterday's catalog/slots. If the intent is "never overwrite", it needs to be keyed per attempt, not per action name.

Comment thread src/components/Resource/CcnConsole.tsx Outdated
queryKey: ["ccn-resource-list", facilityId],
queryFn: query(resourceRequestApi.list, {
// Only requests this facility is receiving (assigned to).
queryParams: { limit: 100, assigned_facility: facilityId },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit: 100 with no pagination. Coordination desk gets busy, request 101 vanishes into the void, and nobody knows why. Either paginate or at least surface that the list is truncated.


iterable.forEach((rawOffer, oi) => {
const offer = asRecord(rawOffer);
const resource = asRecord(resources[oi] ?? resources[0]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources[oi] ?? resources[0] — index-aligning offers to resources and then shrugging to the first one is a coin flip. If the arrays ever disagree you will happily book an appointment against the wrong resource, and nothing will complain. Match on an actual id.

toast.success(t("send_otp_success"));
},
onError: (error: unknown) => {
setPhoneError(extractOtpErrorMessage(error, "send_otp_error"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"send_otp_error" is a translation key being passed as a fallback message string, and extractOtpErrorMessage returns raw backend text otherwise. So the user sees either a raw API string or the literal token send_otp_error. Pick one: translate the fallback with t(), or make the whole thing key-based.

const referral = asRecord(attrs?.referral);
return {
contractId: asString(referral?.id) ?? asString(contract?.id),
coordinationId: asString(attrs?.coordinationId) ?? asString(contract?.id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coordinationId: asString(attrs?.coordinationId) ?? asString(contract?.id) — falling back to the contract id defeats the entire stated purpose of this field, which the comment says is to verify the FE-generated coordinationId round-tripped. A verification that manufactures a plausible value when the real one is missing verifies nothing. Leave it undefined.

): string {
const hasMin = min !== null && min !== undefined;
const hasMax = max !== null && max !== undefined;
if (hasMin && hasMax) return `${min} - ${max}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatRangeBounds interpolates raw values, so a min of 0 works but an empty-string min sneaks past the != null check and renders "> ". toNumber already handles "" — reuse that instead of hand-rolling a second, laxer emptiness rule in the same file.

const doSubmit = useCallback(() => {
if (!option || !transactionId) return;
if (isConsultation) {
const newCoordinationId = crypto.randomUUID();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.randomUUID() is only available on secure contexts. Fine in prod, but any (redacted) dev/staging host will throw here mid-flow with no error handling around it. A guarded fallback costs three lines.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 68 out of 71 changed files in this pull request and generated no new comments.

Suppressed comments (6)

.env:12

  • Changing the committed .env default REACT_CARE_API_URL from https://careapi.ohc.network to https://develop-api.ohc.network changes the out-of-the-box backend target for anyone running the repo, and diverges from the documented fallback in README/.example.env. If this is only for a branch preview, consider reverting this change and using .env.local or REACT_CARE_URL_MAP instead.
# Care API URL without the /api prefix
REACT_CARE_API_URL=https://develop-api.ohc.network

src/components/Resource/CcnConsole.tsx:95

  • The list view shows the raw item.status value, while the detail view uses the localized resource_request_status__… key. This makes the sidebar inconsistent and potentially non-localized. Use the same translation key here too.
                        <Badge
                          variant="secondary"
                          className="shrink-0 text-[10px]"
                        >
                          {item.status}
                        </Badge>

src/pages/PublicAppointments/PatientRegistration.tsx:194

  • reason comes from query params and can be undefined; calling reason.trim() will throw and break patient registration/booking. Trim after defaulting to an empty string instead.
    src/Utils/observationRange.ts:88
  • formatRangeBounds treats empty-string bounds as present (because it only checks null/undefined), which can render > / < even though the docstring says it should return an empty string when neither bound is present. This also contradicts the earlier defensive handling that treats "" as missing.
  const hasMin = min !== null && min !== undefined;
  const hasMax = max !== null && max !== undefined;
  if (hasMin && hasMax) return `${min} - ${max}`;
  if (hasMin) return `> ${min}`;
  if (hasMax) return `< ${max}`;

src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:38

  • The logo is an interactive navigation control but is implemented as an <img> with onClick, which is not keyboard-focusable and won’t be announced as a control by assistive tech. Wrap it in a <button> (or a link) with an accessible label.
    src/hooks/useBecknTransaction.ts:167
  • If callApi(becknApi.action, …) throws (network error, 5xx, etc.), awaiting is never cleared (it’s set before the call, and only cleared on certain res.result values). This can leave the UI stuck in a perpetual "busy" state with polling paused (especially for discover, before a transactionId exists). Add a catch to set an error and clear awaiting.

Copilot AI review requested due to automatic review settings August 12, 2026 23:39

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, 71 files, +6919/-2460, labelled "DO NOT REVIEW/MERGE", and here I am anyway. Fine.

Credit where it's grudgingly due: the Beckn layer is genuinely well structured — defensive parsers instead of blind schema trust, a single hook owning the transaction lifecycle, and actual doc comments explaining why. That's more than most of what crosses my desk.

What still bothers me:

  • .env now ships a dev API URL as the committed default. Preview branch or not, that's the kind of thing that survives a rebase and ruins someone's afternoon.
  • Polling has no brakes. The slice query loops every 1.2s until ready, forever, with no terminal/NACK guard or attempt cap. Networks fail; your poller doesn't know that.
  • resume() is half-finished — new transactionId, stale slices, no poller. It will do the wrong thing the first time anyone uses it.
  • crypto.randomUUID() throws in insecure contexts. Hospital LANs are not famous for their TLS discipline.
  • discover object prop defeats useCallback memoization; an infinite-discover loop is prevented only by a ref. That's a trap, not a design.
  • Missing error states on the CCN console query and an unpaginated limit: 100.

None of it is merge-blocking today since this is explicitly a preview branch, but every one of these will bite in production. Commenting, not blocking.

— filed reluctantly, from behind reading glasses> Generated by Grumpy PR Reviewer for #16676 · opus50 · 94.4 AIC · ⌖ 2.14 AIC · ⊞ 8.7K

Comment thread .env

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointing the committed default API URL at develop-api.ohc.network in .env? Congratulations, every fresh clone now silently talks to a dev backend. Even for a preview branch this belongs in a local/preview-only override, not the checked-in default.

const startDiscover = useCallback(() => {
void act("discover", buildDiscoverBody({ serviceType, ...discover }));
}, [act, serviceType, discover]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discover is an object literal prop, so it's a brand-new reference on every parent render — startDiscover never memoizes, and the effect below depends on it. It's only saved from an infinite discover loop by startedRef. Depend on the primitive fields (discover.textSearch, discover.healthServiceType) instead of relying on a ref as a guardrail.

if (!option || !transactionId) return;
if (isConsultation) {
const newCoordinationId = crypto.randomUUID();
setCoordinationId(newCoordinationId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.randomUUID() is undefined in non-secure contexts (plain http on a LAN IP, which hospital deployments love). This will throw mid-submit with no error handling. Use a guarded fallback, or better, let the backend mint the coordination id.

}),
enabled: !!transactionId && !!sliceAction,
refetchInterval: (q) => (q.state.data?.ready ? false : SLICE_POLL_MS),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slice poll has no stop condition other than ready. If the BE never flips ready (NACK, dropped callback, whatever), you poll every 1.2s forever until the user navigates away. Add a NACK/ERROR/terminal guard or an attempt cap.

if (data?.ready && sliceAction) {
setSlices((prev) =>
prev[sliceAction] ? prev : { ...prev, [sliceAction]: data },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prev[sliceAction] ? prev : ... means a slice is cached once and never refreshed. Fine for a one-shot flow, but if the same action fires twice (retry, start-over without reset) the UI silently shows the stale catalog/slots. At minimum key this by transactionId so a new txn can't inherit the old one's slices.

// console can fire `confirm` and poll a transaction it did not discover.
const resume = useCallback((existingTransactionId: string) => {
setTransactionId(existingTransactionId);
setError(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resume sets a new transactionId but leaves slices from the previous transaction untouched, and doesn't set awaiting, so the status poller never starts. Resuming into a stale catalog with no polling is a fun bug to debug at 2am. Clear slices here.

queryFn: query(resourceRequestApi.list, {
// Only requests this facility is receiving (assigned to).
queryParams: { limit: 100, assigned_facility: facilityId },
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit: 100 with no pagination and no error state. What happens on request 101, or when this query fails? The user stares at an empty sidebar and assumes there's nothing to do. Add pagination (or at least a "showing first 100" hint) and handle isError.

<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{slots.map((slot, index) => {
const key = slot.id ?? String(index);
const active = value === key;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slot.id ?? String(index) as the React key and the selection identity: if the BPP returns slots without ids, the selected slot silently shifts when the list re-orders. Also, these are semantically radio buttons — plain <button>s give screen readers nothing about selected state. Add aria-pressed/role="radio".

Comment thread src/components/Resource/CcnConsole.tsx Outdated
import resourceRequestApi from "@/types/resourceRequest/resourceRequestApi";

// Health service types the coordinator can book an appointment for.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A dangling comment with nothing under it. Either it documents HEALTH_SERVICE_TYPES (which lives in another file) or it's leftover — delete it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 68 out of 71 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/components/Resource/CcnConsole.tsx:95

  • The request status in the list view is rendered as the raw enum value (item.status), but the detail view correctly uses the i18n key resource_request_status__*. This makes the list inconsistent and potentially non-localized.
                        <Badge
                          variant="secondary"
                          className="shrink-0 text-[10px]"
                        >
                          {item.status}
                        </Badge>

src/components/Resource/beckn/SlotPicker.tsx:38

  • These slot buttons behave like a single-select control but don’t expose state to assistive tech. Add aria-pressed (or aria-selected with appropriate roles) so screen readers can announce which slot is currently selected.
          <button
            key={key}
            type="button"
            disabled={disabled}
            onClick={() => onChange(slot, key)}
            className={[

.env:12

  • Committing a different default REACT_CARE_API_URL in the tracked .env changes the out-of-the-box backend target for everyone pulling this branch, which can be surprising and impact local dev/CI that relies on the previous default.
REACT_CARE_API_URL=https://develop-api.ohc.network

src/pages/PublicAppointments/PatientRegistration.tsx:194

  • reason comes from useQueryParams() and can be undefined; calling reason.trim() will throw at runtime. Also, trim() always returns a string so ?? "" is redundant.
    src/components/Patient/AppointmentTokenPass.tsx:57
  • This label uses t(appointment.resource_type, { count: 1 }), which doesn’t match the established translation key pattern used elsewhere for schedulable resources (schedulable_resource__${resource_type}), e.g. src/pages/Appointments/AppointmentDetail.tsx:731. Using a different key risks missing translations.
        <Field label={t(appointment.resource_type, { count: 1 })}>

src/hooks/useBecknTransaction.ts:69

  • This introduces a fairly complex async polling workflow (Beckn transaction orchestration) without any accompanying automated tests. Given the number of states (awaiting, slice readiness, terminal error statuses), adding Playwright coverage (or at least unit tests around the hook’s state transitions) would help prevent regressions.

Select was carrying appointmentWindowStart/End on contractAttributes. Per the
network's reference payload the requested window belongs in performance[],
alongside the health service type, and the contract keeps only the service
type itself.

This is also the shape select gets back: on_select answers with the concrete
bookable slots in performance[], which extractSlots already reads. Request and
response now speak the same structure.

Adds the HealthPerformance JSON-LD context to the select schemaContext -- onyx
validates every *Attributes object and NACKs without it -- and splits the
per-flow bodies into early returns, matching buildConfirmBody, rather than
running a third parallel ternary through a shared literal.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 12, 2026 23:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 68 out of 71 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/components/Resource/CcnConsole.tsx:94

  • item.status is rendered as a raw enum value in the list, while the detail view correctly uses the localized resource_request_status__… key. This makes the UI inconsistent and bypasses i18n.
                        <Badge
                          variant="secondary"
                          className="shrink-0 text-[10px]"
                        >
                          {item.status}

src/pages/PublicAppointments/PatientRegistration.tsx:194

  • reason comes from query params and can be undefined; calling trim() will throw and break registration/booking when no reason is present. Use optional chaining (or default to an empty string) before trimming.
    src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:38
  • The logo is rendered as a clickable , which is not keyboard-accessible and doesn’t convey button semantics to assistive tech. Wrap it in a (or a link) and move the click handler there.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grumpy Review 🔥

71 files, ~7k added lines, marked WIP and DO NOT MERGE. Fine — I reviewed the changed lines anyway, because that is apparently my life now.

The credit where it is due: the doc comments on useBecknTransaction and BecknFlow actually explain why, not just what. observationRange.ts defensively coerces API strings to numbers instead of trusting the type declaration. Someone here has been burned before. Good.

The parts that will page someone at 3am:

  1. .env now points the default build at develop-api.ohc.network. Preview branch or not, that is a merge accident waiting to happen.
  2. Both Beckn pollers run unbounded. No timeout, no attempt cap. A BPP that never calls back = infinite request loop plus a permanent spinner with no escape hatch, in a referral booking flow.
  3. crypto.randomUUID() with no fallback, called mid-transaction.
  4. CcnConsole fetches limit: 100 with no pagination — referral 101 just does not exist as far as the coordinator is concerned.
  5. usePatientOtpLogin leaks a raw i18n key into user-facing error text.
  6. PatientUserProvider splits auth routing across two effects and a render-time null. Redirect loops love that pattern.

Also noted, and not commented on individually: zero tests for a 7k-line feature touching authentication and clinical referral flows. The merge checklist boxes are all unticked, so I assume you already know.

Non-blocking, since you told me not to review it. Fix the polling and the .env before this goes anywhere near develop.> Generated by Grumpy PR Reviewer for #16676 · opus50 · 103.2 AIC · ⌖ 2.23 AIC · ⊞ 8.7K

Comment thread .env

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committing REACT_CARE_API_URL=(developapi.ohc.network/redacted) to the checked-in .env points every default build at the develop API. Even for a preview branch, this is exactly the kind of change that survives a careless merge and quietly repoints production tooling. Use a preview-specific env override instead.

const startDiscover = useCallback(() => {
void act("discover", buildDiscoverBody({ serviceType, ...discover }));
}, [act, serviceType, discover]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discover is an object prop, so this useCallback dep is a fresh reference on every parent render. startDiscover therefore changes every render, which re-runs the autoStart effect below — the only thing saving you from a request storm is the startedRef guard. Depend on the primitive fields (discover.textSearch, discover.healthServiceType) instead of the object.

if (!option || !transactionId) return;
if (isConsultation) {
const newCoordinationId = crypto.randomUUID();
setCoordinationId(newCoordinationId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.randomUUID() is only available in secure contexts. Any non-HTTPS deployment (or an older Safari) throws right here, in the middle of placing a referral, with no fallback and no try/catch. Guard it or generate the id server-side.

}),
enabled: !!transactionId,
refetchInterval: awaiting ? POLL_MS : false,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling every 1.5s with no timeout and no attempt cap. If the BPP never sends the on_* callback, this hammers the status endpoint forever while the user stares at a spinner. Add a max-attempts / deadline that flips the flow into the error state.

}),
enabled: !!transactionId && !!sliceAction,
refetchInterval: (q) => (q.state.data?.ready ? false : SLICE_POLL_MS),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same unbounded-poll problem here, plus refetchInterval keeps firing whenever ready is falsy — including when the response is malformed. A never-ready slice means an infinite 1.2s request loop for the lifetime of the page.

queryFn: query(resourceRequestApi.list, {
// Only requests this facility is receiving (assigned to).
queryParams: { limit: 100, assigned_facility: facilityId },
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit: 100 with no pagination and no status filter. The moment a busy facility has 101 incoming referrals, request 101 silently vanishes from the coordinator's console. Silent data loss in a clinical queue is not a nitpick.

}
return response;
},
onSuccess: (response: LoginByOtpResponse) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPhoneError(extractOtpErrorMessage(error, "send_otp_error")) passes a raw i18n key as the fallback, so on an unrecognised error shape the user gets the literal string send_otp_error in the UI. It should be t("send_otp_error"). Also: this happily renders backend-supplied strings straight into the form — fine for text, but do not let that habit spread.

enabled: !!tokenData?.token,
});

const patients = useMemo(() => userData?.results ?? [], [userData]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two navigation effects plus a render-time return null when !tokenData: the redirect effect never runs on the first render path that matters, and the second effect can fire a select-profile redirect on a transient empty patients list. Consolidate the auth/profile routing into one guarded effect — split navigation logic is how redirect loops are born.

// than the raw status — means the gaps between submit and callback show a
// loader instead of a stale interactive state.
const busy = flow.acting || !!flow.awaiting;
const showPicker = !busy && phase === "discovered";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

busy collapses "POST in flight" and "awaiting callback" into one boolean that hides the entire UI behind a spinner. With no polling timeout upstream, a dropped callback leaves the user with a permanent spinner and no way out — not even the Start Over button, since it is rendered only in the error branch.

carefe-bot and others added 2 commits August 13, 2026 05:40
Reverts 228c53a, which carried the window on CatalogOption. The option is
a flat descriptor of what the user picked; the window is now read back out
of the on_discover slice on demand, keyed by the option's catalog-offer
index, so nothing about the selection has to be memoised for it.

The format was also wrong. The BPP publishes availabilitySchedule times as
bare times of day ("22:00"), and the old helper only appended the offset,
producing "22:00+05:30" — not a timestamp, and not parseable as one. They
are now stamped with today's date in IST to give
"2026-07-02T10:00:00+05:30", per the network's reference payload. An
overnight window rolls its end to the following day, values that already
carry a date keep it, and anything unrecognised passes through untouched.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Entire-Checkpoint: 25765807dacf
Copilot AI review requested due to automatic review settings August 13, 2026 00:18
…ork/care_fe into amjithtitus09-ccn-beckn-referral

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 69 out of 72 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/pages/PublicAppointments/PatientRegistration.tsx:194

  • reason comes from query params and can be undefined; calling reason.trim() will throw at runtime and block booking flow after patient creation. Use optional chaining (or default to an empty string) before trimming.
    src/pages/PublicAppointments/Success.tsx:96
  • navigator.clipboard.writeText can throw (e.g. permission denied / insecure context), which would result in an unhandled rejection and no user feedback. Wrap clipboard copy in try/catch and surface a fallback toast on failure.
    src/pages/Encounters/tabs/overview/summary-panel-actions.tab.tsx:61
  • This action can navigate to a URL containing "undefined" if selectedEncounter is temporarily null/undefined (e.g. while loading), because the template string interpolates optional-chained values. Guard the click handler so it no-ops until the encounter is available.
    src/Utils/observationRange.ts:89
  • formatRangeBounds treats empty-string bounds as present, so the UI can render "> " / "< " / " - " when the backend sends "" for missing min/max (which this module already anticipates elsewhere). Normalize "" to undefined before deciding which format to render.
  const hasMin = min !== null && min !== undefined;
  const hasMax = max !== null && max !== undefined;
  if (hasMin && hasMax) return `${min} - ${max}`;
  if (hasMin) return `> ${min}`;
  if (hasMax) return `< ${max}`;
  return "";

Copilot AI review requested due to automatic review settings August 13, 2026 02:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 69 out of 72 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

src/components/Resource/CcnConsole.tsx:161

  • The request status badge renders the raw enum value (item.status) instead of using the existing i18n mapping used elsewhere in this file (resource_request_status__*). This will show untranslated values in the UI.
                          <Badge
                            variant="secondary"
                            className="shrink-0 text-[10px]"
                          >
                            {item.status}
                          </Badge>

src/components/Resource/CcnConsole.tsx:86

  • The query that is documented as “everything assigned to this facility” is filtering by origin_facility, which will not return the set of requests routed/assigned here (and makes the incoming/outgoing split incorrect).
  // One fetch for both tabs — everything assigned to this facility.
  const { data: list, isLoading: listLoading } = useQuery({
    queryKey: ["ccn-resource-list", facilityId],
    queryFn: query(resourceRequestApi.list, {
      queryParams: { limit: 100, origin_facility: facilityId },
    }),
  });

src/Utils/observationRange.ts:88

  • formatRangeBounds treats empty-string bounds as present, but earlier in this module toNumber documents that the API can return bounds as strings (including ""). With min === "" this currently renders "> ", which is incorrect.
  const hasMin = min !== null && min !== undefined;
  const hasMax = max !== null && max !== undefined;
  if (hasMin && hasMax) return `${min} - ${max}`;
  if (hasMin) return `> ${min}`;
  if (hasMax) return `< ${max}`;

src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:38

  • The logo is an <img> with an onClick, which is not keyboard-accessible and lacks proper interactive semantics. Use a <button> or a <Link> so keyboard and assistive tech users can activate it.
    src/Routers/PatientRouter.tsx:27
  • This PR introduces/changes substantial patient-OTP portal routing and booking flows, but also deletes an existing Playwright spec (tests/facility/patient/patientDetails/request/requestCreate.spec.ts) without a replacement. This reduces E2E coverage for critical patient/resource-request workflows; add/restore Playwright specs covering the new patient login + booking path and CCN referral flow.
/** Routes available to a patient signed in with an OTP session. */
const DashboardRoutes = {
  "/nearby_facilities": () => <BookFacility />,
  "/facility/:facilityId/appointments/:appointmentId/success": ({

createAppointment({
patient: data.id,
note: reason ?? "",
note: reason.trim() ?? "",
Entire-Checkpoint: e83f3cbb144c
Copilot AI review requested due to automatic review settings August 13, 2026 06:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 70 out of 73 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/pages/PublicAppointments/PatientRegistration.tsx:194

  • reason can be undefined (query param absent), so calling reason.trim() can throw at runtime. Use optional chaining and fall back to an empty note.
    src/pages/PublicAppointments/auth/PatientAuthLayout.tsx:37
  • Clickable <img> isn’t keyboard-accessible, so the “go home” affordance can’t be used by keyboard/screen-reader users. Wrap the logo in a real <button> (or <Link>) and keep the image purely presentational inside it.
    src/pages/PublicAppointments/Success.tsx:95
  • navigator.clipboard.writeText can reject (no permission / insecure context), which would surface as an unhandled rejection. Wrap it in try/catch and show a failure toast.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusts glasses, sighs.

Right. 73 files, 8.5k added lines, marked DO NOT REVIEW — so of course I get handed it anyway. Credit where it is due: the Beckn parsers are genuinely defensive, the useBecknTransaction state machine is documented better than most production code I have suffered through, and the epoch guard against stale in-flight responses shows someone actually thought about race conditions. I am mildly impressed. Do not let it go to your head.

The things that actually matter:

  1. Unbounded polling — both the status and slice queries poll forever with no cap. A BPP that never returns on_select means an infinite request loop. This is the top issue.
  2. .env points at develop-api — do not let that near develop.
  3. Silent data losslimit: 100 with client-side tab splitting in a referral console. Requests past 100 vanish.
  4. resources[oi] ?? resources[0] — guessing at offer/resource pairing means potentially booking the wrong resource.
  5. crypto.randomUUID() — throws on non-secure origins, unguarded.
  6. coordinationRef={undefined} — the referral link the whole flow exists for is not wired.

Also, ResourceForm just deleted reason, emergency, contacts, assigned facility and priority. I assume that is deliberate for the CCN flow, but that is a lot of data that quietly stopped being collected.

One more that did not fit in my comment budget: usePatientOtpLogin.ts pipes the raw backend error message straight into the UI on an unauthenticated login endpoint. Map it to i18n keys.

Non-blocking, since you told me not to review it. Fix the polling before this is real.> Generated by Grumpy PR Reviewer for #16676 · opus50 · 121.7 AIC · ⌖ 2.16 AIC · ⊞ 8.7K

Comment thread .env

# Care API URL without the /api prefix
REACT_CARE_API_URL=https://careapi.ohc.network
REACT_CARE_API_URL=https://develop-api.ohc.network

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committing develop-api.ohc.network as the default API URL in .env? That is a shared default for everyone, not your preview branch scratchpad. Use a .env.local or a preview-specific env var before this ever gets near develop.

pathParams: { transactionId: transactionId as string },
}),
enabled: !!transactionId,
refetchInterval: awaiting ? POLL_MS : false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we poll every 1.5s forever if the callback never lands? No max attempts, no timeout, no backoff. One BPP that silently drops on_select and this tab hammers the BE until the user gives up and closes it. Add an attempt cap that flips to an error state.

queryParams: { action: sliceAction as string },
}),
enabled: !!transactionId && !!sliceAction,
refetchInterval: (q) => (q.state.data?.ready ? false : SLICE_POLL_MS),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same unbounded-poll problem, second verse: a slice that never becomes ready refetches every 1.2s until the heat death of the component. Bound it.

now: Date = new Date(),
): AvailabilityWindow {
if (!option) return {};
const [catalogIndex, offerIndex] = option.key.split("-").map(Number);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding a composite key as "0-1" and then reverse-parsing it with split("-") is exactly the kind of stringly-typed cleverness that bites later. Carry {catalogIndex, offerIndex} on CatalogOption and skip the parsing round-trip entirely.

const IST_OFFSET_MINUTES = 330;

/** `YYYY-MM-DD` for "today" in IST, whatever zone the browser is in. */
function istToday(now: Date): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

istToday adds 330 minutes then calls .toISOString() — that only works because you re-read the UTC date off a deliberately-wrong instant. It works, but it is a landmine for the next reader. dayjs is already a dependency and does .tz("Asia/Kolkata") without the hand-rolled arithmetic.


iterable.forEach((rawOffer, oi) => {
const offer = asRecord(rawOffer);
const resource = asRecord(resources[oi] ?? resources[0]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources[oi] ?? resources[0] — silently pairing offer N with resource 0 when lengths disagree means we can send resourceIds for a resource the offer has nothing to do with. Booking the wrong resource is not a defect I want to debug in production; skip or flag the mismatch instead of guessing.

const { data: list, isLoading: listLoading } = useQuery({
queryKey: ["ccn-resource-list", facilityId],
queryFn: query(resourceRequestApi.list, {
queryParams: { limit: 100, origin_facility: facilityId },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit: 100 with no pagination, then filtered client-side into two tabs. Facility 101 requests and the coordinator quietly never sees them. Either paginate or filter server-side — an invisible truncation in a referral console is a patient-safety issue, not a UI nit.

} as const;

/** Map a Care resource category to the Beckn `healthServiceType` code. */
export function healthServiceTypeForCategory(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A regex over a free-text category to decide LAB_TEST vs PHYSICAL_CONSULTATION? ResourceRequestCategory is an enum sitting right there. Any new category containing "lab" silently routes to diagnostics. Map the enum explicitly.

const doSubmit = useCallback(() => {
if (!option || !transactionId) return;
if (isConsultation) {
const newCoordinationId = crypto.randomUUID();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto.randomUUID() is undefined on non-secure origins (plain http staging/LAN deployments). This throws and kills the whole submit path with no error handling. Guard it or use a uuid helper.

// coordinationRef links the booking to the originating referral. The RR
// record does not currently expose the Beckn coordinationId; wire it
// here once the BE surfaces it.
coordinationRef={undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coordinationRef={undefined} with a TODO comment — so the booking is never actually linked back to the referral it came from. That is the entire point of the coordination flow. Ship it wired, or this console books orphan appointments.

@github-actions github-actions Bot added the Merge Conflict pull requests with merge conflict label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

Conflicts have been detected against the base branch. Please merge the base branch into your branch.
cc: @amjithtitus09

See: https://docs.ohc.network/docs/contributing#how-to-resolve-merge-conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Merge Conflict pull requests with merge conflict Type Changes Contains changes in typescript types Work in Progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants