450 lines
16 KiB
TypeScript
450 lines
16 KiB
TypeScript
import {
|
|
reservationTaskAudits,
|
|
reservationTaskDetails,
|
|
} from '@/fixtures/reservation'
|
|
import { getReservationHotelId } from '@/config/reservationConfig'
|
|
import { getJson, sendJson } from '@/services/httpClient'
|
|
import type {
|
|
ManualInvoiceGenerationRequest,
|
|
ManualInvoiceGenerationResult,
|
|
} from '@/types/manualInvoice'
|
|
import type {
|
|
ReservationManualReviewConversionRequest,
|
|
ReservationManualReviewConversionResult,
|
|
ReservationManualReviewResolutionRequest,
|
|
ReservationManualReviewResolutionResult,
|
|
ReservationOperaOperationResult,
|
|
ReservationOperaSimulationRequest,
|
|
ReservationOrderDetailResult,
|
|
ReservationOrderListFilters,
|
|
ReservationOrderListResult,
|
|
ReservationRecord,
|
|
ReservationTaskAuditListResult,
|
|
ReservationTaskDetailResult,
|
|
ReservationTaskListFilters,
|
|
ReservationTaskListResult,
|
|
ReservationTaskPayloadMutationRequest,
|
|
ReservationTaskPayloadMutationResult,
|
|
ReservationV4CardConfirmRequest,
|
|
ReservationV4CatalogLookupFilters,
|
|
ReservationV4CatalogLookupResult,
|
|
ReservationV4OrderTaskDetailResult,
|
|
ReservationV4OrderTaskListFilters,
|
|
ReservationV4OrderTaskListResult,
|
|
ReservationV4ReviewResolutionRequest,
|
|
ReservationV4SourceNotificationAckRequest,
|
|
ReservationV4SourceNotificationDetailResult,
|
|
ReservationV4WorkbenchFilters,
|
|
ReservationV4WorkbenchListResult,
|
|
SourceMessageConversationResult,
|
|
} from '@/types/reservation'
|
|
|
|
const useReservationFixtures = import.meta.env.VITE_RESERVATION_USE_FIXTURES === 'true'
|
|
|
|
export async function fetchReservationOrders(
|
|
filters: ReservationOrderListFilters = {},
|
|
): Promise<ReservationOrderListResult> {
|
|
return getJson<ReservationOrderListResult>(withQuery('/api/reservation/orders', withReservationHotel(filters)))
|
|
}
|
|
|
|
export async function fetchReservationTaskList(
|
|
filters: ReservationTaskListFilters = {},
|
|
): Promise<ReservationTaskListResult> {
|
|
return getJson<ReservationTaskListResult>(withQuery('/api/reservation/tasks', withReservationHotel(filters)))
|
|
}
|
|
|
|
export async function fetchReservationV4WorkbenchItems(
|
|
filters: ReservationV4WorkbenchFilters = {},
|
|
): Promise<ReservationV4WorkbenchListResult> {
|
|
return getJson<ReservationV4WorkbenchListResult>(
|
|
withQuery('/api/reservation/workbench-items', withReservationHotel(filters)),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationV4OrderTasks(
|
|
filters: ReservationV4OrderTaskListFilters = {},
|
|
): Promise<ReservationV4OrderTaskListResult> {
|
|
return getJson<ReservationV4OrderTaskListResult>(
|
|
withQuery('/api/reservation/order-tasks', withReservationHotel(filters)),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationV4AccountLookups(
|
|
filters: ReservationV4CatalogLookupFilters = {},
|
|
): Promise<ReservationV4CatalogLookupResult> {
|
|
return getJson<ReservationV4CatalogLookupResult>(
|
|
withQuery('/api/reservation/lookups/accounts', withReservationHotel(filters)),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationV4RoomTypeLookups(
|
|
filters: ReservationV4CatalogLookupFilters = {},
|
|
): Promise<ReservationV4CatalogLookupResult> {
|
|
return getJson<ReservationV4CatalogLookupResult>(
|
|
withQuery('/api/reservation/lookups/room-types', withReservationHotel(filters)),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationV4RateCodeLookups(
|
|
filters: ReservationV4CatalogLookupFilters = {},
|
|
): Promise<ReservationV4CatalogLookupResult> {
|
|
return getJson<ReservationV4CatalogLookupResult>(
|
|
withQuery('/api/reservation/lookups/rate-codes', withReservationHotel(filters)),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationOrderDetail(orderId: string): Promise<ReservationOrderDetailResult> {
|
|
return getJson<ReservationOrderDetailResult>(
|
|
withQuery(`/api/reservation/orders/${encodeURIComponent(orderId)}`, {
|
|
hotel_id: getReservationHotelId(),
|
|
include_tasks: true,
|
|
include_source_summary: true,
|
|
}),
|
|
)
|
|
}
|
|
|
|
export async function fetchSourceMessageConversation(
|
|
sourceMessageId: string,
|
|
): Promise<SourceMessageConversationResult> {
|
|
return getJson<SourceMessageConversationResult>(
|
|
`/api/source-messages/${encodeURIComponent(sourceMessageId)}/conversation`,
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureTaskDetail(taskId)
|
|
}
|
|
return getJson<ReservationTaskDetailResult>(`/api/reservation/tasks/${taskId}`)
|
|
}
|
|
|
|
export async function fetchReservationV4OrderTaskDetail(
|
|
orderTaskId: string,
|
|
): Promise<ReservationV4OrderTaskDetailResult> {
|
|
return getJson<ReservationV4OrderTaskDetailResult>(
|
|
withQuery(`/api/reservation/order-tasks/${encodeURIComponent(orderTaskId)}`, {
|
|
hotel_id: getReservationHotelId(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationV4SourceNotificationDetail(
|
|
notificationId: string,
|
|
): Promise<ReservationV4SourceNotificationDetailResult> {
|
|
return getJson<ReservationV4SourceNotificationDetailResult>(
|
|
withQuery(`/api/reservation/source-notifications/${encodeURIComponent(notificationId)}`, {
|
|
hotel_id: getReservationHotelId(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
export async function confirmReservationV4OrderTaskCard(
|
|
orderTaskId: string,
|
|
cardId: string,
|
|
request: ReservationV4CardConfirmRequest,
|
|
): Promise<ReservationV4OrderTaskDetailResult> {
|
|
return sendJson<ReservationV4OrderTaskDetailResult>(
|
|
`/api/reservation/order-tasks/${encodeURIComponent(orderTaskId)}/cards/${encodeURIComponent(cardId)}/confirm`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function resolveReservationV4OrderTaskCardReview(
|
|
orderTaskId: string,
|
|
cardId: string,
|
|
request: ReservationV4ReviewResolutionRequest,
|
|
): Promise<ReservationV4OrderTaskDetailResult> {
|
|
return sendJson<ReservationV4OrderTaskDetailResult>(
|
|
`/api/reservation/order-tasks/${encodeURIComponent(orderTaskId)}/cards/${encodeURIComponent(cardId)}/review-resolution`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function ackReservationV4SourceNotification(
|
|
notificationId: string,
|
|
request: ReservationV4SourceNotificationAckRequest,
|
|
): Promise<ReservationV4SourceNotificationDetailResult> {
|
|
return sendJson<ReservationV4SourceNotificationDetailResult>(
|
|
`/api/reservation/source-notifications/${encodeURIComponent(notificationId)}/ack`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function saveReservationTaskDraft(
|
|
taskId: string,
|
|
request: ReservationTaskPayloadMutationRequest,
|
|
): Promise<ReservationTaskPayloadMutationResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureTaskMutation(taskId, request, false)
|
|
}
|
|
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/draft`, 'PUT', request)
|
|
}
|
|
|
|
export async function confirmReservationTask(
|
|
taskId: string,
|
|
request: ReservationTaskPayloadMutationRequest,
|
|
): Promise<ReservationTaskPayloadMutationResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureTaskMutation(taskId, request, true)
|
|
}
|
|
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/confirm`, 'POST', request)
|
|
}
|
|
|
|
export async function convertReservationManualReviewTask(
|
|
taskId: string,
|
|
request: ReservationManualReviewConversionRequest,
|
|
): Promise<ReservationManualReviewConversionResult> {
|
|
return sendJson<ReservationManualReviewConversionResult>(
|
|
`/api/reservation/tasks/${encodeURIComponent(taskId)}/manual-review-conversions`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function resolveReservationManualReviewTask(
|
|
taskId: string,
|
|
request: ReservationManualReviewResolutionRequest,
|
|
): Promise<ReservationManualReviewResolutionResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureManualReviewResolution(taskId, request)
|
|
}
|
|
return sendJson<ReservationManualReviewResolutionResult>(
|
|
`/api/reservation/tasks/${encodeURIComponent(taskId)}/manual-review-resolutions`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function executeReservationOperaOperation(
|
|
taskId: string,
|
|
operationId: string,
|
|
request: ReservationOperaSimulationRequest,
|
|
): Promise<ReservationOperaOperationResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureOperaOperation(taskId, operationId, request, 'execute')
|
|
}
|
|
return sendJson<ReservationOperaOperationResult>(
|
|
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/execute`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function retryReservationOperaOperation(
|
|
taskId: string,
|
|
operationId: string,
|
|
request: ReservationOperaSimulationRequest,
|
|
): Promise<ReservationOperaOperationResult> {
|
|
if (useReservationFixtures) {
|
|
return fixtureOperaOperation(taskId, operationId, request, 'retry')
|
|
}
|
|
return sendJson<ReservationOperaOperationResult>(
|
|
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/retry`,
|
|
'POST',
|
|
request,
|
|
)
|
|
}
|
|
|
|
export async function fetchReservationTaskAudits(taskId: string): Promise<ReservationTaskAuditListResult> {
|
|
if (useReservationFixtures) {
|
|
return Promise.resolve(
|
|
reservationTaskAudits.find((audit) => audit.task_id === taskId) ?? {
|
|
task_id: taskId,
|
|
items: [],
|
|
},
|
|
)
|
|
}
|
|
return getJson<ReservationTaskAuditListResult>(`/api/reservation/tasks/${taskId}/audits`)
|
|
}
|
|
|
|
export async function generateManualReservationInvoice(
|
|
request: ManualInvoiceGenerationRequest,
|
|
): Promise<ManualInvoiceGenerationResult> {
|
|
return sendJson<ManualInvoiceGenerationResult>('/api/reservation/invoices/manual-generations', 'POST', request)
|
|
}
|
|
|
|
function withQuery(path: string, params: object): string {
|
|
const searchParams = new URLSearchParams()
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (
|
|
value === undefined ||
|
|
value === null ||
|
|
value === '' ||
|
|
!['string', 'number', 'boolean'].includes(typeof value)
|
|
) {
|
|
return
|
|
}
|
|
searchParams.set(key, String(value))
|
|
})
|
|
const query = searchParams.toString()
|
|
return query ? `${path}?${query}` : path
|
|
}
|
|
|
|
function withReservationHotel<T extends { hotel_id?: string }>(filters: T): T {
|
|
return {
|
|
hotel_id: getReservationHotelId() ?? undefined,
|
|
...filters,
|
|
}
|
|
}
|
|
|
|
function fixtureTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
|
|
const detail = reservationTaskDetails.find((item) => item.task_id === taskId)
|
|
if (!detail) {
|
|
throw new Error(`Reservation task ${taskId} was not found in frontend fixture data.`)
|
|
}
|
|
return Promise.resolve(structuredClone(detail))
|
|
}
|
|
|
|
function fixtureTaskMutation(
|
|
taskId: string,
|
|
request: ReservationTaskPayloadMutationRequest,
|
|
confirmed: boolean,
|
|
): Promise<ReservationTaskPayloadMutationResult> {
|
|
return fixtureTaskDetail(taskId).then((detail) => ({
|
|
task_id: detail.task_id,
|
|
order_id: detail.order_id,
|
|
task_status: confirmed ? 'READY' : detail.task_status,
|
|
draft_payload: request.field_values,
|
|
confirmed_payload: confirmed ? request.field_values : detail.confirmed_payload,
|
|
opera_operations: confirmed ? detail.opera_operations : [],
|
|
}))
|
|
}
|
|
|
|
function fixtureManualReviewResolution(
|
|
taskId: string,
|
|
request: ReservationManualReviewResolutionRequest,
|
|
): Promise<ReservationManualReviewResolutionResult> {
|
|
return fixtureTaskDetail(taskId).then((detail) => {
|
|
const fieldValues = Object.fromEntries(
|
|
request.field_overrides.map((item) => [fieldOverrideToFixturePath(item), item.value]),
|
|
)
|
|
return {
|
|
task_id: detail.task_id,
|
|
order_id: detail.order_id,
|
|
task_status: 'READY',
|
|
review_status: 'RESOLVED',
|
|
review_resolution: {
|
|
confirmed_order_id: request.confirmed_order_id ?? detail.order_id,
|
|
reason: request.reason ?? null,
|
|
field_overrides: request.field_overrides,
|
|
},
|
|
confirmed_payload: {
|
|
field_values: fieldValues,
|
|
legacy_field_values: legacyFieldValues(fieldValues),
|
|
effective_payload: effectivePayload(fieldValues),
|
|
},
|
|
opera_operations: detail.opera_operations,
|
|
}
|
|
})
|
|
}
|
|
|
|
function fieldOverrideToFixturePath(
|
|
fieldOverride: ReservationManualReviewResolutionRequest['field_overrides'][number],
|
|
): string {
|
|
if (fieldOverride.field_path) {
|
|
return canonicalFixtureFieldPath(fieldOverride.field_path)
|
|
}
|
|
if (fieldOverride.field_pointer) {
|
|
return canonicalFixtureFieldPath(fieldOverride.field_pointer.replace(/^\//, '').replace(/\//g, '.'))
|
|
}
|
|
throw new Error('Manual review field override requires field_pointer or field_path')
|
|
}
|
|
|
|
function canonicalFixtureFieldPath(fieldPath: string): string {
|
|
if (fieldPath === 'extracted_fields.room_quantity') {
|
|
return 'extracted_fields.room_items.0.room_quantity'
|
|
}
|
|
if (fieldPath === 'extracted_fields.room_type') {
|
|
return 'extracted_fields.room_items.0.room_type_raw'
|
|
}
|
|
if (fieldPath === 'extracted_fields.pms_room_type_code') {
|
|
return 'extracted_fields.room_items.0.pms_room_type_code'
|
|
}
|
|
return fieldPath
|
|
}
|
|
|
|
function legacyFieldValues(fieldValues: Record<string, unknown>): Record<string, unknown> {
|
|
return Object.entries(fieldValues).reduce<Record<string, unknown>>((legacyValues, [fieldPath, value]) => {
|
|
if (fieldPath === 'extracted_fields.room_items.0.room_quantity') {
|
|
legacyValues['extracted_fields.room_quantity'] = value
|
|
}
|
|
if (fieldPath === 'extracted_fields.room_items.0.room_type_raw') {
|
|
legacyValues['extracted_fields.room_type'] = value
|
|
}
|
|
if (fieldPath === 'extracted_fields.room_items.0.pms_room_type_code') {
|
|
legacyValues['extracted_fields.pms_room_type_code'] = value
|
|
}
|
|
return legacyValues
|
|
}, {})
|
|
}
|
|
|
|
function effectivePayload(fieldValues: Record<string, unknown>): ReservationRecord {
|
|
return Object.entries(fieldValues).reduce<ReservationRecord>((payload, [fieldPath, value]) => {
|
|
writeNestedFieldValue(payload, fieldPath, value)
|
|
return payload
|
|
}, {})
|
|
}
|
|
|
|
function writeNestedFieldValue(payload: ReservationRecord, fieldPath: string, value: unknown): void {
|
|
const segments = fieldPath.split('.')
|
|
let current: ReservationRecord | unknown[] = payload
|
|
segments.forEach((segment, index) => {
|
|
const last = index === segments.length - 1
|
|
const nextSegment = segments[index + 1]
|
|
if (last) {
|
|
if (Array.isArray(current)) {
|
|
current[Number(segment)] = value
|
|
} else {
|
|
current[segment] = value
|
|
}
|
|
return
|
|
}
|
|
const nextIsArray = nextSegment !== undefined && /^\d+$/.test(nextSegment)
|
|
if (Array.isArray(current)) {
|
|
const arrayIndex = Number(segment)
|
|
current[arrayIndex] ??= nextIsArray ? [] : {}
|
|
current = current[arrayIndex] as ReservationRecord | unknown[]
|
|
return
|
|
}
|
|
current[segment] ??= nextIsArray ? [] : {}
|
|
current = current[segment] as ReservationRecord | unknown[]
|
|
})
|
|
}
|
|
|
|
async function fixtureOperaOperation(
|
|
taskId: string,
|
|
operationId: string,
|
|
request: ReservationOperaSimulationRequest,
|
|
action: 'execute' | 'retry',
|
|
): Promise<ReservationOperaOperationResult> {
|
|
const detail = await fixtureTaskDetail(taskId)
|
|
const operation = detail.opera_operations.find((item) => item.operation_id === operationId)
|
|
if (!operation) {
|
|
throw new Error(`Reservation OPERA operation ${operationId} was not found in frontend fixture data.`)
|
|
}
|
|
|
|
const success = request.simulate_success !== false
|
|
const attemptNumber = operation.attempt_count + 1
|
|
return {
|
|
...operation,
|
|
operation_status: success ? 'SUCCEEDED' : 'FAILED',
|
|
attempt_count: attemptNumber,
|
|
last_error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
|
|
attempts: [
|
|
...operation.attempts,
|
|
{
|
|
attempt_id: `${operationId}-${action}-${attemptNumber}`,
|
|
attempt_number: attemptNumber,
|
|
attempt_status: success ? 'SUCCEEDED' : 'FAILED',
|
|
response_payload: success ? { simulated: true } : null,
|
|
error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
|
|
started_at: new Date().toISOString(),
|
|
finished_at: new Date().toISOString(),
|
|
},
|
|
],
|
|
}
|
|
}
|