Merge tenant branding instead of replacing it (PMS-758) #512
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/PMS-758-logo-rendering"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PMS-758. One of the three reasons the org logo never rendered, and the only one that also breaks the client's email. The SPA half is mokosh-apps #511.
The symptom
GET /api/v1/public/tenants/4a870d0c-.../logoreturns 404 right now, on the correct URL, for a tenant that has just uploaded a logo.branding.logo_urlstill points at it, so the request-form email renders a broken image rather than none, which is exactly the failure the compose-time check was written to avoid.The cause
That route answers 404 when
branding.logo_mimeis missing, and it was missing because saving the organisation settings page deleted it.brandingis one JSONB document with two writers: the settings page owns the contact keys, the logo upload ownslogo_urlandlogo_mime.update_tenantwrotebranding = $n, replacing the whole document, so whichever caller saved last erased the other's half.Typing the field as
TenantBrandingmade this worse rather than catching it. serde filled in every field the caller did not send as an explicit null, so even a merge at the SQL level would have written those nulls over the top. That is why the fix is in two parts: the update now carries a raw object, so only the keys a caller actually sent are bound, and the service merges withbranding || $n::jsonb.A key sent as an explicit null still clears, which is how the settings page empties a contact field, and is why the SPA now sends nulls instead of omitting empty values.
Non-object branding is rejected rather than stored:
||concatenates two non-objects instead of merging, and a string or an array there would leave a document no reader can destructure.The logo handlers also stop reading the document to write it back, which removes a read-modify-write that could clobber a concurrent edit from the settings page.
Testing
cargo fmt --all --check,cargo clippy --all-targets(clean), tenants suite green (20 tests). The new one walks the exact sequence from the report: write the logo's keys, then write the settings page's keys, then assert the logo's content type survived; then clear a contact with an explicit null and assert the logo is still untouched.This is a behaviour change to
PUT /tenants/{id}and/tenants/current:brandingis now a patch document rather than a replacement. Nothing else in the codebase writes it.PMS-758. `GET /api/v1/public/tenants/{id}/logo` answered 404 on a tenant that had just uploaded a logo, so the client's request-form email rendered a broken image rather than none, which is the failure the compose-time check exists to avoid. The route answers 404 when `branding.logo_mime` is missing, and it was missing because saving the organisation settings page deleted it. `branding` is one JSONB document with two writers: the settings page owns the contact keys, the logo upload owns `logo_url` and `logo_mime`. `update_tenant` wrote `branding = $n`, replacing the document, so whichever caller saved last erased the other's keys. Typing the field as `TenantBranding` made it worse rather than caught it: serde filled every field the caller did not send as an explicit null, so even a merge at the SQL level would have written those nulls over the top. The update now carries a raw object, so only the keys a caller actually sent are bound, and the service merges with `branding || $n::jsonb`. A key sent as an explicit null still clears, which is how the settings page empties a contact field. Non-object branding is rejected rather than stored: `||` concatenates two non-objects instead of merging, and a string or array there would leave a document no reader can destructure. The logo handlers now send only the two keys they own rather than reading the document to write it back, which also removes a read-modify-write that could clobber a concurrent edit from the settings page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E48b8YHWYwmaKjFBy5esXp