backend: Patient Portal over the relay + wallet linking

- services/portal.ts: clinic-scoped portal actions (clinic info, doctors,
  availability, linkWallet, conflict-checked booking, results, downloadable lab
  files) plus handlePortalRequest to dispatch a relayed portal:request.
- relay-client.ts: handle portal:request on the hub and ack the result back down
  the relay (device path identifies the patient by verified wallet number).
- patients: new nullable wallet_number column (+ migration); linking stores it,
  and walletNumberForPatient now resolves via it so pushes work after a portal
  link, not only after a permanent share.
- routes/portal.ts: GET /:clinic/link returns the relay-based pairing descriptor
  (clinic signing key + relay URL) for the QR — no more localhost-baked API URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-08 20:08:36 +03:00
parent 1c5e71eb39
commit dd64d689c8
8 changed files with 5057 additions and 0 deletions
+17
View File
@@ -3,6 +3,7 @@ import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
import { db } from "../db/index.js";
import { organization } from "../db/schema/auth.js";
import { patients } from "../db/schema/patients.js";
import { walletRecordUpdates } from "../db/schema/wallet-updates.js";
import { walletShareRequests } from "../db/schema/wallet-share.js";
import { HttpError } from "../lib/http-error.js";
@@ -70,6 +71,22 @@ export async function walletNumberForPatient(
orgId: string,
fileNumber: string,
): Promise<string | null> {
// Preferred: the wallet number the patient linked from the Patient Portal
// (stored directly on the file). Falls back to a permanent, approved,
// committed share for records imported the older way.
const [linked] = await db
.select({ walletNumber: patients.walletNumber })
.from(patients)
.where(
and(
eq(patients.organizationId, orgId),
eq(patients.fileNumber, fileNumber),
isNotNull(patients.walletNumber),
),
)
.limit(1);
if (linked?.walletNumber) return linked.walletNumber;
const [row] = await db
.select({ walletNumber: walletShareRequests.walletNumber })
.from(walletShareRequests)