Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Partner Cover Image Self-Serve — Operational Runbook

Ship: Sep 2026. Endpoint PATCH/DELETE /api/partner/v1/restaurant_packages/:id/cover_image and PATCH /api/partner/v1/package_menus/:id.


Flipper flags

FlagTypeDefaultPurpose
ff_partner_cover_image_selfserve_v1globaldisabledMaster switch on both image endpoints. When disabled, endpoints return HTTP 501.
partner_full_package_controlper-restaurant actordisabledOpts a single restaurant back into Admin-created package management. Affects change_active and serializer can_toggle / can_edit_other_fields.

Enable the global feature flag

Flipper.enable(:ff_partner_cover_image_selfserve_v1)

Opt a single restaurant into Admin-created package management

Flipper.enable(:partner_full_package_control, Restaurant.find(123))

Verify state

Flipper.enabled?(:ff_partner_cover_image_selfserve_v1)
Flipper.enabled?(:partner_full_package_control, Restaurant.find(123))

Audit log queries

Every successful image set/remove writes one row to hh_package_package_image_audit_logs. The same table backs the rate-limit query, so a single source of truth exists for both billing/audit and throttling.

Recent edits for a specific package

HhPackage::PackageImageAuditLog
  .for_target(restaurant_package)
  .order(created_at: :desc)
  .limit(20)
  .pluck(:action_type, :restaurant_id, :staff_id, :created_at)

All edits in last 24h for a restaurant

HhPackage::PackageImageAuditLog
  .where(restaurant_id: restaurant.id)
  .where('created_at >= ?', 24.hours.ago)
  .count

Identify rate-limited partners

Rate-limit failures emit BUSINESS_LOGGER entries via the controller’s handle_service_failure path. Filter by message:

HhPackage::PackageImageAuditLog
  .where(action_type: nil) # placeholder; use log search instead

The controller’s BUSINESS_LOGGER calls do not currently emit a structured “rate_limited” log line. Add one if you need alerting — see app/controllers/api/partner/v1/restaurant_packages_controller.rb#cover_image as the right place to instrument.


Uploader change (admin impact)

HhPackage::Package::Base previously mounted TncUploader on the cover_image column; this PR replaces it with HhPackage::CoverImageUploader.

Behaviour changes for Admin:

  • Whitelist narrower: PDF no longer accepted. Allowed extensions: [jpg, jpeg, png, heic, tif, tiff, webp].
  • Size limit enforced: 1 byte .. 10 MB (previously unlimited for non-birthday covers).

Existing cover images on disk are not migrated — they keep rendering. New uploads must conform.


Rate-limit rationale

10 changes per record per rolling 24h in production (configurable via AdminSetting.partner_image_rate_limit_per_24h, default 100 to allow broader staging testing). The threshold is enforced via HhPackage::PackageImageAuditLog.recent_changes_for(record, limit: <config>). Count is on image_target_type, image_target_id, created_at. Reset semantics: rolling window — once the oldest in-window entry ages out (≥ 24h old), a new edit becomes available immediately.

Retry-After header value: seconds until the oldest in-window entry exits the window, clamped to [0, 86400].

Adjusting the limit (no deploy needed)

AdminSetting.partner_image_rate_limit_per_24h = 100  # staging
AdminSetting.partner_image_rate_limit_per_24h = 10   # production

Unblocking a rate-limited partner in staging

Two options:

  1. Raise the limit (preferred): AdminSetting.partner_image_rate_limit_per_24h = 100 — instantly allows up to 100 changes/24h.
  2. Clear audit rows for the specific record:
    HhPackage::PackageImageAuditLog
      .for_target(HhPackage::RestaurantPackage.find(5155))
      .delete_all
    

Rollback plan

  1. Disable the global flag: Flipper.disable(:ff_partner_cover_image_selfserve_v1).
  2. The new endpoints immediately return HTTP 501.
  3. change_active keeps the new tightened behaviour (no flag gate). If restaurants need to be opted back in, enable partner_full_package_control per-restaurant. Or revert the controller action to the previous behaviour by removing the HhPackage::PackageSubmission.package_created_by_partner? guard and the Flipper.enabled?(:partner_full_package_control, …) check.
  4. The audit table can stay — it’s write-only and harmless if unused.