import { query, withTransaction } from '../../db/client.js' import { toJsonString, toPositiveInteger } from './shared.js' import { getWorkOrderById, getWorkOrderByIdWithClient } from './work-order-query-repo.js' import { createWorkOrderEventWithClient } from './work-order-event-repo.js' import { normalizeWorkOrderAcceptanceMode, normalizeWorkOrderGiftCooldownHours, normalizeWorkOrderGiftCooldownMinutes, WORK_ORDER_ACCEPTANCE_MODE, WORK_ORDER_GIFT_PHASE, } from '../../domain/work-order-acceptance-mode.js' import type { CreateWorkOrderInput, WorkOrderRow } from './types.js' export async function createWorkOrder(input: CreateWorkOrderInput): Promise { const acceptanceMode = normalizeWorkOrderAcceptanceMode(input.acceptanceMode) const isFriendGift = acceptanceMode === WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT const result = await query<{ id: number }>( ` INSERT INTO work_orders ( work_order_no, product_rule_id, order_id, order_item_id, task_id, platform_order_id, product_name, category_id, status, reward_amount, required_deposit_amount, deposit_threshold_amount, sharing_enabled, sharing_total_quantity, sharing_unit_reward, timeout_minutes, timeout_policy, acceptance_mode, gift_phase, gift_cooldown_hours, gift_cooldown_minutes, material_json, requirement_json, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22::jsonb, $23::jsonb, $24, $25 ) RETURNING id `, [ input.workOrderNo, input.productRuleId || null, input.orderId || null, input.orderItemId || null, input.taskId || null, input.platformOrderId, input.productName, input.categoryId || null, input.status, input.rewardAmount, input.requiredDepositAmount, input.depositThresholdAmount, input.sharingEnabled === true, toPositiveInteger(input.sharingTotalQuantity, 1), toPositiveInteger(input.sharingUnitReward, 0), toPositiveInteger(input.timeoutMinutes, 0), String(input.timeoutPolicy || 'reopen').trim() || 'reopen', acceptanceMode, isFriendGift ? WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED : '', normalizeWorkOrderGiftCooldownHours(input.giftCooldownHours), normalizeWorkOrderGiftCooldownMinutes(input.giftCooldownMinutes), input.materialJson, input.requirementJson, input.now, input.now, ], ) return getWorkOrderById(result.rows[0]?.id || 0) } export async function updateWorkOrder( workOrderId: number | string, patch: Partial< Pick< WorkOrderRow, | 'status' | 'assigned_worker_id' | 'material_json' | 'acceptance_json' | 'draft_acceptance_json' | 'problem_note' | 'reward_amount' | 'sharing_enabled' | 'sharing_total_quantity' | 'sharing_unit_reward' | 'published_at' | 'hall_queued_at' | 'pinned_at' | 'assigned_at' | 'submitted_at' | 'accepted_at' | 'updated_at' > >, ): Promise { const current = await getWorkOrderById(workOrderId) if (!current) return null const next = { ...current, ...patch } await query( ` UPDATE work_orders SET status = $1, assigned_worker_id = $2, material_json = $3::jsonb, acceptance_json = $4::jsonb, draft_acceptance_json = $5::jsonb, problem_note = $6, reward_amount = $7, sharing_enabled = $8, sharing_total_quantity = $9, sharing_unit_reward = $10, published_at = $11, hall_queued_at = $12, pinned_at = $13, assigned_at = $14, submitted_at = $15, accepted_at = $16, updated_at = $17 WHERE id = $18 `, [ next.status, next.assigned_worker_id || null, toJsonString(next.material_json), toJsonString(next.acceptance_json), toJsonString(next.draft_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.hall_queued_at || null, next.pinned_at || null, next.assigned_at || null, next.submitted_at || null, next.accepted_at || null, next.updated_at, Number(workOrderId), ], ) return getWorkOrderById(workOrderId) } /** 将未分配工单原子发布到大厅,并在事务内记录发布事件。 */ export async function publishUnassignedWorkOrder(input: { workOrderId: number | string now: string actorName?: string }): Promise<{ order: WorkOrderRow | null failureReason: 'work_order_not_publishable' | null }> { return withTransaction(async (client) => { const currentResult = await client.query( ` SELECT * FROM work_orders WHERE id = $1 FOR UPDATE `, [Number(input.workOrderId)], ) const current = currentResult.rows[0] || null if (!current || current.status !== 'unassigned' || current.assigned_worker_id) { return { order: null, failureReason: 'work_order_not_publishable' } } const updatedResult = await client.query<{ id: number }>( ` UPDATE work_orders SET status = 'open', published_at = $1, hall_queued_at = $1, updated_at = $1 WHERE id = $2 AND status = 'unassigned' AND assigned_worker_id IS NULL AND reward_amount > 0 RETURNING id `, [input.now, Number(input.workOrderId)], ) if (!updatedResult.rows[0]) { return { order: null, failureReason: 'work_order_not_publishable' } } await createWorkOrderEventWithClient(client, { workOrderId: Number(input.workOrderId), actorType: 'admin', actorId: input.actorName || '', eventType: 'published', fromStatus: current.status, toStatus: 'open', payloadJson: JSON.stringify({ voucherConsumeDeferred: true }), now: input.now, }) return { order: await getWorkOrderByIdWithClient(client, input.workOrderId), failureReason: null, } }) } export async function updateWorkOrderBasic( workOrderId: number | string, patch: { productName?: string platformOrderId?: string categoryId?: number | null rewardAmount?: number requiredDepositAmount?: number depositThresholdAmount?: number requirementJson?: string timeoutMinutes?: number timeoutPolicy?: string updatedAt: string }, ): Promise { const current = await getWorkOrderById(workOrderId) if (!current) return null await query( ` UPDATE work_orders SET platform_order_id = $1, product_name = $2, category_id = $3, reward_amount = $4, required_deposit_amount = $5, deposit_threshold_amount = $6, requirement_json = $7::jsonb, timeout_minutes = $8, timeout_policy = $9, updated_at = $10 WHERE id = $11 `, [ String((patch.platformOrderId ?? current.platform_order_id) || '').trim(), String((patch.productName ?? current.product_name) || '').trim(), patch.categoryId === undefined ? current.category_id : patch.categoryId, toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0), toPositiveInteger(patch.requiredDepositAmount ?? current.required_deposit_amount, 0), toPositiveInteger(patch.depositThresholdAmount ?? current.deposit_threshold_amount, 0), toJsonString(patch.requirementJson ?? current.requirement_json), toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0), String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(), patch.updatedAt, Number(workOrderId), ], ) return getWorkOrderById(workOrderId) } export async function updateWorkOrderAdminNote( workOrderId: number | string, note: string, updatedAt: string, ): Promise { const result = await query<{ id: number }>( `UPDATE work_orders SET admin_note = $1, updated_at = $2 WHERE id = $3 RETURNING id`, [String(note || '').trim(), updatedAt, Number(workOrderId)], ) if (!result.rows[0]) return null return getWorkOrderById(result.rows[0].id) } export async function deleteWorkOrder(workOrderId: number | string): Promise { const result = await query('DELETE FROM work_orders WHERE id = $1', [Number(workOrderId)]) return Number(result.rowCount || 0) > 0 }