售后处置为补救完成或问题不成立时自动退还原追缴金额
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
-- 售后处置为补救完成/问题不成立时,退还此前对同一责任单元已追缴的金额。
|
||||||
|
ALTER TABLE worker_after_sales_cases
|
||||||
|
ADD COLUMN IF NOT EXISTS refunded_amount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS refunded_at TIMESTAMPTZ,
|
||||||
|
ADD COLUMN IF NOT EXISTS refunded_by TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_after_sales_cases.refunded_amount IS '已退还的追缴金额;补救完成/问题不成立时按原追缴额全额退还';
|
||||||
@@ -271,6 +271,8 @@ export async function resolveAfterSalesCase(input: {
|
|||||||
now: string
|
now: string
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
case: WorkerAfterSalesCaseRow | null
|
case: WorkerAfterSalesCaseRow | null
|
||||||
|
refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }>
|
||||||
|
refundedTotalAmount: number
|
||||||
failureReason: 'not_found' | 'not_resolvable' | null
|
failureReason: 'not_found' | 'not_resolvable' | null
|
||||||
}> {
|
}> {
|
||||||
return withTransaction(async (client) => {
|
return withTransaction(async (client) => {
|
||||||
@@ -279,9 +281,20 @@ export async function resolveAfterSalesCase(input: {
|
|||||||
[input.caseId],
|
[input.caseId],
|
||||||
)
|
)
|
||||||
const current = currentResult.rows[0] || null
|
const current = currentResult.rows[0] || null
|
||||||
if (!current) return { case: null, failureReason: 'not_found' }
|
if (!current)
|
||||||
|
return {
|
||||||
|
case: null,
|
||||||
|
refundedRecoveries: [],
|
||||||
|
refundedTotalAmount: 0,
|
||||||
|
failureReason: 'not_found',
|
||||||
|
}
|
||||||
if (!['awaiting_worker_response', 'under_review'].includes(current.status)) {
|
if (!['awaiting_worker_response', 'under_review'].includes(current.status)) {
|
||||||
return { case: current, failureReason: 'not_resolvable' }
|
return {
|
||||||
|
case: current,
|
||||||
|
refundedRecoveries: [],
|
||||||
|
refundedTotalAmount: 0,
|
||||||
|
failureReason: 'not_resolvable',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingDepositDeductionAmount = 0
|
let pendingDepositDeductionAmount = 0
|
||||||
@@ -350,7 +363,107 @@ export async function resolveAfterSalesCase(input: {
|
|||||||
now: input.now,
|
now: input.now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return { case: updated, failureReason: null }
|
|
||||||
|
// 补救完成 / 问题不成立意味着责任解除:同一责任单元此前已追缴且未退还的金额应退还打手。
|
||||||
|
const refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }> = []
|
||||||
|
if (updated && input.action !== 'upheld') {
|
||||||
|
const priorCases = await client.query<WorkerAfterSalesCaseRow>(
|
||||||
|
`
|
||||||
|
SELECT * FROM worker_after_sales_cases
|
||||||
|
WHERE work_order_id = $1
|
||||||
|
AND worker_id = $2
|
||||||
|
AND COALESCE(work_order_share_id, 0) = COALESCE($3, 0)
|
||||||
|
AND id != $4
|
||||||
|
AND resolution_action = 'upheld'
|
||||||
|
AND recovery_amount > 0
|
||||||
|
AND refunded_amount = 0
|
||||||
|
ORDER BY id ASC
|
||||||
|
FOR UPDATE
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
Number(updated.work_order_id),
|
||||||
|
Number(updated.worker_id),
|
||||||
|
updated.work_order_share_id ? Number(updated.work_order_share_id) : null,
|
||||||
|
Number(updated.id),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for (const priorCase of priorCases.rows) {
|
||||||
|
const refundAmount = Number(priorCase.recovery_amount || 0)
|
||||||
|
if (refundAmount <= 0) continue
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE worker_after_sales_debts
|
||||||
|
SET outstanding_amount = 0, status = 'settled', settled_at = $1, updated_at = $1
|
||||||
|
WHERE case_id = $2 AND status = 'open'
|
||||||
|
`,
|
||||||
|
[input.now, Number(priorCase.id)],
|
||||||
|
)
|
||||||
|
await ensureWorkerWalletWithClient(client, Number(priorCase.worker_id), input.now)
|
||||||
|
const wallet = await getWorkerWalletWithClient(client, Number(priorCase.worker_id))
|
||||||
|
const nextAvailable = Number(wallet?.available_amount || 0) + refundAmount
|
||||||
|
await client.query(
|
||||||
|
`UPDATE worker_wallets SET available_amount = $1, updated_at = $2 WHERE worker_id = $3`,
|
||||||
|
[nextAvailable, input.now, Number(priorCase.worker_id)],
|
||||||
|
)
|
||||||
|
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_refund', $2, $3, $4, $5, '售后追缴退还', $6::jsonb, $7)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
Number(priorCase.worker_id),
|
||||||
|
refundAmount,
|
||||||
|
nextAvailable,
|
||||||
|
Number(wallet?.frozen_deposit_amount || 0),
|
||||||
|
Number(priorCase.work_order_id),
|
||||||
|
JSON.stringify({
|
||||||
|
caseId: Number(priorCase.id),
|
||||||
|
triggerCaseId: Number(updated.id),
|
||||||
|
triggerAction: input.action,
|
||||||
|
}),
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE worker_after_sales_cases
|
||||||
|
SET refunded_amount = $1,
|
||||||
|
refunded_at = $2,
|
||||||
|
refunded_by = $3,
|
||||||
|
debt_amount = 0,
|
||||||
|
status = CASE WHEN status = 'recovery_pending' THEN 'closed' ELSE status END,
|
||||||
|
updated_at = $2
|
||||||
|
WHERE id = $4
|
||||||
|
`,
|
||||||
|
[refundAmount, input.now, input.resolvedBy, Number(priorCase.id)],
|
||||||
|
)
|
||||||
|
await createAfterSalesCaseEventWithClient(client, {
|
||||||
|
caseId: Number(priorCase.id),
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.resolvedBy,
|
||||||
|
eventType: 'recovery_refunded',
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
amount: refundAmount,
|
||||||
|
triggerCaseId: Number(updated.id),
|
||||||
|
triggerAction: input.action,
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
refundedRecoveries.push({
|
||||||
|
caseId: Number(priorCase.id),
|
||||||
|
caseNo: String(priorCase.case_no || ''),
|
||||||
|
amount: refundAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
case: updated,
|
||||||
|
refundedRecoveries,
|
||||||
|
refundedTotalAmount: refundedRecoveries.reduce((total, item) => total + item.amount, 0),
|
||||||
|
failureReason: null,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ export type WorkerAfterSalesCaseRow = {
|
|||||||
pending_deposit_deduction_amount: number
|
pending_deposit_deduction_amount: number
|
||||||
available_deduction_amount: number
|
available_deduction_amount: number
|
||||||
debt_amount: number
|
debt_amount: number
|
||||||
|
refunded_amount?: number
|
||||||
|
refunded_at?: string | null
|
||||||
|
refunded_by?: string
|
||||||
created_by: string
|
created_by: string
|
||||||
resolved_by: string
|
resolved_by: string
|
||||||
resolved_at: string | null
|
resolved_at: string | null
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ export async function resolveAdminAfterSalesCase(
|
|||||||
action,
|
action,
|
||||||
recoveryAmount: Number(result.case.recovery_amount || 0),
|
recoveryAmount: Number(result.case.recovery_amount || 0),
|
||||||
debtAmount: Number(result.case.debt_amount || 0),
|
debtAmount: Number(result.case.debt_amount || 0),
|
||||||
|
refundedTotalAmount: result.refundedTotalAmount,
|
||||||
|
refundedRecoveries: result.refundedRecoveries,
|
||||||
}),
|
}),
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
})
|
})
|
||||||
@@ -221,7 +223,11 @@ export async function resolveAdminAfterSalesCase(
|
|||||||
workerIds: workerId > 0 ? [workerId] : [],
|
workerIds: workerId > 0 ? [workerId] : [],
|
||||||
})
|
})
|
||||||
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
||||||
return { case: mapWorkerAfterSalesCase(result.case) }
|
return {
|
||||||
|
case: mapWorkerAfterSalesCase(result.case),
|
||||||
|
refundedTotalAmount: result.refundedTotalAmount,
|
||||||
|
refundedRecoveries: result.refundedRecoveries,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
|
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ export function mapWorkerAfterSalesCase(afterSalesCase: {
|
|||||||
pending_deposit_deduction_amount: number
|
pending_deposit_deduction_amount: number
|
||||||
available_deduction_amount: number
|
available_deduction_amount: number
|
||||||
debt_amount: number
|
debt_amount: number
|
||||||
|
refunded_amount?: number
|
||||||
|
refunded_at?: string | null
|
||||||
|
refunded_by?: string
|
||||||
created_by: string
|
created_by: string
|
||||||
resolved_by: string
|
resolved_by: string
|
||||||
resolved_at: string | null
|
resolved_at: string | null
|
||||||
@@ -245,6 +248,9 @@ export function mapWorkerAfterSalesCase(afterSalesCase: {
|
|||||||
pendingDepositDeductionAmount: Number(afterSalesCase.pending_deposit_deduction_amount || 0),
|
pendingDepositDeductionAmount: Number(afterSalesCase.pending_deposit_deduction_amount || 0),
|
||||||
availableDeductionAmount: Number(afterSalesCase.available_deduction_amount || 0),
|
availableDeductionAmount: Number(afterSalesCase.available_deduction_amount || 0),
|
||||||
debtAmount: Number(afterSalesCase.debt_amount || 0),
|
debtAmount: Number(afterSalesCase.debt_amount || 0),
|
||||||
|
refundedAmount: Number(afterSalesCase.refunded_amount || 0),
|
||||||
|
refundedAt: afterSalesCase.refunded_at || null,
|
||||||
|
refundedBy: String(afterSalesCase.refunded_by || ''),
|
||||||
createdBy: String(afterSalesCase.created_by || ''),
|
createdBy: String(afterSalesCase.created_by || ''),
|
||||||
resolvedBy: String(afterSalesCase.resolved_by || ''),
|
resolvedBy: String(afterSalesCase.resolved_by || ''),
|
||||||
resolvedAt: afterSalesCase.resolved_at,
|
resolvedAt: afterSalesCase.resolved_at,
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export function normalizeWalletLedgerType(value: unknown) {
|
|||||||
'after_sales_deposit_deduction',
|
'after_sales_deposit_deduction',
|
||||||
'after_sales_balance_deduction',
|
'after_sales_balance_deduction',
|
||||||
'after_sales_debt_offset',
|
'after_sales_debt_offset',
|
||||||
|
'after_sales_refund',
|
||||||
].includes(ledgerType)
|
].includes(ledgerType)
|
||||||
) {
|
) {
|
||||||
return ledgerType
|
return ledgerType
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
|
|||||||
resolutionNote: String(values.resolutionNote || '').trim(),
|
resolutionNote: String(values.resolutionNote || '').trim(),
|
||||||
recoveryAmount: action === 'upheld' ? Number(values.recoveryAmount || 0) : 0,
|
recoveryAmount: action === 'upheld' ? Number(values.recoveryAmount || 0) : 0,
|
||||||
}),
|
}),
|
||||||
'售后问题单已处置',
|
(result) =>
|
||||||
|
(result.data?.refundedTotalAmount || 0) > 0
|
||||||
|
? `售后问题单已处置,已退还原追缴 ${formatMoney(result.data.refundedTotalAmount)} 至打手可用余额`
|
||||||
|
: '售后问题单已处置',
|
||||||
)
|
)
|
||||||
if (!succeeded) return
|
if (!succeeded) return
|
||||||
setAfterSalesCase(null)
|
setAfterSalesCase(null)
|
||||||
@@ -264,6 +267,11 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
|
|||||||
追缴 {formatMoney(item.recoveryAmount)},待还 {formatMoney(item.debtAmount)}
|
追缴 {formatMoney(item.recoveryAmount)},待还 {formatMoney(item.debtAmount)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
{item.refundedAmount > 0 ? (
|
||||||
|
<Typography.Text type="success">
|
||||||
|
已退还 {formatMoney(item.refundedAmount)}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -399,7 +407,11 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
|
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : null
|
) : (
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 16 }}>
|
||||||
|
选择补救完成或问题不成立时,系统自动将该工单同一责任打手此前已追缴且未退还的金额退还至其可用余额。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -352,6 +352,11 @@ function WorkerAfterSalesItem({
|
|||||||
待追缴 {formatMoney(item.debtAmount)},后续验收报酬将自动抵扣
|
待追缴 {formatMoney(item.debtAmount)},后续验收报酬将自动抵扣
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
{Number(item.refundedAmount || 0) > 0 ? (
|
||||||
|
<Typography.Text type="success" strong>
|
||||||
|
已退还追缴 {formatMoney(item.refundedAmount)} 至可用余额
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const LEDGER_TYPE_OPTIONS = [
|
|||||||
{ value: 'after_sales_deposit_deduction', label: '售后扣减押金' },
|
{ value: 'after_sales_deposit_deduction', label: '售后扣减押金' },
|
||||||
{ value: 'after_sales_balance_deduction', label: '售后扣减余额' },
|
{ value: 'after_sales_balance_deduction', label: '售后扣减余额' },
|
||||||
{ value: 'after_sales_debt_offset', label: '抵扣售后欠款' },
|
{ value: 'after_sales_debt_offset', label: '抵扣售后欠款' },
|
||||||
|
{ value: 'after_sales_refund', label: '售后追缴退还' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const REQUEST_TYPE_OPTIONS = [
|
const REQUEST_TYPE_OPTIONS = [
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function formatLedgerType(ledgerType: string) {
|
|||||||
after_sales_deposit_deduction: '售后扣减押金',
|
after_sales_deposit_deduction: '售后扣减押金',
|
||||||
after_sales_balance_deduction: '售后扣减余额',
|
after_sales_balance_deduction: '售后扣减余额',
|
||||||
after_sales_debt_offset: '抵扣售后欠款',
|
after_sales_debt_offset: '抵扣售后欠款',
|
||||||
|
after_sales_refund: '售后追缴退还',
|
||||||
}
|
}
|
||||||
return labels[ledgerType] || ledgerType || '-'
|
return labels[ledgerType] || ledgerType || '-'
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ export function resolveLedgerTagColor(ledgerType: string) {
|
|||||||
if (ledgerType === 'after_sales_deposit_deduction') return 'red'
|
if (ledgerType === 'after_sales_deposit_deduction') return 'red'
|
||||||
if (ledgerType === 'after_sales_balance_deduction') return 'red'
|
if (ledgerType === 'after_sales_balance_deduction') return 'red'
|
||||||
if (ledgerType === 'after_sales_debt_offset') return 'orange'
|
if (ledgerType === 'after_sales_debt_offset') return 'orange'
|
||||||
|
if (ledgerType === 'after_sales_refund') return 'green'
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -509,10 +509,11 @@ export function resolveAdminAfterSalesCase(
|
|||||||
recoveryAmount?: number
|
recoveryAmount?: number
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return apiPost<{ case: WorkerAfterSalesCase }>(
|
return apiPost<{
|
||||||
`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`,
|
case: WorkerAfterSalesCase
|
||||||
payload,
|
refundedTotalAmount: number
|
||||||
)
|
refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }>
|
||||||
|
}>(`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkOrderEvents(workOrderId: number) {
|
export function fetchAdminWorkOrderEvents(workOrderId: number) {
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ export type WorkerAfterSalesCase = {
|
|||||||
pendingDepositDeductionAmount: number
|
pendingDepositDeductionAmount: number
|
||||||
availableDeductionAmount: number
|
availableDeductionAmount: number
|
||||||
debtAmount: number
|
debtAmount: number
|
||||||
|
refundedAmount: number
|
||||||
|
refundedAt: string | null
|
||||||
|
refundedBy: string
|
||||||
createdBy: string
|
createdBy: string
|
||||||
resolvedBy: string
|
resolvedBy: string
|
||||||
resolvedAt: string | null
|
resolvedAt: string | null
|
||||||
|
|||||||
Reference in New Issue
Block a user