Files
baya-monorepo/client/src/services/verification/apis/clientApi.ts
T
2026-07-09 03:05:14 +03:30

124 lines
4.8 KiB
TypeScript

import { clientFetch } from '@/lib/api/client';
import { ApiError } from '@/lib/api/errors';
import { unwrap, type ApiEnvelope } from '@/lib/api/types';
import type {
CredentialDetailsInput,
DocumentConfirmedResult,
IdentityKycInput,
RunStepResult,
TrustBadge,
UploadUrlResult,
VerificationApi,
VerificationDocument,
VerificationStatus,
} from '../types';
const BASE = '/api/v1/nurse_verification';
const NURSES_BASE = '/api/v1/nurses';
/**
* Computes the browser-side integrity hash the confirm endpoint records against the uploaded bytes
* (SHA-256 hex). Runs in the browser only (Web Crypto); the mock skips it.
*/
async function sha256Hex(file: File): Promise<string> {
const buffer = await file.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
/**
* PUTs the file bytes to the signed object-storage URL with upload **progress**. This is a direct PUT
* to `IObjectStorage` (not our API), so it uses XHR — `fetch` can't report upload progress and the
* signed URL needs no bearer. The bearer-carrying JSON calls still go through `clientFetch`.
*/
function putSignedUrl(uploadUrl: string, file: File, onProgress?: (percent: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) onProgress?.(Math.round((event.loaded / event.total) * 100));
};
xhr.onload = () =>
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject(new ApiError(xhr.status, 'Object storage upload failed', 'upload_failed'));
xhr.onerror = () => reject(new ApiError(0, 'Network error during upload', 'network_error'));
xhr.send(file);
});
}
/**
* Real HTTP implementation of the VerificationApi seam (b6 contract). Routes are action-style +
* snake_case; JSON bodies/fields are camelCase; step ids come from the route. Automated-step failures
* come back as `200` with `stepStatus:"failed"` — surfaced, not thrown. Selected once
* USE_VERIFICATION_MOCK is false.
*
* Gap: `submitCredentialDetails` has no nurse-facing b6 endpoint (admin enters the structured fields on
* review) — it is filed in `for-backend.md` and no-ops here; the document uploads it accompanies ARE
* contract-backed (upload_url → PUT → documents). The mock persists the details for the standalone demo.
*/
export const verificationClientApi: VerificationApi = {
getStatus: async () => unwrap(await clientFetch<ApiEnvelope<VerificationStatus>>(BASE)),
start: async () =>
unwrap(await clientFetch<ApiEnvelope<VerificationStatus>>(`${BASE}/submit`, { method: 'POST' })),
runIdentityKyc: async ({ nationalId, livenessCaptured }: IdentityKycInput) =>
unwrap(
await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/identity_kyc/run`, {
method: 'POST',
body: JSON.stringify({ nationalId, livenessPayload: livenessCaptured ? 'captured' : null }),
}),
),
runShahkarMatch: async () =>
unwrap(await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/shahkar_match/run`, { method: 'POST' })),
runBankVerification: async () =>
unwrap(
await clientFetch<ApiEnvelope<RunStepResult>>(`${BASE}/steps/bank_account_verification/run`, {
method: 'POST',
}),
),
uploadStepDocument: async (stepId, file, onProgress): Promise<VerificationDocument> => {
const { objectStorageKey, uploadUrl } = unwrap(
await clientFetch<ApiEnvelope<UploadUrlResult>>(`${BASE}/steps/${stepId}/upload_url`, {
method: 'POST',
body: JSON.stringify({ contentType: file.type, fileName: file.name }),
}),
);
await putSignedUrl(uploadUrl, file, onProgress);
const integrityHash = await sha256Hex(file);
const confirmed = unwrap(
await clientFetch<ApiEnvelope<DocumentConfirmedResult>>(`${BASE}/steps/${stepId}/documents`, {
method: 'POST',
body: JSON.stringify({
objectStorageKey,
integrityHash,
contentType: file.type,
fileSizeBytes: file.size,
originalFileName: file.name,
}),
}),
);
return {
id: confirmed.documentId,
contentType: file.type,
fileSizeBytes: file.size,
originalFileName: file.name,
url: '',
};
},
// No nurse-facing endpoint yet (see gap note above / for-backend.md); the accompanying document
// uploads carry the real signal. Kept as a seam method so the mock can persist details unchanged.
submitCredentialDetails: async (_input: CredentialDetailsInput) => {},
getTrustBadge: async (nurseId) =>
unwrap(await clientFetch<ApiEnvelope<TrustBadge>>(`${NURSES_BASE}/${nurseId}/trust_badge`)),
};