This commit is contained in:
2026-09-29 17:56:00 +08:00
parent 4ead51a5fe
commit 2bf4353b1b
46 changed files with 481 additions and 638 deletions
-3
View File
@@ -5,9 +5,6 @@ API_PROXY_TARGET=http://localhost:9000
# AUTH_API_PROXY_TARGET=http://localhost:9001 # AUTH_API_PROXY_TARGET=http://localhost:9001
# IAM_API_PROXY_TARGET=http://localhost:9002 # IAM_API_PROXY_TARGET=http://localhost:9002
# WAREHOUSE_API_PROXY_TARGET=http://localhost:9003 # WAREHOUSE_API_PROXY_TARGET=http://localhost:9003
# 海外业务浏览器请求使用独立代理,代理会将 /overseas-api 重写为海外 Gateway 的 /api。
VITE_OVERSEAS_API_BASE_URL=/overseas-api
# 按本机联调环境填写海外 Gateway origin,不提交真实环境地址。 # 按本机联调环境填写海外 Gateway origin,不提交真实环境地址。
OVERSEAS_API_PROXY_TARGET=http://localhost:9300
# Local Vite fallback. Container deployments should use CLIENT_WEB_URL instead. # Local Vite fallback. Container deployments should use CLIENT_WEB_URL instead.
VITE_CLIENT_WEB_URL=http://localhost:5174 VITE_CLIENT_WEB_URL=http://localhost:5174
+3 -7
View File
@@ -60,13 +60,11 @@ function loginResponse(accessToken = token(), refreshToken = 'refresh-next') {
expiresIn: 1800, expiresIn: 1800,
refreshToken, refreshToken,
refreshExpiresIn: 604800, refreshExpiresIn: 604800,
overseasAccessToken: 'overseas-token',
overseasExpiresIn: 900,
}, },
} }
} }
function domesticOnlyLoginResponse() { function unifiedLoginResponse() {
return { return {
code: '200', code: '200',
message: 'success', message: 'success',
@@ -91,8 +89,8 @@ describe('admin auth session', () => {
afterEach(() => vi.unstubAllGlobals()) afterEach(() => vi.unstubAllGlobals())
it('accepts a domestic-only token response when the test environment has no overseas services', async () => { it('accepts the unified access and refresh token response', async () => {
mocks.post.mockResolvedValue(domesticOnlyLoginResponse()) mocks.post.mockResolvedValue(unifiedLoginResponse())
mocks.listCurrentPlatformPermissionCodes.mockResolvedValue(['platform:warehouse:list']) mocks.listCurrentPlatformPermissionCodes.mockResolvedValue(['platform:warehouse:list'])
const auth = useAuthStore() const auth = useAuthStore()
@@ -116,8 +114,6 @@ describe('admin auth session', () => {
expiresIn: '7200', expiresIn: '7200',
refreshToken: 'refresh-serialized', refreshToken: 'refresh-serialized',
refreshExpiresIn: '604800', refreshExpiresIn: '604800',
overseasAccessToken: null,
overseasExpiresIn: '0',
}, },
}) })
mocks.listCurrentPlatformPermissionCodes.mockResolvedValue(['platform:warehouse:list']) mocks.listCurrentPlatformPermissionCodes.mockResolvedValue(['platform:warehouse:list'])
-9
View File
@@ -8,7 +8,6 @@ import {
type ApiEnvelope, type ApiEnvelope,
} from '@/shared/api/http' } from '@/shared/api/http'
import type { PlatformPermissionResource } from '@/shared/types/platformPermission' import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
import { clearOverseasAccessToken, replaceOverseasAccessToken } from '@/shared/api/overseasHttp'
import { decodeJwtClaims } from './jwt' import { decodeJwtClaims } from './jwt'
import { listCurrentPlatformPermissionCodes } from './platformAuthorization' import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
import { coordinateCrossTabRefresh } from './crossTabRefresh' import { coordinateCrossTabRefresh } from './crossTabRefresh'
@@ -24,8 +23,6 @@ interface LoginResponse {
expiresIn: number | string expiresIn: number | string
refreshToken: string refreshToken: string
refreshExpiresIn: number | string refreshExpiresIn: number | string
overseasAccessToken?: string | null
overseasExpiresIn?: number | string
} }
class ExpiredAccessTokenError extends Error {} class ExpiredAccessTokenError extends Error {}
@@ -100,10 +97,6 @@ export const useAuthStore = defineStore('admin-auth', () => {
async function applySession(result: LoginResponse, expectedGeneration: number) { async function applySession(result: LoginResponse, expectedGeneration: number) {
const session = sessionFromToken(result.accessToken) const session = sessionFromToken(result.accessToken)
ensureCurrentGeneration(expectedGeneration) ensureCurrentGeneration(expectedGeneration)
replaceOverseasAccessToken(
result.overseasAccessToken ?? null,
parseLifetimeSeconds(result.overseasExpiresIn ?? 0, 'overseasExpiresIn'),
)
token.value = result.accessToken token.value = result.accessToken
refreshToken.value = result.refreshToken refreshToken.value = result.refreshToken
user.value = session.user user.value = session.user
@@ -123,7 +116,6 @@ export const useAuthStore = defineStore('admin-auth', () => {
function clearSession() { function clearSession() {
sessionGeneration += 1 sessionGeneration += 1
clearOverseasAccessToken()
token.value = '' token.value = ''
refreshToken.value = '' refreshToken.value = ''
user.value = null user.value = null
@@ -151,7 +143,6 @@ export const useAuthStore = defineStore('admin-auth', () => {
try { try {
const session = sessionFromToken(storedToken) const session = sessionFromToken(storedToken)
const cachedPermissions = JSON.parse(localStorage.getItem(PERMISSION_CODES_KEY) || '[]') as unknown const cachedPermissions = JSON.parse(localStorage.getItem(PERMISSION_CODES_KEY) || '[]') as unknown
if (token.value !== storedToken) clearOverseasAccessToken()
token.value = storedToken token.value = storedToken
refreshToken.value = storedRefreshToken refreshToken.value = storedRefreshToken
user.value = session.user user.value = session.user
@@ -1,20 +1,20 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { listContainerArrivalTimes, updateContainerExpectedArrival } from './containerArrivalTimes' import { listContainerArrivalTimes, updateContainerExpectedArrival } from './containerArrivalTimes'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { post: vi.fn(), put: vi.fn() } }))
describe('container arrival-time overseas API', () => { describe('container arrival-time overseas API', () => {
beforeEach(() => vi.clearAllMocks()) beforeEach(() => vi.clearAllMocks())
it('queries visible containers and updates the whole-container expected arrival', async () => { it('queries visible containers and updates the whole-container expected arrival', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: [{ sourceContainerBookingId: '9001', estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null, version: 0 }] }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: [{ sourceContainerBookingId: '9001', estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null, version: 0 }] })
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { sourceContainerBookingId: '9001', estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', actualWarehouseArrivalAt: null, version: 1 } } as never) vi.mocked(http.put).mockResolvedValue({ code: '200', data: { sourceContainerBookingId: '9001', estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', actualWarehouseArrivalAt: null, version: 1 } } as never)
await listContainerArrivalTimes(['9001']) await listContainerArrivalTimes(['9001'])
await updateContainerExpectedArrival('9001', { estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', expectedVersion: 0 }) await updateContainerExpectedArrival('9001', { estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', expectedVersion: 0 })
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-arrival-times/query', { sourceContainerBookingIds: ['9001'] }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-arrival-times/query', { sourceContainerBookingIds: ['9001'] })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries/9001/expected-arrival', { estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', expectedVersion: 0 }, { headers: { 'Idempotency-Key': expect.stringContaining('container-expected-arrival-9001-') } }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries/9001/expected-arrival', { estimatedWarehouseArrivalAt: '2026-09-25T15:31:00', expectedVersion: 0 }, { headers: { 'Idempotency-Key': expect.stringContaining('container-expected-arrival-9001-') } })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export interface ContainerArrivalTime { export interface ContainerArrivalTime {
sourceContainerBookingId: string sourceContainerBookingId: string
@@ -16,7 +16,7 @@ function operationId(containerBookingId: string) {
export async function listContainerArrivalTimes(sourceContainerBookingIds: string[]) { export async function listContainerArrivalTimes(sourceContainerBookingIds: string[]) {
if (!sourceContainerBookingIds.length) return [] if (!sourceContainerBookingIds.length) return []
const response = await overseasHttp.post<ApiEnvelope<ContainerArrivalTime[]>, ApiEnvelope<ContainerArrivalTime[]>>( const response = await http.post<ApiEnvelope<ContainerArrivalTime[]>, ApiEnvelope<ContainerArrivalTime[]>>(
queryPath, queryPath,
{ sourceContainerBookingIds }, { sourceContainerBookingIds },
) )
@@ -27,7 +27,7 @@ export async function updateContainerExpectedArrival(
sourceContainerBookingId: string, sourceContainerBookingId: string,
body: { estimatedWarehouseArrivalAt: string; expectedVersion: number; remark?: string }, body: { estimatedWarehouseArrivalAt: string; expectedVersion: number; remark?: string },
) { ) {
const response = await overseasHttp.put<ApiEnvelope<ContainerArrivalTime>, ApiEnvelope<ContainerArrivalTime>>( const response = await http.put<ApiEnvelope<ContainerArrivalTime>, ApiEnvelope<ContainerArrivalTime>>(
`/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/expected-arrival`, `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/expected-arrival`,
body, body,
{ headers: { 'Idempotency-Key': operationId(sourceContainerBookingId) } }, { headers: { 'Idempotency-Key': operationId(sourceContainerBookingId) } },
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { adjustOverseasContainerFee, createOverseasContainerFee, listOverseasContainerCustomers, listOverseasContainerFees, reviewOverseasContainerFee, submitOverseasContainerFee, voidOverseasContainerFee, type OverseasContainerFeeSaveRequest } from './overseasContainerFees' import { adjustOverseasContainerFee, createOverseasContainerFee, listOverseasContainerCustomers, listOverseasContainerFees, reviewOverseasContainerFee, submitOverseasContainerFee, voidOverseasContainerFee, type OverseasContainerFeeSaveRequest } from './overseasContainerFees'
const raw = { id: 91, sourceFeeId: 81, sourceVersion: 2, customerId: 11, customerName: '客户A', sourceType: 'OVERSEAS_CONTAINER_FEE', businessType: 'OVERSEAS_CONTAINER', businessId: 99, businessNo: 'CONT-99', containerBookingId: 99, containerNo: 'CONT-99', feeItemCode: 'PORT_FEE', feeItemName: '港口费', direction: 'PAYABLE', amount: 300, currency: 'CNY', reviewStatus: 'DRAFT', sourceStatus: 'ACTIVE', businessDate: '2026-09-08' } const raw = { id: 91, sourceFeeId: 81, sourceVersion: 2, customerId: 11, customerName: '客户A', sourceType: 'OVERSEAS_CONTAINER_FEE', businessType: 'OVERSEAS_CONTAINER', businessId: 99, businessNo: 'CONT-99', containerBookingId: 99, containerNo: 'CONT-99', feeItemCode: 'PORT_FEE', feeItemName: '港口费', direction: 'PAYABLE', amount: 300, currency: 'CNY', reviewStatus: 'DRAFT', sourceStatus: 'ACTIVE', businessDate: '2026-09-08' }
@@ -7,14 +7,14 @@ const fee: OverseasContainerFeeSaveRequest = { customerId: '11', customerName: '
describe('overseas container fees API', () => { describe('overseas container fees API', () => {
beforeEach(() => vi.restoreAllMocks()) beforeEach(() => vi.restoreAllMocks())
it('loads all fees and customers with two container aggregate requests', async () => { it('loads all fees and customers with two container aggregate requests', async () => {
const get = vi.spyOn(overseasHttp, 'get').mockResolvedValueOnce({ code: '200', data: [raw] }).mockResolvedValueOnce({ code: '200', data: [{ customerId: 11, customerName: '客户A' }] }) const get = vi.spyOn(http, 'get').mockResolvedValueOnce({ code: '200', data: [raw] }).mockResolvedValueOnce({ code: '200', data: [{ customerId: 11, customerName: '客户A' }] })
await expect(listOverseasContainerFees('99')).resolves.toMatchObject([{ sourceFeeId: '81', containerBookingId: '99', feeItemCode: 'PORT_FEE' }]) await expect(listOverseasContainerFees('99')).resolves.toMatchObject([{ sourceFeeId: '81', containerBookingId: '99', feeItemCode: 'PORT_FEE' }])
await expect(listOverseasContainerCustomers('99')).resolves.toEqual([{ customerId: '11', customerName: '客户A' }]) await expect(listOverseasContainerCustomers('99')).resolves.toEqual([{ customerId: '11', customerName: '客户A' }])
expect(get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/finance-fee-sources', { params: { containerBookingId: '99', businessType: 'OVERSEAS_CONTAINER' } }) expect(get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/finance-fee-sources', { params: { containerBookingId: '99', businessType: 'OVERSEAS_CONTAINER' } })
expect(get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/finance-fee-sources/container-customers', { params: { containerBookingId: '99' } }) expect(get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/finance-fee-sources/container-customers', { params: { containerBookingId: '99' } })
}) })
it('uses the exact create and lifecycle action contracts', async () => { it('uses the exact create and lifecycle action contracts', async () => {
const post = vi.spyOn(overseasHttp, 'post').mockResolvedValue({ code: '200', data: raw }) const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', data: raw })
await createOverseasContainerFee(fee, 'create-1'); await submitOverseasContainerFee('81', 'submit-1'); await reviewOverseasContainerFee('81', 'APPROVE', null, 'review-1'); await adjustOverseasContainerFee('81', 2, fee, 'adjust-1'); await voidOverseasContainerFee('81', 2, '重复费用', 'void-1') await createOverseasContainerFee(fee, 'create-1'); await submitOverseasContainerFee('81', 'submit-1'); await reviewOverseasContainerFee('81', 'APPROVE', null, 'review-1'); await adjustOverseasContainerFee('81', 2, fee, 'adjust-1'); await voidOverseasContainerFee('81', 2, '重复费用', 'void-1')
expect(post).toHaveBeenCalledWith('/platform/v1/last-leg/finance-fee-sources', fee, { headers: { 'Idempotency-Key': 'create-1' } }) expect(post).toHaveBeenCalledWith('/platform/v1/last-leg/finance-fee-sources', fee, { headers: { 'Idempotency-Key': 'create-1' } })
expect(post).toHaveBeenCalledWith('/platform/v1/last-leg/finance-fee-sources/81/submit', { idempotencyKey: 'submit-1' }) expect(post).toHaveBeenCalledWith('/platform/v1/last-leg/finance-fee-sources/81/submit', { idempotencyKey: 'submit-1' })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
const basePath = '/platform/v1/last-leg/finance-fee-sources' const basePath = '/platform/v1/last-leg/finance-fee-sources'
type RawResource = Record<string, unknown> type RawResource = Record<string, unknown>
@@ -42,25 +42,25 @@ function normalizeFee(raw: RawResource): OverseasContainerFeeSource {
} }
} }
export async function listOverseasContainerFees(containerBookingId: string) { export async function listOverseasContainerFees(containerBookingId: string) {
const response = await overseasHttp.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(basePath, { params: { containerBookingId, businessType: 'OVERSEAS_CONTAINER' } }) const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(basePath, { params: { containerBookingId, businessType: 'OVERSEAS_CONTAINER' } })
return unwrapApiResponse(response).map(normalizeFee) return unwrapApiResponse(response).map(normalizeFee)
} }
export async function listOverseasContainerCustomers(containerBookingId: string) { export async function listOverseasContainerCustomers(containerBookingId: string) {
const response = await overseasHttp.get<ApiEnvelope<Array<{ customerId: unknown; customerName?: unknown }>>, ApiEnvelope<Array<{ customerId: unknown; customerName?: unknown }>>>(`${basePath}/container-customers`, { params: { containerBookingId } }) const response = await http.get<ApiEnvelope<Array<{ customerId: unknown; customerName?: unknown }>>, ApiEnvelope<Array<{ customerId: unknown; customerName?: unknown }>>>(`${basePath}/container-customers`, { params: { containerBookingId } })
return unwrapApiResponse(response).map((item) => ({ customerId: requiredText(item.customerId), customerName: text(item.customerName) })) return unwrapApiResponse(response).map((item) => ({ customerId: requiredText(item.customerId), customerName: text(item.customerName) }))
} }
export async function createOverseasContainerFee(payload: OverseasContainerFeeSaveRequest, idempotencyKey: string) { export async function createOverseasContainerFee(payload: OverseasContainerFeeSaveRequest, idempotencyKey: string) {
return normalizeFee(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey } }))) return normalizeFee(unwrapApiResponse(await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(basePath, payload, { headers: { 'Idempotency-Key': idempotencyKey } })))
} }
export async function submitOverseasContainerFee(sourceFeeId: string, idempotencyKey: string) { export async function submitOverseasContainerFee(sourceFeeId: string, idempotencyKey: string) {
return normalizeFee(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/submit`, { idempotencyKey }))) return normalizeFee(unwrapApiResponse(await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/submit`, { idempotencyKey })))
} }
export async function reviewOverseasContainerFee(sourceFeeId: string, decision: 'APPROVE' | 'REJECT', reason: string | null, idempotencyKey: string) { export async function reviewOverseasContainerFee(sourceFeeId: string, decision: 'APPROVE' | 'REJECT', reason: string | null, idempotencyKey: string) {
return normalizeFee(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/review`, { decision, reason, idempotencyKey }))) return normalizeFee(unwrapApiResponse(await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/review`, { decision, reason, idempotencyKey })))
} }
export async function adjustOverseasContainerFee(sourceFeeId: string, expectedVersion: number, fee: OverseasContainerFeeSaveRequest, idempotencyKey: string) { export async function adjustOverseasContainerFee(sourceFeeId: string, expectedVersion: number, fee: OverseasContainerFeeSaveRequest, idempotencyKey: string) {
return normalizeFee(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/adjust`, { expectedVersion, fee, idempotencyKey }))) return normalizeFee(unwrapApiResponse(await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/adjust`, { expectedVersion, fee, idempotencyKey })))
} }
export async function voidOverseasContainerFee(sourceFeeId: string, expectedVersion: number, reason: string, idempotencyKey: string) { export async function voidOverseasContainerFee(sourceFeeId: string, expectedVersion: number, reason: string, idempotencyKey: string) {
return normalizeFee(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/void`, { expectedVersion, reason, idempotencyKey }))) return normalizeFee(unwrapApiResponse(await http.post<ApiEnvelope<RawResource>, ApiEnvelope<RawResource>>(`${basePath}/${encodeURIComponent(sourceFeeId)}/void`, { expectedVersion, reason, idempotencyKey })))
} }
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
const overseas = vi.hoisted(() => ({ get: vi.fn(), patch: vi.fn(), post: vi.fn(), put: vi.fn() })) const overseas = vi.hoisted(() => ({ get: vi.fn(), patch: vi.fn(), post: vi.fn(), put: vi.fn() }))
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: overseas })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: overseas }))
import { import {
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getUsTruckOrder, createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getUsTruckOrder,
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { import type {
CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardDetail, LocalLooseboardOrder, CreateLocalLooseboardOrderRequest, CreateLocalTransportOrderRequest, LocalLooseboardDetail, LocalLooseboardOrder,
LocalLooseboardQuery, LocalTransportDetail, LocalTransportOrder, LocalTransportPallet, LocalTransportQuery, LocalLooseboardQuery, LocalTransportDetail, LocalTransportOrder, LocalTransportPallet, LocalTransportQuery,
@@ -57,67 +57,67 @@ function looseboardDetail(value: LooseboardDetailPayload): LocalLooseboardDetail
} }
export async function listOverseasPurchaseCustomerOptions(keyword = '', limit = 200) { export async function listOverseasPurchaseCustomerOptions(keyword = '', limit = 200) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPurchaseCustomerOption[]>, ApiEnvelope<OverseasPurchaseCustomerOption[]>>(`${procurementBasePath}/customer-options`, { params: { keyword: keyword.trim() || undefined, limit } }) const response = await http.get<ApiEnvelope<OverseasPurchaseCustomerOption[]>, ApiEnvelope<OverseasPurchaseCustomerOption[]>>(`${procurementBasePath}/customer-options`, { params: { keyword: keyword.trim() || undefined, limit } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listOverseasPurchaseWarehouseOptions() { export async function listOverseasPurchaseWarehouseOptions() {
const response = await overseasHttp.get<ApiEnvelope<OverseasPurchaseWarehouseOption[]>, ApiEnvelope<OverseasPurchaseWarehouseOption[]>>(`${procurementBasePath}/warehouse-options`) const response = await http.get<ApiEnvelope<OverseasPurchaseWarehouseOption[]>, ApiEnvelope<OverseasPurchaseWarehouseOption[]>>(`${procurementBasePath}/warehouse-options`)
return unwrapApiResponse(response).map((item) => ({ ...item, warehouseId: String(item.warehouseId) })) return unwrapApiResponse(response).map((item) => ({ ...item, warehouseId: String(item.warehouseId) }))
} }
export async function listOverseasPurchases(query: OverseasPurchaseQuery = {}) { export async function listOverseasPurchases(query: OverseasPurchaseQuery = {}) {
const response = await overseasHttp.get<ApiEnvelope<PagePayload<ProcurementSummaryPayload>>, ApiEnvelope<PagePayload<ProcurementSummaryPayload>>>(procurementBasePath, { params: { ...query, customerId: query.customerId || undefined, orderNo: query.orderNo?.trim() || undefined, customerKeyword: query.customerKeyword?.trim() || undefined, purchaseCategory: query.purchaseCategory?.trim() || undefined, productName: query.productName?.trim() || undefined, status: query.status || undefined, purchaseWarehouseId: query.purchaseWarehouseId || undefined } }) const response = await http.get<ApiEnvelope<PagePayload<ProcurementSummaryPayload>>, ApiEnvelope<PagePayload<ProcurementSummaryPayload>>>(procurementBasePath, { params: { ...query, customerId: query.customerId || undefined, orderNo: query.orderNo?.trim() || undefined, customerKeyword: query.customerKeyword?.trim() || undefined, purchaseCategory: query.purchaseCategory?.trim() || undefined, productName: query.productName?.trim() || undefined, status: query.status || undefined, purchaseWarehouseId: query.purchaseWarehouseId || undefined } })
return normalizePage(unwrapApiResponse(response), procurementSummary) return normalizePage(unwrapApiResponse(response), procurementSummary)
} }
export async function getOverseasPurchase(orderNo: string, customerId: string) { export async function getOverseasPurchase(orderNo: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } }) const response = await http.get<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response)) return procurementDetail(unwrapApiResponse(response))
} }
export async function updateOverseasPurchase(orderNo: string, customerId: string, payload: UpdateOverseasPurchaseRequest) { export async function updateOverseasPurchase(orderNo: string, customerId: string, payload: UpdateOverseasPurchaseRequest) {
const response = await overseasHttp.patch<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } }) const response = await http.patch<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response)) return procurementDetail(unwrapApiResponse(response))
} }
export async function reviewOverseasPurchase(orderNo: string, customerId: string, payload: ReviewOverseasPurchaseRequest) { export async function reviewOverseasPurchase(orderNo: string, customerId: string, payload: ReviewOverseasPurchaseRequest) {
const response = await overseasHttp.post<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/review`, payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<ProcurementDetailPayload>, ApiEnvelope<ProcurementDetailPayload>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/review`, payload, { params: { customerId } })
return procurementDetail(unwrapApiResponse(response)) return procurementDetail(unwrapApiResponse(response))
} }
export async function uploadOverseasPurchaseFile(orderNo: string, customerId: string, category: 'RECEIPT' | 'VOUCHER', file: File) { export async function uploadOverseasPurchaseFile(orderNo: string, customerId: string, category: 'RECEIPT' | 'VOUCHER', file: File) {
const body = new FormData(); body.append('file', file) const body = new FormData(); body.append('file', file)
const response = await overseasHttp.post<ApiEnvelope<OverseasPurchaseFile>, ApiEnvelope<OverseasPurchaseFile>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/files/${category}`, body, { params: { customerId } }) const response = await http.post<ApiEnvelope<OverseasPurchaseFile>, ApiEnvelope<OverseasPurchaseFile>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/files/${category}`, body, { params: { customerId } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createOverseasPurchaseFee(orderNo: string, customerId: string, payload: { feeItemCode: string; feeItemName: string; amount: number; voucherFileId: string; remark?: string }) { export async function createOverseasPurchaseFee(orderNo: string, customerId: string, payload: { feeItemCode: string; feeItemName: string; amount: number; voucherFileId: string; remark?: string }) {
const response = await overseasHttp.post<ApiEnvelope<OverseasPurchaseFee>, ApiEnvelope<OverseasPurchaseFee>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/fees`, payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<OverseasPurchaseFee>, ApiEnvelope<OverseasPurchaseFee>>(`${procurementBasePath}/${encodeURIComponent(orderNo)}/fees`, payload, { params: { customerId } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listUsTruckOrders(query: LocalTransportQuery = {}) { export async function listUsTruckOrders(query: LocalTransportQuery = {}) {
const response = await overseasHttp.get<ApiEnvelope<PagePayload<TruckSummaryPayload>>, ApiEnvelope<PagePayload<TruckSummaryPayload>>>(truckBasePath, { params: { ...query, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined, transportType: query.transportType || undefined, customerId: query.customerId || undefined } }) const response = await http.get<ApiEnvelope<PagePayload<TruckSummaryPayload>>, ApiEnvelope<PagePayload<TruckSummaryPayload>>>(truckBasePath, { params: { ...query, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined, transportType: query.transportType || undefined, customerId: query.customerId || undefined } })
return normalizePage(unwrapApiResponse(response), truckSummary) return normalizePage(unwrapApiResponse(response), truckSummary)
} }
export async function getUsTruckOrder(orderNo: string, customerId: string) { export async function getUsTruckOrder(orderNo: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(`${truckBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } }) const response = await http.get<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(`${truckBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
return truckDetail(unwrapApiResponse(response)) return truckDetail(unwrapApiResponse(response))
} }
export async function createUsTruckOrder(customerId: string, payload: CreateLocalTransportOrderRequest) { export async function createUsTruckOrder(customerId: string, payload: CreateLocalTransportOrderRequest) {
const response = await overseasHttp.post<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(truckBasePath, payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(truckBasePath, payload, { params: { customerId } })
return truckDetail(unwrapApiResponse(response)) return truckDetail(unwrapApiResponse(response))
} }
export async function updateUsTruckOrder(orderNo: string, customerId: string, payload: UpdateLocalTransportOrderRequest) { export async function updateUsTruckOrder(orderNo: string, customerId: string, payload: UpdateLocalTransportOrderRequest) {
const response = await overseasHttp.patch<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(`${truckBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } }) const response = await http.patch<ApiEnvelope<TruckDetailPayload>, ApiEnvelope<TruckDetailPayload>>(`${truckBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } })
return truckDetail(unwrapApiResponse(response)) return truckDetail(unwrapApiResponse(response))
} }
export async function listLocalLooseboardOrders(query: LocalLooseboardQuery = {}) { export async function listLocalLooseboardOrders(query: LocalLooseboardQuery = {}) {
const response = await overseasHttp.get<ApiEnvelope<PagePayload<LooseboardSummaryPayload>>, ApiEnvelope<PagePayload<LooseboardSummaryPayload>>>(looseboardBasePath, { params: { ...query, customerId: query.customerId || undefined, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined, transportMode: query.transportMode || undefined } }) const response = await http.get<ApiEnvelope<PagePayload<LooseboardSummaryPayload>>, ApiEnvelope<PagePayload<LooseboardSummaryPayload>>>(looseboardBasePath, { params: { ...query, customerId: query.customerId || undefined, orderNo: query.orderNo?.trim() || undefined, routeKeyword: query.routeKeyword?.trim() || undefined, status: query.status || undefined, transportMode: query.transportMode || undefined } })
return normalizePage(unwrapApiResponse(response), looseboardSummary) return normalizePage(unwrapApiResponse(response), looseboardSummary)
} }
export async function getLocalLooseboardOrder(orderNo: string, customerId: string) { export async function getLocalLooseboardOrder(orderNo: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(`${looseboardBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } }) const response = await http.get<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(`${looseboardBasePath}/${encodeURIComponent(orderNo)}`, { params: { customerId } })
return looseboardDetail(unwrapApiResponse(response)) return looseboardDetail(unwrapApiResponse(response))
} }
export async function createLocalLooseboardOrder(customerId: string, payload: CreateLocalLooseboardOrderRequest) { export async function createLocalLooseboardOrder(customerId: string, payload: CreateLocalLooseboardOrderRequest) {
const response = await overseasHttp.post<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(looseboardBasePath, payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(looseboardBasePath, payload, { params: { customerId } })
return looseboardDetail(unwrapApiResponse(response)) return looseboardDetail(unwrapApiResponse(response))
} }
export async function updateLocalLooseboardOrder(orderNo: string, customerId: string, payload: UpdateLocalLooseboardOrderRequest) { export async function updateLocalLooseboardOrder(orderNo: string, customerId: string, payload: UpdateLocalLooseboardOrderRequest) {
const response = await overseasHttp.put<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(`${looseboardBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } }) const response = await http.put<ApiEnvelope<LooseboardDetailPayload>, ApiEnvelope<LooseboardDetailPayload>>(`${looseboardBasePath}/${encodeURIComponent(orderNo)}`, payload, { params: { customerId } })
return looseboardDetail(unwrapApiResponse(response)) return looseboardDetail(unwrapApiResponse(response))
} }
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http' import { ApiResponseError, http } from '@/shared/api/http'
import { overseasHttp } from '@/shared/api/overseasHttp'
import { import {
changeExchangeRateStatus, changeQuoteStatus, changeVehicleStatus, createExchangeRate, createQuote, createVehicle, changeExchangeRateStatus, changeQuoteStatus, changeVehicleStatus, createExchangeRate, createQuote, createVehicle,
exportExchangeRates, exportQuotes, exportVehicles, getExchangeRate, getExchangeRateSummary, getQuote, getVehicle, exportExchangeRates, exportQuotes, exportVehicles, getExchangeRate, getExchangeRateSummary, getQuote, getVehicle,
@@ -8,7 +8,7 @@ import {
type QuoteCreate, type QuoteUpdate, type QuoteCreate, type QuoteUpdate,
} from './overseasMasterData' } from './overseasMasterData'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() } }))
const ok = (data: unknown) => ({ code: '200', message: 'success', data }) const ok = (data: unknown) => ({ code: '200', message: 'success', data })
const longId = '9223372036854775807' const longId = '9223372036854775807'
const quoteSummary = { const quoteSummary = {
@@ -29,7 +29,7 @@ beforeEach(() => vi.clearAllMocks())
describe('overseas master data API', () => { describe('overseas master data API', () => {
it('normalizes quote page/detail decimals and preserves long string IDs', async () => { it('normalizes quote page/detail decimals and preserves long string IDs', async () => {
vi.mocked(overseasHttp.get) vi.mocked(http.get)
.mockResolvedValueOnce(ok({ records: [quoteSummary], page: '1', size: '20', total: '1', pages: '1' })) .mockResolvedValueOnce(ok({ records: [quoteSummary], page: '1', size: '20', total: '1', pages: '1' }))
.mockResolvedValueOnce(ok(quoteDetail)) .mockResolvedValueOnce(ok(quoteDetail))
.mockResolvedValueOnce(ok([{ warehouseId: quoteSummary.warehouseId, warehouseCode: 'US-LAX', warehouseName: '洛杉矶合作仓', countryCode: 'US' }])) .mockResolvedValueOnce(ok([{ warehouseId: quoteSummary.warehouseId, warehouseCode: 'US-LAX', warehouseName: '洛杉矶合作仓', countryCode: 'US' }]))
@@ -44,9 +44,9 @@ describe('overseas master data API', () => {
expect(detail.transitPrices[0]).toMatchObject({ estimatedMiles: 12.5, price: 18.75, minimumCharge: null }) expect(detail.transitPrices[0]).toMatchObject({ estimatedMiles: 12.5, price: 18.75, minimumCharge: null })
await listQuoteWarehouseOptions() await listQuoteWarehouseOptions()
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes', { params: { page: 1, size: 20, keyword: 'V1' } }) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes', { params: { page: 1, size: 20, keyword: 'V1' } })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, `/platform/v1/last-leg/master-data/overseas-quotes/${longId}`) expect(http.get).toHaveBeenNthCalledWith(2, `/platform/v1/last-leg/master-data/overseas-quotes/${longId}`)
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/overseas-quotes/warehouse-options') expect(http.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/overseas-quotes/warehouse-options')
}) })
it('keeps quote create/update DTOs distinct and sends recordVersion on writes', async () => { it('keeps quote create/update DTOs distinct and sends recordVersion on writes', async () => {
@@ -60,24 +60,24 @@ describe('overseas master data API', () => {
name: '报价-编辑', providerName: '服务商', currency: 'USD', effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31', name: '报价-编辑', providerName: '服务商', currency: 'USD', effectiveStart: '2026-08-01', effectiveEnd: '2026-12-31',
warehouseFees: create.warehouseFees, transitPrices: [], remark: null, recordVersion: 7, warehouseFees: create.warehouseFees, transitPrices: [], remark: null, recordVersion: 7,
} }
vi.mocked(overseasHttp.post).mockResolvedValue(ok(quoteDetail)) vi.mocked(http.post).mockResolvedValue(ok(quoteDetail))
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, recordVersion: '8' } })) vi.mocked(http.put).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, recordVersion: '8' } }))
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, status: 'ACTIVE', recordVersion: '9' } })) vi.mocked(http.patch).mockResolvedValue(ok({ ...quoteDetail, summary: { ...quoteSummary, status: 'ACTIVE', recordVersion: '9' } }))
await createQuote(create) await createQuote(create)
const updated = await updateQuote(longId, update) const updated = await updateQuote(longId, update)
await changeQuoteStatus(longId, 'ACTIVE', 8) await changeQuoteStatus(longId, 'ACTIVE', 8)
expect(updated.summary.recordVersion).toBe(8) expect(updated.summary.recordVersion).toBe(8)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes', create) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/master-data/overseas-quotes', create)
expect(overseasHttp.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}`, update) expect(http.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}`, update)
expect(overseasHttp.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}/status`, { status: 'ACTIVE', recordVersion: 8 }) expect(http.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/overseas-quotes/${longId}/status`, { status: 'ACTIVE', recordVersion: 8 })
expect(update).not.toHaveProperty('warehouseId') expect(update).not.toHaveProperty('warehouseId')
expect(update).not.toHaveProperty('versionCode') expect(update).not.toHaveProperty('versionCode')
}) })
it('normalizes exchange-rate decimal/version and uses month as the stable key', async () => { it('normalizes exchange-rate decimal/version and uses month as the stable key', async () => {
vi.mocked(overseasHttp.get) vi.mocked(http.get)
.mockResolvedValueOnce(ok({ records: [exchangeRate], page: '1', size: '20', total: '1', pages: '1' })) .mockResolvedValueOnce(ok({ records: [exchangeRate], page: '1', size: '20', total: '1', pages: '1' }))
.mockResolvedValueOnce(ok(exchangeRate)) .mockResolvedValueOnce(ok(exchangeRate))
.mockResolvedValueOnce(ok({ latestMonth: '2026-08', latestUsdToCnyAverage: '7.12345678', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00' })) .mockResolvedValueOnce(ok({ latestMonth: '2026-08', latestUsdToCnyAverage: '7.12345678', updatedBy: 'admin', updatedAt: '2026-08-24T10:00:00' }))
@@ -88,17 +88,17 @@ describe('overseas master data API', () => {
expect(detail).toMatchObject({ month: '2026-08', usdToCnyAverage: 7.12345678, recordVersion: 3 }) expect(detail).toMatchObject({ month: '2026-08', usdToCnyAverage: 7.12345678, recordVersion: 3 })
expect(summary.latestUsdToCnyAverage).toBe(7.12345678) expect(summary.latestUsdToCnyAverage).toBe(7.12345678)
vi.mocked(overseasHttp.post).mockResolvedValue(ok(exchangeRate)) vi.mocked(http.post).mockResolvedValue(ok(exchangeRate))
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...exchangeRate, recordVersion: '4' })) vi.mocked(http.put).mockResolvedValue(ok({ ...exchangeRate, recordVersion: '4' }))
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...exchangeRate, status: 'INACTIVE', recordVersion: '5' })) vi.mocked(http.patch).mockResolvedValue(ok({ ...exchangeRate, status: 'INACTIVE', recordVersion: '5' }))
await createExchangeRate({ month: '2026-08', usdToCnyAverage: 7.12345678, remark: null }) await createExchangeRate({ month: '2026-08', usdToCnyAverage: 7.12345678, remark: null })
await updateExchangeRate('2026-08', { recordVersion: 3, usdToCnyAverage: 7.2, remark: null }) await updateExchangeRate('2026-08', { recordVersion: 3, usdToCnyAverage: 7.2, remark: null })
await changeExchangeRateStatus('2026-08', 'INACTIVE', 4, '停用') await changeExchangeRateStatus('2026-08', 'INACTIVE', 4, '停用')
expect(overseasHttp.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/exchange-rates/2026-08/status', { status: 'INACTIVE', recordVersion: 4, remark: '停用' }) expect(http.patch).toHaveBeenLastCalledWith('/platform/v1/last-leg/master-data/exchange-rates/2026-08/status', { status: 'INACTIVE', recordVersion: 4, remark: '停用' })
}) })
it('normalizes vehicle decimals/version while preserving long string IDs', async () => { it('normalizes vehicle decimals/version while preserving long string IDs', async () => {
vi.mocked(overseasHttp.get) vi.mocked(http.get)
.mockResolvedValueOnce(ok({ records: [vehicle], page: '1', size: '20', total: '1', pages: '1' })) .mockResolvedValueOnce(ok({ records: [vehicle], page: '1', size: '20', total: '1', pages: '1' }))
.mockResolvedValueOnce(ok(vehicle)) .mockResolvedValueOnce(ok(vehicle))
const result = await listVehicles({ page: 1, size: 20, ownership: 'OWNED' }) const result = await listVehicles({ page: 1, size: 20, ownership: 'OWNED' })
@@ -107,18 +107,18 @@ describe('overseas master data API', () => {
expect(detail.id).toBe(longId) expect(detail.id).toBe(longId)
const editable = { plateNumber: 'CA-01', model: 'Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 101, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, remark: null } const editable = { plateNumber: 'CA-01', model: 'Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 101, driverOrDispatchCompany: null, phone: null, region: 'OVERSEAS' as const, ownership: 'OWNED' as const, remark: null }
vi.mocked(overseasHttp.post).mockResolvedValue(ok(vehicle)) vi.mocked(http.post).mockResolvedValue(ok(vehicle))
vi.mocked(overseasHttp.put).mockResolvedValue(ok({ ...vehicle, recordVersion: '5' })) vi.mocked(http.put).mockResolvedValue(ok({ ...vehicle, recordVersion: '5' }))
vi.mocked(overseasHttp.patch).mockResolvedValue(ok({ ...vehicle, status: 'INACTIVE', recordVersion: '6' })) vi.mocked(http.patch).mockResolvedValue(ok({ ...vehicle, status: 'INACTIVE', recordVersion: '6' }))
await createVehicle({ ...editable, status: 'IN_SERVICE' }) await createVehicle({ ...editable, status: 'IN_SERVICE' })
await updateVehicle(longId, { ...editable, recordVersion: 4 }) await updateVehicle(longId, { ...editable, recordVersion: 4 })
await changeVehicleStatus(longId, 'INACTIVE', 5) await changeVehicleStatus(longId, 'INACTIVE', 5)
expect(overseasHttp.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}`, { ...editable, recordVersion: 4 }) expect(http.put).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}`, { ...editable, recordVersion: 4 })
expect(overseasHttp.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}/status`, { status: 'INACTIVE', recordVersion: 5 }) expect(http.patch).toHaveBeenCalledWith(`/platform/v1/last-leg/master-data/vehicles/${longId}/status`, { status: 'INACTIVE', recordVersion: 5 })
}) })
it('preserves A0409 metadata instead of translating it into success', async () => { it('preserves A0409 metadata instead of translating it into success', async () => {
vi.mocked(overseasHttp.put).mockResolvedValue({ code: 'A0409', message: '记录版本已变化', data: null, requestId: 'request-conflict', traceId: 'trace-conflict' }) vi.mocked(http.put).mockResolvedValue({ code: 'A0409', message: '记录版本已变化', data: null, requestId: 'request-conflict', traceId: 'trace-conflict' })
const error = await updateExchangeRate('2026-08', { recordVersion: 1, usdToCnyAverage: 7.2, remark: null }).catch((reason: unknown) => reason) const error = await updateExchangeRate('2026-08', { recordVersion: 1, usdToCnyAverage: 7.2, remark: null }).catch((reason: unknown) => reason)
expect(error).toBeInstanceOf(ApiResponseError) expect(error).toBeInstanceOf(ApiResponseError)
expect(error).toMatchObject({ code: 'A0409', message: '记录版本已变化', requestId: 'request-conflict', traceId: 'trace-conflict' }) expect(error).toMatchObject({ code: 'A0409', message: '记录版本已变化', requestId: 'request-conflict', traceId: 'trace-conflict' })
@@ -126,7 +126,7 @@ describe('overseas master data API', () => {
it('downloads real CSV Blobs and rejects JSON error Blobs with trace metadata', async () => { it('downloads real CSV Blobs and rejects JSON error Blobs with trace metadata', async () => {
const csv = new Blob(['id,status\n1,ACTIVE\n'], { type: 'text/csv;charset=UTF-8' }) const csv = new Blob(['id,status\n1,ACTIVE\n'], { type: 'text/csv;charset=UTF-8' })
vi.mocked(overseasHttp.get) vi.mocked(http.get)
.mockResolvedValueOnce(csv) .mockResolvedValueOnce(csv)
.mockResolvedValueOnce(csv) .mockResolvedValueOnce(csv)
.mockResolvedValueOnce(csv) .mockResolvedValueOnce(csv)
@@ -138,8 +138,8 @@ describe('overseas master data API', () => {
const error = await exportVehicles({}).catch((reason: unknown) => reason) const error = await exportVehicles({}).catch((reason: unknown) => reason)
expect(error).toBeInstanceOf(ApiResponseError) expect(error).toBeInstanceOf(ApiResponseError)
expect(error).toMatchObject({ code: 'A0409', message: '导出条件已变化', requestId: 'request-export' }) expect(error).toMatchObject({ code: 'A0409', message: '导出条件已变化', requestId: 'request-export' })
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'arraybuffer' }) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'arraybuffer' })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'arraybuffer' }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'arraybuffer' })
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'arraybuffer' }) expect(http.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'arraybuffer' })
}) })
}) })
@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { import type {
MasterDataStatus, MonthlyExchangeRate, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption, MasterDataStatus, MonthlyExchangeRate, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption,
VehicleDetail, VehicleOwnership, VehicleRegion, VehicleStatus, VehicleDetail, VehicleOwnership, VehicleRegion, VehicleStatus,
@@ -23,7 +23,7 @@ function page<T, W>(value: PagePayload<W>, normalize: (record: W) => T): PageRes
return { ...value, records: value.records.map(normalize), page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) } return { ...value, records: value.records.map(normalize), page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) }
} }
async function getPage<T, W>(path: string, params: object, normalize: (record: W) => T) { async function getPage<T, W>(path: string, params: object, normalize: (record: W) => T) {
return page(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PagePayload<W>>, ApiEnvelope<PagePayload<W>>>(path, { params })), normalize) return page(unwrapApiResponse(await http.get<ApiEnvelope<PagePayload<W>>, ApiEnvelope<PagePayload<W>>>(path, { params })), normalize)
} }
const numeric = (value: Numeric) => Number(value) const numeric = (value: Numeric) => Number(value)
const nullableNumeric = (value: Numeric | null) => value == null ? null : Number(value) const nullableNumeric = (value: Numeric | null) => value == null ? null : Number(value)
@@ -71,7 +71,7 @@ function csvBlob(payload: DownloadPayload) {
} }
async function download(path: string, params: object) { async function download(path: string, params: object) {
try { try {
const payload = await overseasHttp.get<DownloadPayload, DownloadPayload>(path, { params, responseType: 'arraybuffer' }) const payload = await http.get<DownloadPayload, DownloadPayload>(path, { params, responseType: 'arraybuffer' })
const error = await csvError(payload, 200) const error = await csvError(payload, 200)
if (error) throw error if (error) throw error
return csvBlob(payload) return csvBlob(payload)
@@ -93,23 +93,23 @@ type QuoteEditableFields = Pick<OverseasWarehouseQuote, 'name' | 'providerName'
export type QuoteCreate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'warehouseId' | 'versionCode'> export type QuoteCreate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'warehouseId' | 'versionCode'>
export type QuoteUpdate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'recordVersion'> export type QuoteUpdate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'recordVersion'>
export const listQuotes = (query: QuoteQuery) => getPage<OverseasWarehouseQuote, QuoteWire>(`${base}/overseas-quotes`, query, normalizeQuote) export const listQuotes = (query: QuoteQuery) => getPage<OverseasWarehouseQuote, QuoteWire>(`${base}/overseas-quotes`, query, normalizeQuote)
export async function getQuote(id: string) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`))) } export async function getQuote(id: string) { return normalizeQuoteDetail(unwrapApiResponse(await http.get<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`))) }
export async function listQuoteWarehouseOptions() { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PartnerWarehouseOption[]>, ApiEnvelope<PartnerWarehouseOption[]>>(`${base}/overseas-quotes/warehouse-options`)) } export async function listQuoteWarehouseOptions() { return unwrapApiResponse(await http.get<ApiEnvelope<PartnerWarehouseOption[]>, ApiEnvelope<PartnerWarehouseOption[]>>(`${base}/overseas-quotes/warehouse-options`)) }
export async function createQuote(payload: QuoteCreate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes`, payload))) } export async function createQuote(payload: QuoteCreate) { return normalizeQuoteDetail(unwrapApiResponse(await http.post<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes`, payload))) }
export async function updateQuote(id: string, payload: QuoteUpdate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload))) } export async function updateQuote(id: string, payload: QuoteUpdate) { return normalizeQuoteDetail(unwrapApiResponse(await http.put<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload))) }
export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion }))) } export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return normalizeQuoteDetail(unwrapApiResponse(await http.patch<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
export const exportQuotes = (query: Omit<QuoteQuery, 'page' | 'size'>) => download(`${base}/overseas-quotes/export`, query) export const exportQuotes = (query: Omit<QuoteQuery, 'page' | 'size'>) => download(`${base}/overseas-quotes/export`, query)
export interface ExchangeRateQuery { page: number; size: number; monthFrom?: string; monthTo?: string; status?: MasterDataStatus } export interface ExchangeRateQuery { page: number; size: number; monthFrom?: string; monthTo?: string; status?: MasterDataStatus }
export const listExchangeRates = (query: ExchangeRateQuery) => getPage<MonthlyExchangeRate, ExchangeRateWire>(`${base}/exchange-rates`, query, normalizeExchangeRate) export const listExchangeRates = (query: ExchangeRateQuery) => getPage<MonthlyExchangeRate, ExchangeRateWire>(`${base}/exchange-rates`, query, normalizeExchangeRate)
export async function getExchangeRate(month: string) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`))) } export async function getExchangeRate(month: string) { return normalizeExchangeRate(unwrapApiResponse(await http.get<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`))) }
export async function getExchangeRateSummary(): Promise<ExchangeRateSummary> { export async function getExchangeRateSummary(): Promise<ExchangeRateSummary> {
const value = unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateSummaryWire>, ApiEnvelope<ExchangeRateSummaryWire>>(`${base}/exchange-rates/summary`)) const value = unwrapApiResponse(await http.get<ApiEnvelope<ExchangeRateSummaryWire>, ApiEnvelope<ExchangeRateSummaryWire>>(`${base}/exchange-rates/summary`))
return { ...value, latestUsdToCnyAverage: nullableNumeric(value.latestUsdToCnyAverage) } return { ...value, latestUsdToCnyAverage: nullableNumeric(value.latestUsdToCnyAverage) }
} }
export async function createExchangeRate(payload: { month: string; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates`, payload))) } export async function createExchangeRate(payload: { month: string; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await http.post<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates`, payload))) }
export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload))) } export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await http.put<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload))) }
export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark }))) } export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return normalizeExchangeRate(unwrapApiResponse(await http.patch<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark }))) }
export const exportExchangeRates = (query: Omit<ExchangeRateQuery, 'page' | 'size'>) => download(`${base}/exchange-rates/export`, query) export const exportExchangeRates = (query: Omit<ExchangeRateQuery, 'page' | 'size'>) => download(`${base}/exchange-rates/export`, query)
export interface VehicleQuery { page: number; size: number; keyword?: string; ownership?: VehicleOwnership; region?: VehicleRegion; status?: VehicleStatus } export interface VehicleQuery { page: number; size: number; keyword?: string; ownership?: VehicleOwnership; region?: VehicleRegion; status?: VehicleStatus }
@@ -117,8 +117,8 @@ type VehicleEditableFields = Omit<VehicleDetail, 'id' | 'recordVersion' | 'updat
export type VehicleCreate = VehicleEditableFields & Pick<VehicleDetail, 'status'> export type VehicleCreate = VehicleEditableFields & Pick<VehicleDetail, 'status'>
export type VehicleUpdate = VehicleEditableFields & Pick<VehicleDetail, 'recordVersion'> export type VehicleUpdate = VehicleEditableFields & Pick<VehicleDetail, 'recordVersion'>
export const listVehicles = (query: VehicleQuery) => getPage<VehicleDetail, VehicleWire>(`${base}/vehicles`, query, normalizeVehicle) export const listVehicles = (query: VehicleQuery) => getPage<VehicleDetail, VehicleWire>(`${base}/vehicles`, query, normalizeVehicle)
export async function getVehicle(id: string) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`))) } export async function getVehicle(id: string) { return normalizeVehicle(unwrapApiResponse(await http.get<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`))) }
export async function createVehicle(payload: VehicleCreate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles`, payload))) } export async function createVehicle(payload: VehicleCreate) { return normalizeVehicle(unwrapApiResponse(await http.post<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles`, payload))) }
export async function updateVehicle(id: string, payload: VehicleUpdate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`, payload))) } export async function updateVehicle(id: string, payload: VehicleUpdate) { return normalizeVehicle(unwrapApiResponse(await http.put<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`, payload))) }
export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion }))) } export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return normalizeVehicle(unwrapApiResponse(await http.patch<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
export const exportVehicles = (query: Omit<VehicleQuery, 'page' | 'size'>) => download(`${base}/vehicles/export`, query) export const exportVehicles = (query: Omit<VehicleQuery, 'page' | 'size'>) => download(`${base}/vehicles/export`, query)
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { import {
getCustomerVisit, getCustomerVisit,
listCustomerVisits, listCustomerVisits,
@@ -8,45 +8,45 @@ import {
uploadCustomerVisitAttachment, uploadCustomerVisitAttachment,
} from './customerVisits' } from './customerVisits'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
describe('customer visit API', () => { describe('customer visit API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
vi.mocked(overseasHttp.put).mockReset() vi.mocked(http.put).mockReset()
}) })
it('loads the customer visit page and detail in customer scope', async () => { 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 } }) vi.mocked(http.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' }) 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' } }) expect(http.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: [] } }) vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } })
await getCustomerVisit('9', '100') await getCustomerVisit('9', '100')
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/customer-visits/9', { params: { customerId: '100' } }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/customer-visits/9', { params: { customerId: '100' } })
}) })
it('updates a delivery to an operator-managed nonterminal status', async () => { it('updates a delivery to an operator-managed nonterminal status', async () => {
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERING' } }) vi.mocked(http.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERING' } })
await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' }) await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' }, { params: { customerId: '100' } }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' }, { params: { customerId: '100' } })
}) })
it('uploads and saves repeatable visit material', async () => { it('uploads and saves repeatable visit material', async () => {
const file = new File(['data'], 'visit.pdf', { type: 'application/pdf' }) 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 } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { fileId: '8', fileName: 'visit.pdf', sizeBytes: 4 } })
await uploadCustomerVisitAttachment('9', '100', file) await uploadCustomerVisitAttachment('9', '100', file)
const body = vi.mocked(overseasHttp.post).mock.calls[0]?.[1] as FormData const body = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
expect(body.get('file')).toBe(file) expect(body.get('file')).toBe(file)
expect(overseasHttp.post).toHaveBeenCalledWith( expect(http.post).toHaveBeenCalledWith(
'/platform/v1/last-leg/customer-visits/9/attachments', '/platform/v1/last-leg/customer-visits/9/attachments',
body, body,
{ params: { customerId: '100' }, headers: { 'Content-Type': 'multipart/form-data' } }, { params: { customerId: '100' }, headers: { 'Content-Type': 'multipart/form-data' } },
) )
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } }) vi.mocked(http.put).mockResolvedValue({ code: '200', data: { visit: { id: '9' }, inboundOrders: [] } })
await saveCustomerVisit('9', '100', { selectedInboundOrderId: '7', visitResult: 'NORMAL', attachments: [] }) 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' } }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/customer-visits/9', expect.objectContaining({ selectedInboundOrderId: '7' }), { params: { customerId: '100' } })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export interface CustomerVisitAttachment { export interface CustomerVisitAttachment {
fileId: string fileId: string
@@ -63,14 +63,14 @@ export async function listCustomerVisits(params: {
completedFrom?: string completedFrom?: string
completedTo?: string completedTo?: string
}) { }) {
const response = await overseasHttp.get<ApiEnvelope<CustomerVisitPage>, ApiEnvelope<CustomerVisitPage>>( const response = await http.get<ApiEnvelope<CustomerVisitPage>, ApiEnvelope<CustomerVisitPage>>(
'/platform/v1/last-leg/customer-visits', { params }, '/platform/v1/last-leg/customer-visits', { params },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function getCustomerVisit(visitId: string, customerId: string) { export async function getCustomerVisit(visitId: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>( const response = await http.get<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>(
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, { params: { customerId } }, `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, { params: { customerId } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -83,7 +83,7 @@ export async function saveCustomerVisit(visitId: string, customerId: string, pay
remark?: string remark?: string
attachments: CustomerVisitAttachment[] attachments: CustomerVisitAttachment[]
}) { }) {
const response = await overseasHttp.put<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>( const response = await http.put<ApiEnvelope<CustomerVisitDetail>, ApiEnvelope<CustomerVisitDetail>>(
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, payload, { params: { customerId } }, `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}`, payload, { params: { customerId } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -92,7 +92,7 @@ export async function saveCustomerVisit(visitId: string, customerId: string, pay
export async function uploadCustomerVisitAttachment(visitId: string, customerId: string, file: File) { export async function uploadCustomerVisitAttachment(visitId: string, customerId: string, file: File) {
const body = new FormData() const body = new FormData()
body.append('file', file) body.append('file', file)
const response = await overseasHttp.post<ApiEnvelope<CustomerVisitAttachment>, ApiEnvelope<CustomerVisitAttachment>>( const response = await http.post<ApiEnvelope<CustomerVisitAttachment>, ApiEnvelope<CustomerVisitAttachment>>(
`/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}/attachments`, body, `/platform/v1/last-leg/customer-visits/${encodeURIComponent(visitId)}/attachments`, body,
{ params: { customerId }, headers: { 'Content-Type': 'multipart/form-data' } }, { params: { customerId }, headers: { 'Content-Type': 'multipart/form-data' } },
) )
@@ -100,7 +100,7 @@ export async function uploadCustomerVisitAttachment(visitId: string, customerId:
} }
export async function updateDeliveryOrderStatus(deliveryNo: string, customerId: string, payload: { status: string; expectedVersion: number; remark?: string }) { export async function updateDeliveryOrderStatus(deliveryNo: string, customerId: string, payload: { status: string; expectedVersion: number; 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 }>>( const response = await http.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, `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, payload,
{ params: { customerId } }, { params: { customerId } },
) )
@@ -1,28 +1,28 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { deliveryDocumentCategories, getDeliveryDocuments, saveDeliveryDocuments, uploadDeliveryDocument, downloadDeliveryDocument } from './deliveryDocuments' import { deliveryDocumentCategories, getDeliveryDocuments, saveDeliveryDocuments, uploadDeliveryDocument, downloadDeliveryDocument } from './deliveryDocuments'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
describe('delivery document API', () => { describe('delivery document API', () => {
beforeEach(() => vi.clearAllMocks()) beforeEach(() => vi.clearAllMocks())
it('scopes reads and full three-category snapshots by customer and delivery number', async () => { it('scopes reads and full three-category snapshots by customer and delivery number', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: {} }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: {} })
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: {} }) vi.mocked(http.put).mockResolvedValue({ code: '200', data: {} })
await getDeliveryDocuments('DL/1', '9007199254740993') await getDeliveryDocuments('DL/1', '9007199254740993')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents', { params: { customerId: '9007199254740993' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents', { params: { customerId: '9007199254740993' } })
const body = { expectedVersion: 8, idempotencyKey: 'save-1', bolNo: null, documents: deliveryDocumentCategories.map(item => ({ category: item.category, fileIds: [] })) } const body = { expectedVersion: 8, idempotencyKey: 'save-1', bolNo: null, documents: deliveryDocumentCategories.map(item => ({ category: item.category, fileIds: [] })) }
await saveDeliveryDocuments('DL/1', '9007199254740993', body) await saveDeliveryDocuments('DL/1', '9007199254740993', body)
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents', body, { params: { customerId: '9007199254740993' } }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents', body, { params: { customerId: '9007199254740993' } })
}) })
it('uploads only the file and downloads through the authenticated category endpoint', async () => { it('uploads only the file and downloads through the authenticated category endpoint', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { fileId: '9007199254740995' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { fileId: '9007199254740995' } })
const file = new File(['data'], 'proof.bin') const file = new File(['data'], 'proof.bin')
await uploadDeliveryDocument('DL/1', '100', 'RECEIPT', file) await uploadDeliveryDocument('DL/1', '100', 'RECEIPT', file)
const [url, body, config] = vi.mocked(overseasHttp.post).mock.calls[0]! const [url, body, config] = vi.mocked(http.post).mock.calls[0]!
expect(url).toBe('/platform/v1/last-leg/delivery-orders/DL%2F1/documents/RECEIPT/files') expect(url).toBe('/platform/v1/last-leg/delivery-orders/DL%2F1/documents/RECEIPT/files')
expect(Array.from((body as FormData).keys())).toEqual(['file']) expect(Array.from((body as FormData).keys())).toEqual(['file'])
expect((body as FormData).get('file')).toBe(file) expect((body as FormData).get('file')).toBe(file)
expect(config).toEqual({ params: { customerId: '100' } }) expect(config).toEqual({ params: { customerId: '100' } })
await downloadDeliveryDocument('DL/1', '100', 'RECEIPT', '9007199254740995') await downloadDeliveryDocument('DL/1', '100', 'RECEIPT', '9007199254740995')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents/RECEIPT/files/9007199254740995/download', { params: { customerId: '100' }, responseType: 'blob' }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DL%2F1/documents/RECEIPT/files/9007199254740995/download', { params: { customerId: '100' }, responseType: 'blob' })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export const deliveryDocumentCategories = [ export const deliveryDocumentCategories = [
{ category: 'LOADING_PHOTO', label: '装车照片' }, { category: 'LOADING_PHOTO', label: '装车照片' },
@@ -18,16 +18,16 @@ export interface DeliveryDocuments extends DeliveryDocumentSummary {
export interface SaveDeliveryDocuments { expectedVersion: number; idempotencyKey: string; bolNo: string | null; documents: Array<{ category: DeliveryDocumentCategory; fileIds: string[] }> } export interface SaveDeliveryDocuments { expectedVersion: number; idempotencyKey: string; bolNo: string | null; documents: Array<{ category: DeliveryDocumentCategory; fileIds: string[] }> }
const path = (deliveryNo: string) => `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/documents` const path = (deliveryNo: string) => `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/documents`
export async function getDeliveryDocuments(deliveryNo: string, customerId: string) { export async function getDeliveryDocuments(deliveryNo: string, customerId: string) {
return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<DeliveryDocuments>, ApiEnvelope<DeliveryDocuments>>(path(deliveryNo), { params: { customerId } })) return unwrapApiResponse(await http.get<ApiEnvelope<DeliveryDocuments>, ApiEnvelope<DeliveryDocuments>>(path(deliveryNo), { params: { customerId } }))
} }
export async function saveDeliveryDocuments(deliveryNo: string, customerId: string, body: SaveDeliveryDocuments) { export async function saveDeliveryDocuments(deliveryNo: string, customerId: string, body: SaveDeliveryDocuments) {
return unwrapApiResponse(await overseasHttp.put<ApiEnvelope<DeliveryDocuments>, ApiEnvelope<DeliveryDocuments>>(path(deliveryNo), body, { params: { customerId } })) return unwrapApiResponse(await http.put<ApiEnvelope<DeliveryDocuments>, ApiEnvelope<DeliveryDocuments>>(path(deliveryNo), body, { params: { customerId } }))
} }
export async function uploadDeliveryDocument(deliveryNo: string, customerId: string, category: DeliveryDocumentCategory, file: File) { export async function uploadDeliveryDocument(deliveryNo: string, customerId: string, category: DeliveryDocumentCategory, file: File) {
const body = new FormData() const body = new FormData()
body.append('file', file) body.append('file', file)
return unwrapApiResponse(await overseasHttp.post<ApiEnvelope<DeliveryDocumentFile>, ApiEnvelope<DeliveryDocumentFile>>(`${path(deliveryNo)}/${category}/files`, body, { params: { customerId } })) return unwrapApiResponse(await http.post<ApiEnvelope<DeliveryDocumentFile>, ApiEnvelope<DeliveryDocumentFile>>(`${path(deliveryNo)}/${category}/files`, body, { params: { customerId } }))
} }
export async function downloadDeliveryDocument(deliveryNo: string, customerId: string, category: DeliveryDocumentCategory, fileId: string) { export async function downloadDeliveryDocument(deliveryNo: string, customerId: string, category: DeliveryDocumentCategory, fileId: string) {
return overseasHttp.get<Blob, Blob>(`${path(deliveryNo)}/${category}/files/${encodeURIComponent(fileId)}/download`, { params: { customerId }, responseType: 'blob' }) return http.get<Blob, Blob>(`${path(deliveryNo)}/${category}/files/${encodeURIComponent(fileId)}/download`, { params: { customerId }, responseType: 'blob' })
} }
@@ -1,48 +1,48 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from './deliveryOrders' import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from './deliveryOrders'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
describe('delivery order API', () => { describe('delivery order API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
}) })
it('passes paging and expanded filters and normalizes page numbers', async () => { 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' } }) vi.mocked(http.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' }) 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(http.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 }) expect(result).toMatchObject({ page: 2, size: 20, total: 41, pages: 3 })
}) })
it('loads an encoded detail in its explicit customer scope', async () => { it('loads an encoded detail in its explicit customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1', version: '0' }, skus: [], fees: [], operations: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1', version: '0' }, skus: [], fees: [], operations: [] } })
await getDeliveryOrder('DEL/1', '1001') await getDeliveryOrder('DEL/1', '1001')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001', page: 1, size: 200 } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001', page: 1, size: 200 } })
}) })
it.each(['0', '7', '9007199254740991'])('reads Java Long version %s for status editing', async version => { it.each(['0', '7', '9007199254740991'])('reads Java Long version %s for status editing', async version => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { version, id: '2097515131537313794' } } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version, id: '2097515131537313794' } } })
const detail = await getDeliveryOrder('DEL/1', '1001') const detail = await getDeliveryOrder('DEL/1', '1001')
expect(detail.order.version).toBe(Number(version)) expect(detail.order.version).toBe(Number(version))
expect(detail.order.id).toBe('2097515131537313794') expect(detail.order.id).toBe('2097515131537313794')
}) })
it.each([null, undefined, '', ' ', '-1', '1.5', '9007199254740992', -1])('rejects invalid version %s', async version => { it.each([null, undefined, '', ' ', '-1', '1.5', '9007199254740992', -1])('rejects invalid version %s', async version => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { version } } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version } } })
await expect(getDeliveryOrder('DEL/1', '1001')).rejects.toThrow('派送单缺少有效版本') await expect(getDeliveryOrder('DEL/1', '1001')).rejects.toThrow('派送单缺少有效版本')
}) })
it('loads return options and marks a return in the explicit customer scope', async () => { it('loads return options and marks a return in the explicit customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: [{ inboundOrderId: '9', inboundCode: 'IN-1' }] }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: [{ inboundOrderId: '9', inboundCode: 'IN-1' }] })
await listDeliveryReturnInboundOptions('DEL/1', '1001') await listDeliveryReturnInboundOptions('DEL/1', '1001')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/return-inbound-options', { params: { customerId: '1001' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/return-inbound-options', { params: { customerId: '1001' } })
const payload = { inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE' as const, items: [{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', packageCount: 2 }], idempotencyKey: 'return-1' } const payload = { inboundOrderId: '9', reason: '客户拒收', returnDirection: 'OVERSEAS_WAREHOUSE' as const, items: [{ outboundPalletItemId: '9007199254740995', inventoryPositionId: '9007199254740996', packageCount: 2 }], idempotencyKey: 'return-1' }
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '19', exceptionNo: 'EXC-1', status: 'PENDING' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { id: '19', exceptionNo: 'EXC-1', status: 'PENDING' } })
await markDeliveryReturn('DEL/1', '1001', payload) await markDeliveryReturn('DEL/1', '1001', payload)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/mark-return', payload, { params: { customerId: '1001' } }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/mark-return', payload, { params: { customerId: '1001' } })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
import type { DeliveryDocumentSummary } from './deliveryDocuments' import type { DeliveryDocumentSummary } from './deliveryDocuments'
@@ -94,14 +94,14 @@ export interface MarkDeliveryReturnPayload {
export async function listDeliveryOrders(query: DeliveryOrderQuery) { export async function listDeliveryOrders(query: DeliveryOrderQuery) {
const { customerId, ...params } = query const { customerId, ...params } = query
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>, ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>, ApiEnvelope<OverseasPagePayload<DeliveryOrderRow>>>(
'/platform/v1/last-leg/delivery-orders', { params: { ...params, ...(customerId ? { customerId } : {}) } }, '/platform/v1/last-leg/delivery-orders', { params: { ...params, ...(customerId ? { customerId } : {}) } },
) )
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function getDeliveryOrder(deliveryNo: string, customerId: string) { export async function getDeliveryOrder(deliveryNo: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<DeliveryOrderDetail>, ApiEnvelope<DeliveryOrderDetail>>( const response = await http.get<ApiEnvelope<DeliveryOrderDetail>, ApiEnvelope<DeliveryOrderDetail>>(
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId, page: 1, size: 200 } }, `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId, page: 1, size: 200 } },
) )
const detail = unwrapApiResponse(response) const detail = unwrapApiResponse(response)
@@ -116,14 +116,14 @@ export async function getDeliveryOrder(deliveryNo: string, customerId: string) {
} }
export async function listDeliveryReturnInboundOptions(deliveryNo: string, customerId: string) { export async function listDeliveryReturnInboundOptions(deliveryNo: string, customerId: string) {
const response = await overseasHttp.get<ApiEnvelope<DeliveryReturnRelation[]>, ApiEnvelope<DeliveryReturnRelation[]>>( const response = await http.get<ApiEnvelope<DeliveryReturnRelation[]>, ApiEnvelope<DeliveryReturnRelation[]>>(
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { params: { customerId } }, `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/return-inbound-options`, { params: { customerId } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: MarkDeliveryReturnPayload) { export async function markDeliveryReturn(deliveryNo: string, customerId: string, payload: MarkDeliveryReturnPayload) {
const response = await overseasHttp.post<ApiEnvelope<{ id: string; exceptionNo: string; status: string }>, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>( const response = await http.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 } }, `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/mark-return`, payload, { params: { customerId } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -1,19 +1,19 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches' import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
describe('dispatch overseas API', () => { describe('dispatch overseas API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
vi.mocked(overseasHttp.put).mockReset() vi.mocked(http.put).mockReset()
vi.mocked(overseasHttp.delete).mockReset() vi.mocked(http.delete).mockReset()
}) })
it('uses the platform dispatch page without customer scope', async () => { it('uses the platform dispatch page without customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ vi.mocked(http.get).mockResolvedValue({
code: '200', message: 'success', data: { records: [], page: 2, size: 50, total: 0, pages: 0 }, code: '200', message: 'success', data: { records: [], page: 2, size: 50, total: 0, pages: 0 },
}) })
@@ -22,7 +22,7 @@ describe('dispatch overseas API', () => {
vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1',
}) })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches', {
params: { params: {
page: 2, size: 50, dispatchNo: 'PC-1', page: 2, size: 50, dispatchNo: 'PC-1',
vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1',
@@ -31,78 +31,78 @@ describe('dispatch overseas API', () => {
}) })
it('loads a platform dispatch detail by encoded dispatch number', async () => { it('loads a platform dispatch detail by encoded dispatch number', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC/1' } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC/1' } })
await getDispatch('PC/1') await getDispatch('PC/1')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1')
}) })
it('requests the generated BOL as a PDF blob', async () => { it('requests the generated BOL as a PDF blob', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue(new Blob(['%PDF-test'])) vi.mocked(http.get).mockResolvedValue(new Blob(['%PDF-test']))
await getDispatchBolPdf('PC/1') await getDispatchBolPdf('PC/1')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/prints/bol', { responseType: 'blob' }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/prints/bol', { responseType: 'blob' })
}) })
it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => { it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } })
await listDispatchCandidates({ planIds: '101,102', fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' }) await listDispatchCandidates({ planIds: '101,102', fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' })
await preArrangeDispatch({ planPalletIds: ['190000000000000099'] }) await preArrangeDispatch({ planPalletIds: ['190000000000000099'] })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/candidates', { params: { planIds: '101,102', fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/candidates', { params: { planIds: '101,102', fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' } })
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: ['190000000000000099'] }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: ['190000000000000099'] })
}) })
it('loads platform candidates without selected inbound plans', async () => { it('loads platform candidates without selected inbound plans', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
await listDispatchCandidates({ sourceStage: 'RECEIVED' }) await listDispatchCandidates({ sourceStage: 'RECEIVED' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/candidates', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/candidates', {
params: { sourceStage: 'RECEIVED' }, params: { sourceStage: 'RECEIVED' },
}) })
}) })
it('locks, unlocks, and releases intelligent dispatch drafts through dedicated endpoints', async () => { it('locks, unlocks, and releases intelligent dispatch drafts through dedicated endpoints', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: null }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: null })
await lockDispatchDraftPallet('99', 'draft-1') await lockDispatchDraftPallet('99', 'draft-1')
await unlockDispatchDraftPallet('99', 'draft-1') await unlockDispatchDraftPallet('99', 'draft-1')
await releaseDispatchDraft('draft-1') await releaseDispatchDraft('draft-1')
expect(overseasHttp.post).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId: '99', draftToken: 'draft-1' }) expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId: '99', draftToken: 'draft-1' })
expect(overseasHttp.post).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId: '99', draftToken: 'draft-1' }) expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId: '99', draftToken: 'draft-1' })
expect(overseasHttp.post).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/dispatches/draft-locks/release', { draftToken: 'draft-1' }) expect(http.post).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/dispatches/draft-locks/release', { draftToken: 'draft-1' })
}) })
it('edits and cancels only by dispatchNo without customer scope or status mutation', async () => { it('edits and cancels only by dispatchNo without customer scope or status mutation', async () => {
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC/1' } }) vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC/1' } })
vi.mocked(overseasHttp.delete).mockResolvedValue({ code: '200', message: 'success', data: null }) vi.mocked(http.delete).mockResolvedValue({ code: '200', message: 'success', data: null })
await updateDispatch('PC/1', { vehiclePlate: 'CA-100' }) await updateDispatch('PC/1', { vehiclePlate: 'CA-100' })
await cancelDispatch('PC/1') await cancelDispatch('PC/1')
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', { vehiclePlate: 'CA-100' }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1', { vehiclePlate: 'CA-100' })
expect(overseasHttp.put).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ status: expect.anything() })) expect(http.put).not.toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ status: expect.anything() }))
expect(overseasHttp.delete).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1') expect(http.delete).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1')
}) })
it('transfers to delivery only by dispatchNo without customerId', async () => { it('transfers to delivery only by dispatchNo without customerId', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { dispatchNo: 'PC/1', outboundNo: 'OUT-1', outboundStatus: 'PICKED', palletCount: 2, packageCount: 20, deliveries: [{ deliveryNo: 'DEL-1', deliveryStatus: 'WAIT_DELIVERY', destinationWarehouseCode: 'ACY2', palletCount: 2, packageCount: 20 }], replayed: false } }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { dispatchNo: 'PC/1', outboundNo: 'OUT-1', outboundStatus: 'PICKED', palletCount: 2, packageCount: 20, deliveries: [{ deliveryNo: 'DEL-1', deliveryStatus: 'WAIT_DELIVERY', destinationWarehouseCode: 'ACY2', palletCount: 2, packageCount: 20 }], replayed: false } })
const payload = { idempotencyKey: 'transfer-1', plannedDeliveryAt: '2026-09-01T10:00:00', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费用', direction: 'RECEIVABLE' as const, currency: 'USD' as const, amount: 10 }] } const payload = { idempotencyKey: 'transfer-1', plannedDeliveryAt: '2026-09-01T10:00:00', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费用', direction: 'RECEIVABLE' as const, currency: 'USD' as const, amount: 10 }] }
await transferDispatchToDelivery('PC/1', payload) await transferDispatchToDelivery('PC/1', payload)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/transfer-delivery', payload) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/transfer-delivery', payload)
}) })
it('loads drafts and preserves long container ids in create and delete paths', async () => { it('loads drafts and preserves long container ids in create and delete paths', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: [] })
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '190000000000000099', capacity: 30, palletIds: [] } }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '190000000000000099', capacity: 30, palletIds: [] } })
vi.mocked(overseasHttp.delete).mockResolvedValue({ code: '200', message: 'success', data: null }) vi.mocked(http.delete).mockResolvedValue({ code: '200', message: 'success', data: null })
await listDispatchDrafts() await listDispatchDrafts()
await createDispatchDraftContainer('draft/1', 30) await createDispatchDraftContainer('draft/1', 30)
await deleteDispatchDraftContainer('draft/1', '190000000000000099') await deleteDispatchDraftContainer('draft/1', '190000000000000099')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts')
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken: 'draft/1', capacity: 30 }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken: 'draft/1', capacity: 30 })
expect(overseasHttp.delete).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts/draft%2F1/containers/190000000000000099') expect(http.delete).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/drafts/draft%2F1/containers/190000000000000099')
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface DispatchQuery { export interface DispatchQuery {
@@ -140,7 +140,7 @@ export interface DispatchItemDto {
} }
export async function listDispatches(query: DispatchQuery) { export async function listDispatches(query: DispatchQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>, ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>, ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>>(
'/platform/v1/last-leg/dispatches', '/platform/v1/last-leg/dispatches',
{ params: query }, { params: query },
) )
@@ -148,42 +148,42 @@ export async function listDispatches(query: DispatchQuery) {
} }
export async function getDispatch(dispatchNo: string) { export async function getDispatch(dispatchNo: string) {
const response = await overseasHttp.get<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>( const response = await http.get<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function getDispatchBolPdf(dispatchNo: string) { export async function getDispatchBolPdf(dispatchNo: string) {
return overseasHttp.get<Blob, Blob>( return http.get<Blob, Blob>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/prints/bol`, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/prints/bol`,
{ responseType: 'blob' }, { responseType: 'blob' },
) )
} }
export async function updateDispatch(dispatchNo: string, payload: UpdateDispatchPayload) { export async function updateDispatch(dispatchNo: string, payload: UpdateDispatchPayload) {
const response = await overseasHttp.put<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>( const response = await http.put<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, payload, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function cancelDispatch(dispatchNo: string) { export async function cancelDispatch(dispatchNo: string) {
const response = await overseasHttp.delete<ApiEnvelope<null>, ApiEnvelope<null>>( const response = await http.delete<ApiEnvelope<null>, ApiEnvelope<null>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function removeDispatchItem(dispatchNo: string, itemId: string) { export async function removeDispatchItem(dispatchNo: string, itemId: string) {
const response = await overseasHttp.delete<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>( const response = await http.delete<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(itemId)}`, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(itemId)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listDispatchCandidates(query: DispatchCandidateQuery) { export async function listDispatchCandidates(query: DispatchCandidateQuery) {
const response = await overseasHttp.get<ApiEnvelope<DispatchPalletCandidateDto[]>, ApiEnvelope<DispatchPalletCandidateDto[]>>( const response = await http.get<ApiEnvelope<DispatchPalletCandidateDto[]>, ApiEnvelope<DispatchPalletCandidateDto[]>>(
'/platform/v1/last-leg/dispatches/candidates', '/platform/v1/last-leg/dispatches/candidates',
{ params: query }, { params: query },
) )
@@ -191,7 +191,7 @@ export async function listDispatchCandidates(query: DispatchCandidateQuery) {
} }
export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) { export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) {
const response = await overseasHttp.post<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>( const response = await http.post<ApiEnvelope<DispatchSummaryDto>, ApiEnvelope<DispatchSummaryDto>>(
'/platform/v1/last-leg/dispatches/pre-arrange', '/platform/v1/last-leg/dispatches/pre-arrange',
payload, payload,
) )
@@ -199,34 +199,34 @@ export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) {
} }
export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string, draftContainerId?: string) { export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string, draftContainerId?: string) {
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>( const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(
'/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken, ...(draftContainerId ? { draftContainerId } : {}) }, '/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken, ...(draftContainerId ? { draftContainerId } : {}) },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listDispatchDrafts() { export async function listDispatchDrafts() {
const response = await overseasHttp.get<ApiEnvelope<DispatchDraftDto[]>, ApiEnvelope<DispatchDraftDto[]>>('/platform/v1/last-leg/dispatches/drafts') const response = await http.get<ApiEnvelope<DispatchDraftDto[]>, ApiEnvelope<DispatchDraftDto[]>>('/platform/v1/last-leg/dispatches/drafts')
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createDispatchDraftContainer(draftToken: string, capacity: number) { export async function createDispatchDraftContainer(draftToken: string, capacity: number) {
const response = await overseasHttp.post<ApiEnvelope<DispatchDraftContainerDto>, ApiEnvelope<DispatchDraftContainerDto>>('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken, capacity }) const response = await http.post<ApiEnvelope<DispatchDraftContainerDto>, ApiEnvelope<DispatchDraftContainerDto>>('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken, capacity })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function deleteDispatchDraftContainer(draftToken: string, containerId: string) { export async function deleteDispatchDraftContainer(draftToken: string, containerId: string) {
const response = await overseasHttp.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/dispatches/drafts/${encodeURIComponent(draftToken)}/containers/${encodeURIComponent(containerId)}`) const response = await http.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/dispatches/drafts/${encodeURIComponent(draftToken)}/containers/${encodeURIComponent(containerId)}`)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function unlockDispatchDraftPallet(planPalletId: string, draftToken: string) { export async function unlockDispatchDraftPallet(planPalletId: string, draftToken: string) {
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>( const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(
'/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId, draftToken }, '/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId, draftToken },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function releaseDispatchDraft(draftToken: string) { export async function releaseDispatchDraft(draftToken: string) {
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>( const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(
'/platform/v1/last-leg/dispatches/draft-locks/release', { draftToken }, '/platform/v1/last-leg/dispatches/draft-locks/release', { draftToken },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -257,7 +257,7 @@ export interface TransferDeliveryPayload {
periodStart?: string; periodEnd?: string; remark?: string }> periodStart?: string; periodEnd?: string; remark?: string }>
} }
export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) { export async function transferDispatchToDelivery(dispatchNo: string, payload: TransferDeliveryPayload) {
const response = await overseasHttp.post<ApiEnvelope<TransferDeliveryResult>, ApiEnvelope<TransferDeliveryResult>>( const response = await http.post<ApiEnvelope<TransferDeliveryResult>, ApiEnvelope<TransferDeliveryResult>>(
`/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, payload, `/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -1,26 +1,26 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from './exceptionOrders' import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation } from './exceptionOrders'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
describe('exception order API', () => { describe('exception order API', () => {
beforeEach(() => { vi.mocked(overseasHttp.get).mockReset(); vi.mocked(overseasHttp.post).mockReset() }) beforeEach(() => { vi.mocked(http.get).mockReset(); vi.mocked(http.post).mockReset() })
it('queries the real paginated exception endpoint', async () => { 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' } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [], page: '1', size: '20', total: '2', pages: '1' } })
const result = await listExceptionOrders({ page: 1, size: 20, keyword: 'EXC' }) 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(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', { params: { page: 1, size: 20, keyword: 'EXC' } })
expect(result.total).toBe(2) expect(result.total).toBe(2)
}) })
it('resolves relation before creating an exception document', async () => { 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' } }) vi.mocked(http.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' }) 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' } }) expect(http.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' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { id: '99', exceptionNo: 'EXC-1', status: 'PENDING' } })
await createExceptionOrder({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', exceptionType: 'RETURN', reason: '拒收' }) 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' })) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', expect.objectContaining({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' }))
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' 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 ExceptionOrderQuery { page: number; size: number; keyword?: string; exceptionType?: string; status?: string; customerId?: string; warehouseId?: string; createdFrom?: string; createdTo?: string }
@@ -19,14 +19,14 @@ export interface ExceptionRelation {
export interface CreateExceptionOrderPayload { customerId?: string; inboundCode?: string; lastMileOrderNo?: string; exceptionType: string; reason: string; packageCount?: number; skuCode?: string } export interface CreateExceptionOrderPayload { customerId?: string; inboundCode?: string; lastMileOrderNo?: string; exceptionType: string; reason: string; packageCount?: number; skuCode?: string }
export async function listExceptionOrders(query: ExceptionOrderQuery) { 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 }) const response = await http.get<ApiEnvelope<OverseasPagePayload<ExceptionOrderRow>>, ApiEnvelope<OverseasPagePayload<ExceptionOrderRow>>>('/platform/v1/last-leg/exception-orders', { params: query })
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function resolveExceptionRelation(params: { customerId?: string; inboundCode?: string; lastMileOrderNo?: string }) { 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 }) const response = await http.get<ApiEnvelope<ExceptionRelation>, ApiEnvelope<ExceptionRelation>>('/platform/v1/last-leg/exception-orders/resolve', { params })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createExceptionOrder(payload: CreateExceptionOrderPayload) { 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) const response = await http.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) return unwrapApiResponse(response)
} }
@@ -1,38 +1,38 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment } from './inboundOrders' import { createFbaAppointment, getFbaAppointment, listInboundOrders, updateFbaAppointment } from './inboundOrders'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
describe('inbound order overseas API', () => { describe('inbound order overseas API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
vi.mocked(overseasHttp.put).mockReset() vi.mocked(http.put).mockReset()
}) })
it('loads appointment details and updates the existing appointment without replacing its lines', async () => { it('loads appointment details and updates the existing appointment without replacing its lines', async () => {
const detail = { id: '1', customerId: '190000000000000001', appointmentNo: 'APT/1', appointmentAt: '2026-08-20T10:30:00', expectedLoadingAt: '2026-08-20T09:00:00', appointmentStatus: 'SUCCESS', selectedCount: 2, fbaWarehouseCode: 'ONT8', items: [] } const detail = { id: '1', customerId: '190000000000000001', appointmentNo: 'APT/1', appointmentAt: '2026-08-20T10:30:00', expectedLoadingAt: '2026-08-20T09:00:00', appointmentStatus: 'SUCCESS', selectedCount: 2, fbaWarehouseCode: 'ONT8', items: [] }
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: detail }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: detail })
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: detail }) vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: detail })
await getFbaAppointment('190000000000000001', 'APT/1') await getFbaAppointment('190000000000000001', 'APT/1')
await updateFbaAppointment('APT/1', { customerId: '190000000000000001', appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: 'SUCCESS' }) await updateFbaAppointment('APT/1', { customerId: '190000000000000001', appointmentAt: detail.appointmentAt, expectedLoadingAt: detail.expectedLoadingAt, appointmentStatus: 'SUCCESS' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', { params: { customerId: '190000000000000001' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', { params: { customerId: '190000000000000001' } })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', expect.objectContaining({ customerId: '190000000000000001' })) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', expect.objectContaining({ customerId: '190000000000000001' }))
}) })
it('loads the paginated list across all customers without a customer scope', async () => { it('loads the paginated list across all customers without a customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } })
await listInboundOrders({ page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }) await listInboundOrders({ page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', {
params: { page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }, params: { page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' },
}) })
}) })
it('preserves selected line ids as strings when creating an appointment', async () => { it('preserves selected line ids as strings when creating an appointment', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', appointmentNo: 'APT-1', appointmentStatus: 'SUCCESS', selectedCount: 1, fbaWarehouseCode: 'ONT8' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', appointmentNo: 'APT-1', appointmentStatus: 'SUCCESS', selectedCount: 1, fbaWarehouseCode: 'ONT8' } })
const payload = { const payload = {
customerId: '190000000000000001', customerId: '190000000000000001',
lineIds: ['190000000000000099'], lineIds: ['190000000000000099'],
@@ -42,6 +42,6 @@ describe('inbound order overseas API', () => {
appointmentStatus: 'SUCCESS' as const, appointmentStatus: 'SUCCESS' as const,
} }
await createFbaAppointment(payload) await createFbaAppointment(payload)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload)
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface InboundOrderQuery { export interface InboundOrderQuery {
@@ -79,21 +79,21 @@ export interface FbaAppointmentDto {
export type UpdateFbaAppointmentPayload = Omit<BatchFbaAppointmentPayload, 'lineIds' | 'appointmentNo'> export type UpdateFbaAppointmentPayload = Omit<BatchFbaAppointmentPayload, 'lineIds' | 'appointmentNo'>
export async function listInboundOrders(query: InboundOrderQuery) { export async function listInboundOrders(query: InboundOrderQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>>(
'/platform/v1/last-leg/inbound-plans', { params: query }, '/platform/v1/last-leg/inbound-plans', { params: query },
) )
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function createFbaAppointment(payload: BatchFbaAppointmentPayload) { export async function createFbaAppointment(payload: BatchFbaAppointmentPayload) {
const response = await overseasHttp.post<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>( const response = await http.post<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>(
'/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload, '/platform/v1/last-leg/inbound-plans/fba-appointments/batch', payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function getFbaAppointment(customerId: string, appointmentNo: string) { export async function getFbaAppointment(customerId: string, appointmentNo: string) {
const response = await overseasHttp.get<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>( const response = await http.get<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>(
`/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`, `/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`,
{ params: { customerId } }, { params: { customerId } },
) )
@@ -101,7 +101,7 @@ export async function getFbaAppointment(customerId: string, appointmentNo: strin
} }
export async function updateFbaAppointment(appointmentNo: string, payload: UpdateFbaAppointmentPayload) { export async function updateFbaAppointment(appointmentNo: string, payload: UpdateFbaAppointmentPayload) {
const response = await overseasHttp.put<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>( const response = await http.put<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>(
`/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`, `/platform/v1/last-leg/inbound-plans/fba-appointments/${encodeURIComponent(appointmentNo)}`,
payload, payload,
) )
@@ -109,7 +109,7 @@ export async function updateFbaAppointment(appointmentNo: string, payload: Updat
} }
export async function updateInboundPlan(planNo: string, payload: { customerId: string; overseasWarehouseId: string; warehouseCode?: string; warehouseName?: string; version: number }) { export async function updateInboundPlan(planNo: string, payload: { customerId: string; overseasWarehouseId: string; warehouseCode?: string; warehouseName?: string; version: number }) {
const response = await overseasHttp.put<ApiEnvelope<InboundOrderSummaryDto>, ApiEnvelope<InboundOrderSummaryDto>>( const response = await http.put<ApiEnvelope<InboundOrderSummaryDto>, ApiEnvelope<InboundOrderSummaryDto>>(
`/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, payload, `/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -1,18 +1,18 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { getInboundPlan, listInboundPlans } from './inboundPlans' import { getInboundPlan, listInboundPlans } from './inboundPlans'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn() } }))
describe('inbound plan overseas API', () => { describe('inbound plan overseas API', () => {
beforeEach(() => vi.mocked(overseasHttp.get).mockReset()) beforeEach(() => vi.mocked(http.get).mockReset())
it('pages plans with explicit customer scope', async () => { it('pages plans with explicit customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'ok', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'ok', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } })
await listInboundPlans({ customerId: '190000000000000001', page: 1, size: 20, inboundCode: 'IN-1' }) await listInboundPlans({ customerId: '190000000000000001', page: 1, size: 20, inboundCode: 'IN-1' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', { params: { customerId: '190000000000000001', page: 1, size: 20, inboundCode: 'IN-1' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', { params: { customerId: '190000000000000001', page: 1, size: 20, inboundCode: 'IN-1' } })
}) })
it('gets detail by encoded plan number', async () => { it('gets detail by encoded plan number', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'ok', data: { id: '1', planNo: 'IP/1', customerId: '2', sourceOrderId: '190000000000000099', lineCount: 0, palletCount: 0, totalGrossWeightKg: 0, totalNetWeightKg: 0, totalVolumeCbm: 0, lines: [], pallets: [], allocations: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'ok', data: { id: '1', planNo: 'IP/1', customerId: '2', sourceOrderId: '190000000000000099', lineCount: 0, palletCount: 0, totalGrossWeightKg: 0, totalNetWeightKg: 0, totalVolumeCbm: 0, lines: [], pallets: [], allocations: [] } })
await getInboundPlan('IP/1', '2') await getInboundPlan('IP/1', '2')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/IP%2F1', { params: { customerId: '2' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/IP%2F1', { params: { customerId: '2' } })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface InboundPlanQuery { export interface InboundPlanQuery {
@@ -74,11 +74,11 @@ export interface InboundPlanDetailDto {
export async function listInboundPlans(query: InboundPlanQuery) { export async function listInboundPlans(query: InboundPlanQuery) {
const { customerId, ...params } = query const { customerId, ...params } = query
return normalizeOverseasPage(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>>('/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } }))) return normalizeOverseasPage(unwrapApiResponse(await http.get<ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>>('/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } })))
} }
export async function getInboundPlan(planNo: string, customerId: string) { export async function getInboundPlan(planNo: string, customerId: string) {
return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<InboundPlanDetailDto>, ApiEnvelope<InboundPlanDetailDto>>(`/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, { params: { customerId } })) return unwrapApiResponse(await http.get<ApiEnvelope<InboundPlanDetailDto>, ApiEnvelope<InboundPlanDetailDto>>(`/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, { params: { customerId } }))
} }
export interface PlatformInventoryMoveRequest { export interface PlatformInventoryMoveRequest {
@@ -101,6 +101,6 @@ export interface PlatformInventoryMoveResult {
} }
export async function moveInboundPallet(payload: PlatformInventoryMoveRequest) { export async function moveInboundPallet(payload: PlatformInventoryMoveRequest) {
const response = await overseasHttp.post<ApiEnvelope<PlatformInventoryMoveResult>, ApiEnvelope<PlatformInventoryMoveResult>>('/platform/v1/last-leg/inventory-moves', payload, { headers: { 'Idempotency-Key': payload.idempotencyKey } }) const response = await http.post<ApiEnvelope<PlatformInventoryMoveResult>, ApiEnvelope<PlatformInventoryMoveResult>>('/platform/v1/last-leg/inventory-moves', payload, { headers: { 'Idempotency-Key': payload.idempotencyKey } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { downloadInventoryBalances, downloadInventoryLedger, getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from './inventory' import { downloadInventoryBalances, downloadInventoryLedger, getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from './inventory'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn() } }))
describe('overseas inventory API', () => { describe('overseas inventory API', () => {
beforeEach(() => { vi.clearAllMocks() }) beforeEach(() => { vi.clearAllMocks() })
@@ -10,44 +10,44 @@ describe('overseas inventory API', () => {
it('returns the binary response directly for inventory exports', async () => { it('returns the binary response directly for inventory exports', async () => {
const balances = new Blob(['customerId\\r\\n10001\\r\\n'], { type: 'text/csv' }) const balances = new Blob(['customerId\\r\\n10001\\r\\n'], { type: 'text/csv' })
const ledger = new Blob(['occurredAt\\r\\n'], { type: 'text/csv' }) const ledger = new Blob(['occurredAt\\r\\n'], { type: 'text/csv' })
vi.mocked(overseasHttp.get).mockResolvedValueOnce(balances).mockResolvedValueOnce(ledger) vi.mocked(http.get).mockResolvedValueOnce(balances).mockResolvedValueOnce(ledger)
await expect(downloadInventoryBalances({ skuCode: 'SKU-1' })).resolves.toBe(balances) await expect(downloadInventoryBalances({ skuCode: 'SKU-1' })).resolves.toBe(balances)
await expect(downloadInventoryLedger({ inboundCode: 'IN-1' })).resolves.toBe(ledger) await expect(downloadInventoryLedger({ inboundCode: 'IN-1' })).resolves.toBe(ledger)
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/export', { expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/export', {
params: { skuCode: 'SKU-1' }, responseType: 'blob', params: { skuCode: 'SKU-1' }, responseType: 'blob',
}) })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger/export', { expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger/export', {
params: { inboundCode: 'IN-1' }, responseType: 'blob', params: { inboundCode: 'IN-1' }, responseType: 'blob',
}) })
}) })
it('keeps skuCode and nullable location filters on real balance queries', async () => { it('keeps skuCode and nullable location filters on real balance queries', async () => {
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }).mockResolvedValueOnce({ code: '200', data: { totalPackages: 0 } }) vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }).mockResolvedValueOnce({ code: '200', data: { totalPackages: 0 } })
const query = { page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } const query = { page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' }
await listInventoryBalances(query) await listInventoryBalances(query)
await getInventorySummary(query) await getInventorySummary(query)
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances', { params: query }) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances', { params: query })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/summary', { params: { page: 1, size: 200, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/summary', { params: { page: 1, size: 200, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } })
}) })
it('uses the read-only position and ledger endpoints without customer scope', async () => { it('uses the read-only position and ledger endpoints without customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: [] }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: [] })
await listInventoryPositions('190000000000000099') await listInventoryPositions('190000000000000099')
await listInventoryLedger({ page: 1, size: 20, skuCode: 'SKU-1' }) await listInventoryLedger({ page: 1, size: 20, skuCode: 'SKU-1' })
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/190000000000000099/positions', { params: {} }) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/190000000000000099/positions', { params: {} })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger', { params: { page: 1, size: 20, skuCode: 'SKU-1' } }) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger', { params: { page: 1, size: 20, skuCode: 'SKU-1' } })
}) })
it('passes the inbound code as a business filter and preserves the returned customer name', async () => { it('passes the inbound code as a business filter and preserves the returned customer name', async () => {
const record = { id: '2093539779795795969', customerId: '2093539779795795970', customerName: '华远贸易', inboundCode: 'NYEXX2608300001', beforePackages: 0, deltaPackages: 2, afterPackages: 2 } const record = { id: '2093539779795795969', customerId: '2093539779795795970', customerName: '华远贸易', inboundCode: 'NYEXX2608300001', beforePackages: 0, deltaPackages: 2, afterPackages: 2 }
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [record], page: 1, size: 20, total: 1, pages: 1 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [record], page: 1, size: 20, total: 1, pages: 1 } })
await expect(listInventoryLedger({ page: 1, size: 20, inboundCode: 'NYEXX2608300001', customerName: '华远贸易' })).resolves.toMatchObject({ records: [record] }) await expect(listInventoryLedger({ page: 1, size: 20, inboundCode: 'NYEXX2608300001', customerName: '华远贸易' })).resolves.toMatchObject({ records: [record] })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/ledger', { params: { page: 1, size: 20, inboundCode: 'NYEXX2608300001', customerName: '华远贸易' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/ledger', { params: { page: 1, size: 20, inboundCode: 'NYEXX2608300001', customerName: '华远贸易' } })
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface InventoryBalanceDto { export interface InventoryBalanceDto {
@@ -111,18 +111,18 @@ export interface InventoryBalanceQuery {
function params(query: InventoryBalanceQuery) { return { ...query } } function params(query: InventoryBalanceQuery) { return { ...query } }
export async function listInventoryBalances(query: InventoryBalanceQuery) { export async function listInventoryBalances(query: InventoryBalanceQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>, ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>, ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>>(
'/platform/v1/last-leg/inventory/balances', { params: params(query) }, '/platform/v1/last-leg/inventory/balances', { params: params(query) },
) )
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function downloadInventoryBalances(query: Partial<InventoryBalanceQuery>) { export async function downloadInventoryBalances(query: Partial<InventoryBalanceQuery>) {
return overseasHttp.get<Blob, Blob>('/platform/v1/last-leg/inventory/balances/export', { params: query, responseType: 'blob' }) return http.get<Blob, Blob>('/platform/v1/last-leg/inventory/balances/export', { params: query, responseType: 'blob' })
} }
export async function getInventorySummary(query: InventoryBalanceQuery) { export async function getInventorySummary(query: InventoryBalanceQuery) {
const response = await overseasHttp.get<ApiEnvelope<InventoryDashboardDto>, ApiEnvelope<InventoryDashboardDto>>( const response = await http.get<ApiEnvelope<InventoryDashboardDto>, ApiEnvelope<InventoryDashboardDto>>(
'/platform/v1/last-leg/inventory/summary', { params: params({ ...query, page: 1, size: 200 }) }, '/platform/v1/last-leg/inventory/summary', { params: params({ ...query, page: 1, size: 200 }) },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -132,7 +132,7 @@ export async function listInventoryPositions(
balanceId: string, balanceId: string,
query: { palletNo?: string; batchNo?: string; inventoryStatus?: string } = {}, query: { palletNo?: string; batchNo?: string; inventoryStatus?: string } = {},
) { ) {
const response = await overseasHttp.get<ApiEnvelope<InventoryPositionDto[]>, ApiEnvelope<InventoryPositionDto[]>>( const response = await http.get<ApiEnvelope<InventoryPositionDto[]>, ApiEnvelope<InventoryPositionDto[]>>(
`/platform/v1/last-leg/inventory/balances/${encodeURIComponent(balanceId)}/positions`, `/platform/v1/last-leg/inventory/balances/${encodeURIComponent(balanceId)}/positions`,
{ params: query }, { params: query },
) )
@@ -159,7 +159,7 @@ export interface InventoryLedgerQuery {
} }
export async function listInventoryLedger(query: InventoryLedgerQuery) { export async function listInventoryLedger(query: InventoryLedgerQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>, ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>, ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>>(
'/platform/v1/last-leg/inventory/ledger', '/platform/v1/last-leg/inventory/ledger',
{ params: { ...query } }, { params: { ...query } },
) )
@@ -167,5 +167,5 @@ export async function listInventoryLedger(query: InventoryLedgerQuery) {
} }
export async function downloadInventoryLedger(query: Partial<InventoryLedgerQuery>) { export async function downloadInventoryLedger(query: Partial<InventoryLedgerQuery>) {
return overseasHttp.get<Blob, Blob>('/platform/v1/last-leg/inventory/ledger/export', { params: query, responseType: 'blob' }) return http.get<Blob, Blob>('/platform/v1/last-leg/inventory/ledger/export', { params: query, responseType: 'blob' })
} }
@@ -1,29 +1,29 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders' import { changeContainerReceivingWarehouse, convertLastMileContainerToInboundPlans, createManualInboundPlan, downloadLastMileOrders, getContainerPlanProgress, getLastMileOrder, getLastMileOrderManualNodes, listContainerPlanProgress, listLastMileOrders, transferLastMileOrders, updateLastMileOrderHold, updateLastMileOrderManualNodes } from './lastMileOrders'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), put: vi.fn(), post: vi.fn() } }))
describe('last-mile order overseas API', () => { describe('last-mile order overseas API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.put).mockReset() vi.mocked(http.put).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
}) })
it('loads all platform orders without a customer scope', async () => { it('loads all platform orders without a customer scope', async () => {
const record = { id: '190000000000000099', customerId: '190000000000000001', sourceContainerBookingId: '190000000000000088', customerName: '华远贸易', orderNo: 'LMO-260827-0001' } const record = { id: '190000000000000099', customerId: '190000000000000001', sourceContainerBookingId: '190000000000000088', customerName: '华远贸易', orderNo: 'LMO-260827-0001' }
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [record], page: '2', size: '20', total: '1', pages: '1' } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [record], page: '2', size: '20', total: '1', pages: '1' } })
await expect(listLastMileOrders({ page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' })).resolves.toEqual({ records: [record], page: 2, size: 20, total: 1, pages: 1 }) await expect(listLastMileOrders({ page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' })).resolves.toEqual({ records: [record], page: 2, size: 20, total: 1, pages: 1 })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', {
params: { page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' }, params: { page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' },
}) })
}) })
it('preserves a long order id as a string in the detail path', async () => { it('preserves a long order id as a string in the detail path', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { order: { id: '190000000000000099' }, cargo: [], statusHistory: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { order: { id: '190000000000000099' }, cargo: [], statusHistory: [] } })
await getLastMileOrder('190000000000000099') await getLastMileOrder('190000000000000099')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099')
}) })
it('does not call outbound or delivery endpoints', () => { it('does not call outbound or delivery endpoints', () => {
@@ -32,43 +32,43 @@ describe('last-mile order overseas API', () => {
}) })
it('passes all supported list filters and pagination without customerId', async () => { it('passes all supported list filters and pagination without customerId', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 2, size: 50, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 2, size: 50, total: 0, pages: 0 } })
await listLastMileOrders({ await listLastMileOrders({
page: 2, size: 50, orderNo: 'LM-1', inboundCode: 'IN-1', sourceOrderNo: 'SRC-1', page: 2, size: 50, orderNo: 'LM-1', inboundCode: 'IN-1', sourceOrderNo: 'SRC-1',
containerNo: 'CONT-1', status: 'PENDING_DELIVERY', transferWarehouseCode: 'ONT8', containerNo: 'CONT-1', status: 'PENDING_DELIVERY', transferWarehouseCode: 'ONT8',
abnormal: true, detained: false, createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59', abnormal: true, detained: false, createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
}) })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', {
params: expect.objectContaining({ page: 2, size: 50, sourceOrderNo: 'SRC-1', transferWarehouseCode: 'ONT8', abnormal: true, detained: false }), params: expect.objectContaining({ page: 2, size: 50, sourceOrderNo: 'SRC-1', transferWarehouseCode: 'ONT8', abnormal: true, detained: false }),
}) })
expect(JSON.stringify(vi.mocked(overseasHttp.get).mock.calls[0]?.[1])).not.toContain('customerId') expect(JSON.stringify(vi.mocked(http.get).mock.calls[0]?.[1])).not.toContain('customerId')
}) })
it('uses the real destination transfer endpoint and edit contract', async () => { it('uses the real destination transfer endpoint and edit contract', async () => {
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: 1 }) vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: 1 })
await transferLastMileOrders({ await transferLastMileOrders({
orderIds: ['190000000000000099'], warehouseId: 'WH-1', warehouseCode: 'ONT8', warehouseName: 'Ontario', orderIds: ['190000000000000099'], warehouseId: 'WH-1', warehouseCode: 'ONT8', warehouseName: 'Ontario',
}) })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/destination', expect.objectContaining({ orderIds: ['190000000000000099'] })) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/destination', expect.objectContaining({ orderIds: ['190000000000000099'] }))
}) })
it('returns the exported CSV blob without reading data twice', async () => { it('returns the exported CSV blob without reading data twice', async () => {
const blob = new Blob(['orders'], { type: 'text/csv' }) const blob = new Blob(['orders'], { type: 'text/csv' })
vi.mocked(overseasHttp.get).mockResolvedValue(blob) vi.mocked(http.get).mockResolvedValue(blob)
await expect(downloadLastMileOrders({ orderNo: 'LM-1' })).resolves.toBe(blob) await expect(downloadLastMileOrders({ orderNo: 'LM-1' })).resolves.toBe(blob)
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/export', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/export', {
params: { orderNo: 'LM-1' }, responseType: 'blob', params: { orderNo: 'LM-1' }, responseType: 'blob',
}) })
}) })
it('exports all authorized orders without pagination or empty filters', async () => { it('exports all authorized orders without pagination or empty filters', async () => {
const blob = new Blob(['orders'], { type: 'text/csv' }) const blob = new Blob(['orders'], { type: 'text/csv' })
vi.mocked(overseasHttp.get).mockResolvedValue(blob) vi.mocked(http.get).mockResolvedValue(blob)
await downloadLastMileOrders({ page: 1, size: 10, orderNo: '', createdFrom: undefined }) await downloadLastMileOrders({ page: 1, size: 10, orderNo: '', createdFrom: undefined })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/export', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/export', {
params: {}, responseType: 'blob', params: {}, responseType: 'blob',
}) })
}) })
@@ -77,7 +77,7 @@ describe('last-mile order overseas API', () => {
const blob = new Blob([JSON.stringify({ const blob = new Blob([JSON.stringify({
code: 'S0500', message: '系统内部错误', data: null, requestId: 'req-1', traceId: 'trace-1', code: 'S0500', message: '系统内部错误', data: null, requestId: 'req-1', traceId: 'trace-1',
})], { type: 'application/json' }) })], { type: 'application/json' })
vi.mocked(overseasHttp.get).mockResolvedValue(blob) vi.mocked(http.get).mockResolvedValue(blob)
await expect(downloadLastMileOrders({})).rejects.toMatchObject({ await expect(downloadLastMileOrders({})).rejects.toMatchObject({
code: 'S0500', message: '系统内部错误', requestId: 'req-1', traceId: 'trace-1', code: 'S0500', message: '系统内部错误', requestId: 'req-1', traceId: 'trace-1',
@@ -85,12 +85,12 @@ describe('last-mile order overseas API', () => {
}) })
it('uses the manual inbound-plan endpoint with the complete form and idempotency evidence', async () => { it('uses the manual inbound-plan endpoint with the complete form and idempotency evidence', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: {} }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: {} })
const payload = { orderId: '190000000000000099', customerId: '190000000000000001', expectedOrderVersion: 4, const payload = { orderId: '190000000000000099', customerId: '190000000000000001', expectedOrderVersion: 4,
palletCount: 8, palletType: 'STANDARD' as const, palletCount: 8, palletType: 'STANDARD' as const,
assigned: false, mergeRequired: false, urgent: true, remark: '加急' } assigned: false, mergeRequired: false, urgent: true, remark: '加急' }
await createManualInboundPlan(payload, 'plan-key-1') await createManualInboundPlan(payload, 'plan-key-1')
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/manual', payload, expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/manual', payload,
{ headers: { 'Idempotency-Key': 'plan-key-1' } }) { headers: { 'Idempotency-Key': 'plan-key-1' } })
expect(payload).not.toHaveProperty('inboundCode') expect(payload).not.toHaveProperty('inboundCode')
expect(payload).not.toHaveProperty('plannedArrivalDate') expect(payload).not.toHaveProperty('plannedArrivalDate')
@@ -100,28 +100,28 @@ describe('last-mile order overseas API', () => {
}) })
it('uses versioned idempotent order hold and complete manual-node snapshot endpoints', async () => { it('uses versioned idempotent order hold and complete manual-node snapshot endpoints', async () => {
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: {} }) vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: {} })
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { nodes: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { nodes: [] } })
await updateLastMileOrderHold('190000000000000099', { held: true, chargeable: true, reason: '海关查验', expectedVersion: 4 }, 'hold-key-1') await updateLastMileOrderHold('190000000000000099', { held: true, chargeable: true, reason: '海关查验', expectedVersion: 4 }, 'hold-key-1')
expect(overseasHttp.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/hold', expect(http.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/hold',
{ held: true, chargeable: true, reason: '海关查验', expectedVersion: 4 }, { headers: { 'Idempotency-Key': 'hold-key-1' } }) { held: true, chargeable: true, reason: '海关查验', expectedVersion: 4 }, { headers: { 'Idempotency-Key': 'hold-key-1' } })
await getLastMileOrderManualNodes('190000000000000099') await getLastMileOrderManualNodes('190000000000000099')
expect(overseasHttp.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/manual-nodes') expect(http.get).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/manual-nodes')
const nodes = [{ nodeCode: 'ESTIMATED_ARRIVAL' as const, nodeAt: '2026-09-10T10:00:00' }] const nodes = [{ nodeCode: 'ESTIMATED_ARRIVAL' as const, nodeAt: '2026-09-10T10:00:00' }]
await updateLastMileOrderManualNodes('190000000000000099', 5, nodes, 'node-key-1') await updateLastMileOrderManualNodes('190000000000000099', 5, nodes, 'node-key-1')
expect(overseasHttp.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/manual-nodes', expect(http.put).toHaveBeenLastCalledWith('/platform/v1/last-leg/orders/190000000000000099/manual-nodes',
{ expectedVersion: 5, nodes }, { headers: { 'Idempotency-Key': 'node-key-1' } }) { expectedVersion: 5, nodes }, { headers: { 'Idempotency-Key': 'node-key-1' } })
}) })
it('uses one atomic container request without looping individual conversions', async () => { it('uses one atomic container request without looping individual conversions', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: [] }) vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: [] })
await expect(convertLastMileContainerToInboundPlans({ await expect(convertLastMileContainerToInboundPlans({
sourceContainerBookingId: '2092873463568597001', sourceContainerBookingId: '2092873463568597001',
snapshotVersion: 9, snapshotVersion: 9,
snapshotHash: 'a'.repeat(64), snapshotHash: 'a'.repeat(64),
}, 'container-plan-idempotency-key')).resolves.toEqual([]) }, 'container-plan-idempotency-key')).resolves.toEqual([])
expect(overseasHttp.post).toHaveBeenCalledTimes(1) expect(http.post).toHaveBeenCalledTimes(1)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/convert-container', { expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/convert-container', {
sourceContainerBookingId: '2092873463568597001', sourceContainerBookingId: '2092873463568597001',
snapshotVersion: 9, snapshotVersion: 9,
snapshotHash: 'a'.repeat(64), snapshotHash: 'a'.repeat(64),
@@ -134,14 +134,14 @@ describe('last-mile order overseas API', () => {
inboundCodeCount: 13, missingInboundCodeCount: 2, planCount: 10, plannedInboundCodeCount: 10, inboundCodeCount: 13, missingInboundCodeCount: 2, planCount: 10, plannedInboundCodeCount: 10,
pendingPlanCount: 5, resourcePendingPlanCount: 0, plannedArrivalAt: '2026-09-01T08:00:00', allPlansReady: false, blockingReason: '存在2票缺少入仓码', pendingPlanCount: 5, resourcePendingPlanCount: 0, plannedArrivalAt: '2026-09-01T08:00:00', allPlansReady: false, blockingReason: '存在2票缺少入仓码',
} }
vi.mocked(overseasHttp.get).mockResolvedValue({ vi.mocked(http.get).mockResolvedValue({
code: '200', message: 'success', data: { records: [record], page: '2', size: '10', total: '21', pages: '3' }, code: '200', message: 'success', data: { records: [record], page: '2', size: '10', total: '21', pages: '3' },
}) })
await expect(listContainerPlanProgress({ await expect(listContainerPlanProgress({
page: 2, size: 10, containerNo: 'CONT-15', page: 2, size: 10, containerNo: 'CONT-15',
createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59', createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
})).resolves.toEqual({ records: [record], page: 2, size: 10, total: 21, pages: 3 }) })).resolves.toEqual({ records: [record], page: 2, size: 10, total: 21, pages: 3 })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries', { expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries', {
params: { params: {
page: 2, size: 10, containerNo: 'CONT-15', page: 2, size: 10, containerNo: 'CONT-15',
createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59', createdFrom: '2026-08-01T00:00:00', createdTo: '2026-08-31T23:59:59',
@@ -151,18 +151,18 @@ describe('last-mile order overseas API', () => {
it('loads one container detail with the encoded stable long id', async () => { it('loads one container detail with the encoded stable long id', async () => {
const detail = { summary: { sourceContainerBookingId: '2092873463568597001' }, orders: [] } const detail = { summary: { sourceContainerBookingId: '2092873463568597001' }, orders: [] }
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: detail }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: detail })
await expect(getContainerPlanProgress('2092873463568597001/unsafe')).resolves.toEqual(detail) await expect(getContainerPlanProgress('2092873463568597001/unsafe')).resolves.toEqual(detail)
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe')
}) })
it('changes one whole container receiving warehouse with version and idempotency evidence', async () => { it('changes one whole container receiving warehouse with version and idempotency evidence', async () => {
const updated = { sourceContainerBookingId: '2092873463568597001', overseasWarehouseId: '9007199254740993' } const updated = { sourceContainerBookingId: '2092873463568597001', overseasWarehouseId: '9007199254740993' }
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: updated }) vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: updated })
await expect(changeContainerReceivingWarehouse('2092873463568597001/unsafe', { await expect(changeContainerReceivingWarehouse('2092873463568597001/unsafe', {
overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜', overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜',
}, 'receiving-key-1')).resolves.toEqual(updated) }, 'receiving-key-1')).resolves.toEqual(updated)
expect(overseasHttp.put).toHaveBeenCalledWith( expect(http.put).toHaveBeenCalledWith(
'/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe/receiving-warehouse', '/platform/v1/last-leg/orders/container-summaries/2092873463568597001%2Funsafe/receiving-warehouse',
{ overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜' }, { overseasWarehouseId: '9007199254740993', expectedVersion: 7, remark: '改派洛杉矶接柜' },
{ headers: { 'Idempotency-Key': 'receiving-key-1' } }, { headers: { 'Idempotency-Key': 'receiving-key-1' } },
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { InboundPlanDetailDto } from './inboundPlans' import type { InboundPlanDetailDto } from './inboundPlans'
export interface OverseasPageResult<T> { export interface OverseasPageResult<T> {
@@ -351,7 +351,7 @@ export interface LastMileOrderDetailDto {
} }
export async function listLastMileOrders(query: LastMileOrderQuery) { export async function listLastMileOrders(query: LastMileOrderQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>>(
'/platform/v1/last-leg/orders', '/platform/v1/last-leg/orders',
{ params: query }, { params: query },
) )
@@ -359,14 +359,14 @@ export async function listLastMileOrders(query: LastMileOrderQuery) {
} }
export async function getLastMileOrder(orderId: string) { export async function getLastMileOrder(orderId: string) {
const response = await overseasHttp.get<ApiEnvelope<LastMileOrderDetailDto>, ApiEnvelope<LastMileOrderDetailDto>>( const response = await http.get<ApiEnvelope<LastMileOrderDetailDto>, ApiEnvelope<LastMileOrderDetailDto>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}`, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function reviewReturnOrder(returnNo: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) { export async function reviewReturnOrder(returnNo: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) {
const response = await overseasHttp.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>( const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(
`/platform/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/review`, `/platform/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/review`,
{ decision, remark: remark?.trim() || undefined }, { decision, remark: remark?.trim() || undefined },
{ headers: { 'X-Customer-Id': customerId } }, { headers: { 'X-Customer-Id': customerId } },
@@ -375,7 +375,7 @@ export async function reviewReturnOrder(returnNo: string, customerId: string, de
} }
export async function reviewDirectRelabelOrder(orderId: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) { export async function reviewDirectRelabelOrder(orderId: string, customerId: string, decision: 'APPROVE' | 'REJECT', remark?: string) {
const response = await overseasHttp.put<ApiEnvelope<unknown>, ApiEnvelope<unknown>>( const response = await http.put<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/relabel-review`, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/relabel-review`,
{ status: decision === 'APPROVE' ? 'PENDING_PROCESSING' : 'REJECTED', remark: remark?.trim() || undefined }, { status: decision === 'APPROVE' ? 'PENDING_PROCESSING' : 'REJECTED', remark: remark?.trim() || undefined },
{ params: { customerId } }, { params: { customerId } },
@@ -388,7 +388,7 @@ export async function downloadLastMileOrders(query: Partial<LastMileOrderQuery>)
const params = Object.fromEntries( const params = Object.fromEntries(
Object.entries(filters).filter(([, value]) => value !== undefined && value !== null && value !== ''), Object.entries(filters).filter(([, value]) => value !== undefined && value !== null && value !== ''),
) )
const blob = await overseasHttp.get<Blob, Blob>('/platform/v1/last-leg/orders/export', { params, responseType: 'blob' }) const blob = await http.get<Blob, Blob>('/platform/v1/last-leg/orders/export', { params, responseType: 'blob' })
if (!blob.type.toLowerCase().includes('json')) return blob if (!blob.type.toLowerCase().includes('json')) return blob
let error: Partial<ApiEnvelope<unknown>> let error: Partial<ApiEnvelope<unknown>>
try { try {
@@ -407,7 +407,7 @@ export async function downloadLastMileOrders(query: Partial<LastMileOrderQuery>)
} }
export async function createLastMileOrderFee(orderId: string, payload: CreateLastMileFeePayload) { export async function createLastMileOrderFee(orderId: string, payload: CreateLastMileFeePayload) {
const response = await overseasHttp.post<ApiEnvelope<LastMileFeeDto>, ApiEnvelope<LastMileFeeDto>>( const response = await http.post<ApiEnvelope<LastMileFeeDto>, ApiEnvelope<LastMileFeeDto>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/fees`, payload, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/fees`, payload,
{ headers: { 'Idempotency-Key': `last-mile-fee-${orderId}-${Date.now()}-${Math.random().toString(36).slice(2)}` } }, { headers: { 'Idempotency-Key': `last-mile-fee-${orderId}-${Date.now()}-${Math.random().toString(36).slice(2)}` } },
) )
@@ -415,14 +415,14 @@ export async function createLastMileOrderFee(orderId: string, payload: CreateLas
} }
export async function transferLastMileOrders(payload: TransferDestinationPayload) { export async function transferLastMileOrders(payload: TransferDestinationPayload) {
const response = await overseasHttp.put<ApiEnvelope<number>, ApiEnvelope<number>>( const response = await http.put<ApiEnvelope<number>, ApiEnvelope<number>>(
'/platform/v1/last-leg/orders/destination', payload, '/platform/v1/last-leg/orders/destination', payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> { export async function listContainerPlanProgress(query: ContainerPlanProgressQuery): Promise<OverseasPageResult<ContainerPlanProgressSummaryDto>> {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>, ApiEnvelope<OverseasPagePayload<ContainerPlanProgressSummaryDto>>>(
'/platform/v1/last-leg/orders/container-summaries', '/platform/v1/last-leg/orders/container-summaries',
{ params: query }, { params: query },
) )
@@ -430,14 +430,14 @@ export async function listContainerPlanProgress(query: ContainerPlanProgressQuer
} }
export async function getContainerPlanProgress(sourceContainerBookingId: string) { export async function getContainerPlanProgress(sourceContainerBookingId: string) {
const response = await overseasHttp.get<ApiEnvelope<ContainerPlanProgressDetailDto>, ApiEnvelope<ContainerPlanProgressDetailDto>>( const response = await http.get<ApiEnvelope<ContainerPlanProgressDetailDto>, ApiEnvelope<ContainerPlanProgressDetailDto>>(
`/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}`, `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createManualInboundPlan(payload: ManualInboundPlanPayload, idempotencyKey: string) { export async function createManualInboundPlan(payload: ManualInboundPlanPayload, idempotencyKey: string) {
const response = await overseasHttp.post<ApiEnvelope<InboundPlanDetailDto>, ApiEnvelope<InboundPlanDetailDto>>( const response = await http.post<ApiEnvelope<InboundPlanDetailDto>, ApiEnvelope<InboundPlanDetailDto>>(
'/platform/v1/last-leg/inbound-plans/manual', payload, '/platform/v1/last-leg/inbound-plans/manual', payload,
{ headers: { 'Idempotency-Key': idempotencyKey } }, { headers: { 'Idempotency-Key': idempotencyKey } },
) )
@@ -445,7 +445,7 @@ export async function createManualInboundPlan(payload: ManualInboundPlanPayload,
} }
export async function updateLastMileOrderHold(orderId: string, payload: HoldLastMileOrderPayload, idempotencyKey: string) { export async function updateLastMileOrderHold(orderId: string, payload: HoldLastMileOrderPayload, idempotencyKey: string) {
const response = await overseasHttp.put<ApiEnvelope<HoldLastMileOrderResponse>, ApiEnvelope<HoldLastMileOrderResponse>>( const response = await http.put<ApiEnvelope<HoldLastMileOrderResponse>, ApiEnvelope<HoldLastMileOrderResponse>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/hold`, payload, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/hold`, payload,
{ headers: { 'Idempotency-Key': idempotencyKey } }, { headers: { 'Idempotency-Key': idempotencyKey } },
) )
@@ -453,7 +453,7 @@ export async function updateLastMileOrderHold(orderId: string, payload: HoldLast
} }
export async function getLastMileOrderManualNodes(orderId: string) { export async function getLastMileOrderManualNodes(orderId: string) {
const response = await overseasHttp.get<ApiEnvelope<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>( const response = await http.get<ApiEnvelope<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -465,7 +465,7 @@ export async function updateLastMileOrderManualNodes(
nodes: ManualNodeValue[], nodes: ManualNodeValue[],
idempotencyKey: string, idempotencyKey: string,
) { ) {
const response = await overseasHttp.put<ApiEnvelope<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>( const response = await http.put<ApiEnvelope<ManualNodesResponse>, ApiEnvelope<ManualNodesResponse>>(
`/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`, `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/manual-nodes`,
{ expectedVersion, nodes }, { expectedVersion, nodes },
{ headers: { 'Idempotency-Key': idempotencyKey } }, { headers: { 'Idempotency-Key': idempotencyKey } },
@@ -478,7 +478,7 @@ export async function changeContainerReceivingWarehouse(
payload: ChangeContainerReceivingWarehousePayload, payload: ChangeContainerReceivingWarehousePayload,
idempotencyKey: string, idempotencyKey: string,
) { ) {
const response = await overseasHttp.put<ApiEnvelope<ContainerPlanProgressSummaryDto>, ApiEnvelope<ContainerPlanProgressSummaryDto>>( const response = await http.put<ApiEnvelope<ContainerPlanProgressSummaryDto>, ApiEnvelope<ContainerPlanProgressSummaryDto>>(
`/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/receiving-warehouse`, `/platform/v1/last-leg/orders/container-summaries/${encodeURIComponent(sourceContainerBookingId)}/receiving-warehouse`,
payload, payload,
{ headers: { 'Idempotency-Key': idempotencyKey } }, { headers: { 'Idempotency-Key': idempotencyKey } },
@@ -487,7 +487,7 @@ export async function changeContainerReceivingWarehouse(
} }
export async function getContainerInboundPlanPreview(sourceContainerBookingId: string) { export async function getContainerInboundPlanPreview(sourceContainerBookingId: string) {
const response = await overseasHttp.get<ApiEnvelope<ContainerInboundPlanPreviewDto>, ApiEnvelope<ContainerInboundPlanPreviewDto>>( const response = await http.get<ApiEnvelope<ContainerInboundPlanPreviewDto>, ApiEnvelope<ContainerInboundPlanPreviewDto>>(
'/platform/v1/last-leg/inbound-plans/container-preview', '/platform/v1/last-leg/inbound-plans/container-preview',
{ params: { sourceContainerBookingId } }, { params: { sourceContainerBookingId } },
) )
@@ -495,7 +495,7 @@ export async function getContainerInboundPlanPreview(sourceContainerBookingId: s
} }
export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload, idempotencyKey: string) { export async function convertLastMileContainerToInboundPlans(payload: ConvertContainerInboundPlanPayload, idempotencyKey: string) {
const response = await overseasHttp.post<ApiEnvelope<InboundPlanDetailDto[]>, ApiEnvelope<InboundPlanDetailDto[]>>( const response = await http.post<ApiEnvelope<InboundPlanDetailDto[]>, ApiEnvelope<InboundPlanDetailDto[]>>(
'/platform/v1/last-leg/inbound-plans/convert-container', payload, '/platform/v1/last-leg/inbound-plans/convert-container', payload,
{ headers: { 'Idempotency-Key': idempotencyKey } }, { headers: { 'Idempotency-Key': idempotencyKey } },
) )
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export type OverseasLocationType = 'VIRTUAL' | 'NORMAL' | 'SHARED_OVERFLOW' export type OverseasLocationType = 'VIRTUAL' | 'NORMAL' | 'SHARED_OVERFLOW'
@@ -33,10 +33,10 @@ export interface SaveOverseasLocationPayload {
export interface OverseasLocationQuery { page?: number; size?: number; keyword?: string; locationType?: OverseasLocationType | ''; status?: OverseasLocationStatus | '' } export interface OverseasLocationQuery { page?: number; size?: number; keyword?: string; locationType?: OverseasLocationType | ''; status?: OverseasLocationStatus | '' }
function base(warehouseId: string) { return `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations` } function base(warehouseId: string) { return `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/locations` }
export async function listOverseasLocations(warehouseId: string, query: OverseasLocationQuery = {}) { export async function listOverseasLocations(warehouseId: string, query: OverseasLocationQuery = {}) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>, ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>>(base(warehouseId), { params: { page: query.page ?? 1, size: query.size ?? 20, ...query } }) const response = await http.get<ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>, ApiEnvelope<OverseasPagePayload<OverseasLocationDto>>>(base(warehouseId), { params: { page: query.page ?? 1, size: query.size ?? 20, ...query } })
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
@@ -47,21 +47,21 @@ export async function listAllOverseasLocations(warehouseIds: string[], query: Ov
} }
export async function createOverseasLocation(warehouseId: string, payload: SaveOverseasLocationPayload) { export async function createOverseasLocation(warehouseId: string, payload: SaveOverseasLocationPayload) {
const response = await overseasHttp.post<ApiEnvelope<string>, ApiEnvelope<string>>(base(warehouseId), payload) const response = await http.post<ApiEnvelope<string>, ApiEnvelope<string>>(base(warehouseId), payload)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function updateOverseasLocation(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) { export async function updateOverseasLocation(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) {
const response = await overseasHttp.put<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload) const response = await http.put<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function changeOverseasLocationStatus(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) { export async function changeOverseasLocationStatus(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) {
const response = await overseasHttp.put<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload) const response = await http.put<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function deleteOverseasLocation(warehouseId: string, locationId: string, version: number) { export async function deleteOverseasLocation(warehouseId: string, locationId: string, version: number) {
const response = await overseasHttp.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, { params: { version } }) const response = await http.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, { params: { version } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
@@ -1,36 +1,36 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { cancelOutbound, createOneClickDropshipOutbound, getOutbound, listOneClickOutboundCandidates, listOutbounds } from './outbounds' import { cancelOutbound, createOneClickDropshipOutbound, getOutbound, listOneClickOutboundCandidates, listOutbounds } from './outbounds'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
describe('outbound overseas API', () => { describe('outbound overseas API', () => {
beforeEach(() => { vi.mocked(overseasHttp.get).mockReset(); vi.mocked(overseasHttp.post).mockReset() }) beforeEach(() => { vi.mocked(http.get).mockReset(); vi.mocked(http.post).mockReset() })
it('lists outbound orders with supported filters', async () => { it('lists outbound orders with supported filters', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } })
await listOutbounds({ page: 1, size: 20, documentNo: 'OUT-1', warehouseId: 'WH-1', status: 'PICKED' }) await listOutbounds({ page: 1, size: 20, documentNo: 'OUT-1', warehouseId: 'WH-1', status: 'PICKED' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders', { params: { page: 1, size: 20, documentNo: 'OUT-1', warehouseId: 'WH-1', status: 'PICKED' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders', { params: { page: 1, size: 20, documentNo: 'OUT-1', warehouseId: 'WH-1', status: 'PICKED' } })
}) })
it('loads detail and cancels only by outboundNo without customerId', async () => { it('loads detail and cancels only by outboundNo without customerId', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { outbound: { id: '1', outboundNo: 'OUT/1', status: 'PICKED' }, deliveries: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { outbound: { id: '1', outboundNo: 'OUT/1', status: 'PICKED' }, deliveries: [] } })
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: null }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: null })
await getOutbound('OUT/1') await getOutbound('OUT/1')
await cancelOutbound('OUT/1', '运营取消') await cancelOutbound('OUT/1', '运营取消')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/OUT%2F1') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/OUT%2F1')
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/OUT%2F1/cancel', { reason: '运营取消' }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/OUT%2F1/cancel', { reason: '运营取消' })
}) })
it('keeps customerId explicit only for one-click dropship creation', async () => { it('keeps customerId explicit only for one-click dropship creation', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { id: '1', outboundNo: 'OUT-1' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { id: '1', outboundNo: 'OUT-1' } })
const payload = { recipient: 'John / 555 / LA', items: [{ inventoryPositionId: '99', packages: 2, sku: 'SKU-1' }], idempotencyKey: 'dropship-1' } const payload = { recipient: 'John / 555 / LA', items: [{ inventoryPositionId: '99', packages: 2, sku: 'SKU-1' }], idempotencyKey: 'dropship-1' }
await createOneClickDropshipOutbound('customer-1', payload) await createOneClickDropshipOutbound('customer-1', payload)
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId: 'customer-1' } }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId: 'customer-1' } })
}) })
it('normalizes one-click candidate measurements returned as decimal strings', async () => { it('normalizes one-click candidate measurements returned as decimal strings', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: [{ vi.mocked(http.get).mockResolvedValue({ code: '200', data: [{
inventoryPositionId: '99', availablePackages: 4, inventoryPositionId: '99', availablePackages: 4,
grossWeightKg: '40.000', volumeWeightKg: '80.000', volumeCbm: '4.0000', grossWeightKg: '40.000', volumeWeightKg: '80.000', volumeCbm: '4.0000',
}] }) }] })
+11 -11
View File
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
import type { DeliveryOrderRow } from './deliveryOrders' import type { DeliveryOrderRow } from './deliveryOrders'
@@ -26,19 +26,19 @@ export interface OneClickOutboundCandidate { inventoryPositionId: string; orderN
export interface WarehouseTransferOutboundCandidate { customerId: string; customerName?: string | null; transferNo: string; overseasWarehouseId: string; transferStatus: string; needRelabel: boolean; relabelNo?: string | null; destinationType?: 'FBA' | 'PRIVATE' | null; destinationWarehouseCode?: string | null; privateDestinationAddress?: string | null; privateDestinationPostalCode?: string | null; packageCount: number; grossWeightKg?: number | null; volumeCbm?: number | null } export interface WarehouseTransferOutboundCandidate { customerId: string; customerName?: string | null; transferNo: string; overseasWarehouseId: string; transferStatus: string; needRelabel: boolean; relabelNo?: string | null; destinationType?: 'FBA' | 'PRIVATE' | null; destinationWarehouseCode?: string | null; privateDestinationAddress?: string | null; privateDestinationPostalCode?: string | null; packageCount: number; grossWeightKg?: number | null; volumeCbm?: number | null }
export interface WarehouseTransferDestinationPayload { transfers: Array<{ customerId: string; transferNo: string }>; destinationType: 'FBA' | 'PRIVATE'; destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string; privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string; idempotencyKey: string } export interface WarehouseTransferDestinationPayload { transfers: Array<{ customerId: string; transferNo: string }>; destinationType: 'FBA' | 'PRIVATE'; destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string; privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string; idempotencyKey: string }
export async function listOutbounds(query: OutboundQuery) { export async function listOutbounds(query: OutboundQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<OutboundRow>>, ApiEnvelope<OverseasPagePayload<OutboundRow>>>('/platform/v1/last-leg/outbound-orders', { params: query }) const response = await http.get<ApiEnvelope<OverseasPagePayload<OutboundRow>>, ApiEnvelope<OverseasPagePayload<OutboundRow>>>('/platform/v1/last-leg/outbound-orders', { params: query })
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function getOutbound(outboundNo: string) { export async function getOutbound(outboundNo: string) {
const response = await overseasHttp.get<ApiEnvelope<OutboundDetail>, ApiEnvelope<OutboundDetail>>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}`) const response = await http.get<ApiEnvelope<OutboundDetail>, ApiEnvelope<OutboundDetail>>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}`)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createOneClickDropshipOutbound(customerId: string, payload: { recipient: string; items: Array<{ inventoryPositionId: string; packages: number; sku?: string; cartonMarks?: string[] }>; remark?: string; idempotencyKey: string }) { export async function createOneClickDropshipOutbound(customerId: string, payload: { recipient: string; items: Array<{ inventoryPositionId: string; packages: number; sku?: string; cartonMarks?: string[] }>; remark?: string; idempotencyKey: string }) {
const response = await overseasHttp.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/one-click-dropship', payload, { params: { customerId } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listOneClickOutboundCandidates(customerId?: string, query: { warehouseId?: string } = {}) { export async function listOneClickOutboundCandidates(customerId?: string, query: { warehouseId?: string } = {}) {
const response = await overseasHttp.get<ApiEnvelope<OneClickOutboundCandidate[]>, ApiEnvelope<OneClickOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates', { params: { customerId, ...query } }) const response = await http.get<ApiEnvelope<OneClickOutboundCandidate[]>, ApiEnvelope<OneClickOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/one-click-dropship/candidates', { params: { customerId, ...query } })
return unwrapApiResponse(response).map((item) => ({ return unwrapApiResponse(response).map((item) => ({
...item, ...item,
inventoryPositionId: String(item.inventoryPositionId), inventoryPositionId: String(item.inventoryPositionId),
@@ -49,22 +49,22 @@ export async function listOneClickOutboundCandidates(customerId?: string, query:
})) }))
} }
export async function listReturnOutboundCandidates(customerId: string, warehouseId: string) { export async function listReturnOutboundCandidates(customerId: string, warehouseId: string) {
const response = await overseasHttp.get<ApiEnvelope<ReturnOutboundCandidate[]>, ApiEnvelope<ReturnOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/return/candidates', { params: { customerId, warehouseId } }) const response = await http.get<ApiEnvelope<ReturnOutboundCandidate[]>, ApiEnvelope<ReturnOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/return/candidates', { params: { customerId, warehouseId } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function createReturnOutbound(customerId: string, payload: { recipient: string; overseasWarehouseId: string; returnNo: string; cartonMarks: string[]; remark?: string; idempotencyKey: string }) { export async function createReturnOutbound(customerId: string, payload: { recipient: string; overseasWarehouseId: string; returnNo: string; cartonMarks: string[]; remark?: string; idempotencyKey: string }) {
const response = await overseasHttp.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/return', payload, { params: { customerId } }) const response = await http.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/return', payload, { params: { customerId } })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function listWarehouseTransferOutboundCandidates(query: { customerId?: string; warehouseId?: string } = {}) { export async function listWarehouseTransferOutboundCandidates(query: { customerId?: string; warehouseId?: string } = {}) {
const response = await overseasHttp.get<ApiEnvelope<WarehouseTransferOutboundCandidate[]>, ApiEnvelope<WarehouseTransferOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates', { params: query }) const response = await http.get<ApiEnvelope<WarehouseTransferOutboundCandidate[]>, ApiEnvelope<WarehouseTransferOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates', { params: query })
return unwrapApiResponse(response).map(item => ({ ...item, packageCount: Number(item.packageCount || 0) })) return unwrapApiResponse(response).map(item => ({ ...item, packageCount: Number(item.packageCount || 0) }))
} }
export async function bindWarehouseTransferDestination(payload: WarehouseTransferDestinationPayload) { export async function bindWarehouseTransferDestination(payload: WarehouseTransferDestinationPayload) {
const response = await overseasHttp.post<ApiEnvelope<unknown[]>, ApiEnvelope<unknown[]>>('/platform/v1/last-leg/warehouse-transfers/bind-destination', payload) const response = await http.post<ApiEnvelope<unknown[]>, ApiEnvelope<unknown[]>>('/platform/v1/last-leg/warehouse-transfers/bind-destination', payload)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function cancelOutbound(outboundNo: string, reason: string) { export async function cancelOutbound(outboundNo: string, reason: string) {
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason }) const response = await http.post<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
@@ -19,9 +19,9 @@ describe('Admin pallet warehouse routing contract', () => {
}) })
it('never calls the nonexistent overseas warehouse-admin root and always scopes pallet calls by warehouseId', () => { it('never calls the nonexistent overseas warehouse-admin root and always scopes pallet calls by warehouseId', () => {
expect(palletsApi).not.toMatch(/overseasHttp\.get[^\n]*\(\s*['"]\/platform\/v1\/warehouse-admin['"]/) expect(palletsApi).not.toMatch(/http\.get[^\n]*\(\s*['"]\/platform\/v1\/warehouse-admin['"]/)
expect(palletsApi).not.toContain('listCustomerWarehouses') expect(palletsApi).not.toContain('listCustomerWarehouses')
expect(palletsApi).toContain('`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`') expect(palletsApi).toContain('`/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets`')
expect(palletsApi).toContain('`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`') expect(palletsApi).toContain('`/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`')
}) })
}) })
@@ -1,32 +1,32 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam' import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { createPallet, getPallet, getPalletLabelPdf, listPallets, listReceivingOverseasWarehouses, updatePallet } from './pallets' import { createPallet, getPallet, getPalletLabelPdf, listPallets, listReceivingOverseasWarehouses, updatePallet } from './pallets'
vi.mock('@/modules/iam', () => ({ listSharedReceivingOverseasWarehouseOptions: vi.fn() })) vi.mock('@/modules/iam', () => ({ listSharedReceivingOverseasWarehouseOptions: vi.fn() }))
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
describe('overseas pallet API', () => { describe('overseas pallet API', () => {
beforeEach(() => { vi.clearAllMocks() }) beforeEach(() => { vi.clearAllMocks() })
it('downloads only the selected pallet through the overseas gateway', async () => { it('downloads only the selected pallet through the overseas gateway', async () => {
const pdf = new Blob(['%PDF-label'], { type: 'application/pdf' }) const pdf = new Blob(['%PDF-label'], { type: 'application/pdf' })
vi.mocked(overseasHttp.get).mockResolvedValueOnce(pdf) vi.mocked(http.get).mockResolvedValueOnce(pdf)
await expect(getPalletLabelPdf('200', '9007199254740993')).resolves.toBe(pdf) await expect(getPalletLabelPdf('200', '9007199254740993')).resolves.toBe(pdf)
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9007199254740993/label', { responseType: 'blob', timeout: 60_000 }) expect(http.get).toHaveBeenCalledWith('/platform/v1/overseas-warehouse-resources/200/pallets/9007199254740993/label', { responseType: 'blob', timeout: 60_000 })
}) })
it('surfaces a JSON business failure instead of saving it as a PDF', async () => { it('surfaces a JSON business failure instead of saving it as a PDF', async () => {
const error = new Blob([JSON.stringify({ code: 'A0422', message: '托盘编码不支持打印', requestId: 'r1' })], { type: 'application/json' }) const error = new Blob([JSON.stringify({ code: 'A0422', message: '托盘编码不支持打印', requestId: 'r1' })], { type: 'application/json' })
vi.mocked(overseasHttp.get).mockResolvedValueOnce(error) vi.mocked(http.get).mockResolvedValueOnce(error)
await expect(getPalletLabelPdf('200', '9')).rejects.toMatchObject({ message: '托盘编码不支持打印', code: 'A0422', requestId: 'r1' }) await expect(getPalletLabelPdf('200', '9')).rejects.toMatchObject({ message: '托盘编码不支持打印', code: 'A0422', requestId: 'r1' })
}) })
it('uses the public warehouse scoped pallet CRUD contract', async () => { it('uses the public warehouse scoped pallet CRUD contract', async () => {
const page = { records: [], page: 1, size: 20, total: 0, pages: 0 } const page = { records: [], page: 1, size: 20, total: 0, pages: 0 }
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: page }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: page })
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: '9' }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: '9' })
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: null }) vi.mocked(http.put).mockResolvedValue({ code: '200', data: null })
vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: '200', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }]) vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: '200', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }])
const payload = { palletCode: 'PLT-NEW', palletType: 'WOOD', fbaWarehouseCode: 'ABE3', capacityCbm: 2, unitPrice: 10, status: 'IDLE' } const payload = { palletCode: 'PLT-NEW', palletType: 'WOOD', fbaWarehouseCode: 'ABE3', capacityCbm: 2, unitPrice: 10, status: 'IDLE' }
@@ -37,11 +37,11 @@ describe('overseas pallet API', () => {
await updatePallet('200', '9', { ...payload, version: 7 }) await updatePallet('200', '9', { ...payload, version: 7 })
expect(listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledOnce() expect(listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledOnce()
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/warehouse-admin/200/pallets', { params: { page: 1, size: 20, keyword: 'PLT' } }) expect(http.get).toHaveBeenNthCalledWith(1, '/platform/v1/overseas-warehouse-resources/200/pallets', { params: { page: 1, size: 20, keyword: 'PLT' } })
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/warehouse-admin/200/pallets/9', {}) expect(http.get).toHaveBeenNthCalledWith(2, '/platform/v1/overseas-warehouse-resources/200/pallets/9', {})
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets', payload) expect(http.post).toHaveBeenCalledWith('/platform/v1/overseas-warehouse-resources/200/pallets', payload)
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', { ...payload, version: 7 }) expect(http.put).toHaveBeenCalledWith('/platform/v1/overseas-warehouse-resources/200/pallets/9', { ...payload, version: 7 })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', expect.objectContaining({ palletCode: 'PLT-NEW', fbaWarehouseCode: 'ABE3', status: 'IDLE', version: 7 })) expect(http.put).toHaveBeenCalledWith('/platform/v1/overseas-warehouse-resources/200/pallets/9', expect.objectContaining({ palletCode: 'PLT-NEW', fbaWarehouseCode: 'ABE3', status: 'IDLE', version: 7 }))
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets', expect.not.objectContaining({ version: expect.anything() })) expect(http.post).toHaveBeenCalledWith('/platform/v1/overseas-warehouse-resources/200/pallets', expect.not.objectContaining({ version: expect.anything() }))
}) })
}) })
+14 -14
View File
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam' import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
@@ -58,29 +58,29 @@ export async function listReceivingOverseasWarehouses() {
} }
export async function listPallets(warehouseId: string | undefined, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { export async function listPallets(warehouseId: string | undefined, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<PalletDto>>, ApiEnvelope<OverseasPagePayload<PalletDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<PalletDto>>, ApiEnvelope<OverseasPagePayload<PalletDto>>>(
warehouseId ? `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets` : '/platform/v1/warehouse-admin/pallets', warehouseId ? `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets` : '/platform/v1/overseas-warehouse-resources/pallets',
{ params: { ...query } }, { params: { ...query } },
) )
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function getPalletSummary(warehouseId: string, query: { keyword?: string; palletType?: string }) { export async function getPalletSummary(warehouseId: string, query: { keyword?: string; palletType?: string }) {
const path = warehouseId ? `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/summary` : '/platform/v1/warehouse-admin/pallets/summary' const path = warehouseId ? `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/summary` : '/platform/v1/overseas-warehouse-resources/pallets/summary'
const response = await overseasHttp.get<ApiEnvelope<PalletSummaryDto>, ApiEnvelope<PalletSummaryDto>>(path, { params: { page: 1, size: 1, ...query } }) const response = await http.get<ApiEnvelope<PalletSummaryDto>, ApiEnvelope<PalletSummaryDto>>(path, { params: { page: 1, size: 1, ...query } })
const value = unwrapApiResponse(response); return { totalCount: Number(value.totalCount), idleCount: Number(value.idleCount), occupiedCount: Number(value.occupiedCount), scrappedCount: Number(value.scrappedCount) } const value = unwrapApiResponse(response); return { totalCount: Number(value.totalCount), idleCount: Number(value.idleCount), occupiedCount: Number(value.occupiedCount), scrappedCount: Number(value.scrappedCount) }
} }
export async function getPallet(warehouseId: string, palletId: string) { export async function getPallet(warehouseId: string, palletId: string) {
const response = await overseasHttp.get<ApiEnvelope<PalletDto>, ApiEnvelope<PalletDto>>( const response = await http.get<ApiEnvelope<PalletDto>, ApiEnvelope<PalletDto>>(
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`,
{}, {},
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function getPalletLabelPdf(warehouseId: string, palletId: string) { export async function getPalletLabelPdf(warehouseId: string, palletId: string) {
const blob = await overseasHttp.get<Blob, Blob>( const blob = await http.get<Blob, Blob>(
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}/label`, `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}/label`,
{ responseType: 'blob', timeout: 60_000 }, { responseType: 'blob', timeout: 60_000 },
) )
if (blob.type.includes('application/json')) unwrapApiResponse(JSON.parse(await blob.text()) as ApiEnvelope<unknown>) if (blob.type.includes('application/json')) unwrapApiResponse(JSON.parse(await blob.text()) as ApiEnvelope<unknown>)
@@ -89,15 +89,15 @@ export async function getPalletLabelPdf(warehouseId: string, palletId: string) {
} }
export async function createPallet(warehouseId: string, payload: SavePalletPayload) { export async function createPallet(warehouseId: string, payload: SavePalletPayload) {
const response = await overseasHttp.post<ApiEnvelope<string>, ApiEnvelope<string>>( const response = await http.post<ApiEnvelope<string>, ApiEnvelope<string>>(
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, payload, `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets`, payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function updatePallet(warehouseId: string, palletId: string, payload: UpdatePalletPayload) { export async function updatePallet(warehouseId: string, palletId: string, payload: UpdatePalletPayload) {
const response = await overseasHttp.put<ApiEnvelope<null>, ApiEnvelope<null>>( const response = await http.put<ApiEnvelope<null>, ApiEnvelope<null>>(
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, `/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`,
payload, payload,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -1,45 +1,45 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam' import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { changeRelabelOrderStatus, createRelabelOrder, getRelabelOrder, listRelabelOrders, listRelabelWarehouseOptions } from './relabelOrders' import { changeRelabelOrderStatus, createRelabelOrder, getRelabelOrder, listRelabelOrders, listRelabelWarehouseOptions } from './relabelOrders'
vi.mock('@/modules/iam', () => ({ listSharedReceivingOverseasWarehouseOptions: vi.fn() })) vi.mock('@/modules/iam', () => ({ listSharedReceivingOverseasWarehouseOptions: vi.fn() }))
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn() } }))
describe('relabel overseas API', () => { describe('relabel overseas API', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(overseasHttp.get).mockReset() vi.mocked(http.get).mockReset()
vi.mocked(overseasHttp.post).mockReset() vi.mocked(http.post).mockReset()
vi.mocked(overseasHttp.put).mockReset() vi.mocked(http.put).mockReset()
vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockReset() vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockReset()
}) })
it('uses the platform page endpoint without customer scope', async () => { it('uses the platform page endpoint without customer scope', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } })
await listRelabelOrders({ page: 1, size: 20, skuCode: 'SKU-1' }) await listRelabelOrders({ page: 1, size: 20, skuCode: 'SKU-1' })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', { params: { page: 1, size: 20, skuCode: 'SKU-1' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', { params: { page: 1, size: 20, skuCode: 'SKU-1' } })
}) })
it('encodes the relabel number and does not call a mutation endpoint', async () => { it('encodes the relabel number and does not call a mutation endpoint', async () => {
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { application: { id: '1', relabelNo: 'RL/1' }, items: [], history: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: { application: { id: '1', relabelNo: 'RL/1' }, items: [], history: [] } })
await getRelabelOrder('RL/1') await getRelabelOrder('RL/1')
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1')
}) })
it('uses the real create and status endpoints', async () => { it('uses the real create and status endpoints', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: {} }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: {} })
vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: {} }) vi.mocked(http.put).mockResolvedValue({ code: '200', data: {} })
const payload = { customerOrderNo: 'CO-1', overseasWarehouseId: '9', labelDeadline: '2026-08-20T10:00:00', contactName: 'A', contactPhone: '1', chargeAmount: 20, idempotencyKey: 'k', items: [{ skuCode: 'SKU', productNameZh: 'P', totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: 'S', newSkuCode: 'SKU-2' }] } const payload = { customerOrderNo: 'CO-1', overseasWarehouseId: '9', labelDeadline: '2026-08-20T10:00:00', contactName: 'A', contactPhone: '1', chargeAmount: 20, idempotencyKey: 'k', items: [{ skuCode: 'SKU', productNameZh: 'P', totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: 'S', newSkuCode: 'SKU-2' }] }
await createRelabelOrder('10001', payload) await createRelabelOrder('10001', payload)
await changeRelabelOrderStatus('RL/1', 'PROCESSING', 'ok') await changeRelabelOrderStatus('RL/1', 'PROCESSING', 'ok')
expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', payload, { params: { customerId: '10001' } }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', payload, { params: { customerId: '10001' } })
expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1/status', { status: 'PROCESSING', remark: 'ok' }) expect(http.put).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1/status', { status: 'PROCESSING', remark: 'ok' })
}) })
it('loads shared ACTIVE OVERSEAS RECEIVING warehouses from the domestic candidate contract', async () => { it('loads shared ACTIVE OVERSEAS RECEIVING warehouses from the domestic candidate contract', async () => {
vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: '200', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }]) vi.mocked(listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: '200', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }])
await expect(listRelabelWarehouseOptions()).resolves.toMatchObject({ records: [{ id: '200', customerId: '0', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] }) await expect(listRelabelWarehouseOptions()).resolves.toMatchObject({ records: [{ id: '200', customerId: '0', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
expect(listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledOnce() expect(listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledOnce()
expect(overseasHttp.get).not.toHaveBeenCalledWith('/platform/v1/warehouse-admin', expect.anything()) expect(http.get).not.toHaveBeenCalledWith('/platform/v1/warehouse-admin', expect.anything())
}) })
}) })
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam' import { listSharedReceivingOverseasWarehouseOptions } from '@/modules/iam'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
@@ -122,14 +122,14 @@ export interface RelabelDetailDto {
} }
export async function listRelabelOrders(query: RelabelOrderQuery) { export async function listRelabelOrders(query: RelabelOrderQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>, ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>, ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>>(
'/platform/v1/last-leg/relabel-applications', { params: query }, '/platform/v1/last-leg/relabel-applications', { params: query },
) )
return normalizeOverseasPage(unwrapApiResponse(response)) return normalizeOverseasPage(unwrapApiResponse(response))
} }
export async function getRelabelOrder(relabelNo: string) { export async function getRelabelOrder(relabelNo: string) {
const response = await overseasHttp.get<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>( const response = await http.get<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
@@ -148,14 +148,14 @@ export async function listRelabelWarehouseOptions() {
} }
export async function createRelabelOrder(customerId: string, payload: CreateRelabelPayload) { export async function createRelabelOrder(customerId: string, payload: CreateRelabelPayload) {
const response = await overseasHttp.post<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>( const response = await http.post<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>(
'/platform/v1/last-leg/relabel-applications', payload, { params: { customerId } }, '/platform/v1/last-leg/relabel-applications', payload, { params: { customerId } },
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function changeRelabelOrderStatus(relabelNo: string, status: string, remark?: string) { export async function changeRelabelOrderStatus(relabelNo: string, status: string, remark?: string) {
const response = await overseasHttp.put<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>( const response = await http.put<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`,
{ status, remark }, { status, remark },
) )
@@ -163,17 +163,17 @@ export async function changeRelabelOrderStatus(relabelNo: string, status: string
} }
export async function getRelabelReviewTask(relabelNo: string) { export async function getRelabelReviewTask(relabelNo: string) {
const response = await overseasHttp.get<ApiEnvelope<{ version: number; taskStatus: string }>, ApiEnvelope<{ version: number; taskStatus: string }>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/review-task`) const response = await http.get<ApiEnvelope<{ version: number; taskStatus: string }>, ApiEnvelope<{ version: number; taskStatus: string }>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/review-task`)
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function returnRelabelForRework(relabelNo: string, expectedVersion: number, remark: string, idempotencyKey: string) { export async function returnRelabelForRework(relabelNo: string, expectedVersion: number, remark: string, idempotencyKey: string) {
const response = await overseasHttp.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/rework`, { decision: 'REWORK', expectedVersion, remark, idempotencyKey }) const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/rework`, { decision: 'REWORK', expectedVersion, remark, idempotencyKey })
return unwrapApiResponse(response) return unwrapApiResponse(response)
} }
export async function confirmRelabelCompletion(relabelNo: string, remark?: string) { export async function confirmRelabelCompletion(relabelNo: string, remark?: string) {
const response = await overseasHttp.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>( const response = await http.post<ApiEnvelope<unknown>, ApiEnvelope<unknown>>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`,
{ idempotencyKey: crypto.randomUUID(), remark }, { idempotencyKey: crypto.randomUUID(), remark },
) )
@@ -183,7 +183,7 @@ export async function confirmRelabelCompletion(relabelNo: string, remark?: strin
export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TOTAL' | 'ITEM', itemId?: string) { export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TOTAL' | 'ITEM', itemId?: string) {
const body = new FormData() const body = new FormData()
body.append('file', file) body.append('file', file)
const response = await overseasHttp.post<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>( const response = await http.post<ApiEnvelope<RelabelDetailDto>, ApiEnvelope<RelabelDetailDto>>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, body, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, body,
{ params: { scope, itemId }, headers: { 'Content-Type': 'multipart/form-data' } }, { params: { scope, itemId }, headers: { 'Content-Type': 'multipart/form-data' } },
) )
@@ -191,14 +191,14 @@ export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TO
} }
export async function downloadRelabelPdf(relabelNo: string, fileId: string, scope: 'TOTAL' | 'ITEM', itemId?: string) { export async function downloadRelabelPdf(relabelNo: string, fileId: string, scope: 'TOTAL' | 'ITEM', itemId?: string) {
return overseasHttp.get<Blob, Blob>( return http.get<Blob, Blob>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files/${encodeURIComponent(fileId)}/download`, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files/${encodeURIComponent(fileId)}/download`,
{ params: { scope, itemId }, responseType: 'blob' }, { params: { scope, itemId }, responseType: 'blob' },
) )
} }
export async function downloadRelabelCompletionPhoto(relabelNo: string, fileId: string) { export async function downloadRelabelCompletionPhoto(relabelNo: string, fileId: string) {
return overseasHttp.get<Blob, Blob>( return http.get<Blob, Blob>(
`/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/completion-photos/${encodeURIComponent(fileId)}/download`, `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/completion-photos/${encodeURIComponent(fileId)}/download`,
{ responseType: 'blob' }, { responseType: 'blob' },
) )
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface ReturnStockRow { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; totalQuantity: number; availableQuantity: number; cartonQuantity: number; updatedAt: string } export interface ReturnStockRow { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; totalQuantity: number; availableQuantity: number; cartonQuantity: number; updatedAt: string }
@@ -9,8 +9,8 @@ export interface ReturnCartonLabel extends ReturnCartonDetail { printableHtml: s
export interface ReturnInventoryMovement { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; movementType: 'RECEIPT' | 'MERGE' | 'OUTBOUND'; referenceNo: string; totalDelta: number; availableDelta: number; occurredAt: string } export interface ReturnInventoryMovement { customerId: string; overseasWarehouseId: string; skuCode: string; productNameZh?: string; movementType: 'RECEIPT' | 'MERGE' | 'OUTBOUND'; referenceNo: string; totalDelta: number; availableDelta: number; occurredAt: string }
export interface ReturnInventoryQuery { page: number; size: number; customerId?: string; overseasWarehouseId?: string; skuCode?: string; cartonMark?: string; returnNo?: string; status?: string } export interface ReturnInventoryQuery { page: number; size: number; customerId?: string; overseasWarehouseId?: string; skuCode?: string; cartonMark?: string; returnNo?: string; status?: string }
export async function listReturnStock(params: ReturnInventoryQuery) { const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ReturnStockRow>>, ApiEnvelope<OverseasPagePayload<ReturnStockRow>>>('/platform/v1/last-leg/return-inventory', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } export async function listReturnStock(params: ReturnInventoryQuery) { const response = await http.get<ApiEnvelope<OverseasPagePayload<ReturnStockRow>>, ApiEnvelope<OverseasPagePayload<ReturnStockRow>>>('/platform/v1/last-leg/return-inventory', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) }
export async function listReturnCartons(params: ReturnInventoryQuery) { const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ReturnCartonRow>>, ApiEnvelope<OverseasPagePayload<ReturnCartonRow>>>('/platform/v1/last-leg/return-inventory/cartons', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } export async function listReturnCartons(params: ReturnInventoryQuery) { const response = await http.get<ApiEnvelope<OverseasPagePayload<ReturnCartonRow>>, ApiEnvelope<OverseasPagePayload<ReturnCartonRow>>>('/platform/v1/last-leg/return-inventory/cartons', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) }
export async function listReturnInventoryMovements(params: ReturnInventoryQuery) { const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<ReturnInventoryMovement>>, ApiEnvelope<OverseasPagePayload<ReturnInventoryMovement>>>('/platform/v1/last-leg/return-inventory/movements', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) } export async function listReturnInventoryMovements(params: ReturnInventoryQuery) { const response = await http.get<ApiEnvelope<OverseasPagePayload<ReturnInventoryMovement>>, ApiEnvelope<OverseasPagePayload<ReturnInventoryMovement>>>('/platform/v1/last-leg/return-inventory/movements', { params }); return normalizeOverseasPage(unwrapApiResponse(response)) }
export async function getReturnCarton(cartonMark: string) { const response = await overseasHttp.get<ApiEnvelope<ReturnCartonDetail>, ApiEnvelope<ReturnCartonDetail>>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}`); return unwrapApiResponse(response) } export async function getReturnCarton(cartonMark: string) { const response = await http.get<ApiEnvelope<ReturnCartonDetail>, ApiEnvelope<ReturnCartonDetail>>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}`); return unwrapApiResponse(response) }
export async function getReturnCartonLabel(cartonMark: string) { const response = await overseasHttp.get<ApiEnvelope<ReturnCartonLabel>, ApiEnvelope<ReturnCartonLabel>>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label`); return unwrapApiResponse(response) } export async function getReturnCartonLabel(cartonMark: string) { const response = await http.get<ApiEnvelope<ReturnCartonLabel>, ApiEnvelope<ReturnCartonLabel>>(`/platform/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label`); return unwrapApiResponse(response) }
@@ -1,25 +1,25 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { overseasHttp } from '@/shared/api/overseasHttp' import { http } from '@/shared/api/http'
import { getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer } from './warehouseTransfers' import { getWarehouseTransfer, listWarehouseTransfers, reviewWarehouseTransfer } from './warehouseTransfers'
vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn() } }))
describe('warehouseTransfers API', () => { describe('warehouseTransfers API', () => {
beforeEach(() => vi.clearAllMocks()) beforeEach(() => vi.clearAllMocks())
it('always scopes platform list and detail requests to the selected customer', async () => { it('always scopes platform list and detail requests to the selected customer', async () => {
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 10, total: 0, pages: 0 } }) vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 10, total: 0, pages: 0 } })
vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } }) vi.mocked(http.get).mockResolvedValueOnce({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } })
await listWarehouseTransfers('20001', { page: 1, size: 10, status: 'PENDING_REVIEW' }) await listWarehouseTransfers('20001', { page: 1, size: 10, status: 'PENDING_REVIEW' })
await getWarehouseTransfer('20001', 'WT-1') await getWarehouseTransfer('20001', 'WT-1')
expect(vi.mocked(overseasHttp.get).mock.calls[0]).toEqual(['/platform/v1/last-leg/warehouse-transfers', { params: { page: 1, size: 10, status: 'PENDING_REVIEW', customerId: '20001' } }]) expect(vi.mocked(http.get).mock.calls[0]).toEqual(['/platform/v1/last-leg/warehouse-transfers', { params: { page: 1, size: 10, status: 'PENDING_REVIEW', customerId: '20001' } }])
expect(vi.mocked(overseasHttp.get).mock.calls[1]).toEqual(['/platform/v1/last-leg/warehouse-transfers/WT-1', { params: { customerId: '20001' } }]) expect(vi.mocked(http.get).mock.calls[1]).toEqual(['/platform/v1/last-leg/warehouse-transfers/WT-1', { params: { customerId: '20001' } }])
}) })
it('posts the real review decision with customer scope', async () => { it('posts the real review decision with customer scope', async () => {
vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { transferNo: 'WT-1', items: [], cartons: [], relabelBindings: [] } })
await reviewWarehouseTransfer('20001', 'WT-1', 'APPROVE', '资料无误') await reviewWarehouseTransfer('20001', 'WT-1', 'APPROVE', '资料无误')
expect(overseasHttp.post).toHaveBeenCalledWith( expect(http.post).toHaveBeenCalledWith(
'/platform/v1/last-leg/warehouse-transfers/WT-1/review', '/platform/v1/last-leg/warehouse-transfers/WT-1/review',
{ decision: 'APPROVE', remark: '资料无误' }, { decision: 'APPROVE', remark: '资料无误' },
{ params: { customerId: '20001' } }, { params: { customerId: '20001' } },
@@ -1,5 +1,5 @@
import { overseasHttp } from '@/shared/api/overseasHttp' import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
export interface WarehouseTransferQuery { export interface WarehouseTransferQuery {
@@ -87,7 +87,7 @@ export interface WarehouseTransferDetailDto extends WarehouseTransferSummaryDto
} }
export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) { export async function listWarehouseTransfers(customerId: string, query: WarehouseTransferQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>, ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>>( const response = await http.get<ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>, ApiEnvelope<OverseasPagePayload<WarehouseTransferSummaryDto>>>(
'/platform/v1/last-leg/warehouse-transfers', '/platform/v1/last-leg/warehouse-transfers',
{ params: { ...query, customerId } }, { params: { ...query, customerId } },
) )
@@ -95,7 +95,7 @@ export async function listWarehouseTransfers(customerId: string, query: Warehous
} }
export async function getWarehouseTransfer(customerId: string, transferNo: string) { export async function getWarehouseTransfer(customerId: string, transferNo: string) {
const response = await overseasHttp.get<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>( const response = await http.get<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>(
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}`, `/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}`,
{ params: { customerId } }, { params: { customerId } },
) )
@@ -108,7 +108,7 @@ export async function reviewWarehouseTransfer(
decision: 'APPROVE' | 'REJECT', decision: 'APPROVE' | 'REJECT',
remark?: string, remark?: string,
) { ) {
const response = await overseasHttp.post<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>( const response = await http.post<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>(
`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/review`, `/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/review`,
{ decision, remark }, { decision, remark },
{ params: { customerId } }, { params: { customerId } },
+2 -4
View File
@@ -1,23 +1,21 @@
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from './http' import { http } from './http'
import { overseasHttp } from './overseasHttp'
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation } from './inboundCodeRelations' import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, getPlatformInboundCodeAssociation } from './inboundCodeRelations'
vi.mock('./http', async (original) => ({ ...(await original<typeof import('./http')>()), http: { get: vi.fn() } })) vi.mock('./http', async (original) => ({ ...(await original<typeof import('./http')>()), http: { get: vi.fn() } }))
vi.mock('./overseasHttp', () => ({ overseasHttp: { get: vi.fn() } }))
describe('inbound-code relationship APIs', () => { describe('inbound-code relationship APIs', () => {
beforeEach(() => vi.clearAllMocks()) beforeEach(() => vi.clearAllMocks())
it('uses the frozen domestic and overseas read models and encodes the business code', async () => { it('uses the frozen domestic and overseas read models and encodes the business code', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', orderNo: 'ORD-1' } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', orderNo: 'ORD-1' } })
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', found: false, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [] } }) vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundCode: 'IN/01', found: false, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [] } })
await getPlatformInboundCodeAssociation('IN/01') await getPlatformInboundCodeAssociation('IN/01')
await getLoadingAssociationSummary('IN/01') await getLoadingAssociationSummary('IN/01')
await getOverseasInboundCodeRelationship('IN/01') await getOverseasInboundCodeRelationship('IN/01')
expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/inbound-code-associations/IN%2F01') expect(http.get).toHaveBeenCalledWith('/platform/v1/orders/first-leg/inbound-code-associations/IN%2F01')
expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-associations/summary', { params: { inboundCode: 'IN/01' } }) expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-associations/summary', { params: { inboundCode: 'IN/01' } })
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/inbound-code-relationships/IN%2F01') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/inventory/inbound-code-relationships/IN%2F01')
}) })
it('does not accept customer or internal ids as lookup parameters', () => { it('does not accept customer or internal ids as lookup parameters', () => {
+1 -2
View File
@@ -1,5 +1,4 @@
import { http, unwrapApiResponse, type ApiEnvelope } from './http' import { http, unwrapApiResponse, type ApiEnvelope } from './http'
import { overseasHttp } from './overseasHttp'
export interface PlatformInboundCodeAssociation { export interface PlatformInboundCodeAssociation {
inboundCode: string inboundCode: string
@@ -68,7 +67,7 @@ export async function getLoadingAssociationSummaryByInboundId(inboundId: string)
} }
export async function getOverseasInboundCodeRelationship(inboundCode: string) { export async function getOverseasInboundCodeRelationship(inboundCode: string) {
const response = await overseasHttp.get<ApiEnvelope<OverseasInboundCodeRelationship>, ApiEnvelope<OverseasInboundCodeRelationship>>( const response = await http.get<ApiEnvelope<OverseasInboundCodeRelationship>, ApiEnvelope<OverseasInboundCodeRelationship>>(
`/platform/v1/last-leg/inventory/inbound-code-relationships/${encodeURIComponent(inboundCode)}`, `/platform/v1/last-leg/inventory/inbound-code-relationships/${encodeURIComponent(inboundCode)}`,
) )
return unwrapApiResponse(response) return unwrapApiResponse(response)
-99
View File
@@ -1,99 +0,0 @@
import axios, { type InternalAxiosRequestConfig } from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { registerAuthRefreshHandler } from './http'
import { clearOverseasAccessToken, overseasHttp, replaceOverseasAccessToken } from './overseasHttp'
const originalOverseasAdapter = overseasHttp.defaults.adapter
function success(config: InternalAxiosRequestConfig, data: unknown) {
return Promise.resolve({ status: 200, statusText: 'OK', headers: {}, config, data })
}
describe('overseas HTTP client', () => {
beforeEach(() => {
localStorage.setItem('cc-admin-token', 'domestic-token')
clearOverseasAccessToken()
registerAuthRefreshHandler(async () => 'domestic-token')
})
afterEach(() => {
overseasHttp.defaults.adapter = originalOverseasAdapter
localStorage.clear()
clearOverseasAccessToken()
})
it('uses the overseas token supplied by the login session for concurrent requests', async () => {
replaceOverseasAccessToken('overseas-token', 900)
const authorizations: unknown[] = []
overseasHttp.defaults.adapter = async (config) => {
authorizations.push(config.headers.Authorization)
return success(config, { code: '200', message: 'success', data: [] })
}
await Promise.all([
overseasHttp.get('/platform/v1/last-leg/orders'),
overseasHttp.get('/platform/v1/warehouse-admin'),
])
expect(authorizations).toEqual(['Bearer overseas-token', 'Bearer overseas-token'])
})
it('refreshes the unified session once and replays once after an overseas 401', async () => {
replaceOverseasAccessToken('overseas-1', 900)
const refresh = vi.fn(async () => {
replaceOverseasAccessToken('overseas-2', 900)
return 'domestic-2'
})
registerAuthRefreshHandler(refresh)
let requests = 0
const authorizations: unknown[] = []
overseasHttp.defaults.adapter = async (config) => {
authorizations.push(config.headers.Authorization)
if (requests++ === 0) {
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined, {
status: 401,
statusText: 'Unauthorized',
headers: {},
config,
data: { code: 'A0401', message: '海外令牌失效' },
})
}
return success(config, { code: '200', message: 'success', data: [] })
}
await overseasHttp.get('/platform/v1/last-leg/orders')
expect(refresh).toHaveBeenCalledTimes(1)
expect(requests).toBe(2)
expect(authorizations).toEqual(['Bearer overseas-1', 'Bearer overseas-2'])
})
it('refreshes the unified session when no usable overseas token is present', async () => {
const refresh = vi.fn(async () => {
replaceOverseasAccessToken('overseas-refreshed', 900)
return 'domestic-refreshed'
})
registerAuthRefreshHandler(refresh)
let authorization: unknown
overseasHttp.defaults.adapter = async (config) => {
authorization = config.headers.Authorization
return success(config, { code: '200', message: 'success', data: [] })
}
await overseasHttp.get('/platform/v1/last-leg/orders')
expect(refresh).toHaveBeenCalledTimes(1)
expect(authorization).toBe('Bearer overseas-refreshed')
})
it('fails before sending a business request when refresh is domestic-only', async () => {
const refresh = vi.fn(async () => 'domestic-only')
registerAuthRefreshHandler(refresh)
const adapter = vi.fn(async (config: InternalAxiosRequestConfig) => success(config, { code: '200', message: 'success', data: [] }))
overseasHttp.defaults.adapter = adapter
await expect(overseasHttp.get('/platform/v1/last-leg/orders')).rejects.toThrow('当前环境未签发海外访问令牌')
expect(refresh).toHaveBeenCalledTimes(1)
expect(adapter).not.toHaveBeenCalled()
})
})
-77
View File
@@ -1,77 +0,0 @@
import axios, { type InternalAxiosRequestConfig } from 'axios'
import { parseApiJsonResponse } from '@chongchong/shared'
import { refreshAuthenticatedSession } from './http'
interface CachedOverseasToken {
accessToken: string
refreshAfter: number
}
type RetryableOverseasRequestConfig = InternalAxiosRequestConfig & { _overseasAuthRetry?: boolean }
const TOKEN_REFRESH_SAFETY_WINDOW_MS = 30_000
let cachedToken: CachedOverseasToken | null = null
export const overseasHttp = axios.create({
baseURL: import.meta.env.VITE_OVERSEAS_API_BASE_URL || '/overseas-api',
timeout: 15_000,
transformResponse: [(data: unknown) => parseApiJsonResponse(data)],
})
function currentOverseasAccessToken() {
return cachedToken && cachedToken.refreshAfter > Date.now() ? cachedToken.accessToken : ''
}
async function requireOverseasAccessToken() {
const current = currentOverseasAccessToken()
if (current) return current
await refreshAuthenticatedSession()
const refreshed = currentOverseasAccessToken()
if (!refreshed) throw new Error('当前环境未签发海外访问令牌')
return refreshed
}
export function replaceOverseasAccessToken(accessToken: string | null, expiresIn: number) {
if (!accessToken && expiresIn === 0) {
cachedToken = null
return
}
if (!accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
throw new Error('海外访问令牌响应无效')
}
cachedToken = {
accessToken,
refreshAfter: Date.now() + Math.max(0, expiresIn * 1000 - TOKEN_REFRESH_SAFETY_WINDOW_MS),
}
}
export function clearOverseasAccessToken() {
cachedToken = null
}
overseasHttp.interceptors.request.use(async (config) => {
const accessToken = await requireOverseasAccessToken()
config.headers.Authorization = `Bearer ${accessToken}`
return config
})
overseasHttp.interceptors.response.use(
(response) => response.data,
async (error) => {
const config = error.config as RetryableOverseasRequestConfig | undefined
if (error.response?.status !== 401 || !config || config._overseasAuthRetry) {
return Promise.reject(error)
}
config._overseasAuthRetry = true
clearOverseasAccessToken()
try {
await refreshAuthenticatedSession()
const accessToken = await requireOverseasAccessToken()
config.headers.Authorization = `Bearer ${accessToken}`
return overseasHttp.request(config)
} catch {
return Promise.reject(error)
}
},
)
+49
View File
@@ -0,0 +1,49 @@
import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { http, registerAuthRefreshHandler } from './http'
const originalAdapter = http.defaults.adapter
const paths = ['/platform/v1/orders', '/platform/v1/last-leg/orders', '/platform/v1/overseas-warehouse-resources/pallets']
describe('unified business session', () => {
beforeEach(() => localStorage.setItem('cc-admin-token', 'session-1'))
afterEach(() => {
http.defaults.adapter = originalAdapter
localStorage.clear()
registerAuthRefreshHandler(async () => { throw new Error('no active test session') })
})
it('uses one base URL and the same token across order, lastleg and warehouse resources', async () => {
const requests: unknown[] = []
const refresh = vi.fn()
registerAuthRefreshHandler(refresh)
http.defaults.adapter = async (config) => {
requests.push([config.baseURL, config.headers.Authorization])
return { status: 200, statusText: 'OK', headers: {}, config, data: { code: '200', data: [] } }
}
await Promise.all(paths.map(path => http.get(path)))
expect(requests).toEqual(paths.map(() => [http.defaults.baseURL, 'Bearer session-1']))
expect(refresh).not.toHaveBeenCalled()
})
it('shares a single refresh across simultaneous 401s from both business domains', async () => {
const refresh = vi.fn(async () => {
await Promise.resolve()
localStorage.setItem('cc-admin-token', 'session-2')
return 'session-2'
})
registerAuthRefreshHandler(refresh)
const successes: string[] = []
http.defaults.adapter = async (config) => {
if (config.headers.Authorization === 'Bearer session-1') {
throw new axios.AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined,
{ status: 401, statusText: 'Unauthorized', headers: {}, config, data: {} })
}
successes.push(String(config.headers.Authorization))
return { status: 200, statusText: 'OK', headers: {}, config, data: { code: '200', data: [] } }
}
await Promise.all(paths.map(path => http.get(path)))
expect(refresh).toHaveBeenCalledTimes(1)
expect(successes).toEqual(paths.map(() => 'Bearer session-2'))
})
})
-11
View File
@@ -11,7 +11,6 @@ export default defineConfig(({ mode }) => {
const authProxyTarget = process.env.AUTH_API_PROXY_TARGET || env.AUTH_API_PROXY_TARGET || proxyTarget const authProxyTarget = process.env.AUTH_API_PROXY_TARGET || env.AUTH_API_PROXY_TARGET || proxyTarget
const iamProxyTarget = process.env.IAM_API_PROXY_TARGET || env.IAM_API_PROXY_TARGET || proxyTarget const iamProxyTarget = process.env.IAM_API_PROXY_TARGET || env.IAM_API_PROXY_TARGET || proxyTarget
const warehouseProxyTarget = process.env.WAREHOUSE_API_PROXY_TARGET || env.WAREHOUSE_API_PROXY_TARGET || proxyTarget const warehouseProxyTarget = process.env.WAREHOUSE_API_PROXY_TARGET || env.WAREHOUSE_API_PROXY_TARGET || proxyTarget
const overseasProxyTarget = process.env.OVERSEAS_API_PROXY_TARGET || env.OVERSEAS_API_PROXY_TARGET || 'http://localhost:9300'
return { return {
plugins: [ plugins: [
@@ -69,16 +68,6 @@ export default defineConfig(({ mode }) => {
}) })
}, },
}, },
'/overseas-api': {
target: overseasProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/overseas-api/, '/api'),
configure(proxy) {
proxy.on('proxyReq', (proxyRequest) => {
proxyRequest.removeHeader('origin')
})
},
},
}, },
}, },
} }