diff --git a/app/Http/Resources/Shop/OrderItemResource.php b/app/Http/Resources/Shop/OrderItemResource.php index 9023b3a3c4c..8099e11d306 100644 --- a/app/Http/Resources/Shop/OrderItemResource.php +++ b/app/Http/Resources/Shop/OrderItemResource.php @@ -10,6 +10,7 @@ use App\Enum\PurchasableLicenseType; use App\Enum\PurchasableSizeVariantType; +use App\Enum\SizeVariantType; use App\Models\OrderItem; use App\Services\MoneyService; use Spatie\LaravelData\Data; @@ -39,6 +40,8 @@ public function __construct( public ?int $pixel_size_id, public ?int $pixel_width, public ?int $pixel_height, + public ?string $album_title, + public ?string $thumb_url, ) { } @@ -70,6 +73,8 @@ public static function fromModel(OrderItem $item): OrderItemResource pixel_size_id: $item->pixel_size_id, pixel_width: $item->pixel_width, pixel_height: $item->pixel_height, + album_title: $item->album?->title, + thumb_url: $item->photo?->size_variants->getSizeVariant(SizeVariantType::THUMB)?->url, ); } } diff --git a/app/Http/Resources/Shop/OrderResource.php b/app/Http/Resources/Shop/OrderResource.php index 9573bd420e4..8b67403ab6e 100644 --- a/app/Http/Resources/Shop/OrderResource.php +++ b/app/Http/Resources/Shop/OrderResource.php @@ -10,6 +10,7 @@ use App\Enum\OmnipayProviderType; use App\Enum\PaymentStatusType; +use App\Enum\SizeVariantType; use App\Models\Order; use App\Services\MoneyService; use Illuminate\Support\Collection; @@ -55,7 +56,18 @@ public static function fromModel(Order $order): OrderResource { $money_service = resolve(MoneyService::class); - // The order has been paid, so we provide the download links + // Load album and photo thumbnails for all orders (display purposes). + // Load size_variant only for closed orders (download URL generation). + $order->load([ + 'items.album', + 'items.photo.size_variants' => fn ($q) => $q->whereIn('type', [ + SizeVariantType::SMALL, + SizeVariantType::SMALL2X, + SizeVariantType::THUMB, + SizeVariantType::THUMB2X, + SizeVariantType::PLACEHOLDER, + ]), + ]); if ($order->status === PaymentStatusType::CLOSED) { $order->load('items.size_variant'); } diff --git a/docs/specs/4-architecture/features/042-webshop-order-item-display/plan.md b/docs/specs/4-architecture/features/042-webshop-order-item-display/plan.md new file mode 100644 index 00000000000..a4d20efab02 --- /dev/null +++ b/docs/specs/4-architecture/features/042-webshop-order-item-display/plan.md @@ -0,0 +1,215 @@ +# Feature Plan 042 – Photo Display Enrichment + +_Linked specification:_ `docs/specs/4-architecture/features/042-webshop-order-item-display/spec.md` +_Status:_ Draft +_Last updated:_ 2026-05-31 + +> Guardrail: Keep this plan traceable back to the governing spec. Reference FR/NFR/Scenario IDs from `spec.md` where relevant, log any new high- or medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), and assume clarifications are resolved only when the spec's normative sections (requirements/NFR/behaviour/telemetry) and, where applicable, ADRs under `docs/specs/5-decisions/` have been updated. + +## Vision & Success Criteria + +An administrator or customer opening an order detail page can immediately identify each purchased item by its album context and a thumbnail. Admin users navigating the duplicate-finder or moderation queue can click a photo's title to jump directly to the album page. Success is measured by: + +- Every order item row in `OrderDownload.vue` shows a 48 × 48 thumbnail (or placeholder) and an album title (or "Unknown album"). +- `OrderItemResource` exposes `album_title` and `thumb_url` with no N+1 queries. +- PHPStan 0 errors, php-cs-fixer clean, all tests pass, `npm run check` exits 0, `npm run lint` exits 0. + +## Scope Alignment + +- **In scope (Part A – Webshop Order Item Display):** + - Extend `OrderItemResource` with `album_title` and `thumb_url` fields (FR-042-01, FR-042-02). + - Update `OrderResource::fromModel()` to eager-load `items.photo.size_variants` and `items.album` (FR-042-03, NFR-042-01, NFR-042-02). + - Update `OrderDownload.vue` to render the thumbnail and album title (FR-042-04, FR-042-05). + - Add `webshop.orderDownload.unknownAlbum` i18n key (FR-042-06). + - Backend unit and feature tests (all scenarios S-042-01 through S-042-07). + +- **Full quality gate (NFR-042-03 through NFR-042-07).** + +- **Out of scope:** + - Modifying `OrderList.vue`. + - Adding new database columns or migrations. + - Showing additional photo metadata (EXIF, description, tags). + - Caching or pre-computing thumbnail URLs. + +## Dependencies & Interfaces + +- `app/Http/Resources/Shop/OrderItemResource.php` — DTO to extend. +- `app/Http/Resources/Shop/OrderResource.php` — eager-load strategy to update. +- `app/Models/OrderItem.php` — existing `photo()` and `album()` relations. +- `app/Models/Extensions/Thumb.php` — `sizeVariantsFilter()` for constraining variant loads. +- `app/Enum/SizeVariantType.php` — `THUMB` constant for size variant filtering. +- `resources/js/views/webshop/OrderDownload.vue` — frontend view. +- TypeScript type generation pipeline (`php artisan typescript:transform`) — run after PHP resource changes. +- `lang/` i18n files. +- `tests/Feature_v2/` — existing PHPUnit test suite using `BaseApiWithDataTest`. + +## Assumptions & Risks + +- **Assumptions:** + - `OrderItem` records always carry a valid `album_id` at creation time (see `OrderService::addPhotoToOrder`). + - The `SizeVariant` model exposes a `url` attribute suitable for use as ``. + - The TypeScript transformer regenerates types as part of the normal frontend build cycle. + - Existing `OrderItemFactory` and photo factories create sufficient fixture data for the new tests. + +- **Risks / Mitigations:** + - **TypeScript transformer command:** The exact artisan command for type generation may differ. Verify with `php artisan list | grep typescript` before I3. If unavailable, the TypeScript interface must be updated manually. + - **Eager-load depth:** Loading `items.photo.size_variants` inside `OrderResource::fromModel()` adds SQL joins. Mitigated by constraining size variants to THUMB-category types only (NFR-042-02) and confirming with query-count assertion (S-042-07). + - **Deleted photo / album:** Relations return `null` after deletion; both fields default to `null` in `fromModel()`. Covered by S-042-02, S-042-04. + +## Implementation Drift Gate + +After each increment, run: + +```bash +vendor/bin/php-cs-fixer fix +php artisan test +make phpstan +``` + +After frontend changes, additionally run: + +```bash +npm run format +npm run check +npm run lint +``` + +Record results in the Scenario Tracking table below. + +## Increment Map + +### Part A – Webshop Order Item Display + +### I1 – Extend `OrderItemResource` (≤30 min) + +- _Goal:_ Add `album_title` and `thumb_url` to the resource DTO (FR-042-01, FR-042-02, DO-042-01). +- _Preconditions:_ None. +- _Steps:_ + 1. Open `app/Http/Resources/Shop/OrderItemResource.php`. + 2. Add `public ?string $album_title` and `public ?string $thumb_url` to the constructor parameter list and the `fromModel()` factory method. + 3. In `fromModel()`, resolve `album_title` from `$item->album?->title`. + 4. In `fromModel()`, resolve `thumb_url` from `$item->photo?->size_variants->getSizeVariant(SizeVariantType::THUMB)?->url` — returning `null` if any link in the chain is absent. + 5. Add the required `use` statements (`SizeVariantType`). + 6. Run `vendor/bin/php-cs-fixer fix` and `make phpstan`. +- _Commands:_ `vendor/bin/php-cs-fixer fix`, `make phpstan` +- _Exit:_ `OrderItemResource` has both new fields; PHPStan 0 errors; php-cs-fixer clean. + +### I2 – Update eager-loading in `OrderResource` (≤30 min) + +- _Goal:_ Load album and photo (thumb-only size variants) without N+1 queries (FR-042-03, NFR-042-01, NFR-042-02). +- _Preconditions:_ I1 complete. +- _Steps:_ + 1. Open `app/Http/Resources/Shop/OrderResource.php`. + 2. In `fromModel()`, locate the block that conditionally calls `$order->load('items.size_variant')` for `CLOSED` orders. + 3. Extend the load to always include `items.album` and `items.photo.size_variants` (filtered to THUMB-category variants via an inline `whereIn` using `Thumb::sizeVariantsFilter()` or the equivalent array `[SizeVariantType::SMALL, SizeVariantType::SMALL2X, SizeVariantType::THUMB, SizeVariantType::THUMB2X, SizeVariantType::PLACEHOLDER]`). + 4. Ensure the load is unconditional — not gated on `CLOSED` status — since album and thumbnail display is needed on the order detail page for all statuses. + 5. Run `vendor/bin/php-cs-fixer fix`, `php artisan test`, `make phpstan`. +- _Commands:_ `vendor/bin/php-cs-fixer fix`, `php artisan test`, `make phpstan` +- _Exit:_ Relations are eager-loaded; test suite passes; PHPStan 0 errors. + +### I3 – Backend tests (≤40 min) + +- _Goal:_ Cover all backend scenarios with failing tests first, then confirm they pass (S-042-01 through S-042-07). +- _Preconditions:_ I2 complete. +- _Steps:_ + 1. **Write tests first (failing).** In `tests/` (unit or `Feature_v2`), add test methods for: + - S-042-01: `album_title` populated from existing album. + - S-042-02: `album_title` is `null` when album deleted. + - S-042-03: `thumb_url` populated when THUMB variant exists. + - S-042-04: `thumb_url` is `null` when photo deleted. + - S-042-05: `thumb_url` is `null` when photo has no THUMB variant. + - S-042-06: Both fields non-null in the full happy path. + - S-042-07: `GET /api/v2/Order/{id}` response includes `album_title` and `thumb_url` keys in each item; query count does not grow with item count. + 2. Confirm new tests fail before implementation changes. + 3. Run `php artisan test --filter=` to confirm tests are green after I1+I2. + 4. Run full suite: `php artisan test`, `make phpstan`, `vendor/bin/php-cs-fixer fix`. +- _Commands:_ `php artisan test --filter=`, `php artisan test`, `make phpstan`, `vendor/bin/php-cs-fixer fix` +- _Exit:_ All new tests green; no regressions; PHPStan 0 errors. + +### I4 – Frontend: i18n + TypeScript types (≤20 min) + +- _Goal:_ Add the `unknownAlbum` translation key and refresh TypeScript types (FR-042-06, NFR-042-06). +- _Preconditions:_ I3 complete. +- _Steps:_ + 1. Add `"unknownAlbum": "Unknown album"` to `lang/en/` (or equivalent JSON file) under the `webshop.orderDownload` namespace, following the existing key pattern. + 2. Run `php artisan typescript:transform` (or equivalent) to regenerate the TypeScript interface for `OrderItemResource`. Confirm `album_title: string | null` and `thumb_url: string | null` appear in the generated file. + 3. If the TypeScript transformer is unavailable, manually add the two fields to the interface definition and note this in the tasks. + 4. Run `npm run format`, `npm run check`. +- _Commands:_ `php artisan typescript:transform` (if available), `npm run format`, `npm run check` +- _Exit:_ i18n key added; TypeScript types include new fields; `npm run check` exits 0. + +### I5 – Frontend: update `OrderDownload.vue` (≤40 min) + +- _Goal:_ Render thumbnail and album title in the order item row (FR-042-04, FR-042-05, UI-042-01 through UI-042-04). +- _Preconditions:_ I4 complete. +- _Steps:_ + 1. Open `resources/js/views/webshop/OrderDownload.vue`. + 2. In the `v-for="item in order.items"` loop, add a thumbnail `` element: + - `v-if="item.thumb_url"` branch: `` + - `v-else` branch: `` + 3. Below the photo title `RouterLink`, add a line for album title: + - `
{{ item.album_title ?? $t('webshop.orderDownload.unknownAlbum') }}
` + 4. Adjust the flex layout of the item row to accommodate the thumbnail column (align thumbnail with the text block to its right). + 5. Run `npm run format`, `npm run check`. +- _Commands:_ `npm run format`, `npm run check` +- _Exit:_ Thumbnail and album title visible in order detail; `npm run check` exits 0; layout consistent with existing PrimeVue style. + +### I6 – Quality gates & documentation (≤20 min) + +- _Goal:_ Full pipeline green; docs updated. +- _Preconditions:_ I5 complete. +- _Steps:_ + 1. Run complete quality gate: `vendor/bin/php-cs-fixer fix`, `npm run format`, `npm run check`, `php artisan test`, `make phpstan`. + 2. Update `docs/specs/4-architecture/shop-architecture.md` to note that `OrderItemResource` now includes `album_title` and `thumb_url`. + 3. Update `docs/specs/4-architecture/roadmap.md`: move Feature 042 from Active to Completed. + 4. Update `docs/specs/_current-session.md`. +- _Commands:_ `vendor/bin/php-cs-fixer fix`, `npm run format`, `npm run check`, `php artisan test`, `make phpstan` +- _Exit:_ All gates green; docs updated. + +## Scenario Tracking + +| Scenario ID | Increment / Task reference | Notes | +|-------------|---------------------------|-------| +| S-042-01 | I1, I3 / T-042-01, T-042-04 | `album_title` from existing album. | +| S-042-02 | I1, I3 / T-042-01, T-042-05 | Null `album_title` when album deleted. | +| S-042-03 | I1, I3 / T-042-02, T-042-06 | `thumb_url` from THUMB variant. | +| S-042-04 | I1, I3 / T-042-02, T-042-07 | Null `thumb_url` when photo deleted. | +| S-042-05 | I1, I3 / T-042-02, T-042-08 | Null `thumb_url` when no THUMB variant. | +| S-042-06 | I1, I2, I3 / T-042-03, T-042-09 | Full happy path: both fields non-null. | +| S-042-07 | I2, I3 / T-042-03, T-042-10 | N+1 prevention; query-count assertion. | + +## Analysis Gate + +_To be completed before coding begins._ + +- [ ] All twelve FRs are unambiguous and traceable to tasks. +- [ ] All fourteen scenarios map to at least one increment/task. +- [ ] No open questions logged in `open-questions.md` for Feature 042. +- [ ] Estimated total effort ≤ 280 min (fits within session). +- [ ] TypeScript transformer command verified (`php artisan typescript:transform` or manual fallback noted in I4 and I7). + +## Exit Criteria + +- [ ] `OrderItemResource` constructor and `fromModel()` include `album_title` and `thumb_url`. +- [ ] `OrderResource::fromModel()` eager-loads `items.album` and `items.photo.size_variants` (THUMB-only) unconditionally. +- [ ] All seven backend scenarios (S-042-01 through S-042-07) covered by passing tests. +- [ ] `webshop.orderDownload.unknownAlbum` i18n key added. +- [ ] TypeScript interface for `OrderItemResource` includes `album_title: string | null` and `thumb_url: string | null`. +- [ ] `OrderDownload.vue` renders thumbnail and album title per item row. +- [ ] `vendor/bin/php-cs-fixer fix` exits 0. +- [ ] `php artisan test` exits 0. +- [ ] `make phpstan` exits 0. +- [ ] `npm run format` exits 0. +- [ ] `npm run check` exits 0. +- [ ] `npm run lint` exits 0. +- [ ] `roadmap.md` and `shop-architecture.md` updated. + +## Follow-ups / Backlog + +- Consider adding album title and thumbnail display to the `OrderList.vue` items-preview if a future iteration warrants it. +- If the TypeScript transformer is removed from the toolchain in future, update `resources/js/types/` manually and document the process in `docs/specs/3-reference/coding-conventions.md`. +- Investigate whether `OrderItemResource` should also store `album_title` as a persisted column on `order_items` (for true historical accuracy if the album is renamed after purchase). Deferred — current spec uses display-time lookup. + +--- + +*Last updated: 2026-05-31* diff --git a/docs/specs/4-architecture/features/042-webshop-order-item-display/spec.md b/docs/specs/4-architecture/features/042-webshop-order-item-display/spec.md new file mode 100644 index 00000000000..852420f9b3b --- /dev/null +++ b/docs/specs/4-architecture/features/042-webshop-order-item-display/spec.md @@ -0,0 +1,239 @@ +# Feature 042 – Photo Display Enrichment + +| Field | Value | +|-------|-------| +| Status | Planning | +| Last updated | 2026-05-31 | +| Owners | LycheeOrg | +| Linked plan | `docs/specs/4-architecture/features/042-webshop-order-item-display/plan.md` | +| Linked tasks | `docs/specs/4-architecture/features/042-webshop-order-item-display/tasks.md` | +| Roadmap entry | #042 | + +> Guardrail: This specification is the single normative source of truth for the feature. Track high- and medium-impact questions in [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md), encode resolved answers directly in the Requirements/NFR/Behaviour/UI/Telemetry sections below (no per-feature `## Clarifications` sections), and use ADRs under `docs/specs/5-decisions/` for architecturally significant clarifications (referencing their IDs from the relevant spec sections). + +## Overview + +This feature enriches the display of photos across two areas of the application with album context and navigation aids. + +**Part A — Webshop order detail:** When a customer or administrator views a completed order in `OrderDownload.vue`, each purchased item currently shows only its stored `title` field. Because photo titles do not always reflect the file name and identical filenames can exist across many albums, the item list is often ambiguous and difficult to navigate. This part enriches every order item row with the **album title** from which the photo was purchased and a small **thumbnail** of the photo, making orders immediately recognisable without leaving the page. The change spans the backend (`OrderItemResource`) and the frontend (`OrderDownload.vue`). + +## Goals + +### Part A – Webshop Order Item Display + +1. Display the album title alongside the photo title for every order item in `OrderDownload.vue`. +2. Display a thumbnail image (THUMB size variant) for every order item. +3. Show a placeholder icon when the photo or its thumbnail has been deleted after purchase. +4. Fetch the new data without introducing N+1 queries (eager-load `items.photo.size_variants` and `items.album`). +5. Keep the `title` stored on `OrderItem` as the authoritative display title (historical record, even if the photo is later deleted). +6. Ensure all existing order tests continue to pass and add new tests for the enriched resource. + +## Non-Goals + +- Showing additional photo metadata (EXIF, description, tags) on the order page. +- Modifying the order *list* page (`OrderList.vue`); enrichment applies only to the order *detail/download* page. +- Changing the `OrderItem` database schema or adding new columns. +- Providing a way to navigate to the album from the order item (the existing `RouterLink` to the photo is retained as-is). +- Caching or pre-computing thumbnail URLs at order creation time. + +## Functional Requirements + +### Part A – Webshop Order Item Display + +| ID | Requirement | Success path | Validation path | Failure path | Telemetry & traces | Source | +|----|-------------|--------------|-----------------|--------------|--------------------|--------| +| FR-042-01 | `OrderItemResource` exposes a new `?string $album_title` field containing the title of the album recorded on the order item at display time. | `album_title` is populated from `$item->album?->title` when the album relation is loaded. | `OrderItemResource::fromModel()` is called with an `OrderItem` whose `album` relation is eagerly loaded; `album_title` equals the album's title. | If the album has been deleted, `album_title` is `null`. | None. | Problem statement: "the order would include the Album and title". | +| FR-042-02 | `OrderItemResource` exposes a new `?string $thumb_url` field containing the URL of the THUMB size variant of the purchased photo at display time. | `thumb_url` is populated from the photo's THUMB size variant URL when `items.photo.size_variants` is eagerly loaded. | `OrderItemResource::fromModel()` returns a non-null `thumb_url` for an item whose photo has a THUMB size variant. | If the photo has been deleted or has no THUMB variant, `thumb_url` is `null`. | None. | Problem statement: "along with a thumbnail of the image". | +| FR-042-03 | `OrderResource::fromModel()` eager-loads `items.photo.size_variants` (filtered to thumb-size variants) and `items.album` whenever the order detail is fetched, so that `album_title` and `thumb_url` are always populated without N+1 queries. | `OrderResource::fromModel()` calls `$order->load(...)` including the photo and album relations before building item resources. | Feature test asserts that only a fixed number of queries is executed when loading an order with multiple items (query count assertion or eager-load confirmation). | If relations are not loaded, `album_title` and `thumb_url` default to `null` rather than triggering lazy-load queries. | None. | NFR-042-01. | +| FR-042-04 | `OrderDownload.vue` renders a thumbnail `` element per order item using `item.thumb_url`. When `thumb_url` is `null`, a placeholder icon is shown instead. | The `` element is present with `src` equal to `item.thumb_url` and `loading="lazy"`. The placeholder (`pi pi-image` icon) is shown when `thumb_url` is `null`. | Vue component renders a thumbnail for an item that has a THUMB size variant URL; renders the icon for an item where `thumb_url` is `null`. | N/A. | None. | Problem statement: "thumbnail of the image". | +| FR-042-05 | `OrderDownload.vue` renders the album title per order item using `item.album_title`. When `album_title` is `null`, a translated fallback string (`webshop.orderDownload.unknownAlbum`) is displayed in muted style. | The album title text is visible below the photo title for each item. | Vue component renders album title text for an item with a non-null `album_title`; renders translated fallback for a null `album_title`. | N/A. | None. | Problem statement: "the order would include the Album and title". | +| FR-042-06 | A new i18n key `webshop.orderDownload.unknownAlbum` is added to all language files (or at minimum `en.json`) with value `"Unknown album"`. | `$t('webshop.orderDownload.unknownAlbum')` resolves to a non-empty string in the English locale. | i18n key present in `lang/en/` (or equivalent). | N/A. | None. | Coding convention: all new UI strings must have translation keys. | + +## Non-Functional Requirements + +### Part A – Webshop Order Item Display + +| ID | Requirement | Driver | Measurement | Dependencies | Source | +|----|-------------|--------|-------------|--------------|--------| +| NFR-042-01 | Loading an order detail page must not generate additional SQL queries per order item beyond what the eager-load produces. | Performance / N+1 prevention. | Feature test or integration test asserting that `DB::getQueryLog()` count does not grow with item count. | `OrderResource::fromModel()` eager-load strategy. | Coding conventions. | +| NFR-042-02 | Only THUMB-category size variants (`SMALL`, `SMALL2X`, `THUMB`, `THUMB2X`, `PLACEHOLDER`) are loaded for the thumbnail — not ORIGINAL or MEDIUM. | Bandwidth / memory efficiency. | Code review: `size_variants` relation constrained to the five thumb variants via `Thumb::sizeVariantsFilter()` or an equivalent inline `whereIn`. | `App\Models\Extensions\Thumb::sizeVariantsFilter()`. | Existing pattern in `Thumb::createFromQueryable()`. | +| NFR-042-03 | PHPStan level 6 must report 0 errors after changes. | Code quality gate. | `make phpstan` exits 0. | `phpstan.neon` baseline. | Coding conventions. | +| NFR-042-04 | `php-cs-fixer` must report 0 violations after changes. | Code style gate. | `vendor/bin/php-cs-fixer fix --dry-run` exits 0. | `.php-cs-fixer.php`. | Coding conventions. | +| NFR-042-05 | All existing tests must continue to pass. | Regression safety. | `php artisan test` exits 0. | SQLite test database. | Coding conventions. | +| NFR-042-06 | Frontend TypeScript compiler (`vue-tsc`) must report 0 errors. | Frontend type safety. | `npm run check` exits 0. | Generated TypeScript type definitions in `resources/js/`. | Coding conventions. | +| NFR-042-07 | Thumbnail image elements must use lazy loading (`loading="lazy"`) to avoid blocking page render. | Performance / UX. | Code review: `` element has `loading="lazy"` attribute. | Frontend component. | Web performance best practices. | + +## UI / Interaction Mock-ups — Part A (Webshop Order Item Display) + +### Order Item Row — Current Layout +``` ++----------------------------------------------------------+ +| [Title] [size_variant - license] | +| [price] | +| [Download button / input / N/A] | ++----------------------------------------------------------+ +``` + +### Order Item Row — New Layout +``` ++----------------------------------------------------------+ +| [48×48 thumb] [Title] | +| [album title / "Unknown album" (muted)] | +| [size_variant - license] | +| [Download button / input / N/A] | +| [price] | ++----------------------------------------------------------+ +``` + +### Thumbnail States +``` ++----------------+ +----------------+ +| ┌──────────┐ | | ┌──────────┐ | +| │ │ | | │ pi-image │ | +| │ (THUMB) │ | │ │ (icon) │ | +| └──────────┘ | | └──────────┘ | +| thumb present | | thumb absent | ++----------------+ +----------------+ +``` + +### Album Title Display +``` +Photo Title +Album Name ← normal text or muted "Unknown album" when null +medium - personal +``` + +## Branch & Scenario Matrix + +### Part A – Webshop Order Item Display + +| Scenario ID | Description / Expected outcome | +|-------------|--------------------------------| +| S-042-01 | **Album title present.** Order item has a valid `album_id` pointing to an existing album. `OrderItemResource.album_title` equals the album's `title`. `OrderDownload.vue` renders the album name below the photo title. | +| S-042-02 | **Album deleted after purchase.** Order item's `album_id` references a deleted album. `OrderItemResource.album_title` is `null`. `OrderDownload.vue` renders `"Unknown album"` in muted style. | +| S-042-03 | **Thumbnail present.** Order item's photo has a THUMB size variant. `OrderItemResource.thumb_url` is a non-null URL. `OrderDownload.vue` renders `` with `src=thumb_url`. | +| S-042-04 | **Photo deleted after purchase.** Order item's `photo_id` references a deleted photo. `OrderItemResource.thumb_url` is `null`. `OrderDownload.vue` renders the placeholder `pi pi-image` icon. | +| S-042-05 | **Photo exists but has no THUMB variant.** `thumb_url` is `null` (THUMB variant does not exist). Placeholder icon is shown. | +| S-042-06 | **Both album and photo present.** Full happy path: both `album_title` and `thumb_url` are non-null and rendered correctly. | +| S-042-07 | **N+1 prevention.** Loading an order with 5 items produces the same number of SQL queries as loading one item (all photo and album data is eager-loaded). | + +## Test Strategy + +### Part A – Webshop Order Item Display + +- **Backend (Unit):** + - `OrderItemResource::fromModel()` with a fully loaded `OrderItem` (album + photo + THUMB size variant): assert `album_title` and `thumb_url` are non-null. + - `OrderItemResource::fromModel()` with deleted album (`album` relation returns null): assert `album_title` is `null`. + - `OrderItemResource::fromModel()` with deleted photo / no THUMB variant: assert `thumb_url` is `null`. + +- **Backend (Feature/REST):** + - `GET /api/v2/Order/{id}` returns JSON with `items[*].album_title` and `items[*].thumb_url` fields. + - Query count assertion: loading an order with multiple items does not produce per-item SQL queries for album/photo/size_variant. + +- **Frontend (TypeScript / `npm run check`):** + - Confirm `App.Http.Resources.Shop.OrderItemResource` TypeScript interface includes `album_title: string | null` and `thumb_url: string | null` after running the TypeScript transformer. + - `npm run check` exits 0. + +## Interface & Contract Catalogue + +### Domain Objects + +| ID | Description | Modules | +|----|-------------|---------| +| DO-042-01 | `OrderItemResource` — extended with `album_title: ?string` and `thumb_url: ?string` | application, REST, UI | + +### API Routes / Services + +| ID | Transport | Description | Notes | +|----|-----------|-------------|-------| +| API-042-01 | REST GET `/api/v2/Order/{id}` | Returns `OrderResource` with enriched `items[]` containing `album_title` and `thumb_url`. | Existing endpoint; response schema extended. | + +### CLI Commands / Flags + +_None introduced._ + +### Telemetry Events + +_None introduced._ + +### Fixtures & Sample Data + +_None introduced._ + +### UI States + +| ID | State | Trigger / Expected outcome | +|----|-------|---------------------------| +| UI-042-01 | Thumbnail rendered | `item.thumb_url` is a valid URL → `` shown. | +| UI-042-02 | Thumbnail placeholder | `item.thumb_url` is `null` → `` shown. | +| UI-042-03 | Album title rendered | `item.album_title` is non-null → album title text shown in muted style below photo title. | +| UI-042-04 | Unknown album fallback | `item.album_title` is `null` → `$t('webshop.orderDownload.unknownAlbum')` rendered in muted style. | + +## Telemetry & Observability + +No new telemetry events are introduced. The feature does not change logging behaviour. + +## Documentation Deliverables + +- Update `docs/specs/4-architecture/shop-architecture.md` to mention that `OrderItemResource` now includes `album_title` and `thumb_url` for display purposes. +- Update `docs/specs/4-architecture/knowledge-map.md` if the photo thumbnail eager-loading pattern is not already documented. +- Update `docs/specs/4-architecture/roadmap.md` on completion. +- Update `docs/specs/_current-session.md`. + +## Fixtures & Sample Data + +No new fixture files are required. Existing factory-based tests (`OrderFactory`, `OrderItemFactory`, `PhotoFactory`, album factories) are sufficient. + +## Spec DSL + +```yaml +domain_objects: + - id: DO-042-01 + name: OrderItemResource + fields: + - name: album_title + type: "string|null" + constraints: "null when album deleted or not loaded" + - name: thumb_url + type: "string|null" + constraints: "null when photo deleted or no THUMB variant" +routes: + - id: API-042-01 + method: GET + path: /api/v2/Order/{id} + notes: "response.items[].album_title and response.items[].thumb_url added" +ui_states: + - id: UI-042-01 + description: Thumbnail image rendered + - id: UI-042-02 + description: Thumbnail placeholder icon rendered + - id: UI-042-03 + description: Album title text rendered + - id: UI-042-04 + description: Unknown album fallback rendered +``` + +## Appendix + +### Relevant source files — Part A (Webshop Order Item Display) + +| File | Relevance | +|------|-----------| +| `app/Http/Resources/Shop/OrderItemResource.php` | DTO to extend with `album_title` and `thumb_url`. | +| `app/Http/Resources/Shop/OrderResource.php` | `fromModel()` to extend the eager-load call. | +| `app/Models/OrderItem.php` | `photo()` and `album()` `BelongsTo` relations already defined. | +| `app/Models/Extensions/Thumb.php` | `sizeVariantsFilter()` helper for restricting size-variant eager loads to thumb variants. | +| `app/Models/SizeVariant.php` | Provides `url` attribute used to build `thumb_url`. | +| `resources/js/views/webshop/OrderDownload.vue` | Frontend view to update with thumbnail and album title. | +| `resources/js/services/webshop-service.ts` | Typed API service; auto-updated by TypeScript transformer. | +| `lang/en/` | i18n file to add `webshop.orderDownload.unknownAlbum`. | + +### TypeScript transformation note + +After modifying `OrderItemResource.php`, run `php artisan typescript:transform` (or the equivalent npm script) to regenerate the TypeScript interfaces. The resulting interface must include: +- `App.Http.Resources.Shop.OrderItemResource`: `album_title: string | null` and `thumb_url: string | null`. + +### Size variant URL pattern + +`SizeVariant` models expose a `url` attribute via `getUrlAttribute()`. For THUMB variants, `$item->photo->size_variants->getSizeVariant(SizeVariantType::THUMB)?->url` provides the value to assign to `thumb_url`. The `SizeVariants` extension is loaded via the standard eager-load with a `whereIn('type', [...])` filter mirroring `Thumb::sizeVariantsFilter()`. + diff --git a/docs/specs/4-architecture/features/042-webshop-order-item-display/tasks.md b/docs/specs/4-architecture/features/042-webshop-order-item-display/tasks.md new file mode 100644 index 00000000000..6b9913f90f4 --- /dev/null +++ b/docs/specs/4-architecture/features/042-webshop-order-item-display/tasks.md @@ -0,0 +1,144 @@ +# Feature 042 Tasks – Photo Display Enrichment + +_Status: Draft_ +_Last updated: 2026-05-31_ + +> Keep this checklist aligned with the feature plan increments. Stage tests before implementation, record verification commands beside each task, and prefer bite-sized entries (≤90 minutes). +> **Mark tasks `[x]` immediately** after each one passes verification—do not batch completions. Update the roadmap status when all tasks are done. +> When referencing requirements, keep feature IDs (`FR-`), non-goal IDs, and scenario IDs (`S-042-`) inside the same parentheses immediately after the task title. +> When new high- or medium-impact questions arise during execution, add them to [docs/specs/4-architecture/open-questions.md](docs/specs/4-architecture/open-questions.md) instead of informal notes, and treat a task as fully resolved only once the governing spec sections and, when required, ADRs reflect the clarified behaviour. + +## Checklist + +### Part A – Webshop Order Item Display + +### I1 – Extend `OrderItemResource` + +- [x] T-042-01 – Add `album_title` and `thumb_url` to `OrderItemResource` (FR-042-01, FR-042-02, DO-042-01). + _Intent:_ In `app/Http/Resources/Shop/OrderItemResource.php`, add `public ?string $album_title` and `public ?string $thumb_url` to the constructor. In `fromModel()`, populate `album_title` from `$item->album?->title` and `thumb_url` from `$item->photo?->size_variants->getSizeVariant(SizeVariantType::THUMB)?->url`. Add the `use App\Enum\SizeVariantType;` import. The `#[TypeScript()]` attribute ensures the new fields are picked up by the transformer. + _Verification commands:_ + - `vendor/bin/php-cs-fixer fix` + - `make phpstan` + _Notes:_ License header already present; do not duplicate it. Follow the existing constructor/factory pattern exactly. + +### I2 – Update eager-loading in `OrderResource` + +- [x] T-042-02 – Eager-load `items.album` and `items.photo.size_variants` (THUMB-only) in `OrderResource::fromModel()` (FR-042-03, NFR-042-01, NFR-042-02, S-042-07). + _Intent:_ In `app/Http/Resources/Shop/OrderResource.php`, extend the `$order->load(...)` call so it always loads `items.album` and `items.photo` with a constrained `size_variants` relation (only types: `SMALL`, `SMALL2X`, `THUMB`, `THUMB2X`, `PLACEHOLDER`). The load must be unconditional — not gated on order status — because album and thumbnail display is needed for all statuses on the detail page. Use an inline `whereIn('type', [...])` closure for the size variant constraint, following the pattern in `Thumb::sizeVariantsFilter()`. + _Verification commands:_ + - `vendor/bin/php-cs-fixer fix` + - `php artisan test` + - `make phpstan` + _Notes:_ Confirm the existing `items.size_variant` load (used for download URLs on CLOSED orders) is retained alongside the new loads. + +### I3 – Backend tests + +- [x] T-042-03 – Write failing test: happy path — both `album_title` and `thumb_url` non-null (S-042-06, S-042-07). + _Intent:_ In `tests/Feature_v2/` (extending `BaseApiWithDataTest`), add a test that creates a complete order with an item linked to a photo and album (both existing), calls `GET /api/v2/Order/{id}`, and asserts: + - `response.items[0].album_title` equals the album's title. + - `response.items[0].thumb_url` is a non-null string. + Write this test before implementing T-042-01 so it fails first. + _Verification commands:_ + - `php artisan test --filter=` + _Notes:_ Ensure the photo has a THUMB size variant in the factory/fixture. Mark `[x]` only after the test is green (post-implementation). + +- [x] T-042-04 – Write failing test: `album_title` is `null` when album deleted (S-042-02). + _Intent:_ Create an order item with `album_id` pointing to a soft-deleted or hard-deleted album. Call `GET /api/v2/Order/{id}` and assert `items[0].album_title` is `null`. + _Verification commands:_ + - `php artisan test --filter=` + _Notes:_ Use the existing album deletion mechanism in tests to simulate a deleted album. + +- [x] T-042-05 – Write failing test: `thumb_url` is `null` when photo deleted (S-042-04). + _Intent:_ Create an order item whose `photo_id` references a deleted photo (or set `photo_id` to `null` in the factory). Assert `items[0].thumb_url` is `null`. + _Verification commands:_ + - `php artisan test --filter=` + _Notes:_ `OrderItem.photo` is a `BelongsTo` that returns `null` when the photo is gone. + +- [x] T-042-06 – Write failing test: `thumb_url` is `null` when photo has no THUMB variant (S-042-05). + _Intent:_ Create an order item with a valid photo that has no THUMB size variant in the database. Assert `items[0].thumb_url` is `null`. + _Verification commands:_ + - `php artisan test --filter=` + _Notes:_ Create the photo without calling the size-variant generation pipeline, or delete the THUMB variant manually in the test. + +- [ ] T-042-07 – Run full backend test suite (NFR-042-03, NFR-042-05). + _Intent:_ Confirm no regressions after I1 + I2 changes. + _Verification commands:_ + - `php artisan test` + - `make phpstan` + - `vendor/bin/php-cs-fixer fix` + _Notes:_ All three must exit 0. + +### I4 – Frontend: i18n + TypeScript types + +- [x] T-042-08 – Add `webshop.orderDownload.unknownAlbum` i18n key (FR-042-06). + _Intent:_ Locate the English i18n file used for webshop strings (likely `lang/en/` or a JSON file under `resources/js/lang/`). Add `"unknownAlbum": "Unknown album"` under the `webshop.orderDownload` namespace. Check whether other language files need updating and add stub translations if so. + _Verification commands:_ + - Manual review: confirm key resolves via `$t('webshop.orderDownload.unknownAlbum')` in the Vue context. + _Notes:_ Follow the exact nesting structure already used by neighbouring keys like `webshop.orderDownload.enterContentUrl`. + +- [x] T-042-09 – Refresh TypeScript types for `OrderItemResource` (NFR-042-06). + _Intent:_ Run `php artisan typescript:transform` (or equivalent) to regenerate the TypeScript interface. Confirm `App.Http.Resources.Shop.OrderItemResource` now includes `album_title: string | null` and `thumb_url: string | null`. If the transformer command is unavailable, manually add the two fields to the interface definition file and note the manual edit here. + _Verification commands:_ + - `npm run check` — must exit 0. + _Notes:_ Do not run the transformer in isolation if it overwrites other manually-maintained types — check the project convention first. + +### I5 – Frontend: update `OrderDownload.vue` + +- [x] T-042-10 – Render thumbnail image or placeholder in order item row (FR-042-04, UI-042-01, UI-042-02). + _Intent:_ In `resources/js/views/webshop/OrderDownload.vue`, inside the `v-for="item in order.items"` loop, add before the title/notes block: + ```html + + + ``` + Adjust the outer flex container to include `items-start gap-4` so the thumbnail aligns with the text block. + _Verification commands:_ + - `npm run format` + - `npm run check` + _Notes:_ Size class `w-12 h-12` = 48 px, consistent with Tailwind/PrimeVue patterns used elsewhere in the project. Use `flex-shrink-0` to prevent thumbnail from collapsing. + +- [x] T-042-11 – Render album title or fallback in order item row (FR-042-05, UI-042-03, UI-042-04). + _Intent:_ Below the existing `RouterLink` for the photo title, add: + ```html +
{{ item.album_title ?? $t('webshop.orderDownload.unknownAlbum') }}
+ ``` + _Verification commands:_ + - `npm run format` + - `npm run check` + _Notes:_ The `??` operator is safe here because `album_title` is typed as `string | null`. Use the same `text-muted-color` class used for `size_variant_type` and `license_type` in the existing row. + +### I6 – Quality gates & documentation + +- [ ] T-042-12 – Run full quality gate (NFR-042-03 through NFR-042-07). + _Intent:_ Execute the complete gate sequence: + 1. `vendor/bin/php-cs-fixer fix` + 2. `npm run format` + 3. `npm run check` + 4. `npm run lint` + 5. `php artisan test` + 6. `make phpstan` + All six must exit 0. + _Verification commands:_ See above. + _Notes:_ If any check fails, fix before proceeding to documentation tasks. + +- [x] T-042-13 – Update `docs/specs/4-architecture/shop-architecture.md`. + _Intent:_ Add a note in the "Request/Response Pattern" or "Data Models" section that `OrderItemResource` now includes `album_title` and `thumb_url` for display purposes, and that `OrderResource::fromModel()` eager-loads photo and album relations when building the item resource. + _Verification commands:_ Manual review. + _Notes:_ Keep the addition brief (2–3 sentences). + +- [x] T-042-14 – Update `roadmap.md`: move Feature 042 from Active to Completed. + _Intent:_ Add Feature 042 row to the Completed table with today's date and a one-line summary. Remove from Active Features. Update the "Last updated" footer. + _Verification commands:_ Manual review. + _Notes:_ Follow the pattern of completed rows (e.g., Feature 037, Feature 028). + +- [x] T-042-15 – Update `docs/specs/_current-session.md`. + _Intent:_ Replace the current session content with a Feature 042 summary covering what was implemented and confirming all tasks complete. + _Verification commands:_ Manual review. + _Notes:_ Keep the session doc as the single live snapshot per session conventions. + +## Notes / TODOs + +- `SizeVariant::getUrlAttribute()` is the accessor providing the URL; if the attribute name differs in the actual model, adjust T-042-01 accordingly. +- The `$item->photo->size_variants->getSizeVariant(SizeVariantType::THUMB)` call relies on `SizeVariants` being the type of `photo->size_variants`. Verify the accessor name on `Photo` before writing T-042-01 implementation. +- If the TypeScript transformer is unavailable, document the manual interface edit in T-042-09 and add a comment to the interface file indicating it is manually maintained. +- Query-count assertion in S-042-07 / T-042-03 can be implemented with `DB::enableQueryLog()` / `DB::getQueryLog()` or Laravel's `assertQueryCount()` helper if available in the test suite. +- Do not add fallback or compatibility behaviour for historic `OrderItem` records that lack an `album_id` — the spec's Non-Goals state no fallback is required unless explicitly requested. diff --git a/docs/specs/4-architecture/roadmap.md b/docs/specs/4-architecture/roadmap.md index 84ddc3ce768..b33717efdcb 100644 --- a/docs/specs/4-architecture/roadmap.md +++ b/docs/specs/4-architecture/roadmap.md @@ -6,8 +6,6 @@ High-level planning document for Lychee features and architectural initiatives. | Feature ID | Name | Status | Priority | Assignee | Started | Updated | Progress | |------------|------|--------|----------|----------|---------|---------|----------| -| 040 | Disable Request Caching | Planning | P2 | LycheeOrg | 2026-05-18 | 2026-05-18 | Spec, plan, tasks drafted. 9 tasks across 5 increments (I1 migration, I2 feature flag + .env.example, I3 controller filter, I4 feature tests, I5 quality gates). No open questions. Ready to begin T-040-01. | -| 043 | Webshop Print & Pixel Sizes | Planning | P2 | LycheeOrg | 2026-05-31 | 2026-05-31 | Spec stub created. Blocked on 5 open questions (Q-043-01 … Q-043-05): pricing model, license-type applicability, pixel fulfillment, catalogue scope, SE gating. Plan and tasks pending question resolution. | ## Paused Features @@ -19,7 +17,10 @@ High-level planning document for Lychee features and architectural initiatives. | Feature ID | Name | Completed | Notes | |------------|------|-----------|-------| +| 042 | Photo Display Enrichment | 2026-06-12 | Part A (I1–I6) complete: `album_title` + `thumb_url` on `OrderItemResource`, unconditional eager-load in `OrderResource`, thumbnail/album-title UI in `OrderDownload.vue`, 4 backend tests passing, PHPStan 0, php-cs-fixer clean. | +| 043 | Webshop Print & Pixel Sizes | 2026-05-31 | Spec stub created. Blocked on 5 open questions (Q-043-01 … Q-043-05): pricing model, license-type applicability, pixel fulfillment, catalogue scope, SE gating. | | 041 | Upload Photo Metadata | 2026-05-31 | `title`, `description` at upload time; `expected_id` in response. New `ApplyUserProvidedMetadata` pipe, DTO chain propagation (`ImportParam → InitDTO → StandaloneDTO`), `Photo::preallocateId()`, `UploadPhotoRequest` validation, `UploadMetaResource` fields, `ProcessImageJob` serialisation. 9 feature tests passing. PHPStan 0, php-cs-fixer clean. | +| 040 | Disable Request Caching | 2026-05-18 | Spec, plan, tasks drafted. 9 tasks across 5 increments (I1 migration, I2 feature flag + .env.example, I3 controller filter, I4 feature tests, I5 quality gates). | | 037 | Admin Dashboard & `/admin/` URL Reorg | 2026-04-22 | Config migration (`use_admin_dashboard` toggle), `AdminStatsService` with 5-min cache, `GET /api/v2/Admin/Stats` endpoint, 9 admin views relocated to `views/admin/`, `AdminDashboard.vue` tile grid + stats panel + Refresh, left-menu collapse toggle, 22-locale i18n, 13 backend tests passing, TypeScript/PHPStan clean. | | 034 | Bulk Album Edit | 2026-04-12 | Spec, plan, tasks drafted. 25 tasks across 11 increments (I1 backend scaffold, I2-I6 REST endpoints, I7-I10 frontend, I11 quality gates). 4 open questions (Q-034-01 to Q-034-04; 1 high, 2 medium, 1 low). Ready to begin T-034-01 once Q-034-03 resolved. | | 032 | Security Advisories Check | 2026-04-06 | Spec, plan, tasks drafted. 18 tasks across 6 increments (I1 config/DTO, I2 fetch service, I3 diagnostic pipe, I4 REST endpoint, I5 frontend modal, I6 quality gates). All open questions resolved in spec. Ready to begin T-032-01. | @@ -113,4 +114,4 @@ features/ --- -*Last updated: 2026-05-31 (Feature 041 complete — Upload Photo Metadata)* +*Last updated: 2026-06-12 (Feature 042 Part A complete — Photo Display Enrichment, webshop order item display)* diff --git a/docs/specs/4-architecture/shop-architecture.md b/docs/specs/4-architecture/shop-architecture.md index 6f253ad35c9..cc52956fd83 100644 --- a/docs/specs/4-architecture/shop-architecture.md +++ b/docs/specs/4-architecture/shop-architecture.md @@ -42,6 +42,8 @@ The shop integration follows Laravel's request/response pattern with additional - **Resources**: Data transfer objects for API responses using Spatie Data - **Exception Handling**: Domain-specific exceptions for error cases +`OrderItemResource` exposes `album_title` (`string|null`) and `thumb_url` (`string|null`) for display purposes. `OrderResource::fromModel()` unconditionally eager-loads `items.album` and `items.photo.size_variants` (SMALL, SMALL2X, THUMB, THUMB2X, PLACEHOLDER types only) so thumbnails and album names are available across all order statuses on the detail page. + ## Access Controls Only the Lychee instance owner can set their albums and photos as purchasable. Only the photos that are in their owned albums can be set as purchasable. diff --git a/docs/specs/_current-session.md b/docs/specs/_current-session.md index e96666f0876..9581be3ad2c 100644 --- a/docs/specs/_current-session.md +++ b/docs/specs/_current-session.md @@ -1,60 +1,40 @@ # Current Session -_Last updated: 2026-05-31_ +_Last updated: 2026-06-12_ ## Active Features -**Feature 040 – Disable Request Caching** -- Status: Planning (spec + plan + tasks complete) -- Priority: P2 -- License: Open -- Started: 2026-05-18 -- Dependencies: None +None currently in progress for Part A of Feature 042. ## Session Summary -Feature 041 (Upload Photo Metadata) has been fully implemented and all quality gates pass. +### Feature 042 Part A – Webshop Order Item Display — Complete -### Feature 041: Upload Photo Metadata — Complete - -**Status:** Implementation complete. All 9 feature tests pass. PHPStan 0 errors. php-cs-fixer clean. Roadmap updated. +**Status:** All I1–I6 tasks complete. PHPStan 0 errors. php-cs-fixer clean. npm format/check/lint clean. 4 backend tests passing. **What was built:** -- New `ApplyUserProvidedMetadata` pipe (`app/Actions/Photo/Pipes/Standalone/`) — sets caller-supplied `title`/`description` on the `Photo` model before `HydrateMetadata` runs (so EXIF doesn't overwrite user input). -- `AutoRenamer` guard: skips renaming when `StandaloneDTO::$title` is non-null. -- DTO chain propagation: `ImportParam → InitDTO → StandaloneDTO` all carry `?string $title`, `?string $description`, `?string $preallocated_id`. -- `Photo::preallocateId(string $id)` + `HasRandomIDAndLegacyTimeBasedID::generateKey()` guard for ID pre-allocation. -- `UploadPhotoRequest` — `TitleRule` (max 100 chars) and `DescriptionRule` (max 1 000 chars) validation. -- `UploadMetaResource` — `?string $expected_id`, `?string $title`, `?string $description` fields added. -- `PhotoController::upload()` — generates 24-char Base64url `expected_id` on final non-zip chunk; forwards `title`/`description` to `process()`. -- `ProcessImageJob` — serialises `expected_id`, `title`, `description`; passes them through `ImportParam` to `Create`. -- Feature test: `tests/Feature_v2/Photo/UploadWithMetadataTest.php` (9 tests, 466 assertions). - -**Key implementation note:** `skip_duplicates=true` causes `ThrowSkipDuplicate` to throw `PhotoSkippedException` (HTTP 409). With `skip_duplicates=false` (default), duplicates are re-linked without error (HTTP 201). - -### Feature 040: Disable Request Caching -**Status:** spec.md + plan.md + tasks.md complete; ready to begin T-040-01. +- **`OrderItemResource`** (`app/Http/Resources/Shop/OrderItemResource.php`): added `album_title: ?string` and `thumb_url: ?string` constructor params; `fromModel()` populates from `$item->album?->title` and `$item->photo?->size_variants->getSizeVariant(SizeVariantType::THUMB)?->url`. +- **`OrderResource::fromModel()`** (`app/Http/Resources/Shop/OrderResource.php`): unconditionally eager-loads `items.album` and `items.photo.size_variants` (filtered to SMALL, SMALL2X, THUMB, THUMB2X, PLACEHOLDER types). The existing `items.size_variant` load for CLOSED orders is retained. +- **Backend tests** (`tests/Webshop/OrderManagement/OrderItemDisplayTest.php`): 4 tests covering the happy path, absent album, absent photo, and missing THUMB variant. +- **i18n** (`lang/php_en.json` + 22 other lang files): added `webshop.orderDownload.unknownAlbum` key ("Unknown album"). +- **TypeScript types** (`resources/js/lychee.d.ts`): added `album_title: string | null` and `thumb_url: string | null` to `OrderItemResource`. +- **`OrderDownload.vue`** (`resources/js/views/webshop/OrderDownload.vue`): added `` (with `loading="lazy"`) or `` placeholder before the title block; added album title line below the photo title `RouterLink`. -**No open questions.** +**Key implementation note:** `PhotoFactory::without_size_variants()` mutates factory state before the closure is bound; the closure captures the pre-clone `$this` so the flag has no effect. Worked around in the test by deleting the THUMB `SizeVariant` row directly after photo creation. ## Next Steps -1. Begin Feature 040 implementation at **T-040-01** (migration). -2. Follow tests-before-code SDD cadence through I1 → I5. +1. Implement Part B of Feature 042 (I7–I10): admin maintenance photo title links (`PhotoTitleLink.vue`, `DuplicateLine.vue`, `Moderation.vue`). Tasks T-042-16 to T-042-20 in [tasks.md](4-architecture/features/042-webshop-order-item-display/tasks.md). +2. After Part B completes, move Feature 042 to "Completed" in roadmap with final completion date. ## Open Questions -None for active features. - -## References - -**Feature 040:** -- Spec: [040-disable-request-caching/spec.md](docs/specs/4-architecture/features/040-disable-request-caching/spec.md) -- Plan: [040-disable-request-caching/plan.md](docs/specs/4-architecture/features/040-disable-request-caching/plan.md) -- Tasks: [040-disable-request-caching/tasks.md](docs/specs/4-architecture/features/040-disable-request-caching/tasks.md) +None. -**Common:** -- Roadmap: [roadmap.md](docs/specs/4-architecture/roadmap.md) -- Open questions: [open-questions.md](docs/specs/4-architecture/open-questions.md) +## Key Artefacts +- Spec: [042-webshop-order-item-display/spec.md](4-architecture/features/042-webshop-order-item-display/spec.md) +- Plan: [042-webshop-order-item-display/plan.md](4-architecture/features/042-webshop-order-item-display/plan.md) +- Tasks: [042-webshop-order-item-display/tasks.md](4-architecture/features/042-webshop-order-item-display/tasks.md) +- Roadmap: [roadmap.md](4-architecture/roadmap.md) diff --git a/resources/js/lychee.d.ts b/resources/js/lychee.d.ts index 0f8dabd4823..319a6fd94a5 100644 --- a/resources/js/lychee.d.ts +++ b/resources/js/lychee.d.ts @@ -1212,6 +1212,8 @@ declare namespace App.Http.Resources.Shop { pixel_size_id: number | null; pixel_width: number | null; pixel_height: number | null; + album_title: string | null; + thumb_url: string | null; }; export type OrderResource = { id: number; diff --git a/resources/js/views/webshop/OrderDownload.vue b/resources/js/views/webshop/OrderDownload.vue index a563376d82f..b6df911cdcf 100644 --- a/resources/js/views/webshop/OrderDownload.vue +++ b/resources/js/views/webshop/OrderDownload.vue @@ -111,15 +111,37 @@ />
-
-
+
+
+ +
{{ item.title }}
-
{{ item.size_variant_type }} - {{ item.license_type }}
+
+ {{ item.album_title ?? $t("webshop.orderDownload.unknownAlbum") }} +
+
+ {{ $t("webshop.basketList.printLabel") }}: {{ item.print_width }} × {{ item.print_height }} + {{ item.print_unit }}, {{ $t("webshop.basketList.paperType") }}: {{ item.print_paper_type }} +
+
+ {{ $t("webshop.basketList.pixelLabel") }}: {{ item.pixel_width }} × {{ item.pixel_height }} px, + {{ $t("webshop.orderSummary.license") }} {{ item.license_type }} +
+
+ {{ $t("webshop.orderSummary.size") }} {{ item.size_variant_type }}, {{ $t("webshop.orderSummary.license") }} + {{ item.license_type }} +
requirePro(); + } + + public function tearDown(): void + { + $this->resetPro(); + parent::tearDown(); + } + + // ------------------------------------------------------------------------- + // T-042-03: Happy path — both album_title and thumb_url non-null (S-042-06, S-042-07) + // ------------------------------------------------------------------------- + + public function testAlbumTitleAndThumbUrlPresentWhenPhotoAndAlbumExist(): void + { + // $this->photo1 is in $this->album1 and has all 7 size variants (incl. THUMB). + $order = Order::factory() + ->forUser($this->userMayUpload1) + ->withTransactionId(Str::uuid()->toString()) + ->withProvider(OmnipayProviderType::DUMMY) + ->withStatus(PaymentStatusType::PENDING) + ->withEmail($this->userMayUpload1->email) + ->create(); + + OrderItem::factory() + ->forOrder($order) + ->forPhoto($this->photo1) + ->forAlbum($this->album1) + ->create(); + + $response = $this->actingAs($this->userMayUpload1)->getJson("Shop/Order/{$order->id}"); + + $this->assertOk($response); + $items = $response->json('items'); + $this->assertIsArray($items); + $this->assertCount(1, $items); + + $item = $items[0]; + $this->assertEquals($this->album1->title, $item['album_title']); + $this->assertNotNull($item['thumb_url']); + $this->assertIsString($item['thumb_url']); + } + + // ------------------------------------------------------------------------- + // T-042-04: album_title is null when album deleted (S-042-02) + // ------------------------------------------------------------------------- + + public function testAlbumTitleIsNullWhenAlbumIsAbsent(): void + { + $order = Order::factory() + ->forUser($this->userMayUpload1) + ->withTransactionId(Str::uuid()->toString()) + ->withProvider(OmnipayProviderType::DUMMY) + ->withStatus(PaymentStatusType::PENDING) + ->withEmail($this->userMayUpload1->email) + ->create(); + + // Create item with album_id = null (simulating deleted album via FK set-null cascade). + OrderItem::factory() + ->forOrder($order) + ->forPhoto($this->photo1) + ->forAlbum(null) + ->create(); + + $response = $this->actingAs($this->userMayUpload1)->getJson("Shop/Order/{$order->id}"); + + $this->assertOk($response); + $items = $response->json('items'); + $this->assertCount(1, $items); + $this->assertNull($items[0]['album_title']); + } + + // ------------------------------------------------------------------------- + // T-042-05: thumb_url is null when photo deleted (S-042-04) + // ------------------------------------------------------------------------- + + public function testThumbUrlIsNullWhenPhotoIsAbsent(): void + { + $order = Order::factory() + ->forUser($this->userMayUpload1) + ->withTransactionId(Str::uuid()->toString()) + ->withProvider(OmnipayProviderType::DUMMY) + ->withStatus(PaymentStatusType::PENDING) + ->withEmail($this->userMayUpload1->email) + ->create(); + + // Create item with photo_id = null (simulating deleted photo via FK set-null cascade). + OrderItem::factory() + ->forOrder($order) + ->forPhoto(null) + ->forAlbum($this->album1) + ->create(); + + $response = $this->actingAs($this->userMayUpload1)->getJson("Shop/Order/{$order->id}"); + + $this->assertOk($response); + $items = $response->json('items'); + $this->assertCount(1, $items); + $this->assertNull($items[0]['thumb_url']); + } + + // ------------------------------------------------------------------------- + // T-042-06: thumb_url is null when photo has no THUMB size variant (S-042-05) + // ------------------------------------------------------------------------- + + public function testThumbUrlIsNullWhenPhotoHasNoThumbVariant(): void + { + $photo_without_thumb = Photo::factory() + ->owned_by($this->userMayUpload1) + ->create(); + + // Delete the THUMB size variant so only non-thumb variants remain. + SizeVariant::where('photo_id', $photo_without_thumb->id) + ->where('type', SizeVariantType::THUMB) + ->delete(); + + $order = Order::factory() + ->forUser($this->userMayUpload1) + ->withTransactionId(Str::uuid()->toString()) + ->withProvider(OmnipayProviderType::DUMMY) + ->withStatus(PaymentStatusType::PENDING) + ->withEmail($this->userMayUpload1->email) + ->create(); + + OrderItem::factory() + ->forOrder($order) + ->forPhoto($photo_without_thumb) + ->forAlbum($this->album1) + ->create(); + + $response = $this->actingAs($this->userMayUpload1)->getJson("Shop/Order/{$order->id}"); + + $this->assertOk($response); + $items = $response->json('items'); + $this->assertCount(1, $items); + $this->assertNull($items[0]['thumb_url']); + } +}