feat(portal): wire company-scoped invoices + add portal isolation tests (PMS-25) #125

Merged
longjacksonle merged 1 commit from feat/pms-25-portal-invoices-and-tests into main 2026-06-07 03:29:18 +02:00

What

Completes the Customer Portal API story (PMS-25, F6). The portal surface (/api/v1/portal/*) already had working auth, tickets, and the KB feed; this PR closes the two remaining gaps: the invoice routes were stubbed, and there was no portal test coverage (notably AC6's cross-company isolation test).

Closes PMS-25.

Invoices (AC3) - PMS-33 has landed

list_invoices and get_invoice were placeholders (empty page / always-404) gated on PMS-33, which is now Done. Both now serve real billing data, scoped to the authenticated contact's company:

  • GET /api/v1/portal/invoices filters BillingService::list_invoices by contact.company_id. The scope comes from the portal JWT, never a query param, so a contact cannot widen it.
  • GET /api/v1/portal/invoices/:id reads within the contact's tenant, then enforces invoice.company_id == contact.company_id in code, returning 404 (not 403) for another company's invoice so the portal never confirms its existence.
  • The portal router gains a BillingService dependency, built in create_api_router with the same encryption key as the agent-side billing service.

Tests (AC6 + the rest) - tests/portal.rs

New integration coverage, one test per acceptance criterion:

  • Contact-scoped session (AC1): valid login issues a contact token carrying the company scope; wrong password and a non-portal contact both 401; a staff token is rejected on /portal/auth/me and a portal token is rejected on /auth/me.
  • Tickets (AC2 + AC6): a contact creates a ticket and lists only their company's tickets with a populated DTO (non-empty ticket_number); company B neither lists nor can GET company A's ticket (404).
  • Invoices (AC3 + AC6): the list returns only the contact's company invoice; the contact reads its own invoice but gets 404 for another company's.
  • Knowledge base (AC4): the feed returns only published articles that are public or client_specific scoped to the contact's company; drafts, internal, and another company's client_specific articles are hidden.
  • No 501 (AC5): every protected portal route returns 401 unauthenticated and never 501.

Verification

  • cargo test --test portal -> 5 passed.
  • cargo test --lib -> 110 passed.
  • cargo fmt --check clean; cargo clippy --all-targets -- -D warnings clean.

🤖 Generated with Claude Code

## What Completes the Customer Portal API story (PMS-25, F6). The portal surface (`/api/v1/portal/*`) already had working auth, tickets, and the KB feed; this PR closes the two remaining gaps: the invoice routes were stubbed, and there was no portal test coverage (notably AC6's cross-company isolation test). Closes PMS-25. ## Invoices (AC3) - PMS-33 has landed `list_invoices` and `get_invoice` were placeholders (empty page / always-404) gated on PMS-33, which is now Done. Both now serve real billing data, scoped to the authenticated contact's company: - `GET /api/v1/portal/invoices` filters `BillingService::list_invoices` by `contact.company_id`. The scope comes from the portal JWT, never a query param, so a contact cannot widen it. - `GET /api/v1/portal/invoices/:id` reads within the contact's tenant, then enforces `invoice.company_id == contact.company_id` in code, returning `404` (not `403`) for another company's invoice so the portal never confirms its existence. - The portal router gains a `BillingService` dependency, built in `create_api_router` with the same encryption key as the agent-side billing service. ## Tests (AC6 + the rest) - `tests/portal.rs` New integration coverage, one test per acceptance criterion: - **Contact-scoped session (AC1):** valid login issues a contact token carrying the company scope; wrong password and a non-portal contact both `401`; a staff token is rejected on `/portal/auth/me` and a portal token is rejected on `/auth/me`. - **Tickets (AC2 + AC6):** a contact creates a ticket and lists only their company's tickets with a populated DTO (non-empty `ticket_number`); company B neither lists nor can `GET` company A's ticket (`404`). - **Invoices (AC3 + AC6):** the list returns only the contact's company invoice; the contact reads its own invoice but gets `404` for another company's. - **Knowledge base (AC4):** the feed returns only `published` articles that are `public` or `client_specific` scoped to the contact's company; drafts, `internal`, and another company's `client_specific` articles are hidden. - **No 501 (AC5):** every protected portal route returns `401` unauthenticated and never `501`. ## Verification - `cargo test --test portal` -> 5 passed. - `cargo test --lib` -> 110 passed. - `cargo fmt --check` clean; `cargo clippy --all-targets -- -D warnings` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(portal): wire company-scoped invoices + add portal isolation tests (PMS-25)
All checks were successful
E2E (staging) / Playwright against staging (pull_request) Successful in 23s
Check / fmt + clippy + compile + tests (pull_request) Successful in 55s
Create release / Create release from merged PR (pull_request) Has been skipped
Build OCI container / Build and push mokosh-api image (push) Successful in 4m6s
7e0f275065
The customer portal API (`/api/v1/portal/*`) already implemented auth, tickets, and the KB feed, but the invoice routes were stubbed (`list_invoices` returned an empty page and `get_invoice` always 404'd) pending PMS-33. PMS-33 has since landed, so this wires both invoice handlers to real, company-scoped billing reads: `list_invoices` filters `BillingService::list_invoices` by the authenticated contact's `company_id` (never a query param, so a contact can only ever see its own company's invoices), and `get_invoice` reads within the contact's tenant then enforces the company match in code, returning 404 (not 403) for another company's invoice so the portal never confirms its existence. The portal router gains a `BillingService` dependency, constructed in `create_api_router` with the same encryption key as the agent billing service.

Adds `tests/portal.rs`, the integration coverage the story's AC6 calls for and that was missing entirely. Five tests map to the acceptance criteria: contact-scoped login (valid login, wrong password and non-portal contact both 401, staff token rejected on the portal and portal token rejected on staff routes); tickets create + list scoped to the contact's company with a populated DTO, and a cross-company ticket read returning 404; invoices list scoped to the company plus a cross-company invoice read returning 404; the KB feed returning only published public / company-scoped articles; and every protected portal route returning 401 unauthenticated and never 501.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/pms-25-portal-invoices-and-tests 2026-06-07 03:29:18 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!125
No description provided.