feat: add platform self-operated sales inventory and collection views
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { analyticsApi } from './analytics'
|
||||
|
||||
const http = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() }))
|
||||
vi.mock('@/shared/api/http', () => ({ http, unwrapApiResponse: (value: { data: unknown }) => value.data }))
|
||||
beforeEach(() => { vi.clearAllMocks(); http.get.mockResolvedValue({ data: {} }); http.post.mockResolvedValue({ data: { taskNo: 'COL-001', status: 'QUEUED' } }) })
|
||||
describe('self-operated platform transport contract', () => {
|
||||
it('uses local fixed-version owners, owner overview and inventory attention endpoints', async () => {
|
||||
const fixed = { sourceAccount: 'lingxing-primary', warehouseCode: 'WH-A', salesRunId: '11', inventoryRunId: '12', queryAsOf: '2026-10-12T01:00:00Z' }
|
||||
await analyticsApi.owners({ ...fixed, keyword: 'OWNER_', current: 2, size: 20 })
|
||||
await analyticsApi.ownerOverview({ ...fixed, customerCode: 'OWNER_A', coverageDaysThreshold: 60 })
|
||||
await analyticsApi.inventory({ ...fixed, customerCode: 'OWNER_A', attention: 'COVERAGE_GT_THRESHOLD', sortBy: 'PHYSICAL_DESC', coverageDaysThreshold: 60, current: 2, size: 20 })
|
||||
expect(http.get.mock.calls.map(call => call[0])).toEqual(['owners', 'owner-overview', 'inventory-coverage'].map(path => `/platform/v1/last-leg/self-operated-analytics/${path}`))
|
||||
expect(http.get.mock.calls[1]?.[1].params).toEqual({ ...fixed, customerCode: 'OWNER_A', coverageDaysThreshold: 60 })
|
||||
expect(http.get.mock.calls[2]?.[1].params).toMatchObject({ attention: 'COVERAGE_GT_THRESHOLD', sortBy: 'PHYSICAL_DESC', current: 2 })
|
||||
})
|
||||
it('uses unified gateway domain with fixed context and abort signal, never supplier APIs', async () => {
|
||||
const abort = new AbortController()
|
||||
const query = { sourceAccount: 'lingxing-primary', warehouseCode: 'WH-A', current: 2, size: 20, salesRunId: '9007199254740999', inventoryRunId: '0', queryAsOf: '2026-10-12T01:00:00Z', salesScopeHash: 'hash' }
|
||||
await analyticsApi.summary(query, abort.signal)
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/self-operated-analytics/sku-sales-summary', { params: query, signal: abort.signal })
|
||||
})
|
||||
it('reads task details by escaped taskNo with independent runtime list upper pair', async () => {
|
||||
await analyticsApi.task({ sourceAccount: 'lingxing-primary', warehouseCode: 'WH-A' }, 'COL/001')
|
||||
expect(http.get.mock.calls[0]?.[0]).toBe('/platform/v1/last-leg/self-operated-analytics/sync-tasks/COL%2F001')
|
||||
const params = { sourceAccount: 'lingxing-primary', warehouseCode: 'WH-A', current: 2, size: 20, listUpperCreatedAt: '2026-10-12T01:00:00', listUpperId: '9007199254740999' }
|
||||
await analyticsApi.tasks(params)
|
||||
expect(http.get.mock.calls[1]?.[1].params).toEqual(params)
|
||||
})
|
||||
it('posts one RETRY command with business task number and exact idempotency key', async () => {
|
||||
const command = { action: 'RETRY' as const, taskNo: 'COL-001', warehouseCode: 'WH-A', requestKey: 'request-001', reason: '恢复原任务' }
|
||||
expect(await analyticsApi.retry(command)).toEqual({ taskNo: 'COL-001', status: 'QUEUED' })
|
||||
expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/self-operated-analytics/sync-tasks', command, { signal: undefined })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { BusinessPage, BusinessQuery, DailyResponse, Metric, Overview, OwnerOption, OwnerOverview, OwnerOverviewQuery, OwnerQuery, RetryCommand, RetryResult, Run, RuntimePage, RuntimeQuery, SalesSummary, Scope, TaskDetail } from '../types'
|
||||
|
||||
const base = '/platform/v1/last-leg/self-operated-analytics'
|
||||
async function get<T>(path: string, params: object, signal?: AbortSignal): Promise<T> {
|
||||
const response = await http.get<ApiEnvelope<T>, ApiEnvelope<T>>(`${base}/${path}`, { params, signal })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export const analyticsApi = {
|
||||
owners: (query: OwnerQuery, signal?: AbortSignal) => get<BusinessPage<OwnerOption>>('owners', query, signal),
|
||||
ownerOverview: (query: OwnerOverviewQuery, signal?: AbortSignal) => get<OwnerOverview | null>('owner-overview', query, signal),
|
||||
summary: (query: BusinessQuery, signal?: AbortSignal) => get<BusinessPage<SalesSummary>>('sku-sales-summary', query, signal),
|
||||
daily: (query: BusinessQuery, signal?: AbortSignal) => get<DailyResponse>('sku-daily-sales', query, signal),
|
||||
hot: (query: BusinessQuery, signal?: AbortSignal) => get<BusinessPage<Metric>>('hot-skus', query, signal),
|
||||
slow: (query: BusinessQuery, signal?: AbortSignal) => get<BusinessPage<Metric>>('slow-skus', query, signal),
|
||||
inventory: (query: BusinessQuery, signal?: AbortSignal) => get<BusinessPage<Metric>>('inventory-coverage', query, signal),
|
||||
overview: (sourceAccount: string, warehouseCode?: string, signal?: AbortSignal) => get<Overview>('sync-overview', { sourceAccount, warehouseCode }, signal),
|
||||
tasks: (query: RuntimeQuery, signal?: AbortSignal) => get<RuntimePage<TaskDetail['task']>>('sync-tasks', query, signal),
|
||||
task: (scope: Scope, taskNo: string, signal?: AbortSignal) => get<TaskDetail>(`sync-tasks/${encodeURIComponent(taskNo)}`, scope, signal),
|
||||
runs: (query: RuntimeQuery, signal?: AbortSignal) => get<RuntimePage<Run>>('sync-runs', query, signal),
|
||||
async retry(command: RetryCommand, signal?: AbortSignal): Promise<RetryResult> {
|
||||
const response = await http.post<ApiEnvelope<RetryResult>, ApiEnvelope<RetryResult>>(`${base}/sync-tasks`, command, { signal })
|
||||
return unwrapApiResponse(response)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user