{"path":"apps/api-server/src/routes/jobs.ts","size":8351,"locked":false,"binary":false,"truncated":false,"content":"import { Router, type IRouter, type Request, type Response } from \"express\";\nimport { eq, and, sql, type SQL } from \"drizzle-orm\";\nimport { db, jobsTable } from \"@workspace/db\";\nimport {\n  ListJobsQueryParams,\n  CreateJobBody,\n  GetJobParams,\n  GetJobResponse,\n  UpdateJobParams,\n  UpdateJobBody,\n  SubmitDeliverableParams,\n  SubmitDeliverableBody,\n  SubmitDeliverableResponse,\n  GetStatsResponse,\n} from \"@workspace/api-zod\";\nimport { logger } from \"../lib/logger\";\n\nconst router: IRouter = Router();\n\nconst ORACLE_URL = process.env[\"ORACLE_URL\"] ?? \"http://localhost:3001\";\n\n// ─── LIST JOBS ────────────────────────────────────────────────────────────────\n\nrouter.get(\"/jobs\", async (req, res): Promise<void> => {\n  const parsed = ListJobsQueryParams.safeParse(req.query);\n  if (!parsed.success) {\n    res.status(400).json({ error: parsed.error.message });\n    return;\n  }\n\n  const { status, clientPubkey } = parsed.data;\n\n  const conditions: SQL[] = [];\n  if (status) conditions.push(eq(jobsTable.status, status));\n  if (clientPubkey) conditions.push(eq(jobsTable.clientPubkey, clientPubkey));\n\n  let query = db.select().from(jobsTable).$dynamic();\n  if (conditions.length > 0) {\n    query = query.where(and(...conditions));\n  }\n\n  const jobs = await query.orderBy(jobsTable.createdAt);\n  res.json(jobs);\n});\n\n// ─── CREATE JOB ───────────────────────────────────────────────────────────────\n\nrouter.post(\"/jobs\", async (req, res): Promise<void> => {\n  const parsed = CreateJobBody.safeParse(req.body);\n  if (!parsed.success) {\n    res.status(400).json({ error: parsed.error.message });\n    return;\n  }\n\n  const { jobId, clientPubkey, freelancerPubkey, oraclePubkey, amountLamports, description, acceptanceCriteria } = parsed.data;\n\n  const [job] = await db\n    .insert(jobsTable)\n    .values({\n      jobId,\n      clientPubkey,\n      freelancerPubkey,\n      oraclePubkey,\n      amountLamports,\n      status: \"pending\",\n      description: description ?? null,\n      acceptanceCriteria: acceptanceCriteria ? JSON.stringify(acceptanceCriteria) : null,\n    })\n    .returning();\n\n  req.log.info({ jobId }, \"Job registered\");\n  res.status(201).json(GetJobResponse.parse(job));\n});\n\n// ─── GET JOB ─────────────────────────────────────────────────────────────────\n\nrouter.get(\"/jobs/:jobId\", async (req, res): Promise<void> => {\n  const raw = Array.isArray(req.params.jobId) ? req.params.jobId[0] : req.params.jobId;\n  const params = GetJobParams.safeParse({ jobId: raw });\n  if (!params.success) {\n    res.status(400).json({ error: params.error.message });\n    return;\n  }\n\n  const [job] = await db.select().from(jobsTable).where(eq(jobsTable.jobId, params.data.jobId));\n\n  if (!job) {\n    res.status(404).json({ error: \"Job not found\" });\n    return;\n  }\n\n  res.json(GetJobResponse.parse(job));\n});\n\n// ─── UPDATE JOB ───────────────────────────────────────────────────────────────\n\nrouter.patch(\"/jobs/:jobId\", async (req, res): Promise<void> => {\n  const raw = Array.isArray(req.params.jobId) ? req.params.jobId[0] : req.params.jobId;\n  const params = UpdateJobParams.safeParse({ jobId: raw });\n  if (!params.success) {\n    res.status(400).json({ error: params.error.message });\n    return;\n  }\n\n  const parsed = UpdateJobBody.safeParse(req.body);\n  if (!parsed.success) {\n    res.status(400).json({ error: parsed.error.message });\n    return;\n  }\n\n  const updates: Record<string, unknown> = { updatedAt: new Date() };\n  if (parsed.data.status !== undefined)      updates[\"status\"]  = parsed.data.status;\n  if (parsed.data.txSig !== undefined)       updates[\"txSig\"]   = parsed.data.txSig;\n  if (parsed.data.description !== undefined) updates[\"description\"] = parsed.data.description;\n\n  const [job] = await db\n    .update(jobsTable)\n    .set(updates)\n    .where(eq(jobsTable.jobId, params.data.jobId))\n    .returning();\n\n  if (!job) {\n    res.status(404).json({ error: \"Job not found\" });\n    return;\n  }\n\n  res.json(GetJobResponse.parse(job));\n});\n\n// ─── SUBMIT DELIVERABLE ───────────────────────────────────────────────────────\n\nrouter.post(\"/jobs/:jobId/submit\", async (req, res): Promise<void> => {\n  const raw = Array.isArray(req.params.jobId) ? req.params.jobId[0] : req.params.jobId;\n  const params = SubmitDeliverableParams.safeParse({ jobId: raw });\n  if (!params.success) {\n    res.status(400).json({ error: params.error.message });\n    return;\n  }\n\n  const parsed = SubmitDeliverableBody.safeParse(req.body);\n  if (!parsed.success) {\n    res.status(400).json({ error: parsed.error.message });\n    return;\n  }\n\n  const { jobId } = params.data;\n\n  let oracleResult: unknown;\n  try {\n    const oracleRes = await fetch(`${ORACLE_URL}/submit`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ jobId, ...parsed.data }),\n    });\n    oracleResult = await oracleRes.json();\n  } catch (err) {\n    req.log.warn({ err, jobId }, \"Oracle unreachable — returning stub escalation\");\n    const stub = SubmitDeliverableResponse.parse({\n      success: false,\n      jobId,\n      outcome: null,\n      txSig: null,\n      error: \"Oracle is not reachable. Ensure the oracle middleware is running with valid API keys.\",\n    });\n    res.status(503).json(stub);\n    return;\n  }\n\n  const result = SubmitDeliverableResponse.safeParse(oracleResult);\n  if (!result.success) {\n    res.status(500).json({ error: \"Unexpected oracle response shape\" });\n    return;\n  }\n\n  if (result.data.outcome === \"RELEASE\") {\n    await db.update(jobsTable).set({ status: \"completed\", txSig: result.data.txSig ?? null, updatedAt: new Date() }).where(eq(jobsTable.jobId, jobId));\n  } else if (result.data.outcome === \"REFUND\") {\n    await db.update(jobsTable).set({ status: \"cancelled\", txSig: result.data.txSig ?? null, updatedAt: new Date() }).where(eq(jobsTable.jobId, jobId));\n  }\n\n  req.log.info({ jobId, outcome: result.data.outcome }, \"Deliverable submitted\");\n  res.json(result.data);\n});\n\n// ─── STATS ────────────────────────────────────────────────────────────────────\n\nrouter.get(\"/stats\", async (_req, res): Promise<void> => {\n  const rows = await db\n    .select({\n      status: jobsTable.status,\n      count:  sql<number>`cast(count(*) as int)`,\n      totalLamports: sql<string>`coalesce(sum(cast(${jobsTable.amountLamports} as bigint))::text, '0')`,\n    })\n    .from(jobsTable)\n    .groupBy(jobsTable.status);\n\n  let total = 0;\n  let pending = 0;\n  let completed = 0;\n  let cancelled = 0;\n  let pendingLamports = BigInt(0);\n\n  for (const row of rows) {\n    total += row.count;\n    if (row.status === \"pending\") {\n      pending = row.count;\n      pendingLamports = BigInt(row.totalLamports ?? \"0\");\n    } else if (row.status === \"completed\") {\n      completed = row.count;\n    } else if (row.status === \"cancelled\") {\n      cancelled = row.count;\n    }\n  }\n\n  res.json(GetStatsResponse.parse({\n    total,\n    pending,\n    completed,\n    cancelled,\n    totalEscrowedLamports: pendingLamports.toString(),\n  }));\n});\n\n// ─── ORACLE HEALTH PROXY ──────────────────────────────────────────────────────\n\nrouter.get(\"/oracle/health\", async (_req, res): Promise<void> => {\n  try {\n    const oracleRes = await fetch(`${ORACLE_URL}/health`);\n    const body = await oracleRes.json();\n    res.json(body);\n  } catch (err) {\n    logger.warn({ err }, \"Oracle health check failed\");\n    res.status(503).json({ status: \"unreachable\", pendingJobs: 0, timestamp: new Date().toISOString() });\n  }\n});\n\nexport default router;\n"}