Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
166 lines
5.0 KiB
TypeScript
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)
|
|
}
|
|
}
|