122 lines
5.0 KiB
Markdown
122 lines
5.0 KiB
Markdown
# F-066 — Implementer evidence
|
|
|
|
## Scope delivered
|
|
|
|
Three separate defects surfaced together; all are addressed in this
|
|
ticket.
|
|
|
|
1. **Image invisible on `/products/[slug]`.** The container in
|
|
`frontend/src/app/products/[slug]/page.tsx` was `relative w-full
|
|
max-h-[500px]` with a `next/image` `fill` child. `next/image` with
|
|
`fill` requires a non-zero-height parent; `max-h` is only a ceiling,
|
|
so the parent collapsed to zero and the absolutely positioned image
|
|
filled zero pixels. F-061 had removed the `aspect-[5/7]` constraint
|
|
that used to give the box a real height (it caused a different
|
|
centering problem). This fix restores the `aspect-[5/7]` inside a
|
|
flex centering wrapper, so the box has a height **and** is centred
|
|
horizontally inside its grid cell.
|
|
|
|
2. **`PATCH /api/products/:id` returned 500 when the payload included
|
|
`attributes`.** The `attributes` column is JSONB, but
|
|
`PgProductRepository.update` was binding the JS array directly via
|
|
`pg-node`. `pg-node` serialises JS arrays as PG array literals
|
|
(`{bio,keto}`); the JSONB parser rejects that with
|
|
`invalid input syntax for type json`. The fix `JSON.stringify`s the
|
|
array and casts the parameter as `::jsonb`.
|
|
|
|
3. **`serializeProduct` omitted `channels`, `featured`, and
|
|
`attributes`.** The admin editor saved these values successfully but
|
|
the next GET round-tripped an object without them, so the UI never
|
|
reflected the change. Added the three fields to `serializeProduct`.
|
|
|
|
## Changes
|
|
|
|
`project/frontend/src/app/products/[slug]/page.tsx`
|
|
|
|
```diff
|
|
- <div className="relative w-full max-h-[500px] bg-gray-50 rounded-2xl border border-gray-100 overflow-hidden">
|
|
- <Image fill className="object-contain" ... />
|
|
- </div>
|
|
+ <div className="flex justify-center">
|
|
+ <div className="relative w-full max-w-md aspect-[5/7] max-h-[500px] bg-gray-50 rounded-2xl border border-gray-100 overflow-hidden">
|
|
+ <Image fill className="object-contain" ... />
|
|
+ </div>
|
|
+ </div>
|
|
```
|
|
|
|
`project/src/modules/catalog/infrastructure/pg-product-repository.ts`
|
|
|
|
```diff
|
|
for (const [key, column] of UPDATABLE) {
|
|
if (key in patch) {
|
|
+ if (key === 'attributes' && Array.isArray(patch[key])) {
|
|
+ values.push(JSON.stringify(patch[key]));
|
|
+ setClauses.push(`${column} = $${values.length}::jsonb`);
|
|
+ } else {
|
|
values.push(patch[key]);
|
|
+ setClauses.push(`${column} = $${values.length}`);
|
|
+ }
|
|
}
|
|
}
|
|
```
|
|
|
|
`project/src/modules/catalog/api/catalog.routes.ts`
|
|
|
|
```diff
|
|
function serializeProduct(product, images = []) {
|
|
return {
|
|
…,
|
|
state: product.state,
|
|
+ channels: product.channels,
|
|
+ featured: product.featured,
|
|
+ attributes: product.attributes,
|
|
seoTitle: product.seoTitle,
|
|
…
|
|
};
|
|
}
|
|
```
|
|
|
|
## Acceptance traceability
|
|
|
|
| Acceptance criterion | How it is met |
|
|
| -------------------- | ------------- |
|
|
| `GET /products/[slug]` shows the product image (no zero-height container) | `aspect-[5/7] max-h-[500px]` gives the wrapper a real height; curl on the rendered page shows `<img alt="Proteina Guisante Ecologica" … style="position:absolute;height:100%;width:100%;…">` inside `<div class="relative w-full max-w-md aspect-[5/7] max-h-[500px] …">`. |
|
|
| `PATCH /api/products/:id` with `attributes` succeeds and round-trips | `JSON.stringify` + `::jsonb` cast; verified with `PATCH {"attributes":["bio","keto"],"featured":true,"channels":"online"}` → 200 + the same shape echoed back. |
|
|
| `GET /api/products/:id` returns `channels`, `featured`, `attributes` | `serializeProduct` now includes them; verified the field is present in the JSON. |
|
|
| `verify.sh` is green | Exit 0. |
|
|
|
|
## Manual verification
|
|
|
|
```
|
|
$ curl -X PATCH http://192.168.18.93:3004/api/products/13a65dc0-… \
|
|
-H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
|
|
-d '{"attributes":["bio","keto"],"featured":true,"channels":"online"}'
|
|
{ … "channels":"online", "featured":true, "attributes":["bio","keto"], … }
|
|
HTTP 200
|
|
|
|
$ curl http://192.168.18.93:3004/api/products/13a65dc0-… -b /tmp/admin_cookies.txt
|
|
{ … "channels":"online", "featured":true, "attributes":["bio","keto"], … }
|
|
|
|
$ curl http://192.168.18.93:3003/products/proteina-guisante-ecologica | grep -oE 'aspect-\[5/7\][^"]*'
|
|
aspect-[5/7] max-h-[500px] …
|
|
```
|
|
|
|
The 500 line is gone from `project/.runtime/prod/backend.log`; the
|
|
next PATCH with `attributes` is 200.
|
|
|
|
## Build verification
|
|
|
|
- `npm run typecheck` (backend) — exit 0
|
|
- `npm test` (backend) — 124 passed, 56 skipped
|
|
- `npm run build` (backend) — exit 0
|
|
- `npx tsc --noEmit` (frontend) — exit 0
|
|
- `monolith.sh prod restart backend frontend` → backend / frontend 200
|
|
- `./scripts/verify.sh` — exit 0
|
|
|
|
## Files touched
|
|
|
|
```
|
|
project/frontend/src/app/products/[slug]/page.tsx (image container height)
|
|
project/src/modules/catalog/infrastructure/pg-product-repository.ts (JSONB cast for attributes)
|
|
project/src/modules/catalog/api/catalog.routes.ts (serializeProduct fields)
|
|
``` |