308 lines
12 KiB
Markdown
308 lines
12 KiB
Markdown
|
|
# 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<string, unknown>) {
|
||
|
|
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 <DataTable columns={columns} data={data} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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=<generated-by-setup-script>
|
||
|
|
|
||
|
|
# PostgreSQL (per-environment, set in docker-compose.yml)
|
||
|
|
DATABASE_URL=postgresql://postgres:<password>@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=<from-current-env>
|
||
|
|
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=<from-current-env>
|
||
|
|
MAIL_FROM=quest@vorteqcoil.com
|
||
|
|
|
||
|
|
# reCAPTCHA
|
||
|
|
RECAPTCHA_SITE_KEY=<from-current-env>
|
||
|
|
RECAPTCHA_SECRET_KEY=<from-current-env>
|
||
|
|
|
||
|
|
# Cloudflare (Traefik SSL - set in .env at stack level)
|
||
|
|
CLOUDFLARE_DNS_API_TOKEN=<from-cloudflare>
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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)
|