diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index bfd6eb23..ba452daa 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -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( - ` - 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( - ` - 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( - `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( - `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( - `${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( - `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( - `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( - `${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' diff --git a/apps/backend/src/repositories/worker-platform/work-order-settlement-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-settlement-repo.ts new file mode 100644 index 00000000..3f2e6d4a --- /dev/null +++ b/apps/backend/src/repositories/worker-platform/work-order-settlement-repo.ts @@ -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( + ` + 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( + ` + 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( + `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( + `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( + `${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( + `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( + `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( + `${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, + } + }) +}