Block Request Group Handling Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Move group block handling from Api::Partner::V1::InventoriesController#block to Api::Partner::V1::BlockRequestsController#create, revert the inventory controller to main, and make group block operations atomic.
Architecture: Controller-level dispatch decides between single-restaurant (BlockRequest::SubmitService) and group (BlockRequest::GroupSubmitService) paths. GroupSubmitService wraps flagged-request creation and direct inventory blocks in one transaction, raising a custom error on any failure so the whole group rolls back.
Tech Stack: Ruby on Rails, RSpec, ActiveRecord, Trailblazer InventoryUpdater
File Structure
| File | Responsibility |
|---|---|
app/controllers/api/partner/v1/block_requests_controller.rb | Dispatches create to single or group handler; renders JSON; runs ChangeTracker for direct blocks in group mode. |
app/controllers/api/partner/v1/inventories_controller.rb | Reverted to main branch: single-restaurant block only, no group logic. |
app/services/block_request/group_submit_service.rb | Partitions restaurants, creates pending requests for flagged restaurants, direct-blocks the rest, all inside a transaction. |
app/services/block_request/group_submit_error.rb | Custom error raised inside the transaction to trigger rollback while preserving the failure message. |
spec/requests/api/partner/v1/block_requests_spec.rb | Adds request specs for group block scenarios. |
spec/requests/api/partner/v1/inventories_spec.rb | Removes group-block specs; keeps single-restaurant specs. |
spec/services/block_request/group_submit_service_spec.rb | Adds atomic rollback spec. |
Task 1: Revert InventoriesController#block to main
Files:
- Modify:
app/controllers/api/partner/v1/inventories_controller.rb:95-258
Context: The current branch added handle_group_block and changed how restaurant_id is parsed. We will restore the main branch version of the block action and remove the private handle_group_block helper.
- Step 1: Replace the
blockaction and helpers with themainbranch version
Replace lines 95–258 with the following (matches main):
def block
valid_params = params.require(:data).require(:attributes).permit(:start_date, :end_date,
:start_time, :end_time, :reason, :restaurant_id,
:service_type)
restaurant_id = valid_params[:restaurant_id].presence || default_restaurant.id
restaurant_ids = restaurant_id == 'all' ? restaurants.pluck(:id) : [restaurant_id]
target_restaurant = restaurant_id == 'all' ? default_restaurant : Restaurant.find(restaurant_id)
if block_request_required?(target_restaurant, valid_params[:start_date])
handle_flagged_block_request(target_restaurant, valid_params)
else
@change_tracker.track_block_inventory(valid_params, restaurant_id)
service = PartnerService::Inventories::BlockService.new(params, restaurant_ids).call
if service.success?
@change_tracker.notify_managers default_restaurant || Restaurant.find(restaurant_id)
render json: { success: true, message: service.message }
else
render json: { success: false, message: service.message }, status: :unprocessable_entity
end
end
end
Keep block_request_required? and handle_flagged_block_request as they are on main. Remove handle_group_block entirely.
- Step 2: Verify the diff against
mainfor the controller
Run:
git diff main -- app/controllers/api/partner/v1/inventories_controller.rb
Expected: only unrelated differences remain (if any); the block action and helpers match main.
- Step 3: Commit
git add app/controllers/api/partner/v1/inventories_controller.rb
git commit -m "revert: restore InventoriesController#block to main branch behavior
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 2: Add custom rollback error for group submissions
Files:
-
Create:
app/services/block_request/group_submit_error.rb -
Step 1: Create the error class
# typed: ignore
# frozen_string_literal: true
module BlockRequest
# Raised inside GroupSubmitService when any part of a group block fails.
# The message is surfaced to the caller; raising triggers an ActiveRecord rollback.
class GroupSubmitError < StandardError
end
end
- Step 2: Commit
git add app/services/block_request/group_submit_error.rb
git commit -m "feat: add GroupSubmitError for atomic group block rollback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 3: Make GroupSubmitService atomic
Files:
-
Modify:
app/services/block_request/group_submit_service.rb:23-70 -
Step 1: Wrap the entire operation in a transaction and raise on failure
Replace the call method and submit_flagged_requests with:
# Processes group block by creating pending requests for flagged restaurants
# and directly blocking non-flagged ones. The whole operation is atomic.
#
# @return [ServiceResult] success with block_requests and direct_block data,
# or failure with error messages
def call
ActiveRecord::Base.transaction do
flagged, direct_block_ids = partition_restaurants
block_requests = submit_flagged_requests(flagged)
direct_result = perform_direct_blocks(direct_block_ids)
build_success_result(block_requests, direct_block_ids, direct_result)
end
rescue BlockRequest::GroupSubmitError => e
ServiceResult.failure(errors: [e.message], message: e.message)
end
Replace submit_flagged_requests with:
# Creates pending block requests for each flagged restaurant.
# Raises GroupSubmitError on any failure so the outer transaction rolls back.
def submit_flagged_requests(flagged_restaurants)
requests = []
flagged_restaurants.each do |restaurant|
result = BlockRequest::SubmitService.new(
restaurant: restaurant,
staff: @staff,
params: @params,
).call
if result.success?
requests << result.data
else
raise BlockRequest::GroupSubmitError, "#{restaurant.name}: #{result.message}"
end
end
requests
end
perform_direct_blocks already returns a ServiceResult on failure. Update it to raise the same error:
# Directly blocks inventory for non-flagged restaurants.
def perform_direct_blocks(restaurant_ids)
return nil if restaurant_ids.empty?
errors = []
success_messages = []
restaurant_ids.each do |restaurant_id|
operation = InventoryUpdater.call(
{
restaurant_id: restaurant_id,
start_date: @params[:start_date].to_date,
end_date: adjusted_end_date,
start_time: @params[:start_time],
end_time: @params[:end_time],
quantity_available: 0,
reason: @params[:reason],
},
'settings' => { notify_staff: true },
)
if operation.success?
msg = operation['result.success_message'].presence || 'success'
success_messages << msg
else
msg = operation['result.fail_message'].presence || 'Unknown error'
errors << msg
end
end
if errors.any?
raise BlockRequest::GroupSubmitError, errors.uniq.to_sentence
else
success_messages.uniq.to_sentence
end
end
- Step 2: Run the existing service specs
Run:
bundle exec rspec spec/services/block_request/group_submit_service_spec.rb
Expected: existing specs still pass (they assert failure on error; raising still produces a failure result).
- Step 3: Commit
git add app/services/block_request/group_submit_service.rb
git commit -m "feat: make GroupSubmitService atomic with all-or-nothing rollback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 4: Extend BlockRequestsController#create for group blocks
Files:
-
Modify:
app/controllers/api/partner/v1/block_requests_controller.rb -
Step 1: Rewrite
createand add private helpers
Replace create with:
def create
attributes = block_request_params
restaurant_id = attributes[:restaurant_id]
if restaurant_id.blank? || restaurant_id == 'all'
handle_group_block(attributes)
else
handle_single_block(attributes)
end
end
span_method :create
Add these private methods before block_request_params:
def handle_single_block(attributes)
restaurant = if attributes[:restaurant_id].present?
current_staff.restaurants.find(attributes[:restaurant_id])
else
current_staff.default_restaurant
end
result = BlockRequest::SubmitService.new(
restaurant: restaurant,
staff: current_staff,
params: attributes,
).call
if result.success?
render json: ::Api::Partner::BlockAllotmentRequestSerializer.new(
result.data,
set_options,
).as_json, status: :created
else
error(result.message, :unprocessable_entity)
end
end
def handle_group_block(attributes)
result = BlockRequest::GroupSubmitService.new(
restaurants: current_staff.restaurants.to_a,
staff: current_staff,
params: attributes,
).call
if result.success?
track_and_notify_group_block(result.data, attributes)
render_group_block_response(result.data)
else
error(result.message, :unprocessable_entity)
end
end
def track_and_notify_group_block(data, attributes)
return if data[:direct_block_ids].empty?
change_tracker = PartnerService::ChangeTracker.new(current_staff)
change_tracker.track_block_inventory(attributes, 'all')
change_tracker.notify_managers(current_staff.default_restaurant)
end
def render_group_block_response(data)
response = ::Api::Partner::BlockAllotmentRequestSerializer.new(
data[:block_requests],
set_options,
).as_json
response[:direct_block_ids] = data[:direct_block_ids]
render json: response, status: :created
end
- Step 2: Run request specs for block_requests
Run:
bundle exec rspec spec/requests/api/partner/v1/block_requests_spec.rb
Expected: existing specs pass; group-block specs added later will fail until Task 6.
- Step 3: Commit
git add app/controllers/api/partner/v1/block_requests_controller.rb
git commit -m "feat: handle single and group block in BlockRequestsController
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 5: Remove group-block specs from inventories_spec.rb
Files:
-
Modify:
spec/requests/api/partner/v1/inventories_spec.rb:53-145 -
Step 1: Delete group-block contexts and keep single-restaurant tests
Remove the entire context 'with restaurant_id == all (group block) and multiple flagged restaurants' block (lines 53–145). Keep only the context 'with a single flagged restaurant' and context 'with a non-flagged restaurant' blocks.
- Step 2: Run the remaining inventory block specs
Run:
bundle exec rspec spec/requests/api/partner/v1/inventories_spec.rb
Expected: all remaining specs pass.
- Step 3: Commit
git add spec/requests/api/partner/v1/inventories_spec.rb
git commit -m "test: remove group-block specs from inventories request spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 6: Add group-block request specs
Files:
-
Modify:
spec/requests/api/partner/v1/block_requests_spec.rb -
Step 1: Add group-block contexts after the existing
POST /api/partner/v1/block_requestsdescribe block
Append the following inside describe 'POST /api/partner/v1/block_requests' (after line 360, before describe 'PATCH ...'):
context 'with restaurant_id == all (group block)' do
let(:other_restaurant) do
create(:valid_restaurant, city: city, country: country).tap { |r| r.update_column(:flag, true) }
end
let(:non_flagged_restaurant) do
create(:valid_restaurant, city: city, country: country, flag: false)
end
let(:staff) do
create(:staff).tap do |s|
create(:staff_role, staff: s, restaurant: restaurant)
create(:staff_role, staff: s, restaurant: other_restaurant)
end
end
let(:group_body) do
{
data: {
attributes: {
start_date: (Date.current + 5.days).to_s,
end_date: (Date.current + 7.days).to_s,
start_time: '10:00',
end_time: '22:00',
reason: 'Restaurant renovation',
restaurant_id: 'all',
},
},
}
end
before do
success_op = double('TrailblazerOperation')
allow(success_op).to receive(:success?).and_return(true)
allow(success_op).to receive(:[]).with('result.success_message').and_return('Inventory blocked')
allow(InventoryUpdater).to receive(:call).and_return(success_op)
end
it 'creates pending block requests for all flagged restaurants' do
expect do
post '/api/partner/v1/block_requests',
params: group_body,
headers: auth_headers
end.to change(BlockAllotmentRequest, :count).by(2)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body, symbolize_names: true)
expect(json[:data].length).to eq(2)
expect(BlockAllotmentRequest.pluck(:restaurant_id)).to contain_exactly(
restaurant.id, other_restaurant.id
)
end
context 'when one restaurant is not flagged' do
let(:staff) do
create(:staff).tap do |s|
create(:staff_role, staff: s, restaurant: restaurant)
create(:staff_role, staff: s, restaurant: non_flagged_restaurant)
end
end
it 'creates pending request for flagged and direct blocks non-flagged' do
expect do
post '/api/partner/v1/block_requests',
params: group_body,
headers: auth_headers
end.to change(BlockAllotmentRequest, :count).by(1)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body, symbolize_names: true)
expect(json[:data].first[:attributes][:status]).to eq('pending')
expect(json).to have_key(:direct_block_ids)
expect(json[:direct_block_ids]).to include(non_flagged_restaurant.id)
end
end
context 'when all restaurants are non-flagged' do
let(:staff) do
create(:staff).tap do |s|
create(:staff_role, staff: s, restaurant: non_flagged_restaurant)
end
end
let(:group_body) do
{
data: {
attributes: {
start_date: (Date.current + 5.days).to_s,
end_date: (Date.current + 7.days).to_s,
start_time: '10:00',
end_time: '22:00',
reason: 'Restaurant renovation',
restaurant_id: 'all',
},
},
}
end
it 'directly blocks all restaurants without pending requests' do
expect do
post '/api/partner/v1/block_requests',
params: group_body,
headers: auth_headers
end.not_to change(BlockAllotmentRequest, :count)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body, symbolize_names: true)
expect(json[:data]).to be_empty
expect(json[:direct_block_ids]).to contain_exactly(non_flagged_restaurant.id)
end
end
context 'when one flagged submission fails' do
let(:staff) do
create(:staff).tap do |s|
create(:staff_role, staff: s, restaurant: restaurant)
create(:staff_role, staff: s, restaurant: other_restaurant)
end
end
before do
call_count = 0
allow(BlockRequest::SubmitService).to receive(:new) do |**kwargs|
call_count += 1
submit_double = instance_double(BlockRequest::SubmitService)
if call_count == 1
allow(submit_double).to receive(:call).and_return(
ServiceResult.success(data: create(:block_allotment_request,
restaurant: kwargs[:restaurant],
requested_by_staff: staff)),
)
else
allow(submit_double).to receive(:call).and_return(
ServiceResult.failure(errors: ['Duplicate'], message: 'already exists'),
)
end
submit_double
end
end
it 'rolls back all changes and returns unprocessable entity' do
expect do
post '/api/partner/v1/block_requests',
params: group_body,
headers: auth_headers
end.not_to change(BlockAllotmentRequest, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
context 'with blank restaurant_id (group block by default)' do
let(:other_restaurant) do
create(:valid_restaurant, city: city, country: country).tap { |r| r.update_column(:flag, true) }
end
let(:staff) do
create(:staff).tap do |s|
create(:staff_role, staff: s, restaurant: restaurant)
create(:staff_role, staff: s, restaurant: other_restaurant)
end
end
let(:group_body) do
{
data: {
attributes: {
start_date: (Date.current + 5.days).to_s,
end_date: (Date.current + 7.days).to_s,
start_time: '10:00',
end_time: '22:00',
reason: 'Restaurant renovation',
},
},
}
end
before do
success_op = double('TrailblazerOperation')
allow(success_op).to receive(:success?).and_return(true)
allow(success_op).to receive(:[]).with('result.success_message').and_return('Inventory blocked')
allow(InventoryUpdater).to receive(:call).and_return(success_op)
end
it 'creates pending block requests for all flagged restaurants' do
expect do
post '/api/partner/v1/block_requests',
params: group_body,
headers: auth_headers
end.to change(BlockAllotmentRequest, :count).by(2)
expect(response).to have_http_status(:created)
end
end
- Step 2: Run the block_requests request specs
Run:
bundle exec rspec spec/requests/api/partner/v1/block_requests_spec.rb
Expected: all specs pass.
- Step 3: Commit
git add spec/requests/api/partner/v1/block_requests_spec.rb
git commit -m "test: add group block request specs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 7: Add atomic rollback service spec
Files:
-
Modify:
spec/services/block_request/group_submit_service_spec.rb -
Step 1: Add a rollback assertion to the existing failure context
In context 'when one flagged submission fails' (around line 141), change the example to:
it 'returns failure and rolls back all created requests' do
expect { service.call }.not_to change(BlockAllotmentRequest, :count)
result = service.call
expect(result).not_to be_success
expect(result.message).to include('already exists')
end
Add a new context for direct-block rollback:
context 'when direct block fails after a pending request was created' do
let(:restaurants) { [flagged_restaurant, non_flagged_restaurant] }
before do
failure_op = double('TrailblazerOperation')
allow(failure_op).to receive(:success?).and_return(false)
allow(failure_op).to receive(:[]).with('result.fail_message').and_return('Inventory update failed')
allow(InventoryUpdater).to receive(:call).and_return(failure_op)
end
it 'rolls back the pending request' do
expect { service.call }.not_to change(BlockAllotmentRequest, :count)
result = service.call
expect(result).not_to be_success
expect(result.message).to include('Inventory update failed')
end
end
- Step 2: Run the service specs
Run:
bundle exec rspec spec/services/block_request/group_submit_service_spec.rb
Expected: all specs pass.
- Step 3: Commit
git add spec/services/block_request/group_submit_service_spec.rb
git commit -m "test: assert atomic rollback in GroupSubmitService
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
Task 8: Final verification
- Step 1: Run the full related test suite
Run:
bundle exec rspec spec/requests/api/partner/v1/block_requests_spec.rb spec/requests/api/partner/v1/inventories_spec.rb spec/services/block_request/group_submit_service_spec.rb spec/services/block_request/submit_service_spec.rb
Expected: all specs pass.
- Step 2: Check diff for correctness
Run:
git diff --stat
Expected changes:
-
app/controllers/api/partner/v1/block_requests_controller.rb -
app/controllers/api/partner/v1/inventories_controller.rb -
app/services/block_request/group_submit_error.rb(new) -
app/services/block_request/group_submit_service.rb -
spec/requests/api/partner/v1/block_requests_spec.rb -
spec/requests/api/partner/v1/inventories_spec.rb -
spec/services/block_request/group_submit_service_spec.rb -
Step 3: Final commit (if not already committed)
If all tests pass and changes are committed per-task, no additional commit is needed.
Spec Coverage Checklist
| Spec Requirement | Implementing Task |
|---|---|
Revert InventoriesController#block to main | Task 1 |
BlockRequestsController#create dispatches single vs group | Task 4 |
Group block handles restaurant_id == 'all' | Task 4, 6 |
Group block handles blank restaurant_id | Task 4, 6 |
| Group block is atomic (all-or-nothing) | Task 2, 3, 7 |
ChangeTracker moved to BlockRequestsController | Task 4 |
Specs moved from inventories_spec to block_requests_spec | Task 5, 6 |
Notes
current_staff.restaurantsis used for group block instead of therestaurantshelper because the helper treats'all'as a literal restaurant ID and would raiseNotAuthorized.BlockRequest::SubmitServicealready validates flagged status, date range, reason, and duplicates.GroupSubmitServicerelies on those validations and rolls back the whole group if any single submission fails.ChangeTrackernotifications are only sent when there are direct blocks (direct_block_ids.any?), matching the original behavior inInventoriesController.