实现V4 Payment附件预览前端

This commit is contained in:
andy
2026-07-21 16:52:49 +07:00
parent 4b869a0ecd
commit 61380a9ed8
9 changed files with 878 additions and 24 deletions

View File

@@ -12,6 +12,7 @@ import type {
ReservationV4SourceNotificationDetailResult,
ReservationV4TaskCardResult,
SourceMessageConversationResult,
SourceMessageOriginalMedia,
} from '@/types/reservation'
import ReservationV4OrderTaskDetailView from '@/views/reservation/ReservationV4OrderTaskDetailView.vue'
import ReservationV4SourceNotificationDetailView from '@/views/reservation/ReservationV4SourceNotificationDetailView.vue'
@@ -287,6 +288,104 @@ describe('reservation V4 pages', () => {
expect(wrapper.text()).not.toContain('RAW_HTML_SHOULD_NOT_RENDER')
})
it('renders Payment attachments with image preview and non-image download from current source message media', async () => {
const detail = createOrderTaskDetail()
usePaymentBusinessCard(detail)
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult({
attachments: createPaymentConversationMedia(),
}))
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(service.fetchSourceMessageConversation).toHaveBeenCalledWith('30001')
const attachmentPreview = wrapper.find('[data-testid="payment-attachment-preview"]')
expect(attachmentPreview.exists()).toBe(true)
expect(attachmentPreview.text()).toContain('voucher-image.jpg')
expect(attachmentPreview.text()).toContain('voucher-document.pdf')
expect(attachmentPreview.text()).toContain(zhCN.taskV4.paymentAttachments.noMatchedMedia)
expect(attachmentPreview.text()).not.toContain('https://oss.example')
expect(attachmentPreview.findAll('[data-testid="payment-attachment-thumbnail"]')).toHaveLength(1)
const thumbnail = attachmentPreview.find('[data-testid="payment-attachment-thumbnail"]')
expect(thumbnail.exists()).toBe(true)
expect(thumbnail.find('img').attributes('src')).toBe('https://oss.example/private/payment-image.jpg')
await thumbnail.trigger('click')
await flushPromises()
const modal = wrapper.find('[data-testid="payment-attachment-modal"]')
expect(modal.exists()).toBe(true)
expect(modal.find('img').attributes('src')).toBe('https://oss.example/private/payment-image.jpg')
expect(wrapper.text()).not.toContain('https://oss.example/private/payment-image.jpg')
const downloadLink = attachmentPreview.find('[data-testid="payment-attachment-download"]')
expect(downloadLink.exists()).toBe(true)
expect(downloadLink.attributes('href')).toBe('https://oss.example/private/payment-document.pdf')
})
it('degrades Payment attachment preview when conversation media cannot be loaded', async () => {
const detail = createOrderTaskDetail()
usePaymentBusinessCard(detail)
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.fetchSourceMessageConversation).mockRejectedValue(new ApiError('Forbidden', 403, {
code: 'SOURCE_MESSAGE_ORIGINAL_READ_FORBIDDEN',
message: 'Forbidden',
}))
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain('voucher-image.jpg')
expect(wrapper.text()).toContain(zhCN.taskV4.paymentAttachments.loadFailed)
expect(wrapper.find('[data-testid="payment-attachment-thumbnail"]').exists()).toBe(false)
expect(wrapper.find('[data-testid="payment-attachment-download"]').exists()).toBe(false)
})
it('confirms Payment cards with version only and never submits attachment ids or attachment URLs', async () => {
const detail = createOrderTaskDetail()
usePaymentBusinessCard(detail)
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
vi.mocked(service.fetchSourceMessageConversation).mockResolvedValue(createSourceMessageConversationResult({
attachments: createPaymentConversationMedia(),
}))
vi.mocked(service.confirmReservationV4OrderTaskCard).mockResolvedValue({
...detail,
business_cards: [
{
...detail.business_cards[0]!,
card_status: 'CONFIRMED',
},
],
})
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
const paymentCard = wrapper
.findAll('.task-card-section')
.find((section) => section.text().includes(zhCN.cardType.PAYMENT))
expect(paymentCard).toBeTruthy()
await paymentCard!.find('button.primary-button').trigger('click')
await flushPromises()
expect(service.confirmReservationV4OrderTaskCard).toHaveBeenCalledWith('9001', 'card-payment', {
version: 9,
})
const submittedRequest = vi.mocked(service.confirmReservationV4OrderTaskCard).mock.calls[0]?.[2]
expect(JSON.stringify(submittedRequest)).not.toContain('attachment_ids')
expect(JSON.stringify(submittedRequest)).not.toContain('https://oss.example')
})
it('renders New Booking Room Information as a business form and confirms stable final values', async () => {
const detail = createOrderTaskDetail({
businessEventType: 'NEW_BOOKING',
@@ -1131,6 +1230,7 @@ function createSourceMessageConversationResult(options: {
sanitizedHtml?: string | null
rawHtml?: string | null
htmlRenderMode?: SourceMessageConversationResult['messages'][number]['html_render_mode']
attachments?: SourceMessageOriginalMedia[]
} = {}): SourceMessageConversationResult {
const sourceMessageId = options.sourceMessageId ?? '30001'
const currentBody = options.currentBody ?? 'Current trigger email body.'
@@ -1157,7 +1257,7 @@ function createSourceMessageConversationResult(options: {
html_body_sanitized: options.sanitizedHtml ?? null,
html_render_mode: options.htmlRenderMode,
received_at: '2026-07-08T03:00:00Z',
attachments: [
attachments: options.attachments ?? [
{
mediaType: 'ATTACHMENT',
fileName: 'current-message.pdf',
@@ -1200,6 +1300,95 @@ function createConversationMessage(
}
}
function usePaymentBusinessCard(detail: ReservationV4OrderTaskDetailResult): void {
detail.business_cards = [
createCard('card-payment', 'PAYMENT', 'PENDING_CONFIRM', {
version: 9,
display_payload: {
payment_attachments: [
{
attachment_id: 'att-img-1',
file_name: 'voucher-image.jpg',
content_type: 'image/jpeg',
size_bytes: 123456,
is_image: true,
preview_available: true,
download_available: true,
external_media_id: 'media-payment-image',
},
{
attachment_id: 'att-pdf-1',
file_name: 'voucher-document.pdf',
content_type: 'application/pdf',
size_bytes: 2048,
is_image: false,
preview_available: false,
download_available: true,
external_media_id: 'media-payment-document',
},
{
attachment_id: 'att-missing-1',
file_name: 'missing-voucher.jpg',
content_type: 'image/jpeg',
size_bytes: 4096,
is_image: true,
preview_available: false,
download_available: false,
external_media_id: 'media-payment-missing',
unavailable_reason_code: 'MEDIA_NOT_AVAILABLE',
},
],
},
fields: [
createField('/payment/attachment_ids', {
display_name: 'attachment_ids',
value: ['att-img-1', 'att-pdf-1'],
editable: true,
raw_readonly: false,
edit_scope: 'CONFIRM',
write_target: 'CONFIRMED_PAYLOAD_JSON',
}),
createField('/payment/payment_note', {
display_name: 'Payment Note',
value: 'Should not submit for Payment confirm.',
editable: true,
edit_scope: 'CONFIRM',
write_target: 'CONFIRMED_PAYLOAD_JSON',
}),
],
}),
]
}
function createPaymentConversationMedia(): SourceMessageOriginalMedia[] {
return [
{
mediaType: 'ATTACHMENT',
fileName: 'voucher-image.jpg',
contentType: 'image/jpeg',
sizeBytes: 123456,
externalUrl: 'https://oss.example/private/payment-image.jpg',
externalMediaId: 'media-payment-image',
},
{
mediaType: 'ATTACHMENT',
fileName: 'voucher-document.pdf',
contentType: 'application/pdf',
sizeBytes: 2048,
externalUrl: 'https://oss.example/private/payment-document.pdf',
externalMediaId: 'media-payment-document',
},
{
mediaType: 'ATTACHMENT',
fileName: 'missing-voucher.jpg',
contentType: 'image/jpeg',
sizeBytes: 4096,
externalUrl: 'https://oss.example/private/same-file-name-should-not-match.jpg',
externalMediaId: 'media-different-same-file-name',
},
]
}
function createRoomInformationDisplayPayload(overrides: {
event_type?: string
booking_type?: string