Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/Controller/AanbodController.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public function __construct(
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @spec openspec/changes/retrofit-2026-05-24-aanbod-listings/tasks.md#task-1
*/
public function getAanbod(): JSONResponse
{
Expand Down Expand Up @@ -157,6 +159,8 @@ public function getAanbod(): JSONResponse
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @spec openspec/changes/retrofit-2026-05-24-aanbod-listings/tasks.md#task-2
*/
public function acceptAanbod(string $uuid): JSONResponse
{
Expand Down Expand Up @@ -256,6 +260,8 @@ function ($key) {
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @spec openspec/changes/retrofit-2026-05-24-aanbod-listings/tasks.md#task-3
*/
public function denyAanbod(string $uuid): JSONResponse
{
Expand Down Expand Up @@ -344,6 +350,8 @@ function ($key) {
* Parse query parameters into options array.
*
* @return array Parsed options array
*
* @spec openspec/changes/retrofit-2026-05-24-aanbod-listings/tasks.md#task-1
*/
private function parseQueryOptions(): array
{
Expand Down
25 changes: 25 additions & 0 deletions openspec/changes/retrofit-2026-05-24-aanbod-listings/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Design — Retrofit Aanbod Listings

Retrofit change. Tasks describe retroactive annotation, not new implementation work.

## Context

`AanbodController` exposes the public HTTP surface for browsing and acting on software-catalogue offers. The companion `AanbodService` is already in Bucket 1 (covered by `method-decomposition#REQ-DECOMP-011`). This change captures the controller-level contract that REQ-DECOMP-011 only touches obliquely.

## Decisions

- **Cluster mode** — no existing capability covers HTTP-level aanbod behaviour.
- **3 REQs / 4 methods** — `parseQueryOptions` collapses into REQ-001 as plumbing. List / accept / deny are the three observable behaviours.
- **HTTP status mapping spelled out per REQ.** The controller has a tight per-endpoint mapping (400 / 403 / 404 / 500 / 200) — every distinct mapping is captured because callers depend on it.
- **No fix for `@PublicPage` posture.** All three endpoints are public and rely on session-level org context for authorisation. This is the observed behaviour and is documented in Notes; revisiting it belongs to a security ADR pass.

## Out of scope

- Refactoring `AanbodController` against ADR-005 (auth annotations) — open a separate issue if needed.
- Service-side behaviour — already in `method-decomposition#REQ-DECOMP-011`.

## References

- Umbrella: ConductionNL/softwarecatalog#285
- Coverage report: openspec/coverage-report.md (2026-05-24)
- Source: lib/Controller/AanbodController.php
18 changes: 18 additions & 0 deletions openspec/changes/retrofit-2026-05-24-aanbod-listings/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Retrofit — aanbod-listings

Describes observed behavior of 4 methods in `AanbodController` as 3 new REQs under a new `aanbod-listings` capability. Code already exists — this change retroactively specifies it.

## Affected code units

- lib/Controller/AanbodController.php::getAanbod
- lib/Controller/AanbodController.php::acceptAanbod
- lib/Controller/AanbodController.php::denyAanbod
- lib/Controller/AanbodController.php::parseQueryOptions

## Approach

- For each method: describe observed inputs, outputs, pre/postconditions, failure modes from the existing source.
- Draft REQs that match observed behavior (not aspirational).
- Notes section flags overlap with `AanbodService` REQs already captured in Bucket 1 (Service-side methods inherit from method-decomposition spec scenarios).

Source: openspec/coverage-report.md generated 2026-05-24. Umbrella: ConductionNL/softwarecatalog#285.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
status: draft
retrofit: true
---

# Aanbod Listings Specification

## Purpose

Captures observed behavior of the public HTTP surface for `aanbod` (software-catalogue offers — modules, diensten, koppelingen, gebruiks) exposed via `OCA\SoftwareCatalog\Controller\AanbodController`. Allows the active organisation to browse offers it consumes or provides, and to accept (claim ownership) or deny (delete) individual offers. Reverse-spec'd from existing code 2026-05-24.

## ADDED Requirements

### REQ-001: The system SHALL list aanbod objects for the active organisation

`GET /api/aanbod` MUST return modules, diensten, and koppelingen where the current organisation is in the `aanbieder` property, plus gebruiks where the current organisation is in the `afnemer` property. Objects whose `@self.organisation` equals the current organisation MUST be excluded. The endpoint MUST honour pagination via `limit`, `offset`, `page` query parameters and MUST force `_source: database` for real-time data. On service-level failure the response MUST be HTTP 500 with `error` set; on unexpected exception the response MUST be HTTP 500 with `error: 'Internal server error: <message>'` and empty paginated envelope.

#### Scenario: Successful listing returns 200 with paginated envelope
- GIVEN the active organisation has 5 aanbod objects assigned
- WHEN `GET /api/aanbod?limit=20` is called
- THEN the response status MUST be `200`
- AND the JSON body MUST contain `results` (array) and `total` (int)

#### Scenario: Service error returns 500 with error key
- GIVEN `AanbodService::getAanbod()` returns a payload containing `error`
- WHEN the controller handles the request
- THEN the response status MUST be `500`
- AND the response body MUST preserve the service payload (including `error`)

#### Scenario: Unhandled exception returns canonical envelope
- GIVEN the service throws an exception
- WHEN the controller catches it
- THEN the response status MUST be `500`
- AND the body MUST equal `{results: [], total: 0, page: 1, pages: 0, limit: 20, offset: 0, error: "Internal server error: <message>"}`

### REQ-002: The system SHALL allow accepting an aanbod offer

`PUT /api/aanbod/{uuid}/accept` MUST set the offer's `@self.organisation` to the active organisation, allowed only when the active org is the offer's `afnemer` (for gebruiks) or `aanbieder` (for modules/diensten/koppelingen). Empty `uuid` MUST return HTTP 400 with `{success: false, error: "Aanbod UUID is required", aanbod: null}`. The endpoint MUST forward optional body parameters (excluding `uuid`) to `AanbodService::acceptAanbod`. Status code mapping MUST be: success → 200; `Aanbod object not found` → 404; service error string containing `Operation not allowed` → 403; any other service failure or exception → 500.

#### Scenario: Empty uuid is rejected before service call
- GIVEN the request path resolves uuid to an empty string
- WHEN the controller runs
- THEN the response status MUST be `400`
- AND the body MUST equal `{success: false, error: "Aanbod UUID is required", aanbod: null}`

#### Scenario: Permission denial maps to 403
- GIVEN `AanbodService::acceptAanbod()` returns `{success: false, error: "Operation not allowed: not the afnemer"}`
- WHEN the controller maps the response
- THEN the response status MUST be `403`

#### Scenario: Not-found maps to 404
- GIVEN the service returns `{success: false, error: "Aanbod object not found"}`
- WHEN the controller maps the response
- THEN the response status MUST be `404`

### REQ-003: The system SHALL allow denying an aanbod offer by deletion

`DELETE /api/aanbod/{uuid}/deny` MUST delete the offer when the active org is the `afnemer` (for gebruiks) or `aanbieder` (for modules/diensten/koppelingen). Empty `uuid` MUST return HTTP 400 with `{success: false, error: "Aanbod UUID is required", deleted: false}`. Optional body parameters (excluding `uuid`) MUST be forwarded to `AanbodService::denyAanbod`. Status code mapping MUST mirror REQ-002 (success → 200; not found → 404; `Operation not allowed` → 403; otherwise → 500).

#### Scenario: Successful deletion returns 200
- GIVEN the service returns `{success: true, deleted: true}`
- WHEN the controller maps the response
- THEN the response status MUST be `200`
- AND the response body MUST contain `deleted: true`

#### Scenario: Empty uuid is rejected
- GIVEN the request path resolves uuid to an empty string
- WHEN the controller runs
- THEN the response status MUST be `400`

## Notes

- **`parseQueryOptions` is private plumbing**, not a separate REQ. Its behaviour (force `_source: database`, duplicate `limit`/`_limit` keys for compatibility) is implied by REQ-001's contract and tested indirectly via the listing scenarios.
- **`@PublicPage` + `@NoCSRFRequired` + `@NoAdminRequired`** are all present on every endpoint. The endpoints accept unauthenticated requests but rely on session-resolved `aanbieder`/`afnemer` matching for authorisation. This is intentional for the listing UX but should be documented in the security ADRs (out of scope here).
- **Service-side behaviour** (`AanbodService::getAanbod / acceptAanbod / denyAanbod`) is already covered by Bucket 1 REQ-DECOMP-011; this spec scopes the HTTP-level contract only.
- **Acceptance Criteria:** Endpoint contracts are covered by Newman collection runs against `/api/aanbod*`; no PHPUnit coverage of the controller layer at retrofit time.
5 changes: 5 additions & 0 deletions openspec/changes/retrofit-2026-05-24-aanbod-listings/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Tasks

- [x] task-1: aanbod-listings#REQ-001 — System SHALL list aanbod objects for the active organisation (retroactive annotation)
- [x] task-2: aanbod-listings#REQ-002 — System SHALL allow accepting an aanbod offer (retroactive annotation)
- [x] task-3: aanbod-listings#REQ-003 — System SHALL allow denying an aanbod offer by deletion (retroactive annotation)
Loading