fix(gst): 0% GST rate no longer falls back to 18%

parseFloat('0') is falsy in JS so `|| 0.18` silently discarded the user's
explicit 0% selection. Replaced with an explicit empty-string guard.
Adds e2e spec gst-rate.spec.ts covering all five GST rates (0/5/12/18/28%).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-27 00:00:51 +05:30
parent e61f052062
commit 1c5727850a
5 changed files with 169 additions and 40 deletions

View file

@ -9,20 +9,28 @@ metadata:
| Service | Port | Startup Command | Directory | | Service | Port | Startup Command | Directory |
|---|---|---|---| |---|---|---|---|
| pelagia-portal (Next.js) | 3000 | `node node_modules/next/dist/bin/next dev --turbopack` | `App/` | | pelagia-portal (Next.js) | 3000 | `node node_modules\next\dist\bin\next dev --turbopack` | `App/` |
| GstService (Express + Playwright) | 3003 | `npm run dev` (runs `tsx watch src/index.ts`) | `GstService/` | | GstService (Express + Playwright) | 3003 | `npm run dev` (runs `tsx watch src/index.ts`) | `GstService/` |
| PostgreSQL 18 | 5432 | Windows service / already running | — | | PostgreSQL 18 | 5432 | Windows service / already running | — |
## Directory Structure (verified 2026-05-26)
- Portal source: `App/` (contains package.json, next.config.ts, prisma/, app/, etc.)
- GstService source: `GstService/` (contains src/index.ts)
- `App/pelagia-portal/` — built artifact / deployed copy, NOT the source; has node_modules + .env.local but no package.json at root
- Project root (`C:\Users\shad0w\Documents\src\Peliagia_Portal`) has no package.json — the portal source is in `App/`
## Startup Order ## Startup Order
1. Verify PostgreSQL is up on port 5432 (`pg_isready -U postgres`) 1. Verify PostgreSQL is up on port 5432 (`pg_isready -U postgres`)
2. Create DB if missing: `createdb -U postgres pelagia_portal` (PGPASSWORD=postgres) 2. Install App/ deps if missing: `cd App && pnpm install --frozen-lockfile` (pnpm v10 — allowBuilds in pnpm-workspace.yaml handles build scripts cleanly)
3. Run `prisma migrate deploy` from `App/` (non-interactive, applies all pending migrations) 3. Ensure `App/.env` exists with `DATABASE_URL` (Prisma reads `.env`, not `.env.local`)
4. Run `prisma generate` from `App/` (generates Prisma client into node_modules) 4. Run `prisma migrate deploy` from `App/` using `.\node_modules\.bin\prisma.cmd migrate deploy`
5. Start GstService: `cd GstService && npm run dev` 5. Run `prisma generate` from `App/` using `.\node_modules\.bin\prisma.cmd generate`
6. Start pelagia-portal: `node node_modules/next/dist/bin/next dev --turbopack` from `App/` 6. Start GstService: `cd GstService && npm run dev > logfile 2>&1` (background)
- DO NOT use `pnpm dev` — pnpm pre-flight runs `pnpm install` which hits ERR_PNPM_IGNORED_BUILDS and aborts 7. Start pelagia-portal: `cd App && node node_modules\next\dist\bin\next dev --turbopack > logfile 2>&1` (background)
- Call next directly via node to bypass pnpm dependency check - DO NOT use `pnpm dev` — pnpm 10 still runs pre-install hooks that may interfere; calling next directly is safer and consistent
8. Health check both services
## Health Checks ## Health Checks
@ -32,35 +40,43 @@ metadata:
## Environment ## Environment
- App is in `K:\src\pelagia-portal\App\` (NOT `App/pelagia-portal/` — that path is stale git history) - Portal `.env.local` location: `App/.env.local` (copy from `App/pelagia-portal/.env.local` if App/ is a fresh checkout)
- `.env.local` in `App/` holds actual dev secrets (NEXTAUTH_SECRET, DATABASE_URL) - Portal `.env` location: `App/.env` — must contain `DATABASE_URL` for Prisma CLI (Prisma only reads `.env`, not `.env.local`)
- Required dev vars: `NEXTAUTH_SECRET`, `NEXTAUTH_URL`, `DATABASE_URL` - Required dev vars: `NEXTAUTH_SECRET`, `NEXTAUTH_URL`, `DATABASE_URL`
- R2 and Resend vars not needed in dev (files go to `.dev-uploads/`, emails log to console) - R2 and Resend vars not needed in dev (files go to `.dev-uploads/`, emails log to console)
- DATABASE_URL: `postgresql://postgres:postgres@localhost:5432/pelagia_portal` - DATABASE_URL: `postgresql://postgres:postgres@localhost:5432/pelagia_portal`
- NEXTAUTH_SECRET: in .env.local
- NEXTAUTH_URL: `http://localhost:3000` - NEXTAUTH_URL: `http://localhost:3000`
## Node / Package Manager Setup ## Node / Package Manager Setup
- Node.js: `C:\Program Files\nodejs` (NOT on system PATH by default — must prepend) - Node.js: `C:\Program Files\nodejs` (must prepend to PATH)
- pnpm: installed globally at `C:\Users\shad0w\AppData\Roaming\npm\node_modules\pnpm` - pnpm: v10.33.2, installed at `C:\Users\shad0w\AppData\Roaming\npm\node_modules\pnpm`
- $env:PATH must include `$env:APPDATA\npm` to find pnpm - `$env:PATH` must include `$env:APPDATA\npm` to find pnpm
- PostgreSQL 18 bin: `C:\Program Files\PostgreSQL\18\bin` - PostgreSQL 18 bin: `C:\Program Files\PostgreSQL\18\bin`
- Set PATH at start of each session: `$env:PATH = "C:\Program Files\nodejs;C:\Program Files\PostgreSQL\18\bin;$env:APPDATA\npm;$env:PATH"` - Set PATH at start of each session:
`$env:PATH = "C:\Program Files\nodejs;C:\Program Files\PostgreSQL\18\bin;$env:APPDATA\npm;$env:PATH"`
- PGPASSWORD=postgres for psql/createdb commands - PGPASSWORD=postgres for psql/createdb commands
## pnpm ERR_PNPM_IGNORED_BUILDS Issue ## Prisma Notes
- pnpm 11.1.2 blocks build scripts for: @prisma/client, @prisma/engines, esbuild, prisma, sharp, unrs-resolver - Use `.\node_modules\.bin\prisma.cmd` (not `node node_modules\.bin\prisma`) — the `.bin\prisma` shim is a bash script and fails on Windows/PowerShell
- The `pnpm.onlyBuiltDependencies` field in package.json does NOT fix this (field not recognized by pnpm 11) - Prisma reads `App/.env` for DATABASE_URL; `.env.local` is NOT read by Prisma CLI
- Workaround: use `node node_modules/next/dist/bin/next dev --turbopack` instead of `pnpm dev` - `prisma generate` EPERM on Windows = DLL locked by running Node process — normal
- Run `prisma generate` manually after install: `.\node_modules\.bin\prisma generate` - 14 migrations as of 2026-05-26 (latest: `20260521000000_remove_vessel_imo_number`)
- The pnpm install itself completes (all 885 packages are installed) — only postinstall scripts are blocked
## Log Files (when started as background processes)
- GstService: `C:\Users\shad0w\AppData\Local\Temp\gstservice.log`
- pelagia-portal: `C:\Users\shad0w\AppData\Local\Temp\portal.log`
## pnpm workspace
- `App/pnpm-workspace.yaml` uses `allowBuilds:` field (pnpm v10 syntax) to allow build scripts for @prisma/client, @prisma/engines, esbuild, prisma, sharp, unrs-resolver
- This resolves the ERR_PNPM_IGNORED_BUILDS issue from pnpm v11 era
## Notes ## Notes
- `prisma generate` EPERM on Windows = the DLL is locked by a running Node process (Next.js) — this is normal - pelagia-portal 307 response = auth redirect to /login — healthy, not an error
- pelagia-portal 307 response = auth redirect to /login — this is healthy, not an error - GstService `browserConnected: false` at startup is normal — Playwright browser connects on first session
- Prisma migration scripts: `pnpm db:migrate` (dev), `pnpm db:migrate:deploy` (CI/prod) - Prisma migration scripts: `pnpm db:migrate` (dev), `pnpm db:migrate:deploy` (CI/prod) — run from App/
- `node_modules` was missing on first setup (fresh checkout) — both App and GstService need install - `App/pelagia-portal/` is a deployed artifact directory (has .next/, node_modules, .env.local) — do not confuse with source
- 13 Prisma migrations as of 2026-05-18

View file

@ -31,20 +31,27 @@ You create, execute, and persist Playwright browser tests that validate the Pela
- Parameterize tests for data-driven scenarios where the user story covers multiple input variations. - Parameterize tests for data-driven scenarios where the user story covers multiple input variations.
### 3. Test Execution ### 3. Test Execution
- Run tests using the Playwright CLI or the project's configured test runner.
- Execute tests in headed mode first if debugging is needed, then confirm they pass in headless mode. **ALWAYS use a saved script — never run ad-hoc Playwright code.** For every verification task:
- If a test fails:
1. **Check for an existing script first.** Before writing anything, glob `App/tests/e2e/**/*.spec.ts` for a test that already covers the area (e.g., `gst-rate.spec.ts` for GST, `auth.spec.ts` for login). If one exists, run it directly with `pnpm test:e2e -- <path>` rather than writing a new one.
2. **If no script exists, write and save one** (see Saving Tests below) before running it. Do not execute test logic that lives only in memory or a temp file.
3. **Run using the project test runner:** `pnpm test:e2e -- App/tests/e2e/<file>.spec.ts` from the `App/` directory.
4. Execute tests in headless mode by default; use headed mode only when debugging a selector failure.
5. If a test fails:
- Analyze the failure output and screenshots/traces carefully. - Analyze the failure output and screenshots/traces carefully.
- Distinguish between a bug in the implementation vs. a mistake in the test. - Distinguish between a bug in the implementation vs. a mistake in the test.
- If it is a test authoring issue, fix and re-run. - If it is a test authoring issue, fix the saved script and re-run.
- If it appears to be a real application bug, document it clearly and report it before saving the test. - If it appears to be a real application bug, document it clearly and report it before saving the test.
- Do not save a test that does not pass. - Do not mark a verification as passed unless the saved script exits 0.
- Confirm all assertions are meaningful — avoid tests that pass vacuously. 6. Confirm all assertions are meaningful — avoid tests that pass vacuously.
### 4. Saving Tests ### 4. Saving Tests
- Once a test passes reliably, save it to the `Tests/` directory following the structure and naming conventions defined in `PLAYRIGHT_TEST_DESGN.md`. - Save every test to `App/tests/e2e/` (or a subdirectory) **immediately after writing it** — before the first run. This ensures the canonical script exists on disk from the start.
- Include a file-level comment block documenting: the user story ID(s) covered, a brief description, the date created, and any known limitations. - Follow the naming conventions and structure in `PLAYRIGHT_TEST_DESGN.md` and mirror the style of existing specs (file-level JSDoc comment, `test.describe` block where applicable, `beforeEach` login helper).
- Include a file-level comment block documenting: the user story ID(s) or bug ID covered, a brief description, the date created, and any known limitations.
- Ensure the saved file is self-contained and runnable without modification. - Ensure the saved file is self-contained and runnable without modification.
- When verifying a bug fix, reuse the same script for both the "repro" run (before fix) and the "green" run (after fix) — just run it twice. Do not write separate scripts for repro vs. verification.
## Test Quality Standards ## Test Quality Standards

View file

@ -18,7 +18,21 @@
"Bash(sed 's/ D //')", "Bash(sed 's/ D //')",
"Bash(sed 's/?? //')", "Bash(sed 's/?? //')",
"WebFetch(domain:web.archive.org)", "WebFetch(domain:web.archive.org)",
"WebFetch(domain:www.pexels.com)" "WebFetch(domain:www.pexels.com)",
"Bash(npm run *)",
"Bash(npm install *)",
"Bash(node *)",
"Bash(taskkill *)",
"Bash(pg_isready *)",
"PowerShell(netstat *)",
"PowerShell(Get-Process *)",
"PowerShell(Stop-Process *)",
"PowerShell(Get-ChildItem *)",
"PowerShell(Test-NetConnection *)",
"PowerShell(Invoke-WebRequest *)",
"PowerShell(npm *)",
"PowerShell(pnpm *)",
"PowerShell(node *)"
] ]
} }
} }

View file

@ -83,7 +83,7 @@ function toLineItem(row: EditRow): LineItemInput {
unit: row.unit, unit: row.unit,
size: row.size || undefined, size: row.size || undefined,
unitPrice: parseFloat(row.unitPrice) || 0, unitPrice: parseFloat(row.unitPrice) || 0,
gstRate: parseFloat(row.gstRate) || 0.18, gstRate: row.gstRate !== "" && row.gstRate != null ? parseFloat(row.gstRate) : 0.18,
productId: row.productId || undefined, productId: row.productId || undefined,
accountId: row.accountId || undefined, accountId: row.accountId || undefined,
}; };

View file

@ -0,0 +1,92 @@
/**
* E2E GST rate selection correctness on the PO line-items editor.
*
* Bug covered: GST-0PCT-FALLBACK (fixed 2026-05-26)
* `parseFloat('0') || 0.18` is falsy, so selecting 0% GST silently fell back
* to 18%. Fixed by changing the expression to an explicit null/empty-string
* guard: `row.gstRate !== "" && row.gstRate != null ? parseFloat(row.gstRate) : 0.18`
*
* User story: S-01 (create PO, line items with correct GST calculation)
* Acceptance criterion: selecting any GST rate (0 / 5 / 12 / 18 / 28 %) on a
* line item must produce the mathematically correct Grand Total in the footer.
*
* Preconditions:
* - Dev server running at http://localhost:3000
* - Seed data present (at least one vessel and account selectable)
* - tech@pelagia.local / tech1234 credentials valid
*/
import { test, expect, type Page } from "@playwright/test";
import { login, fillPoHeader, USERS } from "./helpers/login";
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Selects a GST rate in the first line-item row's GST dropdown. */
async function selectGstRate(page: Page, value: string): Promise<void> {
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
const firstRow = lineItemsSection.locator("tbody tr").first();
// The GST select is the second <select> in the row (first is the UOM select)
await firstRow.locator("select").nth(1).selectOption(value);
}
/**
* Reads the Grand Total cell text from the line-items tfoot.
* The tfoot last row has a label cell containing "Grand Total" and a value cell.
*/
async function getGrandTotal(page: Page): Promise<string> {
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
// The last row of the tfoot contains "Grand Total"
const grandTotalRow = lineItemsSection.locator("tfoot tr").last();
// The value is the last <td> in that row
return (await grandTotalRow.locator("td").last().textContent()) ?? "";
}
// ── Test data ─────────────────────────────────────────────────────────────────
const GST_CASES = [
{ label: "0%", value: "0", expected: "₹1,000.00" },
{ label: "5%", value: "0.05", expected: "₹1,050.00" },
{ label: "12%", value: "0.12", expected: "₹1,120.00" },
{ label: "18%", value: "0.18", expected: "₹1,180.00" },
{ label: "28%", value: "0.28", expected: "₹1,280.00" },
] as const;
// ── Tests ─────────────────────────────────────────────────────────────────────
test.describe("GST rate selection — Grand Total correctness (GST-0PCT-FALLBACK)", () => {
test.beforeEach(async ({ page }) => {
await login(page, USERS.TECH);
await page.goto("/po/new");
// Fill required header fields so the form is valid
await fillPoHeader(page, `E2E_GST_${Date.now()}`);
// Fill the first line item: name=Bearing Assembly, qty=1, unitPrice=1000
const lineItemsSection = page.locator("section").filter({
has: page.getByRole("heading", { name: /line items/i }),
});
const firstRow = lineItemsSection.locator("tbody tr").first();
await firstRow.getByPlaceholder("Item name *").fill("Bearing Assembly");
// qty input — the first number input in the row
await firstRow.locator('input[type="number"]').first().fill("1");
// unit price input — placeholder "0.00"
await firstRow.locator('input[placeholder="0.00"]').fill("1000");
});
for (const { label, value, expected } of GST_CASES) {
test(`GST-0PCT-FALLBACK: selecting ${label} GST shows Grand Total ${expected}`, async ({ page }) => {
await selectGstRate(page, value);
// The Grand Total footer cell must update reactively — assert with auto-retry
await expect(async () => {
const total = await getGrandTotal(page);
expect(total.trim()).toBe(expected);
}).toPass({ timeout: 5_000 });
});
}
});