feat:拼单订单新增一个取消部分的按钮功能,点击之后,打手进行中和待接单的都会取消订单
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
acceptWorkOrderShareAndSettle,
|
||||
cancelWorkOrderShare,
|
||||
cancelWorkOrderSharing,
|
||||
cancelWorkOrderSharingPart,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
reduceWorkOrderShareQuantity,
|
||||
updateWorkOrder,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
import {
|
||||
mapWorkOrderAdmin,
|
||||
mapWorkOrderShare,
|
||||
normalizeAmountFen,
|
||||
normalizeOptionalId,
|
||||
resolveWorkOrderOperationalStatus,
|
||||
} from './mappers.js'
|
||||
@@ -329,6 +331,95 @@ export async function cancelAdminWorkOrderSharing(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼单「取消部分」:保留已验收份额,取消打手进行中(joined / submitted)与待接单的剩余份数,
|
||||
* 取消的份额退还押金;一份都没验收时整单取消。
|
||||
*/
|
||||
export async function cancelAdminWorkOrderSharingPart(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
actorPermissions: string[] = [],
|
||||
) {
|
||||
if (!actorPermissions.includes('worker_order.cancel_share')) {
|
||||
throw createHttpError('当前账号没有取消部分拼单的权限', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_permission_denied',
|
||||
})
|
||||
}
|
||||
const reason = String(payload.reason || '').trim()
|
||||
if (!reason) {
|
||||
throw createHttpError('取消部分时请填写原因', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_sharing_part_cancel_reason_required',
|
||||
})
|
||||
}
|
||||
const rawRefundAmount = payload.refundAmount
|
||||
let refundAmountFen: number | null = null
|
||||
if (
|
||||
rawRefundAmount !== undefined &&
|
||||
rawRefundAmount !== null &&
|
||||
String(rawRefundAmount).trim() !== ''
|
||||
) {
|
||||
refundAmountFen = normalizeAmountFen(rawRefundAmount, -1)
|
||||
if (refundAmountFen < 0) {
|
||||
throw createHttpError('退款金额不能为负数', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_sharing_part_cancel_refund_invalid',
|
||||
})
|
||||
}
|
||||
}
|
||||
const financeConfig = getWorkerFinanceConfig()
|
||||
const result = await cancelWorkOrderSharingPart({
|
||||
workOrderId: Number(workOrderId),
|
||||
refundAmountFen,
|
||||
reason,
|
||||
now: nowIso(),
|
||||
actorName,
|
||||
unfreezeDays: financeConfig.depositUnfreezeDays,
|
||||
rewardUnfreezeEnabled: financeConfig.rewardUnfreezeEnabled,
|
||||
})
|
||||
if (result.failureReason === 'work_order_not_cancellable') {
|
||||
throw createHttpError('当前订单不是可取消部分的拼单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_part_cancel_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'status_invalid') {
|
||||
throw createHttpError('当前订单状态不能取消部分', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_part_cancel_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'nothing_to_cancel') {
|
||||
throw createHttpError('没有可取消的未验收份数', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_part_cancel_nothing_to_cancel',
|
||||
})
|
||||
}
|
||||
if (!result.order) {
|
||||
throw createHttpError('取消部分失败,订单状态可能已变化', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_part_cancel_conflict',
|
||||
})
|
||||
}
|
||||
const activeWorkerIds = await resolveWorkOrderRealtimeWorkerIds(result.order)
|
||||
const workerIds = [...new Set([...activeWorkerIds, ...result.cancelledWorkerIds])]
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(workOrderId),
|
||||
workerIds,
|
||||
hallChanged: true,
|
||||
})
|
||||
for (const workerId of result.cancelledWorkerIds) publishWorkerWalletRealtimeChange(workerId)
|
||||
return {
|
||||
order: mapWorkOrderAdmin(result.order),
|
||||
cancelledShareCount: result.cancelledShareIds.length,
|
||||
cancelledQuantity: result.cancelledQuantity,
|
||||
releasedDepositAmount: result.releasedDepositAmount,
|
||||
refundAmount: result.refundAmount,
|
||||
}
|
||||
}
|
||||
|
||||
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
|
||||
const workOrderIds = [
|
||||
...new Set(
|
||||
|
||||
@@ -54,6 +54,7 @@ export {
|
||||
acceptAdminWorkOrders,
|
||||
cancelAdminWorkOrderShare,
|
||||
cancelAdminWorkOrderSharing,
|
||||
cancelAdminWorkOrderSharingPart,
|
||||
reduceAdminWorkOrderShareQuantity,
|
||||
} from './admin-work-order-acceptance-service.js'
|
||||
export * from './worker-product-match-config-service.js'
|
||||
|
||||
@@ -248,6 +248,7 @@ export function mapWorkOrderAdmin(
|
||||
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
|
||||
const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0))
|
||||
const sharingTotalQuantity = Math.max(1, Number(workOrder.sharing_total_quantity || 1))
|
||||
const acceptedShareQuantity = Math.max(0, Number(workOrder.accepted_share_quantity || 0))
|
||||
const hasActiveShares = activeShareQuantity > 0
|
||||
const sharingFilled =
|
||||
workOrder.sharing_enabled === true && activeShareQuantity >= sharingTotalQuantity
|
||||
@@ -306,6 +307,10 @@ export function mapWorkOrderAdmin(
|
||||
workOrder.sharing_enabled === true &&
|
||||
hasActiveShares &&
|
||||
['open', 'pending_acceptance', 'in_progress'].includes(workOrder.status),
|
||||
canCancelSharingPart:
|
||||
workOrder.sharing_enabled === true &&
|
||||
sharingTotalQuantity > acceptedShareQuantity &&
|
||||
['open', 'pending_acceptance', 'in_progress', 'problem'].includes(workOrder.status),
|
||||
canResolveProblem: workOrder.status === WORK_ORDER_STATUS.PROBLEM,
|
||||
canDeduct: workOrder.status === WORK_ORDER_STATUS.ACCEPTED,
|
||||
canConfirmFriend:
|
||||
@@ -369,11 +374,15 @@ export function mapWorkOrderAdmin(
|
||||
joinedQuantity: activeShareQuantity,
|
||||
pendingSubmissionCount: pendingShareCount,
|
||||
remainingQuantity: Math.max(0, sharingTotalQuantity - activeShareQuantity),
|
||||
acceptedQuantity: acceptedShareQuantity,
|
||||
cancellableQuantity: Math.max(0, sharingTotalQuantity - acceptedShareQuantity),
|
||||
filled: sharingFilled,
|
||||
},
|
||||
capabilities,
|
||||
rewardAmount: Number(workOrder.reward_amount || 0),
|
||||
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
|
||||
/** 来源订单实付金额(分):取消部分时用于预填退款金额。 */
|
||||
sourceOrderAmount: Number(workOrder.source_order_amount_fen || 0),
|
||||
sharing: {
|
||||
enabled: workOrder.sharing_enabled === true,
|
||||
totalQuantity: Number(workOrder.sharing_total_quantity || 1),
|
||||
|
||||
@@ -3501,3 +3501,49 @@ test('拼单每次参与都新增一条份额,并按 requestId 幂等、按参
|
||||
'押金必须按该打手在本工单的累计份数与已冻结合计计算增量',
|
||||
)
|
||||
})
|
||||
|
||||
test('取消部分:只取消未验收份额并保留已验收,剩余份数收敛后收尾母单', () => {
|
||||
const repoSrc = fs.readFileSync(
|
||||
new URL('../../repositories/worker-platform/work-order-settlement-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const block = repoSrc.slice(
|
||||
repoSrc.indexOf('export async function cancelWorkOrderSharingPart('),
|
||||
repoSrc.length,
|
||||
)
|
||||
assert.ok(block.length > 0, 'cancelWorkOrderSharingPart 必须存在')
|
||||
assert.ok(
|
||||
block.includes("['joined', 'submitted']"),
|
||||
'只允许取消进行中(joined / submitted)份额,已验收份额必须保留',
|
||||
)
|
||||
assert.ok(
|
||||
!block.includes("status = 'accepted'"),
|
||||
'母单转已验收必须走统一收尾函数,不得直接改状态',
|
||||
)
|
||||
assert.ok(
|
||||
block.includes('finalizeSharingOrderAcceptedWithClient('),
|
||||
'有已验收份额时必须走统一收尾(含寄卖结算)',
|
||||
)
|
||||
assert.ok(block.includes('deposit_release'), '取消的份额必须退还押金')
|
||||
assert.ok(block.includes('admin_partial'), '取消原因必须标记为 admin_partial')
|
||||
assert.ok(block.includes('problem_marked_at = NULL'), '问题标记要随被取消的份额一起清掉')
|
||||
assert.ok(block.includes('sharing_total_quantity = $1'), '取消后必须把母单总量收敛为已验收份数')
|
||||
assert.ok(
|
||||
block.includes('source_order_amount_fen') && block.includes('resolveSharingPartCancelRefund('),
|
||||
'退款默认值必须按实付金额比例计算',
|
||||
)
|
||||
|
||||
const routeSrc = fs.readFileSync(
|
||||
new URL('../../routes/admin/worker-platform/acceptance.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(
|
||||
routeSrc.includes("'/worker-platform/orders/:workOrderId/sharing/cancel-part'"),
|
||||
'取消部分接口必须存在',
|
||||
)
|
||||
const mapperSrc = fs.readFileSync(
|
||||
new URL('./work-order-display-mappers.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(mapperSrc.includes('canCancelSharingPart:'), '前端按钮依赖 canCancelSharingPart 能力位')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user