import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response' import { request } from '@/utils/request' import type { ApiResponseOf, PadBindingRequest, PadSessionUploadInput, SendSmsDeliveryRequest, SignDeliveryResponseDto, SignatureUploadResponse, SigningTokenUploadInput, TokenConsumeRequest, TokenConsumeResponse, } from './types' function createIdempotencyKey() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return `clinical-web-${crypto.randomUUID()}` } return `clinical-web-${Date.now()}-${Math.random().toString(36).slice(2)}` } function idempotencyHeaders(value?: string) { return { 'Idempotency-Key': value || createIdempotencyKey() } } export function sendSigningSms( taskId: string, payload: SendSmsDeliveryRequest, idempotencyKey?: string, ): Promise { return request .post>( `/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/send`, payload, { headers: idempotencyHeaders(idempotencyKey) }, ) .then(unwrapNullableApiResponse) } export function resendSigningSms( taskId: string, idempotencyKey?: string, ): Promise { return request .post>( `/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/resend`, undefined, { headers: idempotencyHeaders(idempotencyKey) }, ) .then(unwrapNullableApiResponse) } export function createPadSigningSession( taskId: string, payload: PadBindingRequest = {}, idempotencyKey?: string, ): Promise { return request .post>( `/v1/sign-deliveries/${encodeURIComponent(taskId)}/pad/sessions`, payload, { headers: idempotencyHeaders(idempotencyKey) }, ) .then(unwrapApiResponse) } export function consumeSigningToken( payload: TokenConsumeRequest, idempotencyKey?: string, ): Promise { return request .post>('/v1/sign-deliveries/token/consume', payload, { headers: idempotencyHeaders(idempotencyKey), }) .then(unwrapApiResponse) } function createSignatureFormData(file: File | Blob, metadata: string) { const formData = new FormData() const fileName = typeof File !== 'undefined' && file instanceof File && file.name ? file.name : 'signature.png' formData.append('file', file, fileName) formData.append('metadata', metadata) return formData } export function uploadSigningSignature( payload: SigningTokenUploadInput, ): Promise { const formData = createSignatureFormData(payload.file, payload.metadata) return request .post>( `/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/signature`, formData, { params: { deliveryId: payload.deliveryId, uploadToken: payload.uploadToken, }, headers: idempotencyHeaders(payload.idempotencyKey), }, ) .then(unwrapApiResponse) } /** * PAD 专用兼容路径。服务端要求同时提交 PAD 元数据和真实 PNG,旧的 JSON-only 提交会返回 409。 * 具体的 challenge、文件摘要和 storageKey 由设备适配层生成,页面不应伪造这些值。 */ export function uploadPadSigningSignature( payload: PadSessionUploadInput, ): Promise { if (!payload.pad) { return Promise.reject(new Error('PAD 上传缺少会话元数据')) } const formData = createSignatureFormData(payload.file, payload.metadata) formData.append('challenge', payload.pad.challenge) formData.append('contentType', payload.pad.contentType) formData.append('sizeBytes', String(payload.pad.sizeBytes ?? payload.file.size)) formData.append('sha256', payload.pad.sha256) formData.append('storageKey', payload.pad.storageKey) return request .post>( `/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/pad/sessions/${encodeURIComponent(payload.deliveryId)}/upload`, formData, { params: { uploadToken: payload.uploadToken }, headers: idempotencyHeaders(payload.idempotencyKey), }, ) .then(unwrapApiResponse) } export function uploadSigningSignatureByProof( payload: SigningTokenUploadInput, ): Promise { const formData = createSignatureFormData(payload.file, payload.metadata) return request .post>('/v1/sign-deliveries/token/signature', formData, { params: { taskId: payload.taskId, deliveryId: payload.deliveryId, uploadToken: payload.uploadToken, }, headers: idempotencyHeaders(payload.idempotencyKey), }) .then(unwrapApiResponse) }