拆分工单验收结算仓储
This commit is contained in:
@@ -1,46 +1,3 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
|
||||
import {
|
||||
ensureWorkerWalletWithClient,
|
||||
getWorkerWalletWithClient,
|
||||
toJsonString,
|
||||
toPositiveInteger,
|
||||
WORK_ORDER_SHARE_SELECT,
|
||||
resolveWorkOrderShareCancellationStatus,
|
||||
} from './shared.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import {
|
||||
countTimeoutEventsByWorkerIds,
|
||||
countWorkerCancellationsSince,
|
||||
countWorkerActiveOrders,
|
||||
countWorkerActiveOrdersWithClient,
|
||||
countWorkerTimeoutEvents,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByIdWithClient,
|
||||
getWorkOrderByOrderItemId,
|
||||
getWorkerOrderOverview,
|
||||
isWorkOrderWithinHallCapacityWithClient,
|
||||
listOverdueWorkOrders,
|
||||
WORK_ORDER_SELECT,
|
||||
} from './work-order-query-repo.js'
|
||||
import type {
|
||||
CreateWorkOrderInput,
|
||||
GrabWorkOrderResult,
|
||||
ListInput,
|
||||
ProblemWorkOrderResolutionAction,
|
||||
WorkerDepositUnfreezeRow,
|
||||
WorkOrderRow,
|
||||
WorkOrderShareRow,
|
||||
WorkOrderStatistics,
|
||||
} from './types.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
|
||||
import {
|
||||
enqueueDepositUnfreezeWithClient,
|
||||
getOutstandingDepositAmountWithClient,
|
||||
resolveOutstandingDepositAmount,
|
||||
} from './work-order-deposit-repo.js'
|
||||
|
||||
export * from './work-order-query-repo.js'
|
||||
export { sumPendingUnfreezeByOrderIds } from './work-order-deposit-query-repo.js'
|
||||
export { listDueDepositUnfreezes } from './work-order-deposit-query-repo.js'
|
||||
@@ -73,701 +30,8 @@ export {
|
||||
resolveProblemWorkOrder,
|
||||
settleOverdueWorkOrder,
|
||||
} from './work-order-issue-repo.js'
|
||||
|
||||
export async function acceptWorkOrderAndSettle(input: {
|
||||
workOrderId: number
|
||||
unfreezeDays: number
|
||||
now: string
|
||||
/** 操作人类型:admin / worker / system */
|
||||
actorType?: string
|
||||
/** 操作人显示名 */
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
failureReason: 'sharing_pending_submissions' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkOrderRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM work_orders
|
||||
WHERE id = $1
|
||||
AND status = 'pending_acceptance'
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = currentResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return { order: null, failureReason: null }
|
||||
}
|
||||
|
||||
const sharesResult = await client.query<WorkOrderShareRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM work_order_shares
|
||||
WHERE work_order_id = $1
|
||||
AND status != 'cancelled'
|
||||
ORDER BY id ASC
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const shares = sharesResult.rows
|
||||
if (shares.length > 0) {
|
||||
const pendingShares = shares.filter((share) => share.status === 'joined')
|
||||
if (pendingShares.length > 0) {
|
||||
return {
|
||||
order: null,
|
||||
failureReason: 'sharing_pending_submissions',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET status = 'accepted', accepted_at = $1, updated_at = $1
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
|
||||
if (shares.length > 0) {
|
||||
// 单独验收过的份额已经完成资金结算,统一验收时只处理尚未结算的份额。
|
||||
const unsettledShares = shares.filter((share) => share.status !== 'accepted')
|
||||
for (const share of unsettledShares) {
|
||||
const shareWorkerId = Number(share.worker_id || 0)
|
||||
if (!shareWorkerId) continue
|
||||
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, shareWorkerId)
|
||||
const releaseAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const shareReward = Number(share.share_reward || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId: shareWorkerId,
|
||||
amount: shareReward,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
shareReward,
|
||||
input.now,
|
||||
shareWorkerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId: shareWorkerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_pending_unfreeze', $2, $3, $4, $5, '拼单验收押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '拼单验收释放押金', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: Number(share.id) }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shareReward > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
shareReward,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
`拼单结算报酬 ${share.quantity} 份`,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
quantity: share.quantity,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
caseId: debtOffset.caseId,
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_order_shares
|
||||
SET status = 'accepted', accepted_at = $1, updated_at = $1
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, Number(share.id)],
|
||||
)
|
||||
await maybeUpgradeWorkerLevelWithClient(client, shareWorkerId, input.now)
|
||||
}
|
||||
}
|
||||
|
||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
const releaseAmount = Math.min(
|
||||
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const rewardAmount = Number(workOrder.reward_amount || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId,
|
||||
amount: rewardAmount,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
rewardAmount +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
rewardAmount,
|
||||
input.now,
|
||||
workerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_pending_unfreeze', $2, $3, $4, $5, '验收通过押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '验收通过释放押金', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (rewardAmount > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'reward_settlement', $2, $3, $4, $5, '验收通过结算报酬', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, rewardAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ caseId: debtOffset.caseId, workOrderId: input.workOrderId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: input.actorType || 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'accepted',
|
||||
fromStatus: 'pending_acceptance',
|
||||
toStatus: 'accepted',
|
||||
payloadJson: JSON.stringify({
|
||||
shareCount: shares.length,
|
||||
settledShareCount: shares.filter((share) => share.status !== 'accepted').length,
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
if (workerId > 0) {
|
||||
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
|
||||
}
|
||||
|
||||
return {
|
||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
|
||||
export async function acceptWorkOrderShareAndSettle(input: {
|
||||
workOrderId: number
|
||||
shareId: number
|
||||
unfreezeDays: number
|
||||
now: string
|
||||
actorType?: string
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
share: WorkOrderShareRow | null
|
||||
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance') FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = orderResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return { order: null, share: null, failureReason: 'work_order_not_accepting' }
|
||||
}
|
||||
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[input.shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share || share.status !== 'submitted') {
|
||||
return { order: workOrder, share, failureReason: 'share_not_submitted' }
|
||||
}
|
||||
|
||||
const workerId = Number(share.worker_id || 0)
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
const releaseAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const shareReward = Number(share.share_reward || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId,
|
||||
amount: shareReward,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
SET available_amount = $1, frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4, updated_at = $5
|
||||
WHERE worker_id = $6`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
shareReward,
|
||||
input.now,
|
||||
workerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0 && shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
}
|
||||
if (releaseAmount > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)`,
|
||||
[
|
||||
workerId,
|
||||
shouldDelayUnfreeze ? 'deposit_pending_unfreeze' : 'deposit_release',
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
shouldDelayUnfreeze ? '拼单份额验收押金待解冻' : '拼单份额验收释放押金',
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
if (shareReward > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)`,
|
||||
[
|
||||
workerId,
|
||||
shareReward,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
`拼单份额验收结算报酬 ${share.quantity} 份`,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)`,
|
||||
[
|
||||
workerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
caseId: debtOffset.caseId,
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: input.shareId,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE work_order_shares SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
)
|
||||
if (workerId > 0) {
|
||||
// 先落库份额已验收状态,再按去重后的订单数计算等级进度。
|
||||
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
|
||||
}
|
||||
const progressResult = await client.query<{
|
||||
joined_quantity: number
|
||||
unfinished_count: number
|
||||
}>(
|
||||
`SELECT COALESCE(SUM(quantity), 0)::int AS joined_quantity,
|
||||
COUNT(*) FILTER (WHERE status != 'accepted' AND status != 'cancelled')::int AS unfinished_count
|
||||
FROM work_order_shares WHERE work_order_id = $1 AND status != 'cancelled'`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const progress = progressResult.rows[0]
|
||||
const filled =
|
||||
Number(progress?.joined_quantity || 0) >= Number(workOrder.sharing_total_quantity || 1)
|
||||
const allAccepted = Number(progress?.unfinished_count || 0) === 0
|
||||
const nextOrderStatus = filled && allAccepted ? 'accepted' : workOrder.status
|
||||
if (nextOrderStatus === 'accepted') {
|
||||
await client.query(
|
||||
`UPDATE work_orders SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
}
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: input.actorType || 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'sharing_share_accepted',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextOrderStatus,
|
||||
payloadJson: JSON.stringify({ shareId: input.shareId, workerId }),
|
||||
now: input.now,
|
||||
})
|
||||
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
|
||||
const updatedShareResult = await client.query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||
[input.shareId],
|
||||
)
|
||||
return {
|
||||
order: updatedOrder,
|
||||
share: updatedShareResult.rows[0] || null,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 后台撤销单个拼单份额,释放该打手押金并把份数重新放回大厅。 */
|
||||
export async function cancelWorkOrderShare(input: {
|
||||
workOrderId: number
|
||||
shareId: number
|
||||
now: string
|
||||
reason?: string
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
share: WorkOrderShareRow | null
|
||||
releasedDepositAmount: number
|
||||
failureReason:
|
||||
| 'work_order_not_cancellable'
|
||||
| 'share_not_found'
|
||||
| 'share_status_not_cancellable'
|
||||
| null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = orderResult.rows[0] || null
|
||||
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share: null,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'work_order_not_cancellable',
|
||||
}
|
||||
}
|
||||
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[input.shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share: null,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'share_not_found',
|
||||
}
|
||||
}
|
||||
const nextOrderStatus = resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
|
||||
if (!nextOrderStatus) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'share_status_not_cancellable',
|
||||
}
|
||||
}
|
||||
|
||||
const workerId = Number(share.worker_id || 0)
|
||||
let releasedDepositAmount = 0
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
releasedDepositAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
if (releasedDepositAmount > 0) {
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
|
||||
const nextFrozen = Math.max(
|
||||
0,
|
||||
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
|
||||
)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||
WHERE worker_id = $4`,
|
||||
[nextAvailable, nextFrozen, input.now, workerId],
|
||||
)
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '后台撤销拼单份额退还押金', $6::jsonb, $7)`,
|
||||
[
|
||||
workerId,
|
||||
releasedDepositAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: input.shareId,
|
||||
reason: String(input.reason || '').trim(),
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE work_order_shares
|
||||
SET status = 'cancelled', updated_at = $1
|
||||
WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
)
|
||||
await client.query(
|
||||
`UPDATE work_orders
|
||||
SET status = $1,
|
||||
submitted_at = NULL,
|
||||
published_at = COALESCE(published_at, $2),
|
||||
updated_at = $2
|
||||
WHERE id = $3`,
|
||||
[nextOrderStatus, input.now, input.workOrderId],
|
||||
)
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'sharing_share_cancelled_by_admin',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextOrderStatus,
|
||||
payloadJson: JSON.stringify({
|
||||
shareId: input.shareId,
|
||||
workerId,
|
||||
quantity: Number(share.quantity || 0),
|
||||
releasedDepositAmount,
|
||||
reason: String(input.reason || '').trim(),
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
const updatedShareResult = await client.query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||
[input.shareId],
|
||||
)
|
||||
return {
|
||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||
share: updatedShareResult.rows[0] || null,
|
||||
releasedDepositAmount,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
acceptWorkOrderAndSettle,
|
||||
acceptWorkOrderShareAndSettle,
|
||||
cancelWorkOrderShare,
|
||||
} from './work-order-settlement-repo.js'
|
||||
|
||||
@@ -0,0 +1,713 @@
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
|
||||
import {
|
||||
ensureWorkerWalletWithClient,
|
||||
getWorkerWalletWithClient,
|
||||
WORK_ORDER_SHARE_SELECT,
|
||||
resolveWorkOrderShareCancellationStatus,
|
||||
} from './shared.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
|
||||
import {
|
||||
enqueueDepositUnfreezeWithClient,
|
||||
getOutstandingDepositAmountWithClient,
|
||||
} from './work-order-deposit-repo.js'
|
||||
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
|
||||
import type { WorkOrderRow, WorkOrderShareRow } from './types.js'
|
||||
|
||||
export async function acceptWorkOrderAndSettle(input: {
|
||||
workOrderId: number
|
||||
unfreezeDays: number
|
||||
now: string
|
||||
/** 操作人类型:admin / worker / system */
|
||||
actorType?: string
|
||||
/** 操作人显示名 */
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
failureReason: 'sharing_pending_submissions' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkOrderRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM work_orders
|
||||
WHERE id = $1
|
||||
AND status = 'pending_acceptance'
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = currentResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return { order: null, failureReason: null }
|
||||
}
|
||||
|
||||
const sharesResult = await client.query<WorkOrderShareRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM work_order_shares
|
||||
WHERE work_order_id = $1
|
||||
AND status != 'cancelled'
|
||||
ORDER BY id ASC
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const shares = sharesResult.rows
|
||||
if (shares.length > 0) {
|
||||
const pendingShares = shares.filter((share) => share.status === 'joined')
|
||||
if (pendingShares.length > 0) {
|
||||
return {
|
||||
order: null,
|
||||
failureReason: 'sharing_pending_submissions',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET status = 'accepted', accepted_at = $1, updated_at = $1
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
|
||||
if (shares.length > 0) {
|
||||
// 单独验收过的份额已经完成资金结算,统一验收时只处理尚未结算的份额。
|
||||
const unsettledShares = shares.filter((share) => share.status !== 'accepted')
|
||||
for (const share of unsettledShares) {
|
||||
const shareWorkerId = Number(share.worker_id || 0)
|
||||
if (!shareWorkerId) continue
|
||||
await ensureWorkerWalletWithClient(client, shareWorkerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, shareWorkerId)
|
||||
const releaseAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const shareReward = Number(share.share_reward || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId: shareWorkerId,
|
||||
amount: shareReward,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
shareReward,
|
||||
input.now,
|
||||
shareWorkerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId: shareWorkerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_pending_unfreeze', $2, $3, $4, $5, '拼单验收押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '拼单验收释放押金', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: Number(share.id) }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shareReward > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
shareReward,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
`拼单结算报酬 ${share.quantity} 份`,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
quantity: share.quantity,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
shareWorkerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
caseId: debtOffset.caseId,
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: Number(share.id),
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE work_order_shares
|
||||
SET status = 'accepted', accepted_at = $1, updated_at = $1
|
||||
WHERE id = $2
|
||||
`,
|
||||
[input.now, Number(share.id)],
|
||||
)
|
||||
await maybeUpgradeWorkerLevelWithClient(client, shareWorkerId, input.now)
|
||||
}
|
||||
}
|
||||
|
||||
const workerId = Number(workOrder.assigned_worker_id || 0)
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
const releaseAmount = Math.min(
|
||||
await getOutstandingDepositAmountWithClient(client, workerId, input.workOrderId),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const rewardAmount = Number(workOrder.reward_amount || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId,
|
||||
amount: rewardAmount,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
rewardAmount +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
SET
|
||||
available_amount = $1,
|
||||
frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4,
|
||||
updated_at = $5
|
||||
WHERE worker_id = $6
|
||||
`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
rewardAmount,
|
||||
input.now,
|
||||
workerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0) {
|
||||
if (shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_pending_unfreeze', $2, $3, $4, $5, '验收通过押金待解冻', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
} else {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '验收通过释放押金', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (rewardAmount > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'reward_settlement', $2, $3, $4, $5, '验收通过结算报酬', '{}'::jsonb, $6)
|
||||
`,
|
||||
[workerId, rewardAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
workerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({ caseId: debtOffset.caseId, workOrderId: input.workOrderId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: input.actorType || 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'accepted',
|
||||
fromStatus: 'pending_acceptance',
|
||||
toStatus: 'accepted',
|
||||
payloadJson: JSON.stringify({
|
||||
shareCount: shares.length,
|
||||
settledShareCount: shares.filter((share) => share.status !== 'accepted').length,
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
if (workerId > 0) {
|
||||
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
|
||||
}
|
||||
|
||||
return {
|
||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
|
||||
export async function acceptWorkOrderShareAndSettle(input: {
|
||||
workOrderId: number
|
||||
shareId: number
|
||||
unfreezeDays: number
|
||||
now: string
|
||||
actorType?: string
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
share: WorkOrderShareRow | null
|
||||
failureReason: 'work_order_not_accepting' | 'share_not_submitted' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 AND status IN ('open', 'pending_acceptance') FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = orderResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return { order: null, share: null, failureReason: 'work_order_not_accepting' }
|
||||
}
|
||||
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[input.shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share || share.status !== 'submitted') {
|
||||
return { order: workOrder, share, failureReason: 'share_not_submitted' }
|
||||
}
|
||||
|
||||
const workerId = Number(share.worker_id || 0)
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
const releaseAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
const shareReward = Number(share.share_reward || 0)
|
||||
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId,
|
||||
amount: shareReward,
|
||||
workOrderId: input.workOrderId,
|
||||
now: input.now,
|
||||
})
|
||||
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
|
||||
const shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
SET available_amount = $1, frozen_deposit_amount = $2,
|
||||
pending_unfreeze_amount = pending_unfreeze_amount + $3,
|
||||
total_settled_amount = total_settled_amount + $4, updated_at = $5
|
||||
WHERE worker_id = $6`,
|
||||
[
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
shouldDelayUnfreeze ? releaseAmount : 0,
|
||||
shareReward,
|
||||
input.now,
|
||||
workerId,
|
||||
],
|
||||
)
|
||||
|
||||
if (releaseAmount > 0 && shouldDelayUnfreeze) {
|
||||
await enqueueDepositUnfreezeWithClient(client, {
|
||||
workerId,
|
||||
workOrderId: input.workOrderId,
|
||||
amount: releaseAmount,
|
||||
unfreezeDays: input.unfreezeDays,
|
||||
now: input.now,
|
||||
})
|
||||
}
|
||||
if (releaseAmount > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)`,
|
||||
[
|
||||
workerId,
|
||||
shouldDelayUnfreeze ? 'deposit_pending_unfreeze' : 'deposit_release',
|
||||
releaseAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
shouldDelayUnfreeze ? '拼单份额验收押金待解冻' : '拼单份额验收释放押金',
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
if (shareReward > 0) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'sharing_reward', $2, $3, $4, $5, $6, $7::jsonb, $8)`,
|
||||
[
|
||||
workerId,
|
||||
shareReward,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
`拼单份额验收结算报酬 ${share.quantity} 份`,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, shareId: input.shareId }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
for (const debtOffset of debtOffsets) {
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, '验收报酬自动抵扣售后欠款', $6::jsonb, $7)`,
|
||||
[
|
||||
workerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
caseId: debtOffset.caseId,
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: input.shareId,
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE work_order_shares SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
)
|
||||
if (workerId > 0) {
|
||||
// 先落库份额已验收状态,再按去重后的订单数计算等级进度。
|
||||
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
|
||||
}
|
||||
const progressResult = await client.query<{
|
||||
joined_quantity: number
|
||||
unfinished_count: number
|
||||
}>(
|
||||
`SELECT COALESCE(SUM(quantity), 0)::int AS joined_quantity,
|
||||
COUNT(*) FILTER (WHERE status != 'accepted' AND status != 'cancelled')::int AS unfinished_count
|
||||
FROM work_order_shares WHERE work_order_id = $1 AND status != 'cancelled'`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const progress = progressResult.rows[0]
|
||||
const filled =
|
||||
Number(progress?.joined_quantity || 0) >= Number(workOrder.sharing_total_quantity || 1)
|
||||
const allAccepted = Number(progress?.unfinished_count || 0) === 0
|
||||
const nextOrderStatus = filled && allAccepted ? 'accepted' : workOrder.status
|
||||
if (nextOrderStatus === 'accepted') {
|
||||
await client.query(
|
||||
`UPDATE work_orders SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
}
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: input.actorType || 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'sharing_share_accepted',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextOrderStatus,
|
||||
payloadJson: JSON.stringify({ shareId: input.shareId, workerId }),
|
||||
now: input.now,
|
||||
})
|
||||
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
|
||||
const updatedShareResult = await client.query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||
[input.shareId],
|
||||
)
|
||||
return {
|
||||
order: updatedOrder,
|
||||
share: updatedShareResult.rows[0] || null,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 后台撤销单个拼单份额,释放该打手押金并把份数重新放回大厅。 */
|
||||
export async function cancelWorkOrderShare(input: {
|
||||
workOrderId: number
|
||||
shareId: number
|
||||
now: string
|
||||
reason?: string
|
||||
actorName?: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
share: WorkOrderShareRow | null
|
||||
releasedDepositAmount: number
|
||||
failureReason:
|
||||
| 'work_order_not_cancellable'
|
||||
| 'share_not_found'
|
||||
| 'share_status_not_cancellable'
|
||||
| null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = orderResult.rows[0] || null
|
||||
if (!workOrder || !['open', 'pending_acceptance'].includes(workOrder.status)) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share: null,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'work_order_not_cancellable',
|
||||
}
|
||||
}
|
||||
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[input.shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share: null,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'share_not_found',
|
||||
}
|
||||
}
|
||||
const nextOrderStatus = resolveWorkOrderShareCancellationStatus(workOrder.status, share.status)
|
||||
if (!nextOrderStatus) {
|
||||
return {
|
||||
order: workOrder,
|
||||
share,
|
||||
releasedDepositAmount: 0,
|
||||
failureReason: 'share_status_not_cancellable',
|
||||
}
|
||||
}
|
||||
|
||||
const workerId = Number(share.worker_id || 0)
|
||||
let releasedDepositAmount = 0
|
||||
if (workerId > 0) {
|
||||
await ensureWorkerWalletWithClient(client, workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, workerId)
|
||||
releasedDepositAmount = Math.min(
|
||||
Number(share.share_deposit || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
)
|
||||
if (releasedDepositAmount > 0) {
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
|
||||
const nextFrozen = Math.max(
|
||||
0,
|
||||
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
|
||||
)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
SET available_amount = $1, frozen_deposit_amount = $2, updated_at = $3
|
||||
WHERE worker_id = $4`,
|
||||
[nextAvailable, nextFrozen, input.now, workerId],
|
||||
)
|
||||
await client.query(
|
||||
`INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'deposit_release', $2, $3, $4, $5, '后台撤销拼单份额退还押金', $6::jsonb, $7)`,
|
||||
[
|
||||
workerId,
|
||||
releasedDepositAmount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
shareId: input.shareId,
|
||||
reason: String(input.reason || '').trim(),
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`UPDATE work_order_shares
|
||||
SET status = 'cancelled', updated_at = $1
|
||||
WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
)
|
||||
await client.query(
|
||||
`UPDATE work_orders
|
||||
SET status = $1,
|
||||
submitted_at = NULL,
|
||||
published_at = COALESCE(published_at, $2),
|
||||
updated_at = $2
|
||||
WHERE id = $3`,
|
||||
[nextOrderStatus, input.now, input.workOrderId],
|
||||
)
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'sharing_share_cancelled_by_admin',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: nextOrderStatus,
|
||||
payloadJson: JSON.stringify({
|
||||
shareId: input.shareId,
|
||||
workerId,
|
||||
quantity: Number(share.quantity || 0),
|
||||
releasedDepositAmount,
|
||||
reason: String(input.reason || '').trim(),
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
const updatedShareResult = await client.query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||
[input.shareId],
|
||||
)
|
||||
return {
|
||||
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
|
||||
share: updatedShareResult.rows[0] || null,
|
||||
releasedDepositAmount,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user