完善拼单状态与份额验收流程
This commit is contained in:
@@ -250,6 +250,9 @@ export type WorkOrderRow = {
|
||||
timeout_policy: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
/** 拼单聚合字段,由工单查询统一计算。 */
|
||||
active_share_quantity?: number
|
||||
pending_share_count?: number
|
||||
category_name?: string
|
||||
worker_username?: string
|
||||
worker_display_name?: string
|
||||
@@ -285,11 +288,15 @@ export type ListInput = {
|
||||
categoryId?: number
|
||||
workerSharingId?: number
|
||||
visibleAfterIso?: string
|
||||
/** 抢单大厅隐藏已被拼完的工单。 */
|
||||
excludeFilledSharing?: boolean
|
||||
/** VIP 免审核后仍在补图期限内的订单;与状态筛选取并集 */
|
||||
vipEvidencePending?: boolean
|
||||
/** 置顶订单优先展示(抢单大厅使用) */
|
||||
pinnedFirst?: boolean
|
||||
sort?: 'id_desc' | 'worker_claimed_at_desc'
|
||||
/** 后台使用拼单聚合业务状态;大厅和打手列表使用原始生命周期。 */
|
||||
useOperationalStatus?: boolean
|
||||
}
|
||||
|
||||
export type WorkOrderStatistics = {
|
||||
@@ -300,6 +307,22 @@ export type WorkOrderStatistics = {
|
||||
statusCounts: Record<string, number>
|
||||
}
|
||||
|
||||
export type WorkOrderCapabilities = {
|
||||
canEdit: boolean
|
||||
canConfigureSharing: boolean
|
||||
canAssign: boolean
|
||||
canPin: boolean
|
||||
canUnpublish: boolean
|
||||
canUpdateMaterial: boolean
|
||||
canAccept: boolean
|
||||
canMarkProblem: boolean
|
||||
canDelete: boolean
|
||||
canPublish: boolean
|
||||
canCancel: boolean
|
||||
canResolveProblem: boolean
|
||||
canDeduct: boolean
|
||||
}
|
||||
|
||||
export type ProductRuleListInput = {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
|
||||
@@ -24,6 +24,16 @@ import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js'
|
||||
const WORK_ORDER_SELECT = `
|
||||
SELECT
|
||||
wo.*,
|
||||
COALESCE((
|
||||
SELECT SUM(wos.quantity)
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = wo.id AND wos.status != 'cancelled'
|
||||
), 0)::int AS active_share_quantity,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = wo.id AND wos.status = 'joined'
|
||||
), 0)::int AS pending_share_count,
|
||||
wc.name AS category_name,
|
||||
wu.username AS worker_username,
|
||||
wu.display_name AS worker_display_name
|
||||
@@ -65,6 +75,22 @@ type WorkOrderStatisticsRow = {
|
||||
refund_amount: number | string
|
||||
}
|
||||
|
||||
/** 后台按母工单拼单参与情况计算业务状态,open 只代表大厅生命周期。 */
|
||||
function adminOperationalStatusExpression(alias = 'wo') {
|
||||
return `CASE
|
||||
WHEN ${alias}.status = 'open'
|
||||
AND ${alias}.sharing_enabled IS TRUE
|
||||
AND COALESCE((SELECT SUM(wos.quantity) FROM work_order_shares wos WHERE wos.work_order_id = ${alias}.id AND wos.status != 'cancelled'), 0) > 0
|
||||
THEN CASE
|
||||
WHEN COALESCE((SELECT SUM(wos.quantity) FROM work_order_shares wos WHERE wos.work_order_id = ${alias}.id AND wos.status != 'cancelled'), 0) >= ${alias}.sharing_total_quantity
|
||||
AND COALESCE((SELECT COUNT(*) FROM work_order_shares wos WHERE wos.work_order_id = ${alias}.id AND wos.status = 'joined'), 0) = 0
|
||||
THEN 'pending_acceptance'
|
||||
ELSE 'in_progress'
|
||||
END
|
||||
ELSE ${alias}.status
|
||||
END`
|
||||
}
|
||||
|
||||
/** 工单统一搜索字段,兼容当前资料结构和历史资料字段。 */
|
||||
function workOrderSearchExpression(alias = 'wo') {
|
||||
return `LOWER(
|
||||
@@ -82,6 +108,27 @@ function workOrderSearchExpression(alias = 'wo') {
|
||||
)`
|
||||
}
|
||||
|
||||
/** 打手订单筛选使用个人份额状态,而不是拼单母工单的 open 状态。 */
|
||||
function workerOrderStatusExpression(workerIdPlaceholder: string) {
|
||||
return `CASE
|
||||
WHEN wo.status IN ('accepted', 'cancelled', 'problem') THEN wo.status
|
||||
ELSE COALESCE((
|
||||
SELECT CASE wos.status
|
||||
WHEN 'joined' THEN 'in_progress'
|
||||
WHEN 'submitted' THEN 'pending_acceptance'
|
||||
WHEN 'accepted' THEN 'accepted'
|
||||
WHEN 'cancelled' THEN 'cancelled'
|
||||
ELSE wo.status
|
||||
END
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = wo.id
|
||||
AND wos.worker_id = ${workerIdPlaceholder}
|
||||
ORDER BY wos.id DESC
|
||||
LIMIT 1
|
||||
), wo.status)
|
||||
END`
|
||||
}
|
||||
|
||||
export async function listWorkOrderShares(
|
||||
workOrderId: number | string,
|
||||
): Promise<WorkOrderShareRow[]> {
|
||||
@@ -143,10 +190,21 @@ export async function getWorkOrderShare(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** 普通拼单使用指定份数,立即抢单则在事务中承接全部剩余份数。 */
|
||||
export function resolveWorkOrderShareJoinQuantity(input: {
|
||||
quantity: number
|
||||
remaining: number
|
||||
takeRemaining?: boolean
|
||||
}): number {
|
||||
return input.takeRemaining ? Math.max(0, input.remaining) : input.quantity
|
||||
}
|
||||
|
||||
export async function joinWorkOrderShare(input: {
|
||||
workOrderId: number
|
||||
workerId: number
|
||||
quantity: number
|
||||
/** 立即抢单时在事务锁内承接全部剩余份数。 */
|
||||
takeRemaining?: boolean
|
||||
maxActiveOrders: number
|
||||
now: string
|
||||
}): Promise<{
|
||||
@@ -194,57 +252,87 @@ export async function joinWorkOrderShare(input: {
|
||||
)
|
||||
const joinedQuantity = Number(joinedResult.rows[0]?.total || 0)
|
||||
const remaining = Math.max(0, totalQuantity - joinedQuantity)
|
||||
if (input.quantity <= 0 || input.quantity > remaining) {
|
||||
const requestedQuantity = resolveWorkOrderShareJoinQuantity({
|
||||
quantity: input.quantity,
|
||||
remaining,
|
||||
takeRemaining: input.takeRemaining === true,
|
||||
})
|
||||
if (requestedQuantity <= 0 || requestedQuantity > remaining) {
|
||||
return { share: null, failureReason: 'sharing_quantity_full' }
|
||||
}
|
||||
|
||||
const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId)
|
||||
if (activeOrderCount >= input.maxActiveOrders) {
|
||||
return { share: null, failureReason: 'worker_active_order_limit' }
|
||||
}
|
||||
|
||||
const existingShare = await client.query<{ id: number }>(
|
||||
'SELECT id FROM work_order_shares WHERE work_order_id = $1 AND worker_id = $2',
|
||||
const existingShareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT *
|
||||
FROM work_order_shares
|
||||
WHERE work_order_id = $1 AND worker_id = $2
|
||||
FOR UPDATE`,
|
||||
[input.workOrderId, input.workerId],
|
||||
)
|
||||
if (existingShare.rows[0]) {
|
||||
const existingShare = existingShareResult.rows[0] || null
|
||||
if (existingShare && (!input.takeRemaining || existingShare.status !== 'joined')) {
|
||||
return { share: null, failureReason: 'sharing_quantity_full' }
|
||||
}
|
||||
if (!existingShare) {
|
||||
const activeOrderCount = await countWorkerActiveOrdersWithClient(client, input.workerId)
|
||||
if (activeOrderCount >= input.maxActiveOrders) {
|
||||
return { share: null, failureReason: 'worker_active_order_limit' }
|
||||
}
|
||||
}
|
||||
|
||||
const unitReward = toPositiveInteger(workOrder.sharing_unit_reward, 0)
|
||||
const shareReward = input.quantity * unitReward
|
||||
const shareDeposit = Math.round(
|
||||
(Number(workOrder.required_deposit_amount || 0) * input.quantity) / totalQuantity,
|
||||
const nextQuantity = Number(existingShare?.quantity || 0) + requestedQuantity
|
||||
const shareReward = nextQuantity * unitReward
|
||||
const nextShareDeposit = Math.round(
|
||||
(Number(workOrder.required_deposit_amount || 0) * nextQuantity) / totalQuantity,
|
||||
)
|
||||
const depositToFreeze = Math.max(
|
||||
0,
|
||||
nextShareDeposit - Number(existingShare?.share_deposit || 0),
|
||||
)
|
||||
|
||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const available = Number(wallet?.available_amount || 0)
|
||||
if (available < shareDeposit) {
|
||||
if (available < depositToFreeze) {
|
||||
return { share: null, failureReason: 'worker_deposit_insufficient' }
|
||||
}
|
||||
|
||||
const shareResult = await client.query<{ id: number }>(
|
||||
`
|
||||
const shareResult = existingShare
|
||||
? await client.query<{ id: number }>(
|
||||
`
|
||||
UPDATE work_order_shares
|
||||
SET
|
||||
quantity = $1,
|
||||
unit_reward = $2,
|
||||
share_reward = $3,
|
||||
share_deposit = $4,
|
||||
updated_at = $5
|
||||
WHERE id = $6
|
||||
RETURNING id
|
||||
`,
|
||||
[nextQuantity, unitReward, shareReward, nextShareDeposit, input.now, existingShare.id],
|
||||
)
|
||||
: await client.query<{ id: number }>(
|
||||
`
|
||||
INSERT INTO work_order_shares (
|
||||
work_order_id, worker_id, quantity, unit_reward, share_reward,
|
||||
share_deposit, status, acceptance_json, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'joined', '{}'::jsonb, $7, $7)
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
input.workOrderId,
|
||||
input.workerId,
|
||||
input.quantity,
|
||||
unitReward,
|
||||
shareReward,
|
||||
shareDeposit,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
[
|
||||
input.workOrderId,
|
||||
input.workerId,
|
||||
requestedQuantity,
|
||||
unitReward,
|
||||
shareReward,
|
||||
nextShareDeposit,
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
|
||||
if (shareDeposit > 0) {
|
||||
const nextAvailable = available - shareDeposit
|
||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + shareDeposit
|
||||
if (depositToFreeze > 0) {
|
||||
const nextAvailable = available - depositToFreeze
|
||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + depositToFreeze
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_wallets
|
||||
@@ -262,12 +350,12 @@ export async function joinWorkOrderShare(input: {
|
||||
`,
|
||||
[
|
||||
input.workerId,
|
||||
shareDeposit,
|
||||
depositToFreeze,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId,
|
||||
`拼单冻结押金 ${input.quantity} 份`,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, quantity: input.quantity }),
|
||||
`拼单冻结押金 ${requestedQuantity} 份`,
|
||||
JSON.stringify({ workOrderId: input.workOrderId, quantity: requestedQuantity }),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
@@ -280,7 +368,11 @@ export async function joinWorkOrderShare(input: {
|
||||
eventType: 'sharing_joined',
|
||||
fromStatus: 'open',
|
||||
toStatus: 'open',
|
||||
payloadJson: JSON.stringify({ workerId: input.workerId, quantity: input.quantity }),
|
||||
payloadJson: JSON.stringify({
|
||||
workerId: input.workerId,
|
||||
quantity: requestedQuantity,
|
||||
takeRemaining: input.takeRemaining === true,
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
@@ -302,6 +394,21 @@ export async function submitWorkOrderShareAcceptance(input: {
|
||||
failureReason: 'share_not_found' | 'share_status_invalid' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<{
|
||||
status: string
|
||||
sharing_total_quantity: number
|
||||
}>(
|
||||
`SELECT status, sharing_total_quantity
|
||||
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 { share: null, failureReason: 'share_not_found' }
|
||||
}
|
||||
|
||||
const lockResult = await client.query<{ id: number }>(
|
||||
`
|
||||
SELECT id
|
||||
@@ -334,6 +441,36 @@ export async function submitWorkOrderShareAcceptance(input: {
|
||||
`,
|
||||
[input.acceptanceJson, input.now, Number(share.id)],
|
||||
)
|
||||
|
||||
const progressResult = await client.query<{
|
||||
joined_quantity: number
|
||||
pending_submission_count: number
|
||||
}>(
|
||||
`SELECT
|
||||
COALESCE(SUM(quantity), 0)::int AS joined_quantity,
|
||||
COUNT(*) FILTER (WHERE status = 'joined')::int AS pending_submission_count
|
||||
FROM work_order_shares
|
||||
WHERE work_order_id = $1
|
||||
AND status != 'cancelled'`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const progress = progressResult.rows[0]
|
||||
const allQuantityAllocated =
|
||||
Number(progress?.joined_quantity || 0) >=
|
||||
toPositiveInteger(workOrder.sharing_total_quantity, 1)
|
||||
if (allQuantityAllocated && Number(progress?.pending_submission_count || 0) === 0) {
|
||||
await client.query(
|
||||
`UPDATE work_orders
|
||||
SET
|
||||
status = 'pending_acceptance',
|
||||
submitted_at = COALESCE(submitted_at, $1),
|
||||
updated_at = $1
|
||||
WHERE id = $2
|
||||
AND status = 'open'`,
|
||||
[input.now, input.workOrderId],
|
||||
)
|
||||
}
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'worker',
|
||||
@@ -911,7 +1048,9 @@ export async function listWorkOrders({
|
||||
categoryId = 0,
|
||||
workerSharingId = 0,
|
||||
visibleAfterIso = '',
|
||||
excludeFilledSharing = false,
|
||||
vipEvidencePending = false,
|
||||
useOperationalStatus = false,
|
||||
pinnedFirst = false,
|
||||
sort = 'id_desc',
|
||||
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
|
||||
@@ -924,7 +1063,9 @@ export async function listWorkOrders({
|
||||
categoryId,
|
||||
workerSharingId,
|
||||
visibleAfterIso,
|
||||
excludeFilledSharing,
|
||||
vipEvidencePending,
|
||||
useOperationalStatus,
|
||||
})
|
||||
const totalResult = await query<{ total: number }>(
|
||||
`
|
||||
@@ -977,9 +1118,12 @@ export async function getWorkOrderStatistics(input: ListInput = {}): Promise<Wor
|
||||
AND wo.acceptance_json->>'evidenceStatus' = 'pending'
|
||||
AND NULLIF(wo.acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
|
||||
`
|
||||
const statusExpression = input.vipEvidencePending
|
||||
? `CASE WHEN ${vipEvidencePendingWhere} THEN 'vip_evidence_pending' ELSE wo.status END`
|
||||
const operationalStatus = input.useOperationalStatus
|
||||
? adminOperationalStatusExpression()
|
||||
: 'wo.status'
|
||||
const statusExpression = input.vipEvidencePending
|
||||
? `CASE WHEN ${vipEvidencePendingWhere} THEN 'vip_evidence_pending' ELSE ${operationalStatus} END`
|
||||
: operationalStatus
|
||||
const paymentExpression = `
|
||||
CASE
|
||||
WHEN COALESCE(wo.material_json->'source'->>'orderAmountFen', '') ~ '^-?[0-9]+$'
|
||||
@@ -1041,7 +1185,17 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise
|
||||
`
|
||||
WITH worker_orders AS (
|
||||
SELECT
|
||||
wo.status,
|
||||
CASE
|
||||
WHEN wos.id IS NOT NULL AND wo.status NOT IN ('accepted', 'cancelled', 'problem')
|
||||
THEN CASE wos.status
|
||||
WHEN 'joined' THEN 'in_progress'
|
||||
WHEN 'submitted' THEN 'pending_acceptance'
|
||||
WHEN 'accepted' THEN 'accepted'
|
||||
WHEN 'cancelled' THEN 'cancelled'
|
||||
ELSE wo.status
|
||||
END
|
||||
ELSE wo.status
|
||||
END AS status,
|
||||
wo.acceptance_json,
|
||||
wos.id IS NOT NULL AS is_worker_share,
|
||||
CASE
|
||||
@@ -2124,6 +2278,162 @@ export async function acceptWorkOrderAndSettle(input: {
|
||||
})
|
||||
}
|
||||
|
||||
/** 单独验收一个拼单份额,只结算该参与者,不影响其他份额。 */
|
||||
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 shouldDelayUnfreeze = input.unfreezeDays > 0
|
||||
const nextAvailable =
|
||||
Number(wallet?.available_amount || 0) +
|
||||
shareReward +
|
||||
(shouldDelayUnfreeze ? 0 : releaseAmount)
|
||||
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,
|
||||
],
|
||||
)
|
||||
}
|
||||
await maybeUpgradeWorkerLevelWithClient(client, workerId, 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],
|
||||
)
|
||||
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 resolveProblemWorkOrder(input: {
|
||||
workOrderId: number
|
||||
action: ProblemWorkOrderResolutionAction
|
||||
@@ -2869,7 +3179,9 @@ function buildWorkOrderWhere({
|
||||
categoryId = 0,
|
||||
workerSharingId = 0,
|
||||
visibleAfterIso = '',
|
||||
excludeFilledSharing = false,
|
||||
vipEvidencePending = false,
|
||||
useOperationalStatus = false,
|
||||
}: ListInput) {
|
||||
const filters: string[] = []
|
||||
const params: unknown[] = []
|
||||
@@ -2883,15 +3195,23 @@ function buildWorkOrderWhere({
|
||||
AND wo.acceptance_json->>'evidenceStatus' = 'pending'
|
||||
AND NULLIF(wo.acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
|
||||
`
|
||||
if (normalizedStatuses.length > 0) {
|
||||
if (normalizedStatuses.length > 0) {
|
||||
params.push(normalizedStatuses)
|
||||
filters.push(`(wo.status = ANY($${params.length}::text[]) OR (${vipEvidencePendingWhere}))`)
|
||||
filters.push(`(${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY($${params.length}::text[]) OR (${vipEvidencePendingWhere}))`)
|
||||
} else {
|
||||
filters.push(`(${vipEvidencePendingWhere})`)
|
||||
}
|
||||
} else if (normalizedStatuses.length > 0) {
|
||||
params.push(normalizedStatuses)
|
||||
filters.push(`wo.status = ANY($${params.length}::text[])`)
|
||||
const statusesPlaceholder = `$${params.length}`
|
||||
if (workerSharingId) {
|
||||
params.push(workerSharingId)
|
||||
filters.push(
|
||||
`${workerOrderStatusExpression(`$${params.length}`)} = ANY(${statusesPlaceholder}::text[])`,
|
||||
)
|
||||
} else {
|
||||
filters.push(`${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`)
|
||||
}
|
||||
}
|
||||
if (workOrderId) {
|
||||
params.push(workOrderId)
|
||||
@@ -2924,6 +3244,17 @@ function buildWorkOrderWhere({
|
||||
params.push(visibleAfterIso)
|
||||
filters.push(`(wo.published_at IS NULL OR wo.published_at <= $${params.length})`)
|
||||
}
|
||||
if (excludeFilledSharing) {
|
||||
filters.push(`(
|
||||
wo.sharing_enabled IS NOT TRUE
|
||||
OR COALESCE((
|
||||
SELECT SUM(wos.quantity)
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = wo.id
|
||||
AND wos.status != 'cancelled'
|
||||
), 0) < wo.sharing_total_quantity
|
||||
)`)
|
||||
}
|
||||
return {
|
||||
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
|
||||
params,
|
||||
|
||||
Reference in New Issue
Block a user