Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { getProductApiErrorMessage } from '@/services/apiErrorLocalization'
|
|
|
|
/**
|
|
* Business-rule / expected client API codes. These are user-facing 4xx
|
|
* outcomes (block, full event, invalid OTP…), not application crashes.
|
|
* Keep INTERNAL_ERROR, SERVICE_UNAVAILABLE, FORBIDDEN, and NOT_FOUND out —
|
|
* those still need Sentry when they leak as unhandled exceptions.
|
|
*/
|
|
export const EXPECTED_PRODUCT_API_CODES = new Set([
|
|
'MESSAGING_BLOCKED',
|
|
'CHAT_CLOSED',
|
|
'NOT_PARTICIPANT',
|
|
'SELF_ACTION_FORBIDDEN',
|
|
'OTP_INVALID',
|
|
'OTP_EXPIRED',
|
|
'OTP_RATE_LIMITED',
|
|
'OTP_ATTEMPTS_EXCEEDED',
|
|
'TERMS_ACCEPTANCE_REQUIRED',
|
|
'EVENT_FULL',
|
|
'EVENT_NOT_BOOKABLE',
|
|
'WAITLIST_NOT_AVAILABLE',
|
|
'ACTIVE_BOOKING_EXISTS',
|
|
'ALREADY_BOOKED',
|
|
'IDENTITY_REQUIRED',
|
|
'HOST_EVENT_LIMIT_REACHED',
|
|
'REVIEW_WINDOW_EXPIRED',
|
|
'HOST_REPLY_WINDOW_EXPIRED',
|
|
'REVIEW_LOCKED',
|
|
'EVENT_NOT_EDITABLE',
|
|
'EVENT_FIELD_LOCKED',
|
|
'PRICE_LOCKED',
|
|
'CAPACITY_BELOW_BOOKED',
|
|
'BOOKING_NOT_CONFIRMED',
|
|
'NOT_CHECKED_IN',
|
|
'ACCOUNT_SUSPENDED',
|
|
'ACCOUNT_DELETED',
|
|
'VALIDATION_FAILED',
|
|
'RATE_LIMITED',
|
|
'PUSH_SUBSCRIPTION_LIMIT_EXCEEDED',
|
|
'EVENT_HAS_ACTIVE_BOOKINGS',
|
|
'UNAUTHORIZED',
|
|
'ACCESS_TOKEN_INVALID',
|
|
'ACCESS_TOKEN_EXPIRED',
|
|
'REFRESH_TOKEN_INVALID',
|
|
'REFRESH_TOKEN_EXPIRED',
|
|
])
|
|
|
|
const EXPECTED_PRODUCT_MESSAGES = new Set(
|
|
[...EXPECTED_PRODUCT_API_CODES].flatMap((code) => {
|
|
const message = getProductApiErrorMessage(code)
|
|
|
|
return message ? [message] : []
|
|
})
|
|
)
|
|
|
|
/** Soft session-expiry signal from axios `handleSessionExpired` — not an app crash. */
|
|
EXPECTED_PRODUCT_MESSAGES.add('نشست منقضی شده است')
|
|
|
|
const readApiCode = (error: object): string | undefined => {
|
|
if ('apiCode' in error && typeof error.apiCode === 'string') return error.apiCode
|
|
if ('code' in error && typeof error.code === 'string') return error.code
|
|
|
|
return undefined
|
|
}
|
|
|
|
export const isExpectedProductMessage = (message: string): boolean => EXPECTED_PRODUCT_MESSAGES.has(message.trim())
|
|
|
|
export const isExpectedProductError = (error: unknown): boolean => {
|
|
if (typeof error === 'string') return isExpectedProductMessage(error)
|
|
if (!error || typeof error !== 'object') return false
|
|
|
|
const apiCode = readApiCode(error)
|
|
|
|
if (apiCode && EXPECTED_PRODUCT_API_CODES.has(apiCode)) return true
|
|
|
|
return 'message' in error && typeof error.message === 'string' && isExpectedProductMessage(error.message)
|
|
}
|
|
|
|
export const shouldDropExpectedProductSentryEvent = (
|
|
event: { exception?: { values?: { value?: string }[] }; message?: string },
|
|
originalException?: unknown
|
|
): boolean => {
|
|
if (isExpectedProductError(originalException)) return true
|
|
|
|
const value = event.exception?.values?.[0]?.value ?? event.message
|
|
|
|
return typeof value === 'string' && isExpectedProductMessage(value)
|
|
}
|