admin/services/identity.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

166 lines
5.0 KiB
TypeScript

import { z } from 'zod'
import {
errorResult,
handleServiceError,
type ServiceCallOptions,
type ServiceResult,
shouldBubbleErrorToParent,
successResult,
} from '@/services/errorHandler'
import { unwrapApiDataUnknown, unwrapApiPayload } from '@/helpers/listResponse'
import { ANALYTICS_EVENTS, trackAnalyticsEventOnce } from '@/lib/analytics'
import { API_ROUTES } from '@/services/config'
import axiosInstance from '@/config/axios'
import { texts } from '@/texts'
export interface MyIdentityVerification {
id: string
status: 'pending' | 'verified' | 'rejected'
nationalCode: string | null
contractVersion?: string | null
contractAcceptedAt?: string | null
contractAcceptanceId?: string | null
reason?: { code: string; label: string } | null
rejectionReason?: string | null
createdAt: string
reviewedAt?: string | null
}
export interface IdentityRejectionReason {
id: number
code: string
label: string
sortOrder: number
}
export interface RequestHostContractOtpPayload {
nationalCode: string
contractAccepted: true
contractVersion: string
}
const identityVerificationSchema = z.object({
id: z.string().min(1),
status: z.enum(['pending', 'verified', 'rejected']),
nationalCode: z.string().nullable().optional(),
contractVersion: z.string().nullable().optional(),
contractAcceptedAt: z.string().nullable().optional(),
contractAcceptanceId: z.string().nullable().optional(),
reason: z.object({ code: z.string(), label: z.string() }).nullable().optional(),
rejectionReason: z.string().nullable().optional(),
createdAt: z.string(),
reviewedAt: z.string().nullable().optional(),
})
const rejectionReasonSchema = z.object({
id: z.number(),
code: z.string(),
label: z.string(),
sortOrder: z.number(),
})
const otpRequestedSchema = z.object({
purpose: z.literal('host_contract'),
expiresIn: z.number(),
contractVersion: z.string(),
})
export const GET_MY_IDENTITY_VERIFICATION = async (options?: ServiceCallOptions): Promise<ServiceResult<MyIdentityVerification | null>> => {
try {
const res = await axiosInstance.get('identity/verifications/me')
const payload = unwrapApiDataUnknown(res.data)
const latest: unknown = Array.isArray(payload) ? payload[0] : payload
if (!latest || typeof latest !== 'object' || !('id' in latest)) return successResult(null)
const parsed = identityVerificationSchema.safeParse(latest)
if (!parsed.success) throw new Error(texts.common.invalidServerResponse)
return successResult({
...parsed.data,
nationalCode: parsed.data.nationalCode ?? null,
})
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const REQUEST_HOST_CONTRACT_OTP = async (
data: RequestHostContractOtpPayload,
options?: ServiceCallOptions
): Promise<ServiceResult<{ purpose: 'host_contract'; expiresIn: number; contractVersion: string }>> => {
try {
const res = await axiosInstance.post(API_ROUTES.IDENTITY.REQUEST_OTP, data)
const parsed = otpRequestedSchema.safeParse(unwrapApiPayload(res.data))
if (!parsed.success) throw new Error(texts.common.invalidServerResponse)
return successResult(parsed.data)
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const CONFIRM_HOST_CONTRACT_OTP = async (
code: string,
options?: ServiceCallOptions
): Promise<ServiceResult<MyIdentityVerification>> => {
try {
const res = await axiosInstance.post(API_ROUTES.IDENTITY.CONFIRM_OTP, { code })
const parsed = identityVerificationSchema.safeParse(unwrapApiPayload(res.data))
if (!parsed.success) throw new Error(texts.common.invalidServerResponse)
trackAnalyticsEventOnce(ANALYTICS_EVENTS.IDENTITY_VERIFICATION_SUBMITTED, parsed.data.id, {
verification_id: parsed.data.id,
verification_status: parsed.data.status,
})
return successResult({
...parsed.data,
nationalCode: parsed.data.nationalCode ?? null,
})
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}
export const GET_IDENTITY_REJECTION_REASONS = async (options?: ServiceCallOptions): Promise<ServiceResult<IdentityRejectionReason[]>> => {
try {
const res = await axiosInstance.get(API_ROUTES.IDENTITY.REJECTION_REASONS)
const payload = unwrapApiDataUnknown(res.data)
const parsed = z.array(rejectionReasonSchema).safeParse(Array.isArray(payload) ? payload : [])
if (!parsed.success) throw new Error(texts.common.invalidServerResponse)
return successResult(parsed.data)
} catch (error) {
const normalizedError = handleServiceError(error, options)
if (shouldBubbleErrorToParent(options)) {
throw normalizedError
}
return errorResult(normalizedError)
}
}