Multi-Branch Package Creation — Backend Contract and Plan
Companion to
Multi_Branch_Package_Creation_20260909000000.md. This document describes thehh-servercontract consumed by book-bite.
| Owner | Backend — hh-server |
| Endpoint family | api/partner/v1 |
| Status | Implemented on the current hh-server working branch; focused RSpec passed Sep 23, 2026. |
1. Scope
In scope
- Accept a selected
restaurant_idslist on package creation for Group Login staff. - Create one shared package with one
RestaurantPackageper 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_idschema 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:
| Record | Behavior |
|---|---|
HhPackage::Package | One shared package definition. |
HhPackage::RestaurantPackage | One row per selected restaurant; active and expiry fields remain here. |
HhPackage::PackageSubmission | One 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_idsis absent, use the existingdefault_restaurantsingle-restaurant path. - If
restaurant_idsis present, every ID must be accessible tocurrent_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_idinside 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_packagesandGET /:idcontinue to return packages the staff can view.PATCH /api/partner/v1/restaurant_packages/:idkeeps the existing edit-review flow.- The existing
change_activeaction remains scoped to aRestaurantPackage. - 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:
- Resolve
restaurantsfrom the server-validated selected IDs, or use[default_restaurant]for the legacy path. - Sanitize and validate the existing package payload.
- Build one package and one nested restaurant package per target restaurant.
- 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.
- Save the package and all nested records in one database transaction.
- Create one
new_packagesubmission withapproval_status: approved, using the default restaurant and first createdRestaurantPackageas the existing submission context. - 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
- Keep the existing single-restaurant create path unchanged when
restaurant_idsis absent. - Add server-side selected-restaurant resolution and access validation.
- Extend the create service with a
restaurantscollection while retaining the current singular reference for existing defaults. - Normalize one nested restaurant-package template into one row per selected restaurant.
- Create the one approved submission inside the same transaction.
- Scope read/mutation lookup to accessible restaurants for normal staff.
- Add the single-staff shared-package mutation guard in the controller and service layer.
- Expose each package’s related restaurants in the list serializer without an N+1 query.
- Add request/service specs for create, view, edit, deactivate, access rejection, and rollback.
6. Test matrix
| Area | Required assertion |
|---|---|
| Legacy create | Existing single-restaurant request still creates one package and one restaurant package. |
| Group create | Selected restaurants produce one package and one published restaurant package per ID. |
| Access validation | Any inaccessible restaurant rejects the request; no rows are created. |
| Single staff create | Multiple selection is rejected. |
| Atomicity | A failure leaves no package, restaurant package, or submission from the attempted create. |
| Submission status | One new-package submission is approved for each create, including a multi-restaurant create. |
| Single staff read | Shared package is visible. |
| Single staff mutation | Edit and deactivate return 403 and do not mutate records. |
| Group edit | Shared package edit creates the existing pending edit submission and applies at package level after approval. |
| Partial access | Group staff can edit through an accessible attached restaurant even when other attached restaurants are not accessible to that staff member. |
| Per-restaurant state | Deactivation and expiry remain isolated to the selected RestaurantPackage. |
| Notification | No 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.