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:
parent
e61f052062
commit
1c5727850a
5 changed files with 169 additions and 40 deletions
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
92
App/tests/e2e/gst-rate.spec.ts
Normal file
92
App/tests/e2e/gst-rate.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue