接单平台新增拼单与取消接单能力

拼单功能:
- 工单/物品规则支持拼单配置(启用、总数量、单价),总价=单价×数量三方联动,启用拼单时工单金额自动同步拼单总价
- 拼单参与按数量冻结比例押金,验收通过后按份额结算报酬,升级统计按数量计入
- 抢单大厅拼单入口:紫色渐变大按钮带单价展示,拼单进度/剩余份数,弹窗输入数量实时预估报酬与押金
- 我的订单展示拼单份额(数量×单价、状态),拼单者各自提交验收,全部提交后管理员统一验收
- 新增 work_order_shares 表与迁移 008

取消接单:
- 进行中订单可取消(押金全额退还、工单回大厅),30 天内限 5 次防滥用
This commit is contained in:
yml2213
2026-08-01 16:31:27 +08:00
parent 52a85f7f9e
commit c33e859e96
12 changed files with 1745 additions and 61 deletions
@@ -108,6 +108,9 @@ export type WorkProductRuleRow = {
reward_amount: number
required_deposit_amount: number
deposit_threshold_amount: number
sharing_enabled: boolean
sharing_total_quantity: number
sharing_unit_reward: number
requirement_json: string | Record<string, unknown>
sort_order: number
created_at: string
@@ -115,6 +118,24 @@ export type WorkProductRuleRow = {
category_name?: string
}
export type WorkOrderShareRow = {
id: number
work_order_id: number
worker_id: number
quantity: number
unit_reward: number
share_reward: number
share_deposit: number
status: string
acceptance_json: string | Record<string, unknown>
submitted_at: string | null
accepted_at: string | null
created_at: string
updated_at: string
worker_username?: string
worker_display_name?: string
}
export type WorkOrderRow = {
id: number
work_order_no: string
@@ -133,6 +154,9 @@ export type WorkOrderRow = {
requirement_json: string | Record<string, unknown>
acceptance_json: string | Record<string, unknown>
problem_note: string
sharing_enabled: boolean
sharing_total_quantity: number
sharing_unit_reward: number
published_at: string | null
assigned_at: string | null
submitted_at: string | null
@@ -167,6 +191,7 @@ type ListInput = {
keyword?: string
workerId?: number
categoryId?: number
workerSharingId?: number
}
type ProductRuleListInput = {
@@ -214,6 +239,9 @@ type CreateWorkOrderInput = {
rewardAmount: number
requiredDepositAmount: number
depositThresholdAmount: number
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
materialJson: string
requirementJson: string
now: string
@@ -853,16 +881,284 @@ export async function getWorkerFinanceRequestSummary(workerId: number | string)
export async function countWorkerAcceptedOrders(workerId: number | string): Promise<number> {
const result = await query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_orders
WHERE assigned_worker_id = $1
AND status = 'accepted'
SELECT
(SELECT COUNT(*)::int FROM work_orders WHERE assigned_worker_id = $1 AND status = 'accepted')
+
COALESCE(
(SELECT SUM(quantity)::int FROM work_order_shares WHERE worker_id = $1 AND status = 'accepted'),
0
) AS total
`,
[Number(workerId)],
)
return Number(result.rows[0]?.total || 0)
}
const WORK_ORDER_SHARE_SELECT = `
SELECT
wos.*,
wu.username AS worker_username,
wu.display_name AS worker_display_name
FROM work_order_shares wos
LEFT JOIN worker_users wu ON wu.id = wos.worker_id
`
export async function listWorkOrderShares(
workOrderId: number | string,
): Promise<WorkOrderShareRow[]> {
const result = await query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.work_order_id = $1 ORDER BY wos.id ASC`,
[Number(workOrderId)],
)
return result.rows
}
export async function listWorkOrderSharesByOrderIds(
workOrderIds: number[],
): Promise<WorkOrderShareRow[]> {
if (workOrderIds.length === 0) return []
const result = await query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT}
WHERE wos.work_order_id = ANY($1::bigint[])
ORDER BY wos.id ASC`,
[workOrderIds],
)
return result.rows
}
export async function listWorkerSharesByWorker(
workerId: number | string,
): Promise<WorkOrderShareRow[]> {
const result = await query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT}
WHERE wos.worker_id = $1 AND wos.status != 'cancelled'
ORDER BY wos.id DESC`,
[Number(workerId)],
)
return result.rows
}
export async function countWorkOrderPendingSharingSubmissions(
workOrderId: number | string,
): Promise<number> {
const result = await query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_order_shares
WHERE work_order_id = $1 AND status = 'joined'
`,
[Number(workOrderId)],
)
return Number(result.rows[0]?.total || 0)
}
export async function getWorkOrderShare(
workOrderId: number | string,
workerId: number | string,
): Promise<WorkOrderShareRow | null> {
const result = await query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT}
WHERE wos.work_order_id = $1 AND wos.worker_id = $2 LIMIT 1`,
[Number(workOrderId), Number(workerId)],
)
return result.rows[0] || null
}
export async function joinWorkOrderShare(input: {
workOrderId: number
workerId: number
quantity: number
maxActiveOrders: number
now: string
}): Promise<{
share: WorkOrderShareRow | null
failureReason:
| 'work_order_not_open'
| 'sharing_disabled'
| 'work_order_owner_conflict'
| 'sharing_quantity_full'
| 'worker_active_order_limit'
| 'worker_deposit_insufficient'
| null
}> {
return withTransaction(async (client) => {
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
const orderResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = orderResult.rows[0] || null
if (!workOrder || workOrder.status !== 'open') {
return { share: null, failureReason: 'work_order_not_open' }
}
if (workOrder.sharing_enabled !== true) {
return { share: null, failureReason: 'sharing_disabled' }
}
if (Number(workOrder.assigned_worker_id || 0) === input.workerId) {
return { share: null, failureReason: 'work_order_owner_conflict' }
}
const totalQuantity = toPositiveInteger(workOrder.sharing_total_quantity, 1)
const joinedResult = await client.query<{ total: number }>(
`
SELECT COALESCE(SUM(quantity), 0)::int AS total
FROM work_order_shares
WHERE work_order_id = $1 AND status != 'cancelled'
`,
[input.workOrderId],
)
const joinedQuantity = Number(joinedResult.rows[0]?.total || 0)
const remaining = Math.max(0, totalQuantity - joinedQuantity)
if (input.quantity <= 0 || input.quantity > 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',
[input.workOrderId, input.workerId],
)
if (existingShare.rows[0]) {
return { share: null, failureReason: 'sharing_quantity_full' }
}
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 wallet = await getWorkerWalletWithClient(client, input.workerId)
const available = Number(wallet?.available_amount || 0)
if (available < shareDeposit) {
return { share: null, failureReason: 'worker_deposit_insufficient' }
}
const shareResult = 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,
],
)
if (shareDeposit > 0) {
const nextAvailable = available - shareDeposit
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0) + shareDeposit
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, 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, 'deposit_freeze', $2, $3, $4, $5, $6, $7::jsonb, $8)
`,
[
input.workerId,
shareDeposit,
nextAvailable,
nextFrozen,
input.workOrderId,
`拼单冻结押金 ${input.quantity}`,
JSON.stringify({ workOrderId: input.workOrderId, quantity: input.quantity }),
input.now,
],
)
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'worker',
actorId: String(input.workerId),
eventType: 'sharing_joined',
fromStatus: 'open',
toStatus: 'open',
payloadJson: JSON.stringify({ workerId: input.workerId, quantity: input.quantity }),
now: input.now,
})
const share = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[Number(shareResult.rows[0]?.id || 0)],
)
return { share: share.rows[0] || null, failureReason: null }
})
}
export async function submitWorkOrderShareAcceptance(input: {
workOrderId: number
workerId: number
acceptanceJson: string
now: string
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> {
return withTransaction(async (client) => {
const shareResult = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT}
WHERE wos.work_order_id = $1 AND wos.worker_id = $2
FOR UPDATE`,
[input.workOrderId, input.workerId],
)
const share = shareResult.rows[0] || null
if (!share) {
return { share: null, failureReason: 'share_not_found' }
}
if (share.status !== 'joined') {
return { share, failureReason: 'share_status_invalid' }
}
await client.query(
`
UPDATE work_order_shares
SET status = 'submitted', acceptance_json = $1::jsonb, submitted_at = $2, updated_at = $2
WHERE id = $3
`,
[input.acceptanceJson, input.now, Number(share.id)],
)
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'worker',
actorId: String(input.workerId),
eventType: 'sharing_acceptance_submitted',
fromStatus: 'joined',
toStatus: 'submitted',
payloadJson: JSON.stringify({ workerId: input.workerId, quantity: share.quantity }),
now: input.now,
})
const updated = await client.query<WorkOrderShareRow>(
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
[Number(share.id)],
)
return { share: updated.rows[0] || null, failureReason: null }
})
}
export async function maybeUpgradeWorkerLevelWithClient(
client: PoolClient,
workerId: number,
@@ -870,10 +1166,13 @@ export async function maybeUpgradeWorkerLevelWithClient(
): Promise<WorkerUserRow | null> {
const acceptedResult = await client.query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_orders
WHERE assigned_worker_id = $1
AND status = 'accepted'
SELECT
(SELECT COUNT(*)::int FROM work_orders WHERE assigned_worker_id = $1 AND status = 'accepted')
+
COALESCE(
(SELECT SUM(quantity)::int FROM work_order_shares WHERE worker_id = $1 AND status = 'accepted'),
0
) AS total
`,
[workerId],
)
@@ -1037,6 +1336,9 @@ export async function upsertWorkProductRule(input: {
rewardAmount: number
requiredDepositAmount: number
depositThresholdAmount: number
sharingEnabled?: boolean
sharingTotalQuantity?: number
sharingUnitReward?: number
requirementJson: string
sortOrder: number
now: string
@@ -1046,13 +1348,17 @@ export async function upsertWorkProductRule(input: {
INSERT INTO work_product_rules (
rule_key, provider, platform, shop_id, sku_code, product_name,
match_type, category_id, enabled, auto_create, reward_amount,
required_deposit_amount, deposit_threshold_amount, requirement_json,
required_deposit_amount, deposit_threshold_amount,
sharing_enabled, sharing_total_quantity, sharing_unit_reward,
requirement_json,
sort_order, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13, $14::jsonb,
$15, $16, $17
$12, $13,
$14, $15, $16,
$17::jsonb,
$18, $19, $20
)
ON CONFLICT (rule_key) DO UPDATE
SET
@@ -1068,6 +1374,9 @@ export async function upsertWorkProductRule(input: {
reward_amount = EXCLUDED.reward_amount,
required_deposit_amount = EXCLUDED.required_deposit_amount,
deposit_threshold_amount = EXCLUDED.deposit_threshold_amount,
sharing_enabled = EXCLUDED.sharing_enabled,
sharing_total_quantity = EXCLUDED.sharing_total_quantity,
sharing_unit_reward = EXCLUDED.sharing_unit_reward,
requirement_json = EXCLUDED.requirement_json,
sort_order = EXCLUDED.sort_order,
updated_at = EXCLUDED.updated_at
@@ -1087,6 +1396,9 @@ export async function upsertWorkProductRule(input: {
input.rewardAmount,
input.requiredDepositAmount,
input.depositThresholdAmount,
input.sharingEnabled === true,
toPositiveInteger(input.sharingTotalQuantity, 1),
toPositiveInteger(input.sharingUnitReward, 0),
input.requirementJson,
input.sortOrder,
input.now,
@@ -1102,10 +1414,11 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
INSERT INTO work_orders (
work_order_no, order_id, order_item_id, task_id, platform_order_id,
product_name, category_id, status, reward_amount, required_deposit_amount,
deposit_threshold_amount, material_json, requirement_json, created_at, updated_at
deposit_threshold_amount, sharing_enabled, sharing_total_quantity,
sharing_unit_reward, material_json, requirement_json, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12::jsonb, $13::jsonb, $14, $15
$11, $12, $13, $14, $15::jsonb, $16::jsonb, $17, $18
)
RETURNING id
`,
@@ -1121,6 +1434,9 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
input.rewardAmount,
input.requiredDepositAmount,
input.depositThresholdAmount,
input.sharingEnabled === true,
toPositiveInteger(input.sharingTotalQuantity, 1),
toPositiveInteger(input.sharingUnitReward, 0),
input.materialJson,
input.requirementJson,
input.now,
@@ -1179,12 +1495,14 @@ export async function listWorkOrders({
keyword = '',
workerId = 0,
categoryId = 0,
workerSharingId = 0,
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
const { whereClause, params } = buildWorkOrderWhere({
status,
keyword,
workerId,
categoryId,
workerSharingId,
})
const totalResult = await query<{ total: number }>(
`
@@ -1216,6 +1534,10 @@ export async function updateWorkOrder(
| 'material_json'
| 'acceptance_json'
| 'problem_note'
| 'reward_amount'
| 'sharing_enabled'
| 'sharing_total_quantity'
| 'sharing_unit_reward'
| 'published_at'
| 'assigned_at'
| 'submitted_at'
@@ -1236,12 +1558,16 @@ export async function updateWorkOrder(
material_json = $3::jsonb,
acceptance_json = $4::jsonb,
problem_note = $5,
published_at = $6,
assigned_at = $7,
submitted_at = $8,
accepted_at = $9,
updated_at = $10
WHERE id = $11
reward_amount = $6,
sharing_enabled = $7,
sharing_total_quantity = $8,
sharing_unit_reward = $9,
published_at = $10,
assigned_at = $11,
submitted_at = $12,
accepted_at = $13,
updated_at = $14
WHERE id = $15
`,
[
next.status,
@@ -1249,6 +1575,10 @@ export async function updateWorkOrder(
toJsonString(next.material_json),
toJsonString(next.acceptance_json),
next.problem_note || '',
toPositiveInteger(next.reward_amount, 0),
next.sharing_enabled === true,
toPositiveInteger(next.sharing_total_quantity, 1),
toPositiveInteger(next.sharing_unit_reward, 0),
next.published_at || null,
next.assigned_at || null,
next.submitted_at || null,
@@ -1355,10 +1685,120 @@ export async function grabWorkOrder(input: {
})
}
export async function cancelWorkerWorkOrder(input: {
workOrderId: number
workerId: number
now: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_owner_required' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
SELECT *
FROM work_orders
WHERE id = $1
FOR UPDATE
`,
[input.workOrderId],
)
const workOrder = currentResult.rows[0] || null
if (!workOrder || workOrder.status !== 'in_progress') {
return { order: null, failureReason: 'work_order_not_in_progress' }
}
if (Number(workOrder.assigned_worker_id || 0) !== input.workerId) {
return { order: null, failureReason: 'work_order_owner_required' }
}
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, input.workerId)
const releaseAmount = Math.min(
await getOutstandingDepositAmountWithClient(client, input.workerId, input.workOrderId),
Number(wallet?.frozen_deposit_amount || 0),
)
await client.query(
`
UPDATE work_orders
SET
status = 'open',
assigned_worker_id = NULL,
assigned_at = NULL,
updated_at = $1
WHERE id = $2
`,
[input.now, input.workOrderId],
)
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 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, updated_at = $3
WHERE worker_id = $4
`,
[nextAvailable, nextFrozen, 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, 'deposit_release', $2, $3, $4, $5, '取消接单退还押金', '{}'::jsonb, $6)
`,
[input.workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
)
}
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'worker',
actorId: String(input.workerId),
eventType: 'cancelled_by_worker',
fromStatus: 'in_progress',
toStatus: 'open',
payloadJson: JSON.stringify({ releaseAmount }),
now: input.now,
})
return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
})
}
export async function countWorkerCancellationsSince(
workerId: number | string,
sinceIso: string,
): Promise<number> {
const result = await query<{ total: number }>(
`
SELECT COUNT(*)::int AS total
FROM work_order_events
WHERE actor_type = 'worker'
AND actor_id = $1
AND event_type = 'cancelled_by_worker'
AND created_at >= $2
`,
[String(workerId), sinceIso],
)
return Number(result.rows[0]?.total || 0)
}
export async function acceptWorkOrderAndSettle(input: {
workOrderId: number
now: string
}): Promise<WorkOrderRow | null> {
}): Promise<{
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
}> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
`
@@ -1372,7 +1812,29 @@ export async function acceptWorkOrderAndSettle(input: {
)
const workOrder = currentResult.rows[0] || null
if (!workOrder) {
return null
return { order: null, failureReason: null }
}
const sharesResult = await client.query<WorkOrderShareRow>(
`
SELECT *
FROM work_order_shares
WHERE work_order_id = $1
AND status != 'cancelled'
ORDER BY id ASC
FOR UPDATE
`,
[input.workOrderId],
)
const shares = sharesResult.rows
if (shares.length > 0) {
const pendingShares = shares.filter((share) => share.status === 'joined')
if (pendingShares.length > 0) {
return {
order: null,
failureReason: 'sharing_pending_submissions',
}
}
}
await client.query(
@@ -1384,6 +1846,94 @@ export async function acceptWorkOrderAndSettle(input: {
[input.now, input.workOrderId],
)
if (shares.length > 0) {
for (const share of shares) {
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 nextAvailable =
Number(wallet?.available_amount || 0) + releaseAmount + shareReward
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,
total_settled_amount = total_settled_amount + $3,
updated_at = $4
WHERE worker_id = $5
`,
[nextAvailable, nextFrozen, shareReward, input.now, shareWorkerId],
)
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, '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,
],
)
}
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)
@@ -1441,7 +1991,7 @@ export async function acceptWorkOrderAndSettle(input: {
eventType: 'accepted',
fromStatus: 'pending_acceptance',
toStatus: 'accepted',
payloadJson: '{}',
payloadJson: JSON.stringify({ shareCount: shares.length }),
now: input.now,
})
@@ -1449,7 +1999,7 @@ export async function acceptWorkOrderAndSettle(input: {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
}
return getWorkOrderByIdWithClient(client, input.workOrderId)
return { order: await getWorkOrderByIdWithClient(client, input.workOrderId), failureReason: null }
})
}
@@ -1779,6 +2329,7 @@ function buildWorkOrderWhere({
keyword = '',
workerId = 0,
categoryId = 0,
workerSharingId = 0,
}: ListInput) {
const filters: string[] = []
const params: unknown[] = []
@@ -1800,6 +2351,16 @@ function buildWorkOrderWhere({
params.push(categoryId)
filters.push(`wo.category_id = $${params.length}`)
}
if (workerSharingId) {
params.push(workerSharingId)
filters.push(
`(wo.assigned_worker_id = $${params.length}
OR wo.id IN (
SELECT work_order_id FROM work_order_shares
WHERE worker_id = $${params.length} AND status != 'cancelled'
))`,
)
}
return {
whereClause: filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '',
params,
@@ -1884,6 +2445,12 @@ function buildWorkProductRuleWhere({ enabled = null, keyword = '' }: ProductRule
}
function toJsonString(value: unknown): string {
if (typeof value === 'string') return value || '{}'
return JSON.stringify(value || {})
if (typeof value === 'string') return value
return JSON.stringify(value ?? {})
}
function toPositiveInteger(value: unknown, fallback: number): number {
const parsed = Number(value ?? '')
if (!Number.isFinite(parsed)) return fallback
return Math.max(0, Math.round(parsed))
}