增加售后相关的
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type {
|
||||
WorkerAfterSalesCaseEventRow,
|
||||
WorkerAfterSalesCaseRow,
|
||||
WorkerAfterSalesDebtRow,
|
||||
WorkerDepositUnfreezeRow,
|
||||
WorkerWalletRow,
|
||||
WorkOrderRow,
|
||||
WorkOrderShareRow,
|
||||
} from './types.js'
|
||||
|
||||
const AFTER_SALES_CASE_SELECT = `
|
||||
SELECT
|
||||
ac.*,
|
||||
COALESCE(NULLIF(BTRIM(wo.platform_order_id), ''), wo.work_order_no) AS work_order_no,
|
||||
wo.platform_order_id,
|
||||
wo.product_name,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(o.shop_name), ''),
|
||||
NULLIF(BTRIM(o.shop_id), ''),
|
||||
NULLIF(BTRIM(ft.shop_name), ''),
|
||||
NULLIF(BTRIM(ft.shop_id), ''),
|
||||
''
|
||||
) AS shop_name,
|
||||
wu.username AS worker_username,
|
||||
wu.display_name AS worker_display_name,
|
||||
wos.quantity AS share_quantity
|
||||
FROM worker_after_sales_cases ac
|
||||
INNER JOIN work_orders wo ON wo.id = ac.work_order_id
|
||||
LEFT JOIN orders o ON o.id = wo.order_id
|
||||
LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id
|
||||
INNER JOIN worker_users wu ON wu.id = ac.worker_id
|
||||
LEFT JOIN work_order_shares wos ON wos.id = ac.work_order_share_id
|
||||
`
|
||||
|
||||
type AfterSalesCaseFailureReason =
|
||||
| 'work_order_not_accepted'
|
||||
| 'work_order_share_not_accepted'
|
||||
| 'worker_missing'
|
||||
| 'active_case_exists'
|
||||
| null
|
||||
|
||||
export async function createAfterSalesCase(input: {
|
||||
caseNo: string
|
||||
workOrderId: number
|
||||
workOrderShareId?: number | null
|
||||
problemType: string
|
||||
complaintNote: string
|
||||
complaintFilesJson: string
|
||||
createdBy: string
|
||||
now: string
|
||||
}): Promise<{ case: WorkerAfterSalesCaseRow | null; failureReason: AfterSalesCaseFailureReason }> {
|
||||
return withTransaction(async (client) => {
|
||||
const workOrderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 AND status = 'accepted' FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = workOrderResult.rows[0] || null
|
||||
if (!workOrder) {
|
||||
return { case: null, failureReason: 'work_order_not_accepted' }
|
||||
}
|
||||
|
||||
let workerId = Number(workOrder.assigned_worker_id || 0)
|
||||
let acceptanceSnapshot: Record<string, unknown> = {
|
||||
capturedAt: input.now,
|
||||
acceptedAt: workOrder.accepted_at,
|
||||
acceptance: workOrder.acceptance_json,
|
||||
}
|
||||
const shareId = Number(input.workOrderShareId || 0)
|
||||
if (shareId > 0) {
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share || share.status !== 'accepted') {
|
||||
return { case: null, failureReason: 'work_order_share_not_accepted' }
|
||||
}
|
||||
workerId = Number(share.worker_id || 0)
|
||||
acceptanceSnapshot = {
|
||||
capturedAt: input.now,
|
||||
acceptedAt: share.accepted_at,
|
||||
acceptance: share.acceptance_json,
|
||||
shareId,
|
||||
shareQuantity: Number(share.quantity || 0),
|
||||
}
|
||||
}
|
||||
if (workerId <= 0) {
|
||||
return { case: null, failureReason: 'worker_missing' }
|
||||
}
|
||||
|
||||
const createdResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`
|
||||
INSERT INTO worker_after_sales_cases (
|
||||
case_no, work_order_id, work_order_share_id, worker_id,
|
||||
problem_type, complaint_note, complaint_files_json, acceptance_snapshot_json,
|
||||
status, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, NULLIF($3, 0), $4, $5, $6, $7::jsonb, $8::jsonb,
|
||||
'awaiting_worker_response', $9, $10, $10)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
input.caseNo,
|
||||
input.workOrderId,
|
||||
shareId,
|
||||
workerId,
|
||||
input.problemType,
|
||||
input.complaintNote,
|
||||
input.complaintFilesJson,
|
||||
JSON.stringify(acceptanceSnapshot),
|
||||
input.createdBy,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
const created = createdResult.rows[0] || null
|
||||
if (!created) {
|
||||
return { case: null, failureReason: 'active_case_exists' }
|
||||
}
|
||||
await createAfterSalesCaseEventWithClient(client, {
|
||||
caseId: Number(created.id),
|
||||
actorType: 'admin',
|
||||
actorId: input.createdBy,
|
||||
eventType: 'created',
|
||||
payloadJson: JSON.stringify({
|
||||
problemType: input.problemType,
|
||||
complaintNote: input.complaintNote,
|
||||
workOrderShareId: shareId || null,
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
return { case: created, failureReason: null }
|
||||
})
|
||||
}
|
||||
|
||||
export async function getAfterSalesCaseById(
|
||||
caseId: number | string,
|
||||
): Promise<WorkerAfterSalesCaseRow | null> {
|
||||
const result = await query<WorkerAfterSalesCaseRow>(
|
||||
`${AFTER_SALES_CASE_SELECT} WHERE ac.id = $1 LIMIT 1`,
|
||||
[Number(caseId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function listAfterSalesCases(
|
||||
input: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: string
|
||||
keyword?: string
|
||||
workOrderId?: number
|
||||
} = {},
|
||||
): Promise<{ items: WorkerAfterSalesCaseRow[]; total: number }> {
|
||||
const params: unknown[] = []
|
||||
const conditions: string[] = []
|
||||
if (String(input.status || '').trim()) {
|
||||
params.push(String(input.status).trim())
|
||||
conditions.push(`ac.status = $${params.length}`)
|
||||
}
|
||||
if (Number(input.workOrderId || 0) > 0) {
|
||||
params.push(Number(input.workOrderId))
|
||||
conditions.push(`ac.work_order_id = $${params.length}`)
|
||||
}
|
||||
if (String(input.keyword || '').trim()) {
|
||||
params.push(`%${String(input.keyword).trim()}%`)
|
||||
conditions.push(
|
||||
`(ac.case_no ILIKE $${params.length} OR wo.work_order_no ILIKE $${params.length} OR wo.platform_order_id ILIKE $${params.length} OR wu.username ILIKE $${params.length} OR wu.display_name ILIKE $${params.length})`,
|
||||
)
|
||||
}
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total FROM worker_after_sales_cases ac
|
||||
INNER JOIN work_orders wo ON wo.id = ac.work_order_id
|
||||
INNER JOIN worker_users wu ON wu.id = ac.worker_id
|
||||
${whereClause}`,
|
||||
params,
|
||||
)
|
||||
const page = Math.max(1, Number(input.page || 1))
|
||||
const pageSize = Math.min(100, Math.max(1, Number(input.pageSize || 20)))
|
||||
const offset = (page - 1) * pageSize
|
||||
params.push(pageSize, offset)
|
||||
const itemsResult = await query<WorkerAfterSalesCaseRow>(
|
||||
`${AFTER_SALES_CASE_SELECT}
|
||||
${whereClause}
|
||||
ORDER BY ac.created_at DESC, ac.id DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params,
|
||||
)
|
||||
return { items: itemsResult.rows, total: Number(totalResult.rows[0]?.total || 0) }
|
||||
}
|
||||
|
||||
export async function listWorkerAfterSalesCases(
|
||||
workerId: number,
|
||||
): Promise<WorkerAfterSalesCaseRow[]> {
|
||||
const result = await query<WorkerAfterSalesCaseRow>(
|
||||
`${AFTER_SALES_CASE_SELECT}
|
||||
WHERE ac.worker_id = $1
|
||||
ORDER BY CASE WHEN ac.status IN ('awaiting_worker_response', 'under_review', 'recovery_pending') THEN 0 ELSE 1 END,
|
||||
ac.created_at DESC, ac.id DESC`,
|
||||
[workerId],
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function listAfterSalesCaseEvents(
|
||||
caseId: number | string,
|
||||
): Promise<WorkerAfterSalesCaseEventRow[]> {
|
||||
const result = await query<WorkerAfterSalesCaseEventRow>(
|
||||
`SELECT * FROM worker_after_sales_case_events WHERE case_id = $1 ORDER BY created_at ASC, id ASC`,
|
||||
[Number(caseId)],
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function submitWorkerAfterSalesResponse(input: {
|
||||
caseId: number
|
||||
workerId: number
|
||||
response: string
|
||||
filesJson: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
case: WorkerAfterSalesCaseRow | null
|
||||
failureReason: 'not_found' | 'not_pending' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`SELECT * FROM worker_after_sales_cases WHERE id = $1 AND worker_id = $2 FOR UPDATE`,
|
||||
[input.caseId, input.workerId],
|
||||
)
|
||||
const current = currentResult.rows[0] || null
|
||||
if (!current) return { case: null, failureReason: 'not_found' }
|
||||
if (current.status !== 'awaiting_worker_response') {
|
||||
return { case: current, failureReason: 'not_pending' }
|
||||
}
|
||||
const updatedResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`
|
||||
UPDATE worker_after_sales_cases
|
||||
SET status = 'under_review', worker_response = $1, worker_response_files_json = $2::jsonb,
|
||||
responded_at = $3, updated_at = $3
|
||||
WHERE id = $4
|
||||
RETURNING *
|
||||
`,
|
||||
[input.response, input.filesJson, input.now, input.caseId],
|
||||
)
|
||||
const updated = updatedResult.rows[0] || null
|
||||
if (updated) {
|
||||
await createAfterSalesCaseEventWithClient(client, {
|
||||
caseId: input.caseId,
|
||||
actorType: 'worker',
|
||||
actorId: String(input.workerId),
|
||||
eventType: 'worker_responded',
|
||||
payloadJson: JSON.stringify({ response: input.response }),
|
||||
now: input.now,
|
||||
})
|
||||
}
|
||||
return { case: updated, failureReason: null }
|
||||
})
|
||||
}
|
||||
|
||||
export async function resolveAfterSalesCase(input: {
|
||||
caseId: number
|
||||
action: 'not_upheld' | 'remedied' | 'upheld'
|
||||
resolutionNote: string
|
||||
recoveryAmount: number
|
||||
resolvedBy: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
case: WorkerAfterSalesCaseRow | null
|
||||
failureReason: 'not_found' | 'not_resolvable' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const currentResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`SELECT * FROM worker_after_sales_cases WHERE id = $1 FOR UPDATE`,
|
||||
[input.caseId],
|
||||
)
|
||||
const current = currentResult.rows[0] || null
|
||||
if (!current) return { case: null, failureReason: 'not_found' }
|
||||
if (!['awaiting_worker_response', 'under_review'].includes(current.status)) {
|
||||
return { case: current, failureReason: 'not_resolvable' }
|
||||
}
|
||||
|
||||
let pendingDepositDeductionAmount = 0
|
||||
let availableDeductionAmount = 0
|
||||
let debtAmount = 0
|
||||
let nextStatus = 'closed'
|
||||
if (input.action === 'upheld' && input.recoveryAmount > 0) {
|
||||
const recovery = await recoverAfterSalesAmountWithClient(client, {
|
||||
caseId: Number(current.id),
|
||||
workOrderId: Number(current.work_order_id),
|
||||
workerId: Number(current.worker_id),
|
||||
amount: input.recoveryAmount,
|
||||
note: input.resolutionNote,
|
||||
now: input.now,
|
||||
})
|
||||
pendingDepositDeductionAmount = recovery.pendingDepositDeductionAmount
|
||||
availableDeductionAmount = recovery.availableDeductionAmount
|
||||
debtAmount = recovery.debtAmount
|
||||
nextStatus = debtAmount > 0 ? 'recovery_pending' : 'closed'
|
||||
}
|
||||
|
||||
const updatedResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`
|
||||
UPDATE worker_after_sales_cases
|
||||
SET
|
||||
status = $1,
|
||||
resolution_action = $2,
|
||||
resolution_note = $3,
|
||||
recovery_amount = $4,
|
||||
pending_deposit_deduction_amount = $5,
|
||||
available_deduction_amount = $6,
|
||||
debt_amount = $7,
|
||||
resolved_by = $8,
|
||||
resolved_at = $9,
|
||||
updated_at = $9
|
||||
WHERE id = $10
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
nextStatus,
|
||||
input.action,
|
||||
input.resolutionNote,
|
||||
input.action === 'upheld' ? input.recoveryAmount : 0,
|
||||
pendingDepositDeductionAmount,
|
||||
availableDeductionAmount,
|
||||
debtAmount,
|
||||
input.resolvedBy,
|
||||
input.now,
|
||||
input.caseId,
|
||||
],
|
||||
)
|
||||
const updated = updatedResult.rows[0] || null
|
||||
if (updated) {
|
||||
await createAfterSalesCaseEventWithClient(client, {
|
||||
caseId: Number(updated.id),
|
||||
actorType: 'admin',
|
||||
actorId: input.resolvedBy,
|
||||
eventType: `resolved_${input.action}`,
|
||||
payloadJson: JSON.stringify({
|
||||
recoveryAmount: input.action === 'upheld' ? input.recoveryAmount : 0,
|
||||
pendingDepositDeductionAmount,
|
||||
availableDeductionAmount,
|
||||
debtAmount,
|
||||
resolutionNote: input.resolutionNote,
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
}
|
||||
return { case: updated, failureReason: null }
|
||||
})
|
||||
}
|
||||
|
||||
/** 在验收结算同一事务中抵扣未结清售后欠款,返回本次各售后单的抵扣明细。 */
|
||||
export async function offsetWorkerAfterSalesDebtsWithClient(
|
||||
client: PoolClient,
|
||||
input: { workerId: number; amount: number; workOrderId: number; now: string },
|
||||
): Promise<Array<{ caseId: number; amount: number }>> {
|
||||
let remaining = Math.max(0, input.amount)
|
||||
if (remaining <= 0) return []
|
||||
const debtsResult = await client.query<WorkerAfterSalesDebtRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_after_sales_debts
|
||||
WHERE worker_id = $1 AND status = 'open' AND outstanding_amount > 0
|
||||
ORDER BY created_at ASC, id ASC
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workerId],
|
||||
)
|
||||
const offsets: Array<{ caseId: number; amount: number }> = []
|
||||
for (const debt of debtsResult.rows) {
|
||||
if (remaining <= 0) break
|
||||
const outstanding = Number(debt.outstanding_amount || 0)
|
||||
const offsetAmount = Math.min(remaining, outstanding)
|
||||
const nextOutstanding = outstanding - offsetAmount
|
||||
const settled = nextOutstanding <= 0
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_after_sales_debts
|
||||
SET outstanding_amount = $1, status = $2,
|
||||
settled_at = CASE WHEN $2 = 'settled' THEN $3 ELSE NULL END,
|
||||
updated_at = $3
|
||||
WHERE id = $4
|
||||
`,
|
||||
[nextOutstanding, settled ? 'settled' : 'open', input.now, debt.id],
|
||||
)
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_after_sales_cases
|
||||
SET debt_amount = $1,
|
||||
status = CASE WHEN $1 = 0 AND status = 'recovery_pending' THEN 'closed' ELSE status END,
|
||||
updated_at = $2
|
||||
WHERE id = $3
|
||||
`,
|
||||
[nextOutstanding, input.now, debt.case_id],
|
||||
)
|
||||
await createAfterSalesCaseEventWithClient(client, {
|
||||
caseId: Number(debt.case_id),
|
||||
actorType: 'system',
|
||||
actorId: '',
|
||||
eventType: settled ? 'debt_settled' : 'debt_offset',
|
||||
payloadJson: JSON.stringify({ workOrderId: input.workOrderId, amount: offsetAmount }),
|
||||
now: input.now,
|
||||
})
|
||||
offsets.push({ caseId: Number(debt.case_id), amount: offsetAmount })
|
||||
remaining -= offsetAmount
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
async function recoverAfterSalesAmountWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
caseId: number
|
||||
workOrderId: number
|
||||
workerId: number
|
||||
amount: number
|
||||
note: string
|
||||
now: string
|
||||
},
|
||||
): Promise<{
|
||||
pendingDepositDeductionAmount: number
|
||||
availableDeductionAmount: number
|
||||
debtAmount: number
|
||||
}> {
|
||||
let remaining = Math.max(0, input.amount)
|
||||
let pendingDepositDeductionAmount = 0
|
||||
const pendingResult = await client.query<WorkerDepositUnfreezeRow>(
|
||||
`
|
||||
SELECT * FROM worker_deposit_unfreezes
|
||||
WHERE work_order_id = $1 AND worker_id = $2 AND status = 'pending'
|
||||
ORDER BY unfreeze_at ASC, id ASC
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workOrderId, input.workerId],
|
||||
)
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
let wallet: WorkerWalletRow | null = await getWorkerWalletWithClient(client, input.workerId)
|
||||
for (const row of pendingResult.rows) {
|
||||
if (remaining <= 0) break
|
||||
const rowAmount = Number(row.amount || 0)
|
||||
const deduction = Math.min(rowAmount, remaining)
|
||||
if (deduction <= 0) continue
|
||||
if (deduction >= rowAmount) {
|
||||
await client.query(
|
||||
`UPDATE worker_deposit_unfreezes SET status = 'deducted', updated_at = $1 WHERE id = $2`,
|
||||
[input.now, row.id],
|
||||
)
|
||||
} else {
|
||||
await client.query(
|
||||
`UPDATE worker_deposit_unfreezes SET amount = amount - $1, updated_at = $2 WHERE id = $3`,
|
||||
[deduction, input.now, row.id],
|
||||
)
|
||||
}
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
SET pending_unfreeze_amount = GREATEST(0, pending_unfreeze_amount - $1), updated_at = $2
|
||||
WHERE worker_id = $3`,
|
||||
[deduction, input.now, input.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, 'after_sales_deposit_deduction', $2, $3, $4, $5, '售后问题单扣减待解冻押金', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
input.workerId,
|
||||
-deduction,
|
||||
Number(wallet?.available_amount || 0),
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
input.workOrderId,
|
||||
JSON.stringify({ caseId: input.caseId, note: input.note, unfreezeId: row.id }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
pendingDepositDeductionAmount += deduction
|
||||
remaining -= deduction
|
||||
}
|
||||
|
||||
wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const availableDeductionAmount = Math.min(remaining, Number(wallet?.available_amount || 0))
|
||||
if (availableDeductionAmount > 0) {
|
||||
const nextAvailable = Number(wallet?.available_amount || 0) - availableDeductionAmount
|
||||
await client.query(
|
||||
`UPDATE worker_wallets SET available_amount = $1, updated_at = $2 WHERE worker_id = $3`,
|
||||
[nextAvailable, input.now, input.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, 'after_sales_balance_deduction', $2, $3, $4, $5, '售后问题单扣减可用余额', $6::jsonb, $7)
|
||||
`,
|
||||
[
|
||||
input.workerId,
|
||||
-availableDeductionAmount,
|
||||
nextAvailable,
|
||||
Number(wallet?.frozen_deposit_amount || 0),
|
||||
input.workOrderId,
|
||||
JSON.stringify({ caseId: input.caseId, note: input.note }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
remaining -= availableDeductionAmount
|
||||
}
|
||||
if (remaining > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_after_sales_debts (
|
||||
case_id, worker_id, original_amount, outstanding_amount, status, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $3, 'open', $4, $4)
|
||||
`,
|
||||
[input.caseId, input.workerId, remaining, input.now],
|
||||
)
|
||||
}
|
||||
return { pendingDepositDeductionAmount, availableDeductionAmount, debtAmount: remaining }
|
||||
}
|
||||
|
||||
async function createAfterSalesCaseEventWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
caseId: number
|
||||
actorType: string
|
||||
actorId: string
|
||||
eventType: string
|
||||
payloadJson: string
|
||||
now: string
|
||||
},
|
||||
) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_after_sales_case_events (
|
||||
case_id, actor_type, actor_id, event_type, payload_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6)
|
||||
`,
|
||||
[input.caseId, input.actorType, input.actorId, input.eventType, input.payloadJson, input.now],
|
||||
)
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export * from './types.js'
|
||||
export * from './shared.js'
|
||||
export * from './worker-repo.js'
|
||||
export * from './work-order-repo.js'
|
||||
export * from './after-sales-repo.js'
|
||||
export * from './product-match-repo.js'
|
||||
export * from './sms-code-repo.js'
|
||||
|
||||
@@ -137,6 +137,62 @@ export type WorkerWalletLedgerRow = {
|
||||
related_platform_order_id?: string | null
|
||||
}
|
||||
|
||||
export type WorkerAfterSalesCaseRow = {
|
||||
id: number
|
||||
case_no: string
|
||||
work_order_id: number
|
||||
work_order_share_id: number | null
|
||||
worker_id: number
|
||||
problem_type: string
|
||||
complaint_note: string
|
||||
complaint_files_json: string | Record<string, unknown> | unknown[]
|
||||
acceptance_snapshot_json: string | Record<string, unknown>
|
||||
status: string
|
||||
worker_response: string
|
||||
worker_response_files_json: string | Record<string, unknown> | unknown[]
|
||||
responded_at: string | null
|
||||
resolution_action: string
|
||||
resolution_note: string
|
||||
recovery_amount: number
|
||||
pending_deposit_deduction_amount: number
|
||||
available_deduction_amount: number
|
||||
debt_amount: number
|
||||
created_by: string
|
||||
resolved_by: string
|
||||
resolved_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
work_order_no?: string
|
||||
platform_order_id?: string
|
||||
product_name?: string
|
||||
shop_name?: string
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
share_quantity?: number | null
|
||||
}
|
||||
|
||||
export type WorkerAfterSalesCaseEventRow = {
|
||||
id: number
|
||||
case_id: number
|
||||
actor_type: string
|
||||
actor_id: string
|
||||
event_type: string
|
||||
payload_json: string | Record<string, unknown>
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type WorkerAfterSalesDebtRow = {
|
||||
id: number
|
||||
case_id: number
|
||||
worker_id: number
|
||||
original_amount: number
|
||||
outstanding_amount: number
|
||||
status: string
|
||||
created_at: string
|
||||
settled_at: string | null
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WorkerFinanceRequestRow = {
|
||||
id: number
|
||||
worker_id: number
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
|
||||
import {
|
||||
ensureWorkerWalletWithClient,
|
||||
getWorkerWalletWithClient,
|
||||
@@ -2956,11 +2957,19 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
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)
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
@@ -3059,6 +3068,29 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
],
|
||||
)
|
||||
}
|
||||
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(
|
||||
`
|
||||
@@ -3081,11 +3113,19 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
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)
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
|
||||
await client.query(
|
||||
@@ -3162,6 +3202,25 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
[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, {
|
||||
@@ -3230,11 +3289,19 @@ export async function acceptWorkOrderShareAndSettle(input: {
|
||||
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)
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount) -
|
||||
debtOffsetAmount
|
||||
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets
|
||||
@@ -3298,6 +3365,27 @@ export async function acceptWorkOrderShareAndSettle(input: {
|
||||
],
|
||||
)
|
||||
}
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user