优化
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import {
|
||||
getCustomerVisit,
|
||||
listCustomerVisits,
|
||||
saveCustomerVisit,
|
||||
updateDeliveryOrderStatus,
|
||||
uploadCustomerVisitAttachment,
|
||||
} from './customerVisits'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
|
||||
|
||||
describe('customer visit API', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(overseasHttp.get).mockReset()
|
||||
vi.mocked(overseasHttp.post).mockReset()
|
||||
vi.mocked(overseasHttp.put).mockReset()
|
||||
})
|
||||
|
||||
it('loads the customer visit page and detail in customer scope', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], current: 1, size: 10, total: 0, pages: 0, visitable: 3, pending: 2, completed: 1 } })
|
||||
await listCustomerVisits({ page: 1, size: 10, keyword: 'DEL' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/customer-visits', { params: { page: 1, size: 10, keyword: 'DEL' } })
|
||||
|
||||
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } })
|
||||
await getCustomerVisit('9', '100')
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/customer-visits/9', { params: { customerId: '100' } })
|
||||
})
|
||||
|
||||
it('updates delivery status so selecting delivered can generate a visit', async () => {
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERED' } })
|
||||
await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERED', remark: '完成' })
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERED', remark: '完成' }, { params: { customerId: '100' } })
|
||||
})
|
||||
|
||||
it('uploads and saves repeatable visit material', async () => {
|
||||
const file = new File(['data'], 'visit.pdf', { type: 'application/pdf' })
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { fileId: '8', fileName: 'visit.pdf', sizeBytes: 4 } })
|
||||
await uploadCustomerVisitAttachment('9', '100', file)
|
||||
const body = vi.mocked(overseasHttp.post).mock.calls[0]?.[1] as FormData
|
||||
expect(body.get('file')).toBe(file)
|
||||
|
||||
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } })
|
||||
await saveCustomerVisit('9', '100', { selectedInboundOrderId: '7', visitResult: 'NORMAL', attachments: [] })
|
||||
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/customer-visits/9', expect.objectContaining({ selectedInboundOrderId: '7' }), { params: { customerId: '100' } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export interface CustomerVisitAttachment {
|
||||
fileId: string
|
||||
fileName: string
|
||||
contentType?: string | null
|
||||
sizeBytes: number
|
||||
}
|
||||
|
||||
export interface CustomerVisitRow {
|
||||
id: string
|
||||
customerId: string
|
||||
deliveryNo: string
|
||||
customerName?: string | null
|
||||
contactName?: string | null
|
||||
contactPhone?: string | null
|
||||
destination?: string | null
|
||||
deliveryCompletedAt: string
|
||||
status: 'PENDING' | 'COMPLETED'
|
||||
selectedInboundCode?: string | null
|
||||
visitResult?: string | null
|
||||
visitedAt?: string | null
|
||||
visitCount: number
|
||||
attachments: CustomerVisitAttachment[]
|
||||
}
|
||||
|
||||
export interface CustomerVisitPage {
|
||||
records: CustomerVisitRow[]
|
||||
current: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
visitable: number
|
||||
pending: number
|
||||
completed: number
|
||||
}
|
||||
|
||||
export interface CustomerVisitInboundOption {
|
||||
inboundOrderId: string
|
||||
inboundCode: string
|
||||
lastMileOrderId: string
|
||||
lastMileOrderNo: string
|
||||
destinationWarehouseCode?: string | null
|
||||
destinationWarehouseName?: string | null
|
||||
packageCount: number
|
||||
}
|
||||
|
||||
export interface CustomerVisitDetail {
|
||||
visit: CustomerVisitRow
|
||||
inboundOrders: CustomerVisitInboundOption[]
|
||||
customerFeedback?: string | null
|
||||
remark?: string | null
|
||||
visitedBy?: string | null
|
||||
}
|
||||
|
||||
export async function listCustomerVisits(params: {
|
||||
page: number
|
||||
size: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
customerId?: string
|
||||
completedFrom?: string
|
||||
completedTo?: string
|
||||
}) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<CustomerVisitPage>, ApiEnvelope<CustomerVisitPage>>(
|
||||
'/platform/v1/last-leg/customer-visits', { params },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function getCustomerVisit(visitId: string, customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>(
|
||||
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function saveCustomerVisit(visitId: string, customerId: string, payload: {
|
||||
selectedInboundOrderId: string
|
||||
visitResult: string
|
||||
customerFeedback?: string
|
||||
remark?: string
|
||||
attachments: CustomerVisitAttachment[]
|
||||
}) {
|
||||
const response = await overseasHttp.put<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>(
|
||||
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, payload, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function uploadCustomerVisitAttachment(visitId: string, customerId: string, file: File) {
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
const response = await overseasHttp.post<ApiEnvelope<CustomerVisitAttachment>, ApiEnvelope<CustomerVisitAttachment>>(
|
||||
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}/attachments`, body,
|
||||
{ params: { customerId }, headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function completeDeliveryOrder(deliveryNo: string, customerId: string) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<{ visitId: string; deliveryNo: string; deliveryStatus: string; visitStatus: string; replayed: boolean }>, ApiEnvelope<{ visitId: string; deliveryNo: string; deliveryStatus: string; visitStatus: string; replayed: boolean }>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, undefined,
|
||||
{ params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function updateDeliveryOrderStatus(deliveryNo: string, customerId: string, payload: { status: string; remark?: string }) {
|
||||
const response = await overseasHttp.put<ApiEnvelope<{ visitId?: string | null; deliveryNo: string; deliveryStatus: string; visitStatus?: string | null; replayed: boolean }>, ApiEnvelope<{ visitId?: string | null; deliveryNo: string; deliveryStatus: string; visitStatus?: string | null; replayed: boolean }>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, payload,
|
||||
{ params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { getDeliveryOrder, listDeliveryOrders } from './deliveryOrders'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } }))
|
||||
|
||||
describe('delivery order API', () => {
|
||||
beforeEach(() => vi.mocked(overseasHttp.get).mockReset())
|
||||
|
||||
it('passes paging and expanded filters and normalizes page numbers', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: '2', size: '20', total: '41', pages: '3' } })
|
||||
const result = await listDeliveryOrders({ page: 2, size: 20, documentNo: 'DEL-1', vehiclePlate: 'CA-1' })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders', { params: { page: 2, size: 20, documentNo: 'DEL-1', vehiclePlate: 'CA-1' } })
|
||||
expect(result).toMatchObject({ page: 2, size: 20, total: 41, pages: 3 })
|
||||
})
|
||||
|
||||
it('loads an encoded detail in its explicit customer scope', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1' }, skus: [], fees: [], operations: [] } })
|
||||
await getDeliveryOrder('DEL/1', '1001')
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001' } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
import type { ExceptionRelation } from './exceptionOrders'
|
||||
|
||||
export interface DeliveryOrderQuery {
|
||||
page: number
|
||||
size: number
|
||||
customerId?: string
|
||||
documentNo?: string
|
||||
dispatchNo?: string
|
||||
waybillNo?: string
|
||||
warehouseId?: string
|
||||
status?: string
|
||||
vehiclePlate?: string
|
||||
driverName?: string
|
||||
deliveryMethod?: string
|
||||
plannedFrom?: string
|
||||
plannedTo?: string
|
||||
}
|
||||
|
||||
export interface DeliveryOrderRow {
|
||||
id: string
|
||||
customerId: string
|
||||
customerName?: string | null
|
||||
deliveryNo: string
|
||||
outboundNo: string
|
||||
dispatchNo: string
|
||||
overseasWarehouseId?: string | null
|
||||
overseasWarehouseName?: string | null
|
||||
status: string
|
||||
waybillNo?: string | null
|
||||
deliveryMethod?: string | null
|
||||
carrierName?: string | null
|
||||
pickupMethod?: string | null
|
||||
loadingType?: string | null
|
||||
vehicleType?: string | null
|
||||
vehicleSize?: string | null
|
||||
vehiclePlate?: string | null
|
||||
driverName?: string | null
|
||||
driverPhone?: string | null
|
||||
appointmentAccount?: string | null
|
||||
sellerName?: string | null
|
||||
palletCount: number
|
||||
packageCount: number
|
||||
totalWeightKg?: number | null
|
||||
totalVolumeCbm?: number | null
|
||||
plannedDeliveryAt?: string | null
|
||||
dispatchedAt?: string | null
|
||||
expectedArrivalAt?: string | null
|
||||
signedAt?: string | null
|
||||
remark?: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface DeliverySkuRow { inboundCode?: string | null; customerName?: string | null; destinationWarehouseCode?: string | null; receivingAddress?: string | null; shipmentNo?: string | null; fbaTrackingNo?: string | null; skuCode?: string | null; productName?: string | null; packageCount: number; weightKg?: number | null; volumeCbm?: number | null }
|
||||
export interface DeliveryFeeRow { id: string; feeDirection: string; feeType: string; currency: string; amount: number; remark?: string | null }
|
||||
export interface DeliveryOperationRow { id: string; operatedAt: string; operationNode: string; operatorName?: string | null; result: string }
|
||||
export interface DeliveryOrderDetail { order: DeliveryOrderRow; skus: DeliverySkuRow[]; fees: DeliveryFeeRow[]; operations: DeliveryOperationRow[] }
|
||||
|
||||
export async function listDeliveryOrders(query: DeliveryOrderQuery) {
|
||||
const { customerId, ...params } = query
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>, ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>>(
|
||||
'/platform/v1/last-leg/delivery-orders', { params: { ...params, ...(customerId ? { customerId } : {}) } },
|
||||
)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getDeliveryOrder(deliveryNo: string, customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<DeliveryOrderDetail>, ApiEnvelope<DeliveryOrderDetail>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listDeliveryReturnInboundOptions(deliveryNo: string, customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<ExceptionRelation[]>, ApiEnvelope<ExceptionRelation[]>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: { inboundOrderId: string; reason: string; returnDirection: string; returnLogisticsNo?: string; packageCount?: number; skuCode?: string }) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<{ id: string; exceptionNo: string; status: string }>, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, payload, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
@@ -171,7 +171,25 @@ export async function releaseDispatchDraft(draftToken: string) {
|
||||
}
|
||||
|
||||
export interface TransferDeliveryResult { dispatchNo: string; outboundNo: string; outboundStatus: string; deliveryNo: string; deliveryStatus: string; palletCount: number; packageCount: number; replayed: boolean }
|
||||
export async function transferDispatchToDelivery(dispatchNo: string, payload: { idempotencyKey: string; remark?: string }) {
|
||||
export interface TransferDeliveryPayload {
|
||||
idempotencyKey: string
|
||||
plannedDeliveryAt: string
|
||||
deliveryMethod: string
|
||||
waybillNo?: string
|
||||
carrierName?: string
|
||||
pickupMethod: string
|
||||
loadingType: string
|
||||
vehicleType?: string
|
||||
vehicleSize?: string
|
||||
driverName?: string
|
||||
driverPhone?: string
|
||||
vehiclePlate?: string
|
||||
appointmentAccount?: string
|
||||
sellerName?: string
|
||||
remark?: string
|
||||
fees: Array<{ feeType: string; currency: string; amount: number; remark?: string }>
|
||||
}
|
||||
export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<TransferDeliveryResult>, ApiEnvelope<TransferDeliveryResult>>(
|
||||
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, payload,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from './exceptionOrders'
|
||||
|
||||
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } }))
|
||||
|
||||
describe('exception order API', () => {
|
||||
beforeEach(() => { vi.mocked(overseasHttp.get).mockReset(); vi.mocked(overseasHttp.post).mockReset() })
|
||||
|
||||
it('queries the real paginated exception endpoint', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: '1', size: '20', total: '2', pages: '1' } })
|
||||
const result = await listExceptionOrders({ page: 1, size: 20, keyword: 'EXC' })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', { params: { page: 1, size: 20, keyword: 'EXC' } })
|
||||
expect(result.total).toBe(2)
|
||||
})
|
||||
|
||||
it('resolves relation before creating an exception document', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { inboundOrderId: '11', inboundCode: 'IN-1', lastMileOrderId: '12', lastMileOrderNo: 'LM-1', customerId: '1' } })
|
||||
await resolveExceptionRelation({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/resolve', { params: { inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' } })
|
||||
|
||||
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '99', exceptionNo: 'EXC-1', status: 'PENDING' } })
|
||||
await createExceptionOrder({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', exceptionType: 'RETURN', reason: '拒收' })
|
||||
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', expect.objectContaining({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' }))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface ExceptionOrderQuery { page: number; size: number; keyword?: string; exceptionType?: string; status?: string; customerId?: string; warehouseId?: string; createdFrom?: string; createdTo?: string }
|
||||
export interface ExceptionOrderRow {
|
||||
id: string; exceptionNo: string; exceptionType: string; status: string; reason: string
|
||||
customerId: string; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null
|
||||
lastMileOrderId?: string | null; lastMileOrderNo?: string | null; inboundOrderId?: string | null; inboundCode?: string | null
|
||||
deliveryOrderId?: string | null; deliveryNo?: string | null; outboundNo?: string | null
|
||||
skuCode?: string | null; packageCount?: number | null; returnDirection?: string | null; returnLogisticsNo?: string | null
|
||||
handlingAction?: string | null; handlerName?: string | null; exceptionAt: string; updatedAt: string
|
||||
}
|
||||
export interface ExceptionRelation {
|
||||
lastMileOrderId?: string | null; lastMileOrderNo?: string | null; inboundOrderId?: string | null; inboundCode?: string | null
|
||||
customerId: string; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null
|
||||
deliveryNo?: string | null; outboundNo?: string | null; availablePackages?: number | null
|
||||
}
|
||||
export interface CreateExceptionOrderPayload { customerId?: string; inboundCode?: string; lastMileOrderNo?: string; exceptionType: string; reason: string; packageCount?: number; skuCode?: string }
|
||||
|
||||
export async function listExceptionOrders(query: ExceptionOrderQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ExceptionOrderRow>>, ApiEnvelope<OverseasPagePayload<ExceptionOrderRow>>>('/platform/v1/last-leg/exception-orders', { params: query })
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
export async function resolveExceptionRelation(params: { customerId?: string; inboundCode?: string; lastMileOrderNo?: string }) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<ExceptionRelation>, ApiEnvelope<ExceptionRelation>>('/platform/v1/last-leg/exception-orders/resolve', { params })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export async function createExceptionOrder(payload: CreateExceptionOrderPayload) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<{ id: string; exceptionNo: string; status: string }>, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>('/platform/v1/last-leg/exception-orders', payload)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
@@ -12,3 +12,7 @@ export async function createOneClickDropshipOutbound(customerId: string, payload
|
||||
const response = await overseasHttp.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export async function cancelOutbound(outboundNo: string, reason: string, customerId?: string) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason }, { params: customerId ? { customerId } : undefined })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user