test: cover merchant phase 5 delivery
This commit is contained in:
@@ -22,7 +22,7 @@ test('phase-five module covers dispatch edit, transfer, PDA pick/depart and deli
|
|||||||
for (const token of [
|
for (const token of [
|
||||||
"const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'",
|
"const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'",
|
||||||
"const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'",
|
"const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'",
|
||||||
'/transfer-delivery', '/items/${encodeURIComponent(id(item.dispatchItemId))}/pick',
|
'query(MERCHANT_DELIVERY_BASE, { dispatchNo })', '/items/${encodeURIComponent(id(item.dispatchItemId))}/pick',
|
||||||
'/complete`', '/depart`', '/outbound-orders/${encodeURIComponent(outboundNo)}',
|
'/complete`', '/depart`', '/outbound-orders/${encodeURIComponent(outboundNo)}',
|
||||||
'/delivery-orders/${encodeURIComponent(deliveryNo)}/status',
|
'/delivery-orders/${encodeURIComponent(deliveryNo)}/status',
|
||||||
]) assert.ok(source.includes(token), token)
|
]) assert.ok(source.includes(token), token)
|
||||||
@@ -33,6 +33,23 @@ test('phase-five module covers dispatch edit, transfer, PDA pick/depart and deli
|
|||||||
assert.match(source, /DELIVERING/)
|
assert.match(source, /DELIVERING/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('phase-five module proves the client delivery create, list and nested detail contract', () => {
|
||||||
|
assert.match(source, /const MERCHANT_DELIVERY_BASE = '\/api\/merchant\/v1\/last-leg\/delivery-orders'/)
|
||||||
|
assert.match(source, /loginMerchant\(context, customerId\)/)
|
||||||
|
assert.match(source, /context\.options\?\.merchantUsername/)
|
||||||
|
assert.match(source, /\/api\/merchant\/v1\/iam\/tenant\/profile/)
|
||||||
|
assert.match(source, /id\(profile\?\.id\) === customerId/)
|
||||||
|
assert.match(source, /客户创建派送并原子转出库单/)
|
||||||
|
assert.match(source, /deliveryMethod: 'TRUCK_PALLET'/)
|
||||||
|
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10/)
|
||||||
|
assert.match(source, /客户分页回查自身派送单/)
|
||||||
|
assert.match(source, /客户读取自身派送详情/)
|
||||||
|
assert.match(source, /merchantDetail\.data\?\.order\?\.deliveryNo/)
|
||||||
|
assert.match(source, /merchantDetail\.data\?\.skus/)
|
||||||
|
assert.match(source, /merchantDetail\.data\?\.fees/)
|
||||||
|
assert.match(source, /merchantDetail\.data\?\.operations/)
|
||||||
|
})
|
||||||
|
|
||||||
test('phase-five module proves completion, customer visit, manual exception and delivery return', () => {
|
test('phase-five module proves completion, customer visit, manual exception and delivery return', () => {
|
||||||
for (const token of [
|
for (const token of [
|
||||||
'/delivery-orders/${encodeURIComponent(deliveryNo)}/complete',
|
'/delivery-orders/${encodeURIComponent(deliveryNo)}/complete',
|
||||||
@@ -51,7 +68,7 @@ test('phase-five module uses marker-owned fixtures and always disables its tempo
|
|||||||
assert.match(source, /resolvePickerAccess\(context\)/)
|
assert.match(source, /resolvePickerAccess\(context\)/)
|
||||||
assert.match(source, /createPicker\(context, 5, warehouseId, access\)/)
|
assert.match(source, /createPicker\(context, 5, warehouseId, access\)/)
|
||||||
assert.match(source, /finally \{[\s\S]*disablePicker\(context, picker, 5\)[\s\S]*picker\.token = null/)
|
assert.match(source, /finally \{[\s\S]*disablePicker\(context, picker, 5\)[\s\S]*picker\.token = null/)
|
||||||
assert.doesNotMatch(source, /8\.163\.|password\s*:/)
|
assert.doesNotMatch(source, /8\.163\.|password\s*:\s*['"`]/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('phase-five writes carry idempotency and record deployment blockers instead of passing empty fixtures', () => {
|
test('phase-five writes carry idempotency and record deployment blockers instead of passing empty fixtures', () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
|||||||
const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'
|
const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'
|
||||||
const LASTLEG_BASE = '/api/platform/v1/last-leg'
|
const LASTLEG_BASE = '/api/platform/v1/last-leg'
|
||||||
const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'
|
const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'
|
||||||
|
const MERCHANT_DELIVERY_BASE = '/api/merchant/v1/last-leg/delivery-orders'
|
||||||
|
|
||||||
function id(value) { return value == null ? '' : String(value) }
|
function id(value) { return value == null ? '' : String(value) }
|
||||||
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
|
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
|
||||||
@@ -41,6 +42,37 @@ async function pda(context, token, method, name, path, body = undefined) {
|
|||||||
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
|
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
|
||||||
return { response, data: success(response) ? response.body?.data : null }
|
return { response, data: success(response) ? response.body?.data : null }
|
||||||
}
|
}
|
||||||
|
async function merchant(context, token, method, name, path, body = undefined) {
|
||||||
|
const response = await context.requestOverseas(method, path, body, {}, token)
|
||||||
|
const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS)
|
||||||
|
return { response, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginMerchant(context, customerId) {
|
||||||
|
const merchantFixture = context.context?.merchantQualificationFixture
|
||||||
|
const credentials = nonBlank(merchantFixture?.username) && nonBlank(merchantFixture?.password)
|
||||||
|
? { username: merchantFixture.username, password: merchantFixture.password }
|
||||||
|
: { username: context.options?.merchantUsername, password: context.options?.merchantPassword }
|
||||||
|
if (!nonBlank(credentials.username) || !nonBlank(credentials.password)) {
|
||||||
|
blocker(context, '取得本轮客户工作台账号', '既无本轮动态客户凭据,也未配置开发验收客户凭据')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const response = await context.request('POST', '/api/auth/login', {
|
||||||
|
loginId: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
}, '')
|
||||||
|
const auth = context.apiSuccess(MODULE, '本轮客户工作台账号登录', response, trace(response), OPTIONS)
|
||||||
|
context.assert(MODULE, '客户登录返回海外访问Token', response, nonBlank(auth?.overseasAccessToken),
|
||||||
|
`tokenPresent=${Boolean(auth?.overseasAccessToken)}; ${trace(response)}`, OPTIONS)
|
||||||
|
if (!nonBlank(auth?.accessToken) || !nonBlank(auth?.overseasAccessToken)) return null
|
||||||
|
const profileResponse = await context.request('GET', '/api/merchant/v1/iam/tenant/profile', undefined, auth.accessToken)
|
||||||
|
const profile = context.apiSuccess(MODULE, '回查客户工作台企业身份', profileResponse,
|
||||||
|
trace(profileResponse), OPTIONS)
|
||||||
|
context.assert(MODULE, '客户工作台企业与五期履约客户严格一致', profileResponse,
|
||||||
|
id(profile?.id) === customerId,
|
||||||
|
`merchantCustomer=${id(profile?.id) || 'missing'}, fulfillmentCustomer=${customerId}; ${trace(profileResponse)}`, OPTIONS)
|
||||||
|
return id(profile?.id) === customerId ? auth.overseasAccessToken : null
|
||||||
|
}
|
||||||
|
|
||||||
function fixture(context) {
|
function fixture(context) {
|
||||||
const dispatch = context.createdResources?.phase4OverseasDispatch
|
const dispatch = context.createdResources?.phase4OverseasDispatch
|
||||||
@@ -123,6 +155,8 @@ export async function run(context) {
|
|||||||
const customerId = id(source.receiving.customerId)
|
const customerId = id(source.receiving.customerId)
|
||||||
const warehouseId = id(source.receiving.overseasWarehouseId)
|
const warehouseId = id(source.receiving.overseasWarehouseId)
|
||||||
context.createdResources.phase5OverseasFulfillment.dispatchNo = dispatchNo
|
context.createdResources.phase5OverseasFulfillment.dispatchNo = dispatchNo
|
||||||
|
const merchantToken = await loginMerchant(context, customerId)
|
||||||
|
if (!merchantToken) return
|
||||||
|
|
||||||
const updateBody = {
|
const updateBody = {
|
||||||
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64),
|
||||||
@@ -140,15 +174,15 @@ export async function run(context) {
|
|||||||
|
|
||||||
const transferBody = {
|
const transferBody = {
|
||||||
idempotencyKey: key(context, 'TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(),
|
idempotencyKey: key(context, 'TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(),
|
||||||
deliveryMethod: 'SELF_DELIVERY', waybillNo: `${context.marker}-WAYBILL`.slice(0, 64),
|
deliveryMethod: 'TRUCK_PALLET', waybillNo: `${context.marker}-WAYBILL`.slice(0, 64),
|
||||||
carrierName: updateBody.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
|
carrierName: updateBody.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
|
||||||
vehicleType: 'TRUCK', vehicleSize: 'MEDIUM', driverName: updateBody.driverName,
|
vehicleType: 'TRUCK', vehicleSize: 'MEDIUM', driverName: updateBody.driverName,
|
||||||
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
|
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
|
||||||
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
|
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
|
||||||
fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 0, remark: `${context.marker}-acceptance` }],
|
fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
|
||||||
}
|
}
|
||||||
const transferred = await platform(context, 'POST', '排车原子转出库与派送单',
|
const transferred = await merchant(context, merchantToken, 'POST', '客户创建派送并原子转出库单',
|
||||||
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody)
|
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
|
||||||
const outboundNo = id(transferred.data?.outboundNo)
|
const outboundNo = id(transferred.data?.outboundNo)
|
||||||
const deliveryNo = id(transferred.data?.deliveryNo)
|
const deliveryNo = id(transferred.data?.deliveryNo)
|
||||||
context.assert(MODULE, '转派送同时生成WAIT_OUTBOUND与WAIT_DELIVERY单据', transferred.response,
|
context.assert(MODULE, '转派送同时生成WAIT_OUTBOUND与WAIT_DELIVERY单据', transferred.response,
|
||||||
@@ -157,18 +191,33 @@ export async function run(context) {
|
|||||||
`outboundNo=${outboundNo || 'missing'}, deliveryNo=${deliveryNo || 'missing'}; ${trace(transferred.response)}`, OPTIONS)
|
`outboundNo=${outboundNo || 'missing'}, deliveryNo=${deliveryNo || 'missing'}; ${trace(transferred.response)}`, OPTIONS)
|
||||||
if (!success(transferred.response) || !outboundNo || !deliveryNo) return
|
if (!success(transferred.response) || !outboundNo || !deliveryNo) return
|
||||||
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo })
|
Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo })
|
||||||
const transferReplay = await platform(context, 'POST', '排车转派送同键重放',
|
const transferReplay = await merchant(context, merchantToken, 'POST', '客户创建派送同键重放',
|
||||||
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody)
|
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
|
||||||
context.assert(MODULE, '转派送重放复用同一业务单据', transferReplay.response,
|
context.assert(MODULE, '转派送重放复用同一业务单据', transferReplay.response,
|
||||||
transferReplay.data?.replayed === true && id(transferReplay.data?.outboundNo) === outboundNo
|
transferReplay.data?.replayed === true && id(transferReplay.data?.outboundNo) === outboundNo
|
||||||
&& id(transferReplay.data?.deliveryNo) === deliveryNo, trace(transferReplay.response), OPTIONS)
|
&& id(transferReplay.data?.deliveryNo) === deliveryNo, trace(transferReplay.response), OPTIONS)
|
||||||
const transferConflict = await context.requestOverseas('POST',
|
const transferConflict = await context.requestOverseas('POST', query(MERCHANT_DELIVERY_BASE, { dispatchNo }),
|
||||||
`${LASTLEG_BASE}/dispatches/${encodeURIComponent(dispatchNo)}/transfer-delivery`,
|
{ ...transferBody, idempotencyKey: key(context, 'TRANSFER-DIFFERENT') }, {}, merchantToken)
|
||||||
{ ...transferBody, idempotencyKey: key(context, 'TRANSFER-DIFFERENT') })
|
|
||||||
context.addStep(MODULE, '同一排车使用另一幂等键仍返回既有单据', transferConflict,
|
context.addStep(MODULE, '同一排车使用另一幂等键仍返回既有单据', transferConflict,
|
||||||
success(transferConflict) && id(transferConflict.body?.data?.outboundNo) === outboundNo
|
success(transferConflict) && id(transferConflict.body?.data?.outboundNo) === outboundNo
|
||||||
&& transferConflict.body?.data?.replayed === true, trace(transferConflict), OPTIONS)
|
&& transferConflict.body?.data?.replayed === true, trace(transferConflict), OPTIONS)
|
||||||
|
|
||||||
|
const merchantPage = await merchant(context, merchantToken, 'GET', '客户分页回查自身派送单',
|
||||||
|
query(MERCHANT_DELIVERY_BASE, { page: 1, size: 20, dispatchNo }))
|
||||||
|
const merchantRows = Array.isArray(merchantPage.data?.records) ? merchantPage.data.records : []
|
||||||
|
context.assert(MODULE, '客户派送列表包含本轮单据且不依赖customerId参数', merchantPage.response,
|
||||||
|
merchantRows.some((value) => id(value?.deliveryNo) === deliveryNo && id(value?.dispatchNo) === dispatchNo),
|
||||||
|
`records=${merchantRows.length}; ${trace(merchantPage.response)}`, OPTIONS)
|
||||||
|
const merchantDetail = await merchant(context, merchantToken, 'GET', '客户读取自身派送详情',
|
||||||
|
`${MERCHANT_DELIVERY_BASE}/${encodeURIComponent(deliveryNo)}`)
|
||||||
|
context.assert(MODULE, '客户派送详情返回订单、货物、费用与操作四段正式契约', merchantDetail.response,
|
||||||
|
id(merchantDetail.data?.order?.deliveryNo) === deliveryNo
|
||||||
|
&& id(merchantDetail.data?.order?.dispatchNo) === dispatchNo
|
||||||
|
&& Array.isArray(merchantDetail.data?.skus) && merchantDetail.data.skus.length > 0
|
||||||
|
&& Array.isArray(merchantDetail.data?.fees) && Array.isArray(merchantDetail.data?.operations),
|
||||||
|
`skus=${merchantDetail.data?.skus?.length ?? 'missing'}, fees=${merchantDetail.data?.fees?.length ?? 'missing'}, operations=${merchantDetail.data?.operations?.length ?? 'missing'}; ${trace(merchantDetail.response)}`,
|
||||||
|
OPTIONS)
|
||||||
|
|
||||||
const access = await resolvePickerAccess(context)
|
const access = await resolvePickerAccess(context)
|
||||||
const picker = access ? await createPicker(context, 5, warehouseId, access) : null
|
const picker = access ? await createPicker(context, 5, warehouseId, access) : null
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user