# Vorteq Quest Portal - Claude Code Instructions ## Project Overview Rebuilding the Vorteq Quest customer portal from a legacy Laravel 9 / Windows Server / Savvior stack to a modern Next.js / TypeScript / PostgreSQL stack running in Docker on Linux. The full PRD is in `docs/Vorteq_Quest_PRD_v1.0.docx`. ## Tech Stack - **Runtime:** Node.js 22 LTS - **Framework:** Next.js 15 with App Router - **Language:** TypeScript (strict mode) - **ORM (App DB):** Prisma with PostgreSQL 16 - **ORM (Epicor):** `mssql` package (read-only connection to SQL Server) - **Auth:** Better Auth with credentials provider - **UI:** shadcn/ui + Tailwind CSS - **Data Tables:** TanStack Table v8 - **State:** React Server Components by default, client components only when needed - **Cache/Queue:** Redis 7 via BullMQ - **Email:** Microsoft Graph API (Office 365) - **PDF:** Puppeteer or @react-pdf/renderer - **Testing:** Vitest (unit/integration), Playwright (E2E) - **Containers:** Docker + Docker Compose - **Reverse Proxy:** Traefik v3 with Cloudflare DNS challenge for SSL - **CI/CD:** Forgejo Actions with self-hosted runner - **Registry:** Forgejo built-in OCI container registry ## Infrastructure ### Server - **Host:** expvtcasp01 (Linux/Ubuntu) - **Netbird IP:** 100.89.62.20 - **Stack directory:** `/opt/stacks/vorteq` - **Runner directory:** `/opt/stacks/forgejo-runner` ### Repository - **URL:** `forgejo.wulfconsulting.cloud/lorentz/quest-vorteq` - **SSH:** `ssh://git@forgejo.wulfconsulting.cloud:222/lorentz/quest-vorteq.git` (port 222) - **Registry:** `forgejo.wulfconsulting.cloud/lorentz/quest-vorteq` ### Domains - **Production:** `quest.vorteq.wulf.cloud` - **Development:** `dev.quest.vorteq.wulf.cloud` - **Testing:** `testing.vorteq.wulf.cloud` - **Traefik Dashboard:** `traefik.vorteq.wulf.cloud` ### Running Services - **Traefik:** Reverse proxy, auto-SSL via Cloudflare DNS challenge - **PostgreSQL 16:** Three databases — `vorteq_dev`, `vorteq_test`, `vorteq_prod` - **Redis 7:** Three DB numbers — 0 (dev), 1 (test), 2 (prod) - **Forgejo Runner:** Docker-in-Docker, registered as `expvtcasp01` ### CI/CD Pipeline Push to `main` → build Docker image → tag as `:latest` → push to Forgejo registry → deploy to prod Push to `development` → build Docker image → tag as `:dev` → push to Forgejo registry → deploy to dev ## Project Structure ``` quest-vorteq/ ├── CLAUDE.md # This file ├── TASKS.md # Task tracking ├── docker-compose.yml # Infrastructure: traefik, postgres, redis, app containers ├── Dockerfile # Multi-stage Next.js build ├── .forgejo/ │ └── workflows/ │ └── deploy.yml # CI/CD pipeline ├── traefik/ │ ├── traefik.yml # Traefik configuration │ ├── acme.json # SSL certificates (gitignored) │ └── config/ # Dynamic Traefik config ├── prisma/ │ ├── schema.prisma # PostgreSQL schema │ ├── migrations/ │ └── seed.ts # Seed data / migration scripts ├── src/ │ ├── app/ # Next.js App Router │ │ ├── (auth)/ # Auth route group (login, forgot-password, account-request) │ │ ├── (portal)/ # Authenticated route group │ │ │ ├── dashboard/ │ │ │ ├── inventory/ │ │ │ ├── orders/ │ │ │ ├── shipments/ │ │ │ ├── coil-activity/ │ │ │ ├── invoices/ │ │ │ ├── shipment-requests/ │ │ │ ├── allocation-requests/ │ │ │ ├── jobs/ │ │ │ └── admin/ # Admin-only pages │ │ ├── api/ # API routes │ │ └── layout.tsx │ ├── components/ │ │ ├── ui/ # shadcn/ui components │ │ ├── data-table/ # Reusable TanStack Table wrapper │ │ ├── layout/ # Navigation, header, sidebar │ │ └── forms/ # Shared form components │ ├── lib/ │ │ ├── db.ts # Prisma client singleton │ │ ├── epicor.ts # MSSQL connection + query helpers │ │ ├── auth.ts # Better Auth configuration │ │ ├── email.ts # Microsoft Graph email service │ │ ├── pdf.ts # PDF generation service │ │ ├── permissions.ts # RBAC helpers │ │ └── utils.ts # General utilities │ ├── services/ │ │ ├── inventory.ts # Inventory data access (calls Epicor SPs) │ │ ├── orders.ts # Order data access │ │ ├── shipments.ts # Shipment data access │ │ ├── coil-activity.ts # Coil activity data access │ │ ├── ship-requests.ts # Shipment request business logic │ │ ├── alloc-requests.ts # Allocation request business logic │ │ └── invoices.ts # Invoice processing │ ├── jobs/ │ │ ├── worker.ts # BullMQ worker setup │ │ ├── shipment-nag.ts │ │ ├── order-ack-email.ts │ │ ├── invoice-cleanup.ts │ │ ├── shipping-reports.ts │ │ └── wave-edi.ts │ └── types/ │ ├── epicor.ts # Epicor query result types │ ├── portal.ts # Portal-specific types │ └── index.ts ├── tests/ │ ├── unit/ │ ├── integration/ │ └── e2e/ ├── scripts/ │ └── migrate-data.ts # SQL Server → PostgreSQL migration └── docs/ └── Vorteq_Quest_PRD_v1.0.docx ``` ## Coding Conventions ### General - TypeScript strict mode, no `any` types - Use `type` over `interface` unless extending - Prefer `const` over `let`, never `var` - Use named exports, not default exports (except page.tsx and layout.tsx) - Error handling: use typed error classes, not string throws - All API responses: `{ data: T }` on success, `{ error: string, code: string }` on failure ### Naming - Files: kebab-case (`shipment-requests.ts`) - Components: PascalCase (`ShipmentRequestForm.tsx`) - Functions/variables: camelCase - Database columns (Prisma): snake_case - API routes: kebab-case (`/api/shipment-requests`) - Environment variables: SCREAMING_SNAKE_CASE ### Database - Prisma schema uses snake_case for all fields - Map legacy PascalCase column names with `@map("LegacyName")` where needed for migration clarity - Always use parameterized queries for Epicor MSSQL — never string interpolation - Epicor queries go in `src/services/` with typed return values - PostgreSQL queries go through Prisma client exclusively ### Epicor Connection Pattern ```typescript // src/lib/epicor.ts import sql from 'mssql'; const epicorPool = new sql.ConnectionPool({ server: process.env.MSSQL_HOST!, database: process.env.MSSQL_DATABASE!, user: process.env.MSSQL_USER!, password: process.env.MSSQL_PASSWORD!, options: { encrypt: false, trustServerCertificate: true }, }); // Always use parameterized queries export async function execStoredProc(name: string, params: Record) { const pool = await epicorPool.connect(); const request = pool.request(); for (const [key, value] of Object.entries(params)) { request.input(key, value); } return request.execute(name); } ``` ### Inventory Query Pattern The Epicor stored procedures require three standard parameters: - `CustID`: The selected company's EpicorCustID - `DBNAME`: The VorteqPortal database name wrapped in brackets, e.g., `[VorteqPortal]` - `sub`: 0 or 1, indicating if the current user is a sub-user (IsSubUser flag) V6 stored procedures (WIP, Finished Goods, Processed Other) use the `sub` parameter. Older procedures (Unprocessed, R&R variants) block sub-users entirely — return empty array if `isSubUser`. ### Component Pattern ```typescript // Server component by default export async function InventorySummary({ category }: { category: string }) { const data = await getInventorySummary(category); return ; } // Client component only when interactivity needed 'use client'; export function ShipmentRequestCart() { ... } ``` ### Auth Pattern (Better Auth) ```typescript // src/lib/auth.ts import { betterAuth } from 'better-auth'; export const auth = betterAuth({ secret: process.env.BETTER_AUTH_SECRET, baseURL: process.env.BETTER_AUTH_URL, database: { type: 'postgres', url: process.env.DATABASE_URL, }, // Credentials provider: validate against auth_user table (bcrypt) // Session includes: userId, questUserId, activeCompanyId, isSubUser, permissionRules[] }); ``` ### Middleware Pattern ```typescript // src/middleware.ts - protect all (portal) routes // src/lib/permissions.ts - check specific permission rules export function requirePermission(rule: string) { // Check user's permission groups contain the rule } ``` ## Important Business Logic ### Company Context After login, the user's active company is stored in the session. All Epicor queries are scoped to that company's `EpicorCustID`. Admin users can switch companies. ### Sub-User Restrictions Users with `IsSubUser=true` on their `quest_User` record: - See filtered inventory data (sub=1 parameter to V6 stored procedures) - See filtered ship-to addresses (only those matching '%NB HANDY%') - Cannot access Unprocessed inventory or R&R data at all ### HDC/HDM Customer Exception Customer 'HDC' uses a special order view (`portal_OrdersHDC`) and maps to CustID 'HDM' in queries. This needs to be handled in the order service. ### VGL Customer Exception Customer 'VGL' uses a special SQL query for coil receipts instead of the standard view. ## Environment Variables ```env # Application NODE_ENV=development|test|production BETTER_AUTH_URL=https://dev.quest.vorteq.wulf.cloud BETTER_AUTH_SECRET= # PostgreSQL (per-environment, set in docker-compose.yml) DATABASE_URL=postgresql://postgres:@postgres:5432/vorteq_dev # Redis (per-environment, set in docker-compose.yml) REDIS_URL=redis://redis:6379/0 # Epicor SQL Server (read-only) MSSQL_HOST=wi-e10test MSSQL_DATABASE=Epicor10Live MSSQL_USER=portal MSSQL_PASSWORD= MSSQL_PORT=1433 # Portal database name (for cross-DB stored procedure parameter) PORTAL_DB_NAME=VorteqPortal # Microsoft Graph (Email) MS_GRAPH_CLIENT_ID=629eca61-d554-4763-b14a-565aee7b7ab8 MS_GRAPH_TENANT_ID=291e541e-003c-463c-8834-863b9e7c1ebe MS_GRAPH_CLIENT_SECRET= MAIL_FROM=quest@vorteqcoil.com # reCAPTCHA RECAPTCHA_SITE_KEY= RECAPTCHA_SECRET_KEY= # Cloudflare (Traefik SSL - set in .env at stack level) CLOUDFLARE_DNS_API_TOKEN= ``` ## Testing Requirements - Unit tests for all service functions (inventory, orders, shipments data transformations) - Integration tests for API routes with mocked DB connections - E2E tests for: login flow, inventory browse + drill-down, shipment request workflow, invoice upload - Data validation: compare Epicor query results with known-good outputs from the legacy system ## Git Workflow - `main` branch: production-ready code → deploys to `quest.vorteq.wulf.cloud` - `development` branch: integration branch → deploys to `dev.quest.vorteq.wulf.cloud` - Feature branches: `feature/F-001-project-scaffold` - Commit messages: `feat(F-001): scaffold Next.js project with TypeScript + Tailwind` - Run `npm run lint && npm run typecheck && npm test` before committing - Push triggers Forgejo Actions → Docker build → registry push → deploy ## Task Tracking All tasks are tracked in `TASKS.md`. Mark tasks as: - `[ ]` Not started - `[~]` In progress - `[x]` Complete - `[!]` Blocked (note blocker in comments)