The new 'shows QR code for desktop scanning' E2E test used plate JKL012,
which is the same plate seeded as a processing order in the dev migrations
(V7__seed_processing_order.sql) and used by the admin dashboard/fulfillment
tests as PROCESSING_PLATE.
Because the E2E chromium (parallel) tests run before the chromium-serial
tests, the QR test created a second order with plate JKL012. When the serial
admin tests then searched for rows matching JKL012, Playwright's strict
mode found 2 matching rows and threw a strict mode violation.
This caused 4 test failures + 2 skipped tests:
- admin-dashboard: click row shows tracking section
- admin-dashboard: click row again collapses it
- admin-dashboard: expanded row shows tracking input and save button
- admin-fulfillment: can register shipment for processing order
- admin-fulfillment: can mark sent order as delivered (skipped)
- admin-fulfillment: can mark delivered order as failed then back to sent (skipped)
Changed the plate to QRA222 — not used in any seed data or other E2E test.
Replace manual "type the number and order ID" flow with:
- Client-side QR code (qrcode npm package) for desktop users
- Pre-filled Swish payment URL (app.swish.nu) for mobile users
- Manual number fallback + "Jag har betalat" confirmation
The Swish C2B URL scheme pre-fills amount and message (order ID)
without requiring any Swish Commerce API certificate or bank agreement.
Supports both personal phone numbers (070...) and Swish Företag
business numbers (123...) via number normalization in buildSwishPaymentUrl().
Set SWISH_NUMBER in .env to a Företags number once set up.
The c7eeaf6 refactor split the monolithic AdminPage into AdminStatsBar,
AdminOrdersTable, and AdminOrderDetailPanel. All CSS rules for the admin
sub-components were left in AdminPage.vue's `<style scoped>` block. Vue 3
scoped styles only apply to elements in the parent's own template — they
do not penetrate multi-root child components. Every element rendered by
the extracted sub-components lost its styling (stats grid, table layout,
column widths, status badges, expand icons, row highlighting, expanded
detail panels, etc.), making the admin page appear visually broken.
Changes:
- frontend/src/pages/AdminPage.vue: remove `scoped` attribute from the
`<style>` block. All selectors are BEM-namespaced under `.admin__*`
so making them global is safe and is the minimal fix.
Visual verification: N/A (sandbox cannot reach production). See git diff
for the one-character change.
Previously an expired token left the frontend in a stuck state: the
router guard only checked token presence (never the exp claim), so the
user could still navigate to protected pages, and every API call then
failed with a generic Swedish "Kunde inte hämta…" message while the
header kept showing the logged-in UI. There was no global response
interceptor, and the backend returned an ambiguous 403 (no body) for
unauthenticated requests because no AuthenticationEntryPoint was
configured, making 403 mean both "no/invalid token" and "forbidden".
Backend:
- Add an AuthenticationEntryPoint in SecurityConfig that returns 401
with a Swedish {"message": ...} ErrorResponse body for
unauthenticated/expired-token requests, and an AccessDeniedHandler
returning 403 with the same body shape for genuine authorization
failures. This makes 401 = not authenticated/expired and
403 = authenticated but forbidden, the standard REST convention.
- Make JwtService(String, long) constructor public so integration
tests can mint expired tokens (was package-private).
- Update the 6 no-auth controller tests from 403 to 401
(OrderControllerTest, AdminControllerTest, PaymentControllerTest,
AuthControllerTest change-password/change-email) and assert the
message body exists; keep shouldReturn403ForNonAdminUser as 403.
- Add OrderControllerTest.shouldReturn401WithSwedishMessageWhenTokenExpired
(expired JWT via TTL -1000ms) and shouldReturn401WithMessageWhenNoAuthHeader.
Frontend:
- Add isTokenExpired() to utils/jwt.ts using the previously-unused exp
claim, and expose it on the auth store.
- Add a global 401 interceptor in api/client.ts: on a 401 from any
non-/auth/ endpoint, call auth.logout() and redirect to
/logga-in?redirect=<currentPath>. Skip /auth/ so wrong-password 401s
on login/change-password stay handled locally. Add isSessionExpired
and isForbidden helpers for per-page catch blocks.
- Harden the router guard to reject tokens whose exp is in the past
(logout + redirect to login with ?redirect=), and let expired-token
users open /logga-in and /registrera instead of bouncing to home.
- Refactor the generic-error catch blocks on OrdersPage, EditOrderPage,
ComposePage, PaymentRedirect, useAdminOrders, and useAdminOrderActions
to skip the generic Swedish message on 401 (handled globally) while
preserving wrong-password 401 handling on change-pw/email pages.
Tests:
- New frontend/src/__tests__/client.spec.ts covering 401 -> logout +
redirect, 401 from /auth/ -> no logout, 403 -> no logout, no-token
401 -> no redirect, and isSessionExpired/isForbidden helpers.
- Add authStore.spec.ts cases for isTokenExpired (no token, past exp,
future exp, missing exp, after logout).
- Add Router.spec.ts cases for expired-token redirects, token clearing,
future-exp access, and guest pages not bouncing expired users.
- Add OrdersPage.spec.ts case asserting 401 triggers no generic error
and the global logout/redirect.
- New E2E expired-token.spec.ts (Docker) covering both the router-guard
expired-token redirect and the API-401 redirect, with logged-out
header and cleared localStorage assertions.
- Mock the API in two pre-existing fake-JWT E2E tests
(auth-guards admin access, header-auth logout redirect) that broke
because the backend now correctly 401s their unsigned test-sig tokens.
Verified with ./gradlew check (frontend lint + 267 unit tests, backend
tests + coverage, Flyway, 92 E2E tests in Docker) and ./gradlew coverage;
all coverage thresholds maintained (jwt.ts at 100%).
Enable pageview tracking when VITE_UMAMI_WEBSITE_ID is set at frontend
build time (Forgejo secret + deploy workflow), with SPA route updates
and no script in local dev. Document setup in docs/umami-analytics.md,
extend integritetspolicy, and add admin Webbstatistik link in prod builds.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract AdminOrderWorkflowService and status rules API; split AdminPage
into composables and components; share order status constants; update tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
Run admin-dashboard with other DB/Mailpit specs after parallel tests.
Stop admin-dashboard from mutating the sent seed order before fulfillment.
Wait longer for backend readiness in the E2E stack.
Co-authored-by: Cursor <cursoragent@cursor.com>
account-settings and password-reset called clearMailpit in parallel with
other tests, wiping emails before waitForEmailChangeToken could read them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align AdminDashboard unit test with API error messages shown in UI.
Stabilize account-settings E2E by relying on waitForEmailChangeToken only.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the brittle filter-empty assertion from admin search helpers.
Run deferred-payment-admin in an isolated Playwright project with one
worker so serial tests keep shared order state. Stabilize unpaid-order
lookup by searching order id first, then plate text from the admin row.
Co-authored-by: Cursor <cursoragent@cursor.com>
Merge admin lookup checks into one serial test, create the plate when
the order is created, and search using the plate shown in the admin row.
Co-authored-by: Cursor <cursoragent@cursor.com>
CI intermittently failed when searching admin orders by registration
number because the table was queried before data and filters settled.
Wait for the admin list to load, clear the search field between queries,
and use a longer timeout when expecting the matching row.
Co-authored-by: Cursor <cursoragent@cursor.com>
Mobile traffic was breaking on narrow viewports because the header nav
overflowed and several pages used desktop-only spacing. This adds a
shared phone breakpoint, a hamburger menu, and scroll-to-top on route
changes so footer and menu navigation always land at the top of the page.
- Add --page-gutter and max-width 639px rules in base.css
- AppHeader: hamburger panel on small screens; flat account links on mobile
- AppFooter: stack footer links vertically on phones
- Home, compose, edit order, orders, auth, and legal pages: tighter gutters
and responsive layout (orders card actions stack; home grids single-column)
- Router scrollBehavior: scroll to top on navigation; restore on browser back
- Tests: AppHeader menu toggle, Router scrollBehavior, mobile Playwright checks
Admin page is intentionally unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the header "Byt lösenord" link with an Inställningar menu for
changing email or password. Email changes are two-step: request with
password, confirmation link to the new address, then password again on
confirm so a wrong inbox cannot take over the account.
- Backend: EmailChangeService, V10 email_change_tokens, confirm API
- Frontend: ChangeEmailPage, ConfirmEmailChangePage, header dropdown
- E2E: account-settings round-trips, Mailpit verification, wrong-password guard
- Flyway: V9 restore for dev DBs, CI migration checks, V10 for email tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
- Create TermsOfServicePage covering Swish payment, letter content rules, and liability
- Describe edit/cancel before payment and refund expectations after posting
- Link to integritetspolicy and support contact in footer CTA
- Add TermsOfServicePage unit tests
Co-authored-by: Cursor <cursoragent@cursor.com>
- Create PrivacyPolicyPage with sections on data, rights, and letter recipients
- Use plain Swedish without technical jargon or operational detail
- Link to kontakt page and mailto for privacy questions
- Add PrivacyPolicyPage unit tests
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add support@bilhej.se for orders and technical issues
- Move complaints to klagomal@bilhej.se instead of personal Gmail
- Show one mailto chip per card instead of duplicate link and button
- Update ContactPage tests and production email checklist for all @bilhej.se addresses
Co-authored-by: Cursor <cursoragent@cursor.com>
- Move the three explanatory paragraphs into the hero card under the lead
- Remove the separate "Vad vi gör" section that repeated the same framing
- Add a light divider between lead and body text for readability
Co-authored-by: Cursor <cursoragent@cursor.com>
- Truncate previews over 120 characters with a Visa mer toggle
- Allow per-order expand state on pending and completed cards
- Add styles for expanded preview and toggle button
- Cover long and short message behavior in OrdersPage tests
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace obsolete hej@bilhalsning.se in ComposePage GDPR footer
- Apply the same correction on EditOrderPage for edited orders
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace single placeholder mailto with two contact cards
- Show kontakt@bilhej.se for general questions and service issues
- Show jcamorling@gmail.com for complaints with clearer labeling
- Add tips section pointing users to Mina beställningar first
- Extend ContactPage tests for both email addresses
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace placeholder about card with hero, prose, steps, and CTA
- Add primary route /om-oss with redirect from legacy /om
- Update footer tagline and Om oss link to match new URL
- Extend AboutPage and AppFooter tests for new content and routing
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add structured use-case cards mapped to real letter templates
- Replace generic hero with bullets on traceability, anonymity, and timing
- Add three-step how-it-works flow with manual posting disclaimer
- Reframe trust section around convenience rather than hidden addresses
- Refresh layout with gradient heroes, icons, and consistent section styling
Co-authored-by: Cursor <cursoragent@cursor.com>
The cancel API returned 500 because ck_orders_status did not include cancelled.
Adds Flyway V9 and an E2E test for cancelling a pending order from /orders.
Co-authored-by: Cursor <cursoragent@cursor.com>
Redesigns the order list so unpaid and paid orders share a consistent
card layout, with clearer payment context and labeled metadata users
need before paying via Swish.
- Split list into Obetalda/Tidigare sections with pending orders first
- Pending cards: preview box, labeled Beställnings-ID, price row, Betala 49 kr
- Completed cards: same header/preview layout, prominent Spåra brev button
- Replace em-dash pay label and update unit/E2E selectors
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds backend endpoints and frontend edit page so pending orders can be updated or soft-cancelled without admin intervention.
Co-authored-by: Cursor <cursoragent@cursor.com>
Operators can fix prod admin passwords without email via Byt lösenord;
end users can use forgot-password when SMTP is configured. Local and CI
use Mailpit to capture outbound mail and verify reset links end-to-end.
- Backend: V8 password_reset_tokens, PasswordResetService, EmailService,
POST /api/auth/forgot-password, reset-password, change-password
- Optional testToken in forgot-password response (docker profile only, for E2E)
- Frontend: ForgotPasswordPage, ResetPasswordPage, ChangePasswordPage,
routes, login link, header Byt lösenord
- Mailpit (ghcr.io/axllent/mailpit:v1.28) in docker-compose + e2e stack
- E2E: password-reset.spec.ts + Mailpit API helper tests SMTP delivery
- Separate dev/e2e Docker image names to avoid overwriting bilhej-frontend
- Docs: README email section, production-email-checklist, .env.example
- Unit/integration tests for reset, change password, and Vitest page specs
Co-authored-by: Cursor <cursoragent@cursor.com>
Local verification should match Forgejo CI. Wire frontendE2E into check
and point it at docker-compose.e2e.yml with the same test env vars.
- Add frontendE2E to check after frontend unit tests
- Run docker-compose.e2e.yml directly from Gradle with CI secrets
- Update npm test:e2e:ci to use the e2e compose file
The admin search label and parallel compose tests made strict-mode
Playwright locators ambiguous after the dashboard rework.
- Assert table columns via columnheader roles instead of getByText
- Target seeded order by ID when opening the message modal
Co-authored-by: Cursor <cursoragent@cursor.com>
Manual Swish flow means users often pay later; admins match payments via
order ID or regnr under Att göra.
- User flow: create order, leave payment, pay from orders page
- Admin: find order via partial ID, full ID, and plate search
- Assert unpaid orders appear under Väntar, not Att göra
- Use unique plates per run to avoid collisions with seed data
Admins need to find orders quickly and read full letter text without a
cramped table column.
- Make stat cards clickable filters (Totalt, Att göra, Betalda, Väntar)
- Add search by partial order ID or registration number
- Show shortened order ID in table with full ID on hover
- Replace message column with "Visa meddelande" opening a modal
- Keep expanded row for tracking only; remove duplicate brevtext block
- Update AdminDashboard unit tests and admin-dashboard e2e specs
The full UUID is required as the Swish message but was easy to miss when
embedded only in instruction text.
- Show beställnings-ID in a dedicated box above payment summary
- Point Swish instructions to that ID instead of repeating inline
- Add PaymentRedirect unit test for full order ID display
Users who leave the payment step can return later and still see what
they ordered. Unpaid orders get a clear path back to Swish checkout.
- Add letterText to frontend Order type
- Show beställnings-ID, message, and formatted date on each order card
- Add "Betala nu" link to payment route for pending_payment orders
- Extend OrdersPage unit tests and order-history e2e for pay-later flow
fix: add preview.allowedHosts and preview.host to vite.config.ts
Vite preview server blocks requests from non-localhost hosts by default.
In the E2E Docker Compose stack, Playwright accesses the frontend via
http://frontend (container hostname). Without allowedHosts, Vite returns
"Blocked request. This host is not allowed." and the SPA never mounts,
causing all 59 E2E tests to fail with blank pages and missing elements.
- Add preview.host: true (bind to 0.0.0.0)
- Add preview.allowedHosts: ['frontend', 'localhost']
test: update payment-redirect E2E tests to match current UI
The payment page was redesigned to a two-step confirmation flow:
"Jag har betalat" → confirmation → "Ja, jag har betalat". The E2E
tests still referenced the old single-step "Genomför testbetalning"
button and a removed .payment__note CSS class.
- Update 'payment button marks order as paid' to click through both steps
- Rename 'shows mock payment note' to 'shows Swish payment instructions'
and assert on actual UI elements (Swish label + payment button)
Result: E2E suite now passes 59/59 tests in the Docker Compose CI stack.
Replace the mock test-payment button with a real manual Swish flow
where the user sends a Swish payment with the order ID as message
and confirms via a button. Admin verifies Swish and processes manually.
Backend
- Rename OrderStatus LOOKUP_STARTED to PROCESSING (Swedish: Hanteras)
- Update V5 migration CHECK constraint from lookup_started to processing
- Rename OrderService.markAsPaid() to confirmPayment(), sets PROCESSING
instead of PAID, stop hardcoding amountPaid
- Add GET /api/payment/swish-info endpoint returning swish number and
letter price from app.payment config
- Permit /api/payment/swish-info without authentication
- Update UpdateStatusRequest regex to accept processing
- Update PaymentControllerTest for renamed method, new status, and
public swish-info endpoint test
Frontend
- Rewrite PaymentRedirect.vue: Swish number, order ID as message,
Jag har betalat button with confirmation dialog
- Add fetchSwishInfo() to api/payment.ts
- AdminPage: rename Skickade stat to Att göra (processing orders),
highlight processing rows with admin__row--todo
- OrdersPage: update status labels/badge classes for new flow
- Refactor ApiError in client.ts to property declaration syntax
- Exclude __tests__ from tsconfig.app.json and Docker builds
Tests
- Rewrite PaymentRedirect.spec.ts for Swish info, confirmation dialog,
cancel flow, and processing status
- Update OrdersPage.spec.ts with processing status test
- Update AdminDashboard.spec.ts with Att göra stat and row highlight
- Add amountPaid to ComposePage.spec.ts mock
Config
- Add SWISH_NUMBER to .env.example and docker-compose.yml
Three problems caused E2E browser tests to fail in Forgejo CI:
1. TypeScript build errors in (frontend.e2e.Dockerfile):
- used parameter property which violates
. Replaced with explicit property declaration.
- included in type-checking, causing
mock Response type mismatches. Added .
- mock Order was missing field.
2. Nginx SSL crash:
- copied production
which references SSL certs that don't exist in the e2e image.
- Replaced nginx entirely with (simpler, no SSL needed).
- Added to so routes to backend.
3. Docker context hygiene:
- excludes so test files don't
bloat the build context or trigger type errors in the container.
All other files untouched.
- HomePage.spec.ts: replace setTimeout fake data with mocked lookupVehicle()
API call, mock resolved/rejected/pending states, add fuel to mock responses
- VehicleInfo.spec.ts: add fuel field to mockVehicle data,
update assertion to include Bensin in rendered text
- enters plate and sees vehicle info with CTA button:
types HDO732, verifies Peugeot 107 1.0, 2011, Gul, Bensin appear,
verifies Fortsatt till brevet link has correct href
- shows not found for unknown plate (ZZZ999)
- CTA navigates to compose when authenticated:
logs in as test@bilhalsning.se, performs lookup, clicks CTA,
verifies redirect to /compose?plate=HDO732
- Add typed API module api/vehicles.ts with lookupVehicle(plate) function
- Replace FAKE_VEHICLES record with async API call in HomePage.vue
- Remove setTimeout-based fake lookup, use lookupVehicle() instead
- Handle errors: show not-found for unknown plates, catch network failures
- Add fuel field to VehicleInfo interface and display (e.g. 'Gul, Bensin')
- VehicleInfo now shows make, model, year, color, and fuel from API
- Rewrite homepage: practical headline, use-case cards, calm trust note
- Switch from purple to blue brand tokens across all pages
- Replace all CTA buttons with brand-primary, reserve green for success
- Remove emoji from template picker and compose page
- Replace unicode chevrons with SVG expand buttons in admin
- Redesign template picker modal with accessibility semantics
- Add aria-invalid, aria-describedby to form validation
- Add role=status/alert to loading, error, and result messages
- Remove inline styles, replace with scoped utility classes
- Update compose submit text, payment button, order empty state copy
- Remove icon field from letter templates
- Add design tokens (colors, spacing, radius, shadows, typography, transitions)
- Add global reset, body/link/focus/typography base styles
- Add utility classes (container, surface-card, btn variants, field, badge, message, divider)
- Replace header ✉ symbol with inline SVG envelope icon
- Update favicon to license-plate shaped mark with blue gradient and bold B
- Rename brand from BilHälsning to Bilhej in header, footer, and HTML title
- Rewrite footer tagline: focus on service, not privacy
- Add theme-color meta tag for browser chrome
Vitest:
- PaymentRedirect.spec.ts (8 tests): renders heading and 49 kr,
shows plate from query, Betalt button exists, calls payOrder on
click, navigates to /orders on success, shows error on failure,
disables button while paying, shows mock note
- ComposePage.spec.ts: update navigation test to expect /betalning
route with orderId param instead of /orders; add payment route
to test router; add PaymentRedirect import
Playwright E2E:
- payment-redirect.spec.ts (4 tests): compose→payment navigation,
Betalt→orders flow, auth guard redirects to login, mock note
visible
- compose.spec.ts: rename test and update assertion from /orders
to /betalning/ URL pattern; use getByRole('heading',
{ name: 'Betalning' }) to avoid strict mode violation with
mock-note paragraph containing the word 'Betalning'