frontend phase 5 & backend phase 12
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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`)),
|
||||
};
|
||||
Reference in New Issue
Block a user