diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a71d887..b6b929a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,13 @@ "permissions": { "allow": [ "Bash(backlog task *)", - "Bash(backlog mcp *)" + "Bash(backlog mcp *)", + "Read(//home/vscode/.config/nvim/**)", + "Read(//home/vscode/.config/nvim/lua/plugins/**)", + "Bash(npx tsc *)" ] - } + }, + "enabledMcpjsonServers": [ + "backlog" + ] } diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d3c123c..6a81a00 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM library/python:3.12-bullseye +FROM library/python:3.12-bookworm ARG USER=vscode ARG USER_UID=1000 @@ -43,8 +43,19 @@ RUN npm install -g backlog.md # RUN apt-get install terraform # RUN terraform -install-autocomplete +# Install Neovim (latest) + LazyVim deps +RUN curl -fsSL https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz \ + | tar -xz -C /opt \ + && ln -s /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim \ + && apt update && apt install -y --no-install-recommends \ + ripgrep fd-find git make unzip \ + && rm -rf /var/lib/apt/lists/* + # Install Claude USER ${USER} +# Bootstrap LazyVim starter config +RUN git clone https://github.com/LazyVim/starter /home/${USER}/.config/nvim \ + && rm -rf /home/${USER}/.config/nvim/.git RUN curl -fsSL https://claude.ai/install.sh | bash \ && export PATH="/home/${USER}/.local/bin:${PATH}" \ && claude plugin marketplace add JuliusBrussee/caveman \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 72efbbc..69f8eeb 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,8 @@ }, "extensions": [ "esbenp.prettier-vscode", - "Anthropic.claude-code" + "Anthropic.claude-code", + "asvetliakov.vscode-neovim" ] } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 1c8e315..ed3c80c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -9,8 +9,12 @@ services: command: sleep infinity ports: - "3000:3000" + - "6420:6420" volumes: - ..:/workspaces/assessment-model + - ~/.gitconfig:/home/vscode/.gitconfig:ro + environment: + - SSH_AUTH_SOCK=${SSH_AUTH_SOCK:-} networks: - frontend-net diff --git a/CLAUDE.md b/CLAUDE.md index 8a6b88d..b8c1d54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,3 +15,19 @@ - Tasks live as markdown under `backlog/tasks/`. Committed to git. Read them for context on outstanding manual work (env vars, IAM, infra) owed by humans. - To start the web UI during development: `backlog browser` (port 6420, forwarded by devcontainer). - Do NOT mirror Backlog.md tasks into Claude's internal todo system. Use one or the other — Backlog for durable cross-session work, internal todos for within-turn progress tracking. + +## Development workflow (spec-driven) + +Follow this loop for all feature work: + +1. **Decompose** — split user request into small Backlog tasks with acceptance criteria. One task = one PR = one session. +2. **Plan first** — before writing code, research codebase and write implementation plan inside the task. Stop and wait for user approval. +3. **Implement** — only after plan approved. One task at a time. +4. **Verify** — run tests/lint, confirm output matches acceptance criteria. + +**Hard rules:** +- Never start coding without an approved plan in the task. +- Never work on multiple tasks in one session. +- If task too big to finish in one session, split it first. + + diff --git a/backlog/tasks/task-1 - Add-BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME-to-staging-and-prod-env.md b/backlog/tasks/task-1 - Add-BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME-to-staging-and-prod-env.md deleted file mode 100644 index b4938e5..0000000 --- a/backlog/tasks/task-1 - Add-BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME-to-staging-and-prod-env.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: TASK-1 -title: Add BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME to staging and prod env -status: To Do -assignee: - - Jun-te Kim -created_date: '2026-04-18 19:01' -labels: - - env - - infra -dependencies: [] -priority: high ---- - -## Description - - -Dev .env.local has it; non-dev envs still missing. Combine route returns 500 'Server misconfiguration' without it. - - -## Acceptance Criteria - -- [ ] #1 Value set in staging env: bulk-address2uprn-combiner-queue-staging (or matching stage suffix) -- [ ] #2 Value set in prod env: bulk-address2uprn-combiner-queue-prod -- [ ] #3 Deploy redeployed; /api/portfolio/{pid}/bulk-uploads/{uid}/combine returns 200 not 500 - diff --git a/backlog/tasks/task-10 - Redirect-to-confirm-matches-when-combined_output_s3_uri-populated.md b/backlog/tasks/task-10 - Redirect-to-confirm-matches-when-combined_output_s3_uri-populated.md new file mode 100644 index 0000000..3dbff76 --- /dev/null +++ b/backlog/tasks/task-10 - Redirect-to-confirm-matches-when-combined_output_s3_uri-populated.md @@ -0,0 +1,31 @@ +--- +id: TASK-10 +title: Redirect to confirm-matches when combined_output_s3_uri populated +status: To Do +assignee: [] +created_date: '2026-04-20' +updated_date: '2026-04-20' +labels: + - frontend + - bulk-upload + - ui +dependencies: + - TASK-8 +priority: medium +ordinal: 5000 +--- + +## Description + + +`OnboardingProgress.tsx` currently fires client-side combine POST when task terminal. Once backend auto-chains (backend-task-5) OR frontend triggers via backend route (task-7), the polling should watch for `bulk_address_uploads.combined_output_s3_uri` to be set. When present, show "Review matches →" CTA (or auto-redirect to confirm-matches page, task-8). + +May require a new GET endpoint that returns `{status, combined_output_s3_uri}` for polling. Or extend existing task summary with upload fields. + + +## Acceptance Criteria + +- [ ] #1 Polling stops once combined_output_s3_uri populated +- [ ] #2 UI surfaces a clear CTA to review matches +- [ ] #3 No duplicate combiner fires across refreshes + diff --git a/backlog/tasks/task-2 - Grant-sqs-SendMessage-IAM-on-combiner-queue-to-assessment-model-runtime.md b/backlog/tasks/task-2 - Grant-sqs-SendMessage-IAM-on-combiner-queue-to-assessment-model-runtime.md deleted file mode 100644 index 5ca196a..0000000 --- a/backlog/tasks/task-2 - Grant-sqs-SendMessage-IAM-on-combiner-queue-to-assessment-model-runtime.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: TASK-2 -title: 'Grant sqs:SendMessage IAM on combiner queue to assessment-model runtime' -status: To Do -assignee: - - Jun-te Kim -created_date: '2026-04-18 19:01' -labels: - - infra - - iam -dependencies: [] -priority: high ---- - -## Description - - -Combine route sends to bulk-address2uprn-combiner-queue-. Runtime role needs sqs:SendMessage + sqs:GetQueueUrl on that queue ARN. - - -## Acceptance Criteria - -- [ ] #1 IAM policy updated in terraform for staging + prod -- [ ] #2 Verified via AWS console or 'aws sqs get-queue-url' using runtime creds - diff --git a/backlog/tasks/task-3 - Deploy-bulk_address2uprn_combiner-Lambda-queue-via-terraform-to-staging-prod.md b/backlog/tasks/task-3 - Deploy-bulk_address2uprn_combiner-Lambda-queue-via-terraform-to-staging-prod.md deleted file mode 100644 index fecfa88..0000000 --- a/backlog/tasks/task-3 - Deploy-bulk_address2uprn_combiner-Lambda-queue-via-terraform-to-staging-prod.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: TASK-3 -title: Deploy bulk_address2uprn_combiner Lambda + queue via terraform to staging/prod -status: To Do -assignee: - - Jun-te Kim -created_date: '2026-04-18 19:01' -labels: - - infra - - terraform -dependencies: [] -priority: high ---- - -## Description - - -Lambda source at /workspaces/home/github/Model/backend/bulk_address2uprn_combiner/. Uses lambda_with_sqs module. Needs S3_BUCKET_NAME=retrofit_sap_data_bucket_name and DB creds envs. Confirm queue name convention bulk-address2uprn-combiner-queue-. - - -## Acceptance Criteria - -- [ ] #1 Lambda + queue exist in staging -- [ ] #2 Lambda + queue exist in prod -- [ ] #3 Lambda has read on ara_raw_outputs/ and write on bulk_final_outputs/ in retrofit_sap_data bucket - diff --git a/backlog/tasks/task-4 - Smoke-test-combiner-end-to-end-on-dev.md b/backlog/tasks/task-4 - Smoke-test-combiner-end-to-end-on-dev.md index 59a8215..95911ab 100644 --- a/backlog/tasks/task-4 - Smoke-test-combiner-end-to-end-on-dev.md +++ b/backlog/tasks/task-4 - Smoke-test-combiner-end-to-end-on-dev.md @@ -1,27 +1,36 @@ --- id: TASK-4 -title: Smoke-test combiner end-to-end on dev +title: Smoke-test full bulk upload flow end-to-end on dev status: To Do assignee: - Jun-te Kim created_date: '2026-04-18 19:02' +updated_date: '2026-04-20' labels: - qa - bulk-upload -dependencies: [] +dependencies: + - TASK-6 + - TASK-7 + - TASK-8 + - TASK-9 + - TASK-10 priority: medium +ordinal: 9000 --- ## Description -After env var + IAM ready, run a real bulk upload -> map columns -> onboard -> wait for terminal complete. Confirm combiner fires. +After frontend + backend refactor ships, run the full flow on dev: upload xlsx → map columns → start onboarding → poll task progress → combiner fires → combined_output_s3_uri populated → review matches page renders → confirm rows → addresses appear in portfolio. ## Acceptance Criteria -- [ ] #1 POST /combine returns 200 with {taskId, subTaskId} -- [ ] #2 CloudWatch for bulk_address2uprn_combiner shows the subtask picked up +- [ ] #1 Frontend no longer needs POSTCODE_SPLITTER_QUEUE_NAME or BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME env vars +- [ ] #2 Backend logs show both splitter + combiner triggered via HTTP route - [ ] #3 bulk_final_outputs/{task_id}/combined_.csv exists in retrofit_sap_data bucket -- [ ] #4 bulk_address_uploads.combined_output_s3_uri populated for the test upload +- [ ] #4 bulk_address_uploads.combined_output_s3_uri populated +- [ ] #5 Confirm-matches page renders with match rows +- [ ] #6 After confirm submit, addresses persist into portfolio diff --git a/backlog/tasks/task-6 - Refactor-onboard-route-to-call-backend-trigger-splitter-endpoint.md b/backlog/tasks/task-6 - Refactor-onboard-route-to-call-backend-trigger-splitter-endpoint.md new file mode 100644 index 0000000..310c2fa --- /dev/null +++ b/backlog/tasks/task-6 - Refactor-onboard-route-to-call-backend-trigger-splitter-endpoint.md @@ -0,0 +1,105 @@ +--- +id: TASK-6 +title: Refactor onboard route to call backend trigger-splitter endpoint +status: In Progress +assignee: [] +created_date: '2026-04-20' +updated_date: '2026-04-20 12:55' +labels: + - frontend + - bulk-upload + - refactor +dependencies: + - BACKEND-TASK-1 +priority: high +ordinal: 1000 +--- + +## Description + + +Currently `src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts` builds an SQS message and sends it to `POSTCODE_SPLITTER_QUEUE_NAME`. Move the SQS send to the backend. Frontend still transforms XLSX → CSV + uploads to S3, then calls backend HTTP `POST /v1/bulk-uploads/trigger-splitter`. Drop `POSTCODE_SPLITTER_QUEUE_NAME`, `sendToQueue`, and SQS IAM dependency. + + +## Acceptance Criteria + +- [x] #1 `onboard/route.ts` no longer imports `sendToQueue` or reads `POSTCODE_SPLITTER_QUEUE_NAME` +- [x] #2 Transformed CSV still uploaded to `bulk_onboarding_inputs/{portfolioId}/{uploadId}.csv` +- [x] #3 Backend trigger-splitter endpoint called with correct payload +- [ ] #4 DB updates (bulk_address_uploads.status="processing", tasks.status, subTasks.inputs) still happen on success +- [ ] #5 4xx/5xx from backend → return 502 to client with useful message + + +## Implementation Plan + + + + +### File changed +`src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts` + +### What stays the same +- Auth check via `getServerSession` +- Upload record + status validation +- `transformFile()` XLSX → CSV +- S3 upload of transformed CSV to `bulk_onboarding_inputs/{portfolioId}/{uploadId}.csv` +- DB writes: `bulkAddressUploads.status = "processing"`, `tasks.status = "in progress"`, `subTasks.inputs` + +### What changes + +**Remove:** +```ts +import { sendToQueue } from "@/app/utils/sqs"; +// POSTCODE_SPLITTER_QUEUE_NAME env var check +``` + +**Add — call backend trigger-splitter:** +```ts +const fastapiUrl = process.env.FASTAPI_API_URL; +const fastapiKey = process.env.FASTAPI_API_KEY; +if (!fastapiUrl || !fastapiKey) { + return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 }); +} + +const sessionToken = + request.cookies.get("__Secure-next-auth.session-token")?.value ?? + request.cookies.get("next-auth.session-token")?.value; + +const triggerRes = await fetch(`${fastapiUrl}/v1/bulk-uploads/trigger-splitter`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": fastapiKey, + Authorization: `Bearer ${sessionToken}`, + }, + body: JSON.stringify({ + task_id: body.taskId, + sub_task_id: body.subTaskId, + s3_uri: s3Uri, + }), +}); + +if (!triggerRes.ok) { + const errText = await triggerRes.text().catch(() => ""); + console.error("Backend trigger-splitter failed:", triggerRes.status, errText); + return NextResponse.json({ error: "Failed to trigger address matching" }, { status: 502 }); +} +``` + +**Order of ops** (no change to structure, just swap): +1. Validate body + upload record +2. Read source file from S3 +3. Transform XLSX → CSV +4. Upload transformed CSV to S3 +5. ~~sendToQueue~~ → fetch backend trigger-splitter +6. DB updates (status, taskId, subTask inputs) — only after step 5 succeeds + +### Env vars required +- `FASTAPI_API_URL` — already set, already used in `plan/trigger/route.ts` +- `FASTAPI_API_KEY` — already set + +### Env vars removed +- `POSTCODE_SPLITTER_QUEUE_NAME` — no longer read by frontend (can remove from .env.local + staging/prod) + + + diff --git a/backlog/tasks/task-7 - Refactor-combine-route-to-call-backend-trigger-combiner.md b/backlog/tasks/task-7 - Refactor-combine-route-to-call-backend-trigger-combiner.md new file mode 100644 index 0000000..19ad491 --- /dev/null +++ b/backlog/tasks/task-7 - Refactor-combine-route-to-call-backend-trigger-combiner.md @@ -0,0 +1,32 @@ +--- +id: TASK-7 +title: Refactor combine route to call backend trigger-combiner endpoint +status: To Do +assignee: [] +created_date: '2026-04-20' +updated_date: '2026-04-20' +labels: + - frontend + - bulk-upload + - refactor +dependencies: + - BACKEND-TASK-2 +priority: high +ordinal: 2000 +--- + +## Description + + +`src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts` sends SQS directly to `BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME`. Replace with call to backend `POST /bulk-uploads/{task_id}/combine`. Drop queue name env var + SendMessage IAM dependency on frontend. + +If backend auto-chains combiner on splitter completion (backend-task-5), this route may simply proxy a manual "re-combine" action or be removed entirely. Decide during implementation. + + +## Acceptance Criteria + +- [ ] #1 `combine/route.ts` no longer imports `sendToQueue` or reads `BULK_ADDRESS2UPRN_COMBINER_QUEUE_NAME` +- [ ] #2 Backend trigger-combiner endpoint called with task_id +- [ ] #3 Frontend still updates subTasks row with inputs on success (or delegates to backend) +- [ ] #4 Decision logged: proxy vs delete-after-auto-chain + diff --git a/backlog/tasks/task-8 - Add-confirm-matches-page-for-bulk-upload-review.md b/backlog/tasks/task-8 - Add-confirm-matches-page-for-bulk-upload-review.md new file mode 100644 index 0000000..1334ca3 --- /dev/null +++ b/backlog/tasks/task-8 - Add-confirm-matches-page-for-bulk-upload-review.md @@ -0,0 +1,34 @@ +--- +id: TASK-8 +title: Add confirm-matches page for bulk upload address→UPRN review +status: To Do +assignee: [] +created_date: '2026-04-20' +updated_date: '2026-04-20' +labels: + - frontend + - bulk-upload + - ui +dependencies: + - TASK-9 + - BACKEND-TASK-3 +priority: high +ordinal: 3000 +--- + +## Description + + +New route `/portfolio/{slug}/bulk-upload/{uploadId}/confirm-matches`. Loads combined CSV from backend (via frontend proxy route, see task-9) and renders review table: original address input | matched UPRN | matched address | confidence. User can accept/reject per row, then POST confirmed rows to backend to persist into portfolio `addresses`. + +Status transitions: `complete` (combiner done) → show "Review matches" CTA on upload detail page → confirm-matches page → on submit, move upload to `confirmed` or similar. + + +## Acceptance Criteria + +- [ ] #1 Page renders match rows in a scrollable table +- [ ] #2 Row actions: accept (default on), reject +- [ ] #3 Submit posts accepted rows to backend confirm-matches route +- [ ] #4 After submit, redirect to portfolio addresses list +- [ ] #5 No useEffect/useMemo (per CLAUDE.md) — use Server Components + Route Handlers where possible + diff --git a/backlog/tasks/task-9 - Add-proxy-api-routes-for-combined-results-and-confirm-matches.md b/backlog/tasks/task-9 - Add-proxy-api-routes-for-combined-results-and-confirm-matches.md new file mode 100644 index 0000000..bcd2145 --- /dev/null +++ b/backlog/tasks/task-9 - Add-proxy-api-routes-for-combined-results-and-confirm-matches.md @@ -0,0 +1,35 @@ +--- +id: TASK-9 +title: Add proxy API routes for combined-results and confirm-matches +status: To Do +assignee: [] +created_date: '2026-04-20' +updated_date: '2026-04-20' +labels: + - frontend + - bulk-upload + - api +dependencies: + - BACKEND-TASK-3 + - BACKEND-TASK-4 +priority: high +ordinal: 4000 +--- + +## Description + + +Two Next.js route handlers that proxy to backend: +- `GET /api/portfolio/{portfolioId}/bulk-uploads/{uploadId}/combined-results` → backend GET `/bulk-uploads/{uploadId}/combined-results`. Returns parsed match rows for the confirm UI. +- `POST /api/portfolio/{portfolioId}/bulk-uploads/{uploadId}/confirm-matches` → backend POST `/bulk-uploads/{uploadId}/confirm-matches`. Body: accepted rows. + +Both must: check session, pass through portfolio scope, translate backend errors to sane frontend responses. + + +## Acceptance Criteria + +- [ ] #1 Both routes auth-gated via getServerSession +- [ ] #2 Params typed as Promise per Next.js 15 convention +- [ ] #3 Backend 4xx/5xx surfaced with appropriate HTTP code + message +- [ ] #4 Upload id is validated against bulk_address_uploads row before proxying + diff --git a/run_backlog_browser.sh b/run_backlog_browser.sh new file mode 100644 index 0000000..bd3fcc4 --- /dev/null +++ b/run_backlog_browser.sh @@ -0,0 +1 @@ +backlog browser diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combined-results/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combined-results/route.ts new file mode 100644 index 0000000..44f74fa --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combined-results/route.ts @@ -0,0 +1,67 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } +) { + const session = await getServerSession(AuthOptions); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { uploadId } = await params; + + const [upload] = await db + .select({ taskId: bulkAddressUploads.taskId }) + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.id, uploadId)) + .limit(1); + + if (!upload) return NextResponse.json({ error: "Not found" }, { status: 404 }); + if (!upload.taskId) return NextResponse.json({ error: "Task not started" }, { status: 409 }); + + const fastapiUrl = process.env.FASTAPI_API_URL; + const fastapiKey = process.env.FASTAPI_API_KEY; + if (!fastapiUrl || !fastapiKey) { + console.error("FASTAPI_API_URL or FASTAPI_API_KEY not set"); + return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 }); + } + + const sessionToken = + request.cookies.get("__Secure-next-auth.session-token")?.value ?? + request.cookies.get("next-auth.session-token")?.value; + + const { searchParams } = new URL(request.url); + const offset = searchParams.get("offset") ?? "0"; + const limit = searchParams.get("limit") ?? "500"; + + try { + const res = await fetch( + `${fastapiUrl}/v1/bulk-uploads/${upload.taskId}/combined-results?offset=${offset}&limit=${limit}`, + { + headers: { + "x-api-key": fastapiKey, + Authorization: `Bearer ${sessionToken}`, + }, + } + ); + + if (!res.ok) { + const errText = await res.text().catch(() => ""); + console.error("Backend combined-results failed:", res.status, errText); + return NextResponse.json( + { error: res.status === 409 ? "Combiner not finished" : "Failed to fetch results" }, + { status: res.status === 409 ? 409 : 502 } + ); + } + + const data = await res.json(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + console.error("Failed to reach backend combined-results:", err); + return NextResponse.json({ error: "Failed to fetch results" }, { status: 502 }); + } +} diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/confirm-matches/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/confirm-matches/route.ts new file mode 100644 index 0000000..53fb143 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/confirm-matches/route.ts @@ -0,0 +1,70 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } +) { + const session = await getServerSession(AuthOptions); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { uploadId } = await params; + + const [upload] = await db + .select({ taskId: bulkAddressUploads.taskId }) + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.id, uploadId)) + .limit(1); + + if (!upload) return NextResponse.json({ error: "Not found" }, { status: 404 }); + if (!upload.taskId) return NextResponse.json({ error: "Task not started" }, { status: 409 }); + + const fastapiUrl = process.env.FASTAPI_API_URL; + const fastapiKey = process.env.FASTAPI_API_KEY; + if (!fastapiUrl || !fastapiKey) { + console.error("FASTAPI_API_URL or FASTAPI_API_KEY not set"); + return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 }); + } + + const sessionToken = + request.cookies.get("__Secure-next-auth.session-token")?.value ?? + request.cookies.get("next-auth.session-token")?.value; + + let body; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + try { + const res = await fetch( + `${fastapiUrl}/v1/bulk-uploads/${upload.taskId}/confirm-matches`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": fastapiKey, + Authorization: `Bearer ${sessionToken}`, + }, + body: JSON.stringify(body), + } + ); + + if (!res.ok) { + const errText = await res.text().catch(() => ""); + console.error("Backend confirm-matches failed:", res.status, errText); + return NextResponse.json({ error: "Failed to confirm matches" }, { status: 502 }); + } + + const data = await res.json(); + return NextResponse.json(data, { status: 200 }); + } catch (err) { + console.error("Failed to reach backend confirm-matches:", err); + return NextResponse.json({ error: "Failed to confirm matches" }, { status: 502 }); + } +} diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts index 5a6d4b0..65c4edb 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/onboard/route.ts @@ -8,7 +8,6 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { z } from "zod"; import { createS3Client } from "@/app/utils/s3"; -import { sendToQueue } from "@/app/utils/sqs"; import S3 from "aws-sdk/clients/s3"; import * as XLSX from "xlsx"; @@ -131,20 +130,41 @@ export async function POST( } const s3Uri = `s3://${outputBucket}/${transformedKey}`; - const queueName = process.env.POSTCODE_SPLITTER_QUEUE_NAME; - if (!queueName) { - console.error("POSTCODE_SPLITTER_QUEUE_NAME not set"); + + const fastapiUrl = process.env.FASTAPI_API_URL; + const fastapiKey = process.env.FASTAPI_API_KEY; + if (!fastapiUrl || !fastapiKey) { + console.error("FASTAPI_API_URL or FASTAPI_API_KEY not set"); return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 }); } + const sessionToken = + request.cookies.get("__Secure-next-auth.session-token")?.value ?? + request.cookies.get("next-auth.session-token")?.value; + try { - await sendToQueue( - { task_id: body.taskId, sub_task_id: body.subTaskId, s3_uri: s3Uri }, - { queueName } - ); + const triggerRes = await fetch(`${fastapiUrl}/v1/bulk-uploads/trigger-splitter`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": fastapiKey, + Authorization: `Bearer ${sessionToken}`, + }, + body: JSON.stringify({ + task_id: body.taskId, + sub_task_id: body.subTaskId, + s3_uri: s3Uri, + }), + }); + + if (!triggerRes.ok) { + const errText = await triggerRes.text().catch(() => ""); + console.error("Backend trigger-splitter failed:", triggerRes.status, errText); + return NextResponse.json({ error: "Failed to trigger address matching" }, { status: 502 }); + } } catch (err) { - console.error("Failed to send SQS message:", err); - return NextResponse.json({ error: "Failed to queue onboarding job" }, { status: 500 }); + console.error("Failed to reach backend trigger-splitter:", err); + return NextResponse.json({ error: "Failed to trigger address matching" }, { status: 502 }); } await Promise.all([