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

Multi-Branch Package Creation — Backend Contract and Plan

Companion to Multi_Branch_Package_Creation_20260909000000.md. This document describes the hh-server contract consumed by book-bite.

OwnerBackend — hh-server
Endpoint familyapi/partner/v1
StatusImplemented on the current hh-server working branch; focused RSpec passed Sep 23, 2026.

1. Scope

In scope

  • Accept a selected restaurant_ids list on package creation for Group Login staff.
  • Create one shared package with one RestaurantPackage per selected restaurant.
  • Validate restaurant access on the server.
  • Save the package, restaurant packages, and one creation submission atomically.
  • Keep new packages auto-approved.
  • Permit Group staff to edit a shared package through the existing approval workflow.
  • Prevent single-restaurant staff from editing or deactivating a shared package.
  • Keep shared-package viewing available to single-restaurant staff.
  • Preserve existing per-restaurant active and expiry behavior.

Out of scope

  • batch_id schema or batch-level submission UI.
  • A new currency compatibility rule.
  • Partial-success creation.
  • New email recipient rules or group-wide email broadcast.
  • Mobile package creation.
  • A partner package delete route.

2. Data model

No migration is required for this behavior.

Existing associations remain the source of truth:

RecordBehavior
HhPackage::PackageOne shared package definition.
HhPackage::RestaurantPackageOne row per selected restaurant; active and expiry fields remain here.
HhPackage::PackageSubmissionOne approved new_package audit row per created Package, anchored to the default restaurant and first created RestaurantPackage for the existing audit/admin-review context. No batch column is required.

The package itself remains shared through the existing package-to-restaurant-package association. A package becomes a shared package when it has more than one non-deleted RestaurantPackage.

3. API contract

3.1 Create

POST /api/partner/v1/restaurant_packages

Existing package fields remain unchanged. Group Login may add a top-level restaurant_ids list:

{
  "package_type": "ayce",
  "restaurant_ids": [101, 102, 103],
  "package": {
    "name_en": "Shared package",
    "restaurant_packages_attributes": {
      "0": {
        "start_date": "2026-09-18",
        "end_date": "2026-12-31"
      }
    }
  }
}

Rules:

  • If restaurant_ids is absent, use the existing default_restaurant single-restaurant path.
  • If restaurant_ids is present, every ID must be accessible to current_staff.
  • Multiple restaurant selection is allowed only for staff with access to multiple restaurants.
  • Duplicate IDs are normalized away; an empty selection is rejected.
  • The selected order is retained for the nested restaurant-package build where possible.
  • The server must not trust a client-submitted restaurant_id inside nested attributes over the selected list.

Success response:

{
  "success": true,
  "restaurant_package_id": 9001,
  "restaurant_package_ids": [9001, 9002, 9003],
  "submission_id": 7001
}

restaurant_package_id remains for backward compatibility; restaurant_package_ids describes all created branches. submission_id is singular because creation has one submission.

3.2 Existing read and mutation endpoints

The existing endpoints remain the integration surface:

  • GET /api/partner/v1/restaurant_packages and GET /:id continue to return packages the staff can view.
  • PATCH /api/partner/v1/restaurant_packages/:id keeps the existing edit-review flow.
  • The existing change_active action remains scoped to a RestaurantPackage.
  • The partner routes intentionally exclude destroy; do not add a delete operation for this feature.

The controller lookup for normal staff must be scoped to all restaurants accessible to that staff member, not only default_restaurant, so a single staff member can view a shared package attached to their restaurant.

4. Business rules

4.1 Create service

PartnerPackages::CreatePackageService should:

  1. Resolve restaurants from the server-validated selected IDs, or use [default_restaurant] for the legacy path.
  2. Sanitize and validate the existing package payload.
  3. Build one package and one nested restaurant package per target restaurant.
  4. Preserve the existing package-creation defaults and payment snapshot logic. The staff default restaurant remains the reference/fallback; no new per-branch payment or commission rules are introduced.
  5. Save the package and all nested records in one database transaction.
  6. Create one new_package submission with approval_status: approved, using the default restaurant and first created RestaurantPackage as the existing submission context.
  7. Publish all created restaurant packages only after all records have succeeded.

Any validation or persistence failure must roll back the whole operation. No partial package or restaurant-package rows may remain.

4.2 Package defaults

The existing PackageDefaultsEnforcer remains authoritative for package-level defaults. Multi-restaurant support must only ensure that each nested RestaurantPackage receives the intended restaurant ID. It must not invent separate payment, commission, currency, or pricing policies for each branch.

4.3 Shared-package edit and access control

Define a shared package as:

package.restaurant_packages.where(deleted_at: nil).count > 1
  • Group staff may edit it. Existing package-level draft and approval behavior applies the change to the shared package.
  • Single-restaurant staff may view it, but update and deactivate must return 403.
  • Group staff with access to only part of the package’s restaurants may still edit the whole package.
  • The API is the security boundary; FE controls are only usability controls.

The list serializer exposes attributes.restaurants for every attached non-deleted restaurant as { id, name, url }. The index action preloads those associations through RelatedRestaurantsLoader, so this must not introduce an N+1 query per package.

4.4 Active and expiry behavior

Do not move active, start_date, or end_date to the shared package. They remain per RestaurantPackage and use the existing workers/services and update semantics.

4.5 Notifications

  • New package creation remains auto-approved and does not enqueue pending-review or approval email workers.
  • Edit notifications continue to target only the staff member who submitted the edit (submission.staff.email).
  • If a future create-confirmation email is introduced, send one email to the creating staff member, not one email per restaurant package.

5. Implementation sequence

  1. Keep the existing single-restaurant create path unchanged when restaurant_ids is absent.
  2. Add server-side selected-restaurant resolution and access validation.
  3. Extend the create service with a restaurants collection while retaining the current singular reference for existing defaults.
  4. Normalize one nested restaurant-package template into one row per selected restaurant.
  5. Create the one approved submission inside the same transaction.
  6. Scope read/mutation lookup to accessible restaurants for normal staff.
  7. Add the single-staff shared-package mutation guard in the controller and service layer.
  8. Expose each package’s related restaurants in the list serializer without an N+1 query.
  9. Add request/service specs for create, view, edit, deactivate, access rejection, and rollback.

6. Test matrix

AreaRequired assertion
Legacy createExisting single-restaurant request still creates one package and one restaurant package.
Group createSelected restaurants produce one package and one published restaurant package per ID.
Access validationAny inaccessible restaurant rejects the request; no rows are created.
Single staff createMultiple selection is rejected.
AtomicityA failure leaves no package, restaurant package, or submission from the attempted create.
Submission statusOne new-package submission is approved for each create, including a multi-restaurant create.
Single staff readShared package is visible.
Single staff mutationEdit and deactivate return 403 and do not mutate records.
Group editShared package edit creates the existing pending edit submission and applies at package level after approval.
Partial accessGroup staff can edit through an accessible attached restaurant even when other attached restaurants are not accessible to that staff member.
Per-restaurant stateDeactivation and expiry remain isolated to the selected RestaurantPackage.
NotificationNo duplicate email is generated per selected restaurant; edit notification recipient remains the submitting staff member.

7. Acceptance criteria

  • One Group Login create request creates one shared package atomically.
  • Single-restaurant behavior remains backward compatible.
  • Single staff can view but cannot mutate a shared package.
  • Group staff can edit a shared package, including when access is partial.
  • Existing defaults and payment snapshot behavior remain unchanged.
  • New packages are auto-approved; edits use the existing approval workflow.
  • No batch_id, cross-currency guard, or group-wide email behavior is introduced by this feature.