Partner Cover Image Self-Serve
History
| Date | Changes | Link | By |
|---|---|---|---|
| Sep 09, 2026 | Initial KB doc created from ClickUp PRD | ClickUp Task 86d3uhmk7 / ClickUp PRD 8ca1fpw-69496 | Engineering |
| Sep 09, 2026 | Decisions resolved, glossary + ADRs written | This doc | Engineering |
| Sep 09, 2026 | Figma reviewed — design covers cover-image flows only; menu-item image edit (Flow 2) has no design yet | Figma — Add: Package Image Self-Service | Engineering |
About
| Context | Partner Portal — package/menu image management improvement |
|---|---|
| Work Type | Improvement (two new instant image-edit endpoints + a permission fix + informational badge, all on existing package infrastructure) |
| Purpose | Operational Efficiency + Usability — partners can swap package cover images and menu-item photos instantly, without Admin review or risk of destroying unrelated menu items. Partners see which packages Admin created. |
| Problem | Partners can’t reliably fix a package’s or menu item’s photo without hitting a review queue, risking an unrelated menu item getting silently deleted, or being told to go ask Admin. They have no way to tell which packages they’re even allowed to touch beyond a photo. |
| Solution | Two purpose-built, instant, creator-agnostic image-edit endpoints (cover image + single menu-item photo) that sit outside the review queue and ownership check. Plus a “Created by Admin” informational badge on package cards, and a per-restaurant Flipper flag (:partner_full_package_control) for Admin to test lifting the toggle/edit gate on specific restaurants. |
| Judge by | North star: 50% reduction in Admin manual image-update requests within 60 days of launch. Adoption: 20% of active partner restaurants use either endpoint within 90 days. |
| Sprint | TBD (2–4 weeks estimated, complex feature) |
| Platform | App (Partner Portal / hungryhubpartner, backed by api/partner/v1) |
| Design Link | Figma — Add: Package Image Self-Service (covers package cover image flows only — menu-item image edit has no design yet) |
| Related Docs | Package Creation (existing self-serve flow), Add-On System |
Glossary
Vocabulary for terms unique to this PRD. Same terms must be used across hh-server, book-bite (partner portal), and analytics — distinct, consistent naming is the whole point.
Cover Image:
The primary visual for a package, shown on package cards and detail screens. Stored on the HhPackage::Package::* subtype record. Currently has no dedicated self-serve edit path.
Avoid: “thumbnail”, “banner”.
Menu Item Image:
A photo attached to a single HhPackage::PackageMenu record within a package’s menu section. Distinguished from the package’s own cover image.
Avoid: “menu photo” (ambiguous — could mean the whole menu), “dish image”.
Created by Admin:
An informational label indicating a package was set up by Hungry Hub Admin (not the partner). Computed via !HhPackage::PackageSubmission.package_created_by_partner?(package). Affects toggle/edit permissions, but not image-edit permissions.
Avoid: “admin package”, “HH-owned”.
Creator Ownership (package_created_by_partner?):
The existing check that determines whether a partner has full toggle and non-image-field-edit control over a package. Used in UpdatePackageService and the change_active endpoint.
Avoid: “ownership check” (too vague — restaurant ownership is a separate gate).
Restaurant Ownership:
The multi-tenant isolation check: does this package/menu item belong to a restaurant this partner manages? Applies to everything in this feature, non-negotiable.
Avoid: “partner ownership” (the partner doesn’t own the package, the restaurant does).
Per-Restaurant Flipper Override (:partner_full_package_control):
A restaurant-scoped Flipper flag that, when enabled, bypasses the creator-ownership gate for a specific restaurant. Used for testing. Does not affect restaurant ownership or image-edit permissions (which are already unrestricted).
Avoid: “override flag”, “test flag” (too vague).
Instant Apply:
The behavior of the new image-edit endpoints: changes take effect immediately, no review queue, no draft state, no Admin approval required. Contrast with the existing UpdatePackageService which queues through PackageSubmission.
Destroy-by-Omission Risk:
The existing bug in the whole-package edit flow (package_menus_attributes): any existing menu item not included in the submitted array gets automatically destroyed. The new single-item menu-photo endpoint (Flow 2) is safe by construction because it touches exactly one record.
Design Decisions
| Topic | Decision | Why |
|---|---|---|
| Image-edit endpoints are instant, no review | Cover image and menu-item image edits apply immediately, bypass PackageSubmission/PackageDraftVersion entirely. | The review queue exists to catch pricing/schedule/name changes that affect customers. A photo swap doesn’t. Adding review to photo changes would eliminate the efficiency gain this feature is supposed to deliver. |
| Image-edit endpoints are creator-agnostic | No package_created_by_partner? check on Flows 1–2. The only gate is restaurant ownership. | A partner who manages a restaurant should be able to fix any package’s photo regardless of who created it. The ownership gate on toggle/edit-other-fields stays because those affect customer-facing behavior (package availability, pricing). Photos don’t. |
| Two separate edit mechanisms kept intentionally | The pre-existing whole-package package_menus_attributes path (reviewed, partner-created-only, whole-array resubmit) stays. The new single-item endpoint is the instant, safe path. | Merging them would re-introduce the destroy-by-omission risk. The safety property of Flow 2 (touches exactly one record) depends on staying separate from the array-based nested-attribute flow. |
| “Created by Admin” badge is informational only | Badge renders on package card/detail. Does not control any behavior; toggle/edit controls are driven by backend can_toggle/can_edit_other_fields booleans, not the badge. | Prevents the badge and actual permissions from drifting out of sync when the Flipper override is toggled. |
| Two independent permission gates kept separate | Restaurant ownership (multi-tenant isolation) vs. creator ownership (toggle/edit-other-fields). Image edits only check restaurant ownership. | Avoids conflating “can I fix a photo?” with “can I toggle this package off?” — they have different risk profiles and should not share a gate. |
| Rate limit per package/menu item | Soft per-record rate limit of 10 image changes per rolling 24h. | Prevents accidental loops or abuse without being restrictive enough to block normal use. |
| Cover image is optional & removable | cover_image can be empty; the design labels it “Cover Image for Marketing (Optional)” and includes a “Remove image” flow with a confirm modal. After removal, the package falls back to a placeholder and the row action returns to “Add Cover Image”. | A mandatory cover image would force partners into the review queue for packages that don’t need one. Removal must also be instant + audited, same as a change. |
🎨 Design Notes (from Figma)
Source: Figma — Add: Package Image Self-Service. Reviewed Sep 09, 2026.
What the design covers: package cover image flows only (list row actions + modal). Menu-item image edit (PRD Flow 2) has no design yet — flag for a follow-up screen.
| Aspect | Design |
|---|---|
| Entry point | Row action on the Main Package list table: “Edit Cover Image” (image present) / “Add Cover Image” (no image). Rows in Pending Review / Rejected status show no image action. |
| Modal | “Edit Cover Image” / “Add Cover Image” title; field label “Cover Image for Marketing (Optional)”; dropzone “Drop image here or click to upload”; when an image is present: “Change Image” overlay + “Remove image” link; helper text “Recommended dimensions: 420x240px (Max size: 10 MB)”; buttons Cancel / Save (Save drawn disabled/gray until an image is selected — confirm). |
| Error states | Inline red text in the modal: “File too large (Max 10MB). Please upload a smaller image.” / “Unsupported format. Please upload a supported image (JPEG, JPG, PNG, HEIC, TIFF, WEBP).” / “Upload failed. Please check your file or network, then try again.” |
| Success | Toast: “Great success!” / “Your cover image is updated.” |
| Remove flow | “Remove image” → confirm modal “Are you sure you want to remove this cover image?” → Back / Remove (red gradient). |
| “Created by Admin” | Small badge on the package row; hover tooltip: “This package was set up by the Hungry Hub team. You can still update its photo — turning it on/off or changing its other details isn’t available here. Contact your Account Manager if you need those changed.” |
| Design wobbles | Delete-confirm modal reuses a “Reject this booking” component (copy-paste, cosmetic). “change Image” is camelCased in the mock (should be “Change Image”). Save disabled state drawn on 4 of 7 modal copies — confirm intended. |
PRD-vs-design deltas to confirm before build:
- PRD text says “Edit Cover Image” lives on the package detail screen; design shows it as a list row action. Likely both are fine, but design only covers the list entry point — confirm whether the detail screen also carries the affordance.
- PRD Flow 2 (menu-item photo edit) has no design — needs a follow-up screen.
- Design marks cover image as “(Optional)” and adds a “Remove image” flow — consistent with an optional, replaceable field.
- Design hides the image action on Pending Review / Rejected rows — PRD didn’t state this; confirm whether the backend endpoint should also enforce a status gate or whether it’s UI-only.
User Stories & Functional Requirements
Flow 1: Partner Changes a Package’s Cover Image
| Field | Detail |
|---|---|
| Trigger | Partner taps “Edit Cover Image” (or “Add Cover Image” if the package has no image yet) on the package’s row in the Main Package list, or on the package’s detail screen (design shows list row; detail-screen placement to confirm — see 🎨 Design Notes). |
| Pre-conditions | Partner is logged in; the package belongs to a restaurant the partner manages (the only check that applies); the package is not soft-deleted; package status is Active or Inactive (design shows no image action on Pending Review / Rejected rows — confirm whether status gate is UI-only). Works identically for every package type (AYCE, Party Pack, Xperience, DIY Set, Set Menu, Hungry Set, Hungry at Home, Buffet Extra, Add-On) and regardless of who created it. |
| What happens | 1. “Add/Edit Cover Image” modal opens: field label “Cover Image for Marketing (Optional)”, dropzone “Drop image here or click to upload”, helper “Recommended dimensions: 420x240px (Max size: 10 MB)”. 2. Partner selects a new image from device gallery/camera. When an image is already present, the modal shows “Change Image” over the image + a “Remove image” link. 3. Client-side validation: file ≤ 10MB, format in {jpeg, jpg, png, heic, tif, tiff, webp}, reusing Restaurants::Picture’s existing constants/messages. 4. New narrow endpoint updates cover_image directly on the relevant HhPackage::Package::* subtype; does not route through PartnerPackages::UpdatePackageService or touch PackageSubmission/PackageDraftVersion. 5. Writes a lightweight audit record so the change is traceable without a review step. 6. No pending-review state, no creator check, live immediately. Save button disabled until an image is selected (per design). |
| Remove flow | “Remove image” → confirm modal “Are you sure you want to remove this cover image?” → Back / Remove. Remove clears cover_image; package returns to placeholder state; row action label returns to “Add Cover Image”. Audit entry recorded. Removal is instant + audited, same as a change. |
| What happens after | cover_image is updated; success toast “Great success! / Your cover image is updated.”; propagates through existing TouchWorker/search-sync paths the same way an Admin-side cover image change would. Audit entry recorded. |
| Watch for | Package belongs to a different restaurant → reject. Package was Admin-created → allowed anyway (“Created by Admin” badge may still show — informational only). Package has a pending edit_request submission in the old review flow → cover image change still applies instantly; pending draft’s stale cover_image snapshot could overwrite the newer image if/when that unrelated submission is later approved (flag for Eng). Upload fails → keep previous image, show retry. Unsupported format / file too large → block with inline error. Package toggled off or non-toggleable → cover image edit still works regardless. |
Flow 2: Partner Changes or Inserts a Single Menu Item’s Photo
| Field | Detail |
|---|---|
| Trigger | Partner taps “Edit” on an existing menu item’s photo within a package’s Menu tab. |
| Pre-conditions | Partner is logged in; the menu item (HhPackage::PackageMenu) belongs to a package belonging to a restaurant the partner manages; the menu item is not soft-deleted. Applies wherever a package actually has menu-item records (packages using menu_type: hh_menu, img, or a combo). Packages using menu_type: imenupro typically have no package_menus content to edit (shows empty state, not a permission block). |
| What happens | 1. Implement the currently-missing PATCH/PUT /api/partner/v1/package_menus/:id action (route exists, controller action doesn’t). Scoped strictly to the image field. 2. Verify the menu item’s package belongs to default_restaurant. 3. No creator/package_created_by_partner? check — works whether the parent package or the item was set up by Admin or partner. 4. Client-side + server-side validation uses HhPackage::PackageMenuUploader’s actual allowed formats: {jpg, jpeg, png, pdf, heic} (differs from restaurant-photo uploader: additionally allows PDF, doesn’t allow tif/tiff/webp). 5. Single-record, single-field update — no destroy-by-omission risk. 6. Applies immediately, no review queue. |
| What happens after | HhPackage::PackageMenu#image updated; image_menu_url reflects the new photo (or falls back to AdminSetting.default_hh_menu_image if cleared); MenuSectionGeneratorWorker fan-out fires as today. |
| Watch for | Menu item belongs to Admin-created package → allowed. Menu item’s package_id is nil (nested under a Menu Section via hh_menu construct) → resolve restaurant-ownership through the section’s package. Package’s menu is purely imenupro-sourced with zero package_menus records → nothing to show/edit. Menu item was soft-deleted → edit unavailable. Pending whole-package edit request for same item → last-applied-wins; flag for Eng. Menu item belongs to different restaurant → reject. Concurrent edits (partner + Admin) → last write wins for v1; log both in audit trail. |
Flow 3: Existing Whole-Package Edit Flow (Not New — Context Only)
| Field | Detail |
|---|---|
| What it is today | PATCH /api/partner/v1/restaurant_packages/:id → PartnerPackages::UpdatePackageService accepts package_menus_attributes as part of a broader package edit. Queues into PackageSubmission/PackageDraftVersion review. Partner-created packages only. |
| Why it stays | Bundled edits (e.g. update three menu photos and the schedule in one submission) are a reasonable use case that the new narrow endpoints don’t cover. |
| Risk to flag | assign_existing_package_menu_ids! matches submitted items by index_number. Any existing menu item not in the submitted array gets destroyed. This isn’t new but is a real risk worth a defensive fix. |
Flow 4: Partner Toggles Package On/Off, Edits Other Fields (With Per-Restaurant Override)
| Field | Detail |
|---|---|
| Trigger | Partner uses the existing on/off toggle or “edit package” flow (Flow 3) on a package. |
| Pre-conditions | Package’s restaurant_package exists and is addressable by the partner. |
| What happens | 1. POST /api/partner/v1/restaurant_packages/change_active currently has no ownership check — add the same package_created_by_partner? guard used in UpdatePackageService. 2. Partner-created path: unchanged, toggle flips active. 3. Admin-created path (and flag is off): return existing change_active_fail error, don’t show toggle control in UI. 4. Editing other fields continues through Flow 3 review path, which already blocks Admin-created packages. 5. Per-restaurant Flipper flag :partner_full_package_control: when on, that restaurant’s partner can toggle and edit non-image fields on any package under it, Admin-created or not. 6. Expose effective permission from backend (can_toggle, can_edit_other_fields booleans). |
| Watch for | Partners currently toggling Admin-created packages lose that ability by default → behavior change, permission tightening. Flag on: Admin-created packages become toggleable/editable, but badge stays visible. Flag off after being on: already-submitted edit requests still resolve normally. Toggling off a package with active/upcoming bookings → follows existing behavior. Photo edits remain independent of toggle/other-field permissions. |
Flow 5: Partner Sees Which Packages Admin Created
| Field | Detail |
|---|---|
| Trigger | Partner opens any package detail screen. |
| Pre-conditions | None, applies to every package view. Does not apply at menu-item level. |
| What happens | 1. Expose created_by_admin on Api::Partner::RestaurantPackageSerializer, computed as !package_created_by_partner?(package). Purely informational. 2. Render “Created by Admin” on the package card/detail screen only when true, never on menu-item cards. 3. Drive toggle and “edit other fields” controls off backend can_toggle/can_edit_other_fields booleans (Flow 4), not directly off created_by_admin, so Flipper override and badge don’t contradict. 4. Cover-image and menu-item-image controls stay enabled regardless. |
| Watch for | A partner-created package’s approval record is later altered by a data fix → package_created_by_partner? must stay single source of truth for both badge and permission booleans. Package types with no self-serve path should never show toggle or “edit other fields” affordance (not even disabled) but should always show photo-edit affordance. |
Rules
- Cover-image and menu-item-image edit controls are always rendered and enabled, everywhere, regardless of
created_by_adminor the Flipper flag. The on/off toggle and “edit other fields” controls followcan_toggle/can_edit_other_fieldsfrom the backend. - Image edits are instant-apply, always, for every package/menu item with restaurant ownership satisfied. No review queue, no creator check.
- Two edit mechanisms for menu photos, on purpose: the pre-existing whole-package path (reviewed, partner-created-only, whole-array resubmit) stays. The new single-item endpoint is the instant, safe path. Don’t merge them.
- “Created by Admin” badge is informational only. Does not control any behavior. Toggle/edit controls are driven by backend booleans.
- Two independent permission gates, don’t conflate them: (1) Restaurant ownership — applies to everything, non-negotiable. (2) Creator ownership — applies only to toggle and non-image-field edits. Image edits only check restaurant ownership.
- A soft rate limit of 10 image changes per package/menu item per rolling 24h prevents accidental loops.
- Audit trail without review: image changes bypass admin review, but are logged so Admin/Support can see who changed an image and when.
Why image edits skip the review queue
The review queue exists to catch pricing/schedule/name changes that affect what customers see and pay. A photo swap doesn’t change the booking experience, pricing, or availability. Adding review to photo changes would eliminate the efficiency gain this feature is supposed to deliver, and would leave partners in the same “wait for Admin” state this feature is designed to fix.
Why creator ownership doesn’t gate image edits
A partner who manages a restaurant should be able to fix any package’s photo regardless of who created it. The creator gate on toggle/edit-other-fields exists because those actions affect customer-facing behavior (package availability, pricing, schedule). Photos don’t. If a partner can’t fix a blurry photo on a package Admin created, they’re stuck filing a support ticket — the exact pain this feature eliminates.
How We’ll Know If This Worked
| Metric | Goal | What it tells us | How we’d check | Status |
|---|---|---|---|---|
| Admin manual image-update requests (North Star) | ↓ 50% within 60 days of launch | Whether partners are self-serving instead of asking Admin | Support/Admin ticket tagging, pre/post launch | Not started |
| Partner-initiated image edits | ≥ 20% of active partner restaurants use either endpoint within 90 days | Whether the new endpoints are actually usable | Product analytics event on successful image update | Not started |
| Failed/rejected uploads | < 5% of upload attempts | Validation/UX isn’t overly restrictive or confusing | Upload attempt vs. success/failure ratio | Not started |
change_active permission errors | No sustained spike post-launch | New ownership check doesn’t break legitimate workflows | Error rate on change_active endpoint | Not started |
| Menu items lost via Flow 3 | 0 unintended deletions/month | Pre-existing destroy-by-omission risk isn’t actively harming partners | Manual audit of PackageMenu deletions correlated with whole-package edit submissions | Not started |
Events we’ll track
| Event | Fires when |
|---|---|
partner_cover_image_updated | Package cover image is successfully updated via Flow 1 |
partner_menu_image_updated | Menu item image is successfully updated via Flow 2 |
partner_created_by_admin_badge_viewed | Partner views a package with the “Created by Admin” badge |
These names must be distinct from existing partner-portal analytics events.
Risks & What We’re Depending On
| Risk | Type | Owner | Mitigation |
|---|---|---|---|
| Permission tightening: partners currently toggling Admin-created packages lose that ability | Behavior change, not a bug | Backend eng + PM | Document in release notes; flag as intentional tightening. Flipper override available for gradual rollout. |
| Pending draft clobbers newer instant image edit | Technical edge case | Backend eng | Flag for Eng: if a pending edit_request in the old review flow has a stale cover_image snapshot, confirm it doesn’t overwrite the newer instant edit when approved. Likely needs a conditional skip on the image field during draft application. |
| Destroy-by-omission risk in Flow 3 remains | Pre-existing, not introduced by this PRD | Backend eng | This PRD’s research surfaced the risk. Worth a defensive fix (e.g. require full current list, or explicit destroy flags) but not blocking for this feature’s launch. |
change_active permission change breaks legitimate partner workflows | Delivery risk | Backend eng | Flipper override allows gradual rollout. Monitor change_active error rates post-launch. |
| App release cadence for Partner Portal UI | Delivery coordination | Frontend eng + app team | Backend flags can flip independently; UI needs an app release to reach users. Coordinate timing. |
What has to be done before launch
| Item | Owner | Needed by | Status |
|---|---|---|---|
hh-server — New PATCH/PUT /api/partner/v1/package_menus/:id action (Flow 2, image-field only) | Backend eng | Sprint TBD | Not started |
| hh-server — New cover-image endpoint (Flow 1, direct update, bypasses review; supports both set + remove) | Backend eng | Sprint TBD | Not started |
hh-server — Ownership check on change_active endpoint (Flow 4) | Backend eng | Sprint TBD | Not started |
hh-server — can_toggle / can_edit_other_fields booleans on serializer | Backend eng | Sprint TBD | Not started |
hh-server — created_by_admin on Api::Partner::RestaurantPackageSerializer | Backend eng | Sprint TBD | Not started |
| hh-server — Audit trail for instant image edits | Backend eng | Sprint TBD | Not started |
| hh-server — Rate limit (10 per 24h per package/menu item) | Backend eng | Sprint TBD | Not started |
hh-server — :partner_full_package_control Flipper flag (restaurant-scoped) | Backend eng | Sprint TBD | Not started |
hh-server — ff_partner_cover_image_selfserve_v1 global Flipper flag | Backend eng | Sprint TBD | Not started |
| book-bite — Cover image edit UI: “Add/Edit Cover Image” row action on package list (hidden on Pending Review / Rejected rows) + modal (drop/change/remove, validation, errors, success toast) | Frontend eng | Sprint TBD | Not started |
| book-bite — Cover image remove confirm modal (“Are you sure you want to remove this cover image?”) | Frontend eng | Sprint TBD | Not started |
| book-bite — Menu item image edit UI on package Menu tab (design pending) | Frontend eng | Sprint TBD | Not started |
| book-bite — “Created by Admin” badge on package cards/detail | Frontend eng | Sprint TBD | Not started |
| book-bite — Toggle / edit-other-fields permission-driven controls | Frontend eng | Sprint TBD | Not started |
Analytics — partner_cover_image_updated + partner_menu_image_updated events | FE + BE eng | Sprint TBD | Not started |
| Copy/UX writing — all toast messages, error messages, badge text for TH/SG/MY | Content / Localization | Pre-launch | Not started |
Still Open
| Question | Where we landed |
|---|---|
| Figma design link | Resolved: Figma — Add: Package Image Self-Service. Covers cover-image flows only. |
| Menu-item image edit UI (Flow 2) | Still open: no Figma design yet — needs a follow-up screen before FE can build it. |
| Cover image edit entry point: list row action vs detail screen | Design shows list row action only; PRD text mentions detail screen. Confirm whether the detail screen should also carry the affordance. |
| Status gate on image action (Pending Review / Rejected rows hide it) | Design hides the action on those rows; confirm whether the backend endpoint should also enforce a status gate or whether it’s UI-only. |
| Cover image removal flow | Design adds it; endpoint must support clearing cover_image (optional field). Confirm clear = set to nil + audit record + TouchWorker/search-sync behavior. |
Exact admin-side Restaurants::Picture uploader constants to reuse for cover image validation | Confirmed reuse; verify with Eng whether to formalize as its own uploader class or keep sharing the constant. |
| Pending draft clobbers newer instant image edit — what exactly happens? | Flagged for Eng to confirm. Likely needs conditional skip on image field during draft application. |
| Sprint assignment | TBD — PRD says 2–4 weeks, PM to confirm. |
| Real active partner restaurant count (for adoption target calculation) | PM to confirm; PRD uses illustrative ~3,000. |
| Imenupro-only packages: empty state for menu image edit | Content-availability state, not a permission block. No PackageMenu content underneath to attach a photo to. |
FAQ
(placeholder — populate post-launch from support tickets / partner feedback)