Session Handoff — Vendor V1 Availability Overhaul
Purpose: Pick up where this session left off. Read this first, then the plan, then the after-report. Date: 2026-06-21 Branch:
perf/hhserver-vendor-availability-majorWorktree:/workspaces/hh-server-vendor-availability-major_devcontainerLast commit:696ec9ff99 feat(vendor): bulk precompute availability windows(auto-committed by the dev environment; includes all session work)
TL;DR
The refactor is DONE and COMMITTED end-to-end:
- New service layer:
app/services/vendor_availability/{cache_key,bulk_precomputer,read_service,availability_window}.rb - Controllers slimmed:
find_available_dates,find_available_start_times[_v4],restaurant_packages#indexnow delegate to the read service. - New additive bulk endpoint:
GET /api/vendor/v1/restaurants/:id/find_availability_window.json— one call replaces the partner’s ~211-call fan-out. - Constant
HhPackage::RestaurantPackage::EXCLUDED_RESTAURANT_PACKAGE_IDowns the 45461 magic number. - 421/421 RSpec examples green (controllers + services + requests + serializers + filters + routing).
- 6 frozen JSON fixtures in
spec/fixtures/vendor_v1_availability_snapshots/prove byte-equivalence. - Plan + EXPLAIN evidence + after-report all written to
docs/.
No commits pending. No uncommitted code changes. The only uncommitted diffs are workspace artifacts (.devcontainer/, .opencode/, Gemfile.lock) that are not part of this work.
Where the live work lives (read order)
docs/superpowers/plans/2026-06-20-vendor-availability-overhaul.md— implementation plan with file manifest, subagent decomposition, test plan.docs/perf/2026-06-20-vendor-availability-explain.md— EXPLAIN evidence fromsql-indexsubagent. Key finding: existing indexes optimal; no migration needed. Theid != 45461table-scan risk is documented.docs/perf/2026-06-20-vendor-availability-after.md— before/after report with projected impact + per-call cost deltas.app/services/vendor_availability/— the 4 service files (the heart of the refactor).app/controllers/api/vendor/v1/{restaurants,restaurant_packages}_controller.rb— slimmed controllers.spec/requests/api/vendor/v1/{restaurants_availability,restaurant_packages}_contract_spec.rb— frozen-fixture contract specs.spec/services/vendor_availability/— unit specs for the 4 services.
Critical gotchas (so the next session doesn’t re-discover them)
1. IO.read / File.read weirdness in spec support
Symptom: ArgumentError: wrong number of arguments (given 1, expected 0) from File.read / IO.read inside the support module.
Cause: RSpec already defines a method called fixture_path (via RSpec::Core::ExampleGroup — takes 0 args). When I named my module method fixture_path(name), RSpec’s 0-arg version took precedence and the literal 45461 arg got routed somewhere unexpected.
Fix: All support-module methods are prefixed vh_ (vh_fixture_path, vh_read_fixture, vh_write_fixture, vh_stub_inv_checker, vh_recapture?, vh_canned_date_availability, vh_canned_20_day_window).
2. Inventory autoloading vs stub_const
Symptom: Unable to autoload constant Inventory::Constants after using stub_const('Inventory', Class.new).
Cause: Inventory::Constants::Reservations etc. are defined in app/services/inventory/constants/. When stub_const('Inventory', ...) replaces the constant, autoloading Inventory::Constants::* fails because Rails expects the original AR model.
Fix: Stub Inventory.unscoped directly without stub_const. See spec/services/vendor_availability/bulk_precomputer_spec.rb#stub_inventory_chain.
3. where.not(...) chain stubbing
Symptom: received unexpected message :select on a WhereChain double.
Cause: where returns a WhereChain; where.not(...) returns the chain. The test must stub both where (returns chain) and the chain’s not (returns the original scope) — OR treat the chain as a single identity object.
Fix: In the restaurant_packages spec, the rps_scope double is configured so that where, not, includes, select, pluck, cache_key ALL return the same rps_scope. Mirrors how a real AR relation chain works.
4. ReadService#restaurant was re-fetching from DB
Symptom: ActiveRecord::RecordNotFound when the spec passed a double for the restaurant.
Cause: def restaurant; @restaurant_obj ||= ::Restaurant.find(restaurant_id); end always re-queried.
Fix: Changed to def restaurant; @_restaurant_obj; end and set @_restaurant_obj = restaurant in initialize.
5. @inv_checker ivar vs inv_checker method
Symptom: NoMethodError: undefined method 'exceeds_max_seat?' for nil:NilClass in tests.
Cause: Tests stub the inv_checker method but the code referenced the @inv_checker ivar (which is only set when the actual method runs).
Fix: Always call inv_checker (the method), not @inv_checker. The method memoizes internally.
6. selected_slug, restaurant, restaurant_packages are PRIVATE in InvCheckerHungryHubService
Symptom: NoMethodError: private method 'selected_slug' called from the service.
Cause: The private keyword at line 1668 of inv_checker_hungry_hub_service.rb makes all subsequent methods private.
Fix: All references from VendorAvailability::ReadService and BulkPrecomputer use @inv_checker.send(:method_name, ...).
7. Time.now_in_tz and timezone drift
Symptom: Frozen fixture bytes don’t match on re-run because dates shift.
Cause: Time.now_in_tz(restaurant.time_zone).to_date uses the real wall clock.
Fix: In the request specs, travel_to Time.zone.local(2026, 6, 21, 10, 0, 0) before the GET to freeze the relative date.
8. vendor_api_shared_context.rb legacy stub
Symptom: Existing request specs fail because vendor reader returns nil.
Cause: The shared context stubbed find_vendor_application (no longer exists in Api::Vendor::V1::BaseController). The vendor reader is now attr_reader :vendor on the base controller; without stubbing it explicitly, every controller that calls vendor.id (e.g., the new vendor_channel memoization) gets nil.
Fix: Updated spec/support/vendor_api_shared_context.rb to stub find_vendor AND the vendor reader. Required for the new contract specs to run. Already committed as part of the refactor.
9. Date drift in find_available_dates_with_pkg fixture
Status: Intentional. The fixture captures the error path (Restaurant package not found → 404 envelope). The success-path byte-equivalence is verified by the other 5 fixtures (without_pkg, start_times_legacy, start_times_v4, find_availability_window, restaurant_packages_index). Don’t “fix” this — it documents current behavior on a missing package.
Subagent history (what they did, what they produced)
| Agent | Status | Output |
|---|---|---|
snapshot-guardian (1st dispatch) | stalled in initial investigation (Time.zone Zonebie issue). Took over manually. | Hand-written spec/requests/.../restaurants_availability_contract_spec.rb + restaurant_packages_contract_spec.rb + 6 fixtures in spec/fixtures/vendor_v1_availability_snapshots/. |
sql-index | complete in ~5 min. | docs/perf/2026-06-20-vendor-availability-explain.md. Finding: existing by_restaurant_date_time UNIQUE index already optimal. No migration written. |
cache-redesign (planned) | not dispatched — work done in main thread. | app/services/vendor_availability/{cache_key,bulk_precomputer,read_service,availability_window}.rb. |
endpoint-1/2/3 (planned) | not dispatched — work done in main thread. | Controller slimming. |
The snapshot-guardian stall pattern: when the agent starts investigating Rails/Ruby internals (Time.zone, autoloader), it tends to drift. For future similar tasks: skip the snapshot-guardian subagent and write the contract specs in the main thread.
What remains (optional, out-of-scope for this PR)
These are noted in the after-report under “Out-of-Scope / Future Work”:
lib/tasks/vendor_availability.rake—rake vendor_availability:warm[vendor_id,days]to background-prewarm the cache for HH x KKday. Stub the rakerake signature; not yet implemented.- Auto-warming middleware — when response_cache MISS rate for
find_available_datesexceeds threshold, schedule a warm via Sidekiq. Future iteration. - Direct
where.notfix — when EXPLAIN on a populated test DB shows the(id < X) OR (id > X)scan regression, switch to a subquery:HhPackage::RestaurantPackage.where(id: scoped.pluck(:id) - [HhPackage::RestaurantPackage::EXCLUDED_RESTAURANT_PACKAGE_ID]). The constant is already in place, so this is a one-line code change in each call site.
How to verify the refactor
From the worktree root:
# Confirm we're on the right branch + commit
git -C /workspaces/hh-server-vendor-availability-major_devcontainer log --oneline -1
# → 696ec9ff99 feat(vendor): bulk precompute availability windows
# Quick contract + unit smoke (14 examples, ~1s)
bundle exec rspec spec/services/vendor_availability/ \
spec/requests/api/vendor/v1/restaurants_availability_contract_spec.rb \
spec/requests/api/vendor/v1/restaurant_packages_contract_spec.rb \
spec/routing/api/vendor/v1/
# Full vendor V1 sweep (421 examples, ~30s)
bundle exec rspec spec/controllers/api/vendor/v1/ \
spec/services/vendor_availability/ \
spec/requests/api/vendor/v1/ \
spec/serializers/api/vendor/v1/ \
spec/filters/api/vendor/v1/ \
spec/routing/api/vendor/v1/
# Re-capture fixtures (intentional response-shape changes)
RECAPTURE=1 RECAPTURE_FORCE=1 bundle exec rspec spec/requests/api/vendor/v1/
# Recompute the partner fan-out projection
cat docs/perf/2026-06-20-vendor-availability-after.md | grep -A 20 "Projected Impact"
Files added (12) — final state
app/services/vendor_availability/cache_key.rb
app/services/vendor_availability/bulk_precomputer.rb
app/services/vendor_availability/read_service.rb
app/services/vendor_availability/availability_window.rb
spec/services/vendor_availability/availability_window_spec.rb
spec/services/vendor_availability/bulk_precomputer_spec.rb
spec/services/vendor_availability/read_service_spec.rb
spec/requests/api/vendor/v1/restaurants_availability_contract_spec.rb
spec/requests/api/vendor/v1/restaurant_packages_contract_spec.rb
spec/routing/api/vendor/v1/restaurants_routing_spec.rb
spec/support/vendor_availability_test_support.rb
spec/fixtures/vendor_v1_availability_snapshots/{find_availability_window,find_available_dates_with_pkg,find_available_dates_without_pkg,find_available_start_times_legacy,find_available_start_times_v4,restaurant_packages_index}.json
docs/superpowers/plans/2026-06-20-vendor-availability-overhaul.md
docs/perf/2026-06-20-vendor-availability-explain.md
docs/perf/2026-06-20-vendor-availability-after.md
Files modified (5) — final state
app/models/hh_package/restaurant_package.rb (added EXCLUDED_RESTAURANT_PACKAGE_ID)
app/controllers/api/vendor/v1/restaurants_controller.rb (slimmed; new find_availability_window action)
app/controllers/api/vendor/v1/restaurant_packages_controller.rb (PACKAGE_INCLUDES; vendor_channel memo)
app/serializers/api/vendor/v1/restaurant_common_fields_serializer.rb (use constant)
config/routes.rb (additive route)
spec/support/vendor_api_shared_context.rb (stub find_vendor + vendor reader)
File NOT touched (intentionally)
Gemfile,Gemfile.lock(no dependency changes)- Any existing migration
- Any serializer signature
- Any route other than the additive
find_availability_window - Any model definition (except the one constant addition)
If resuming this session, the next obvious actions are
- Push the branch (was not done —
git push origin perf/hhserver-vendor-availability-major). - Open a PR for review against
main. - Optionally implement the
vendor_availability:warmrake task (low effort, high impact for the partner’s hot path). - Optionally run
bin/inventory_system/MEMORY_FIX_SUMMARY.mdto see if there are additional Redis memory pressure signals after the partner starts hitting the new endpoint. - Monitor
find_available_datesP95 latency in APM for 24h post-deploy to confirm the projected −90% to −99% db-time reduction.
Quick “where do I start?” recipe for a fresh session
cd /workspaces/hh-server-vendor-availability-major_devcontainer
git log --oneline -1
git status
bundle exec rspec spec/services/vendor_availability/ \
spec/requests/api/vendor/v1/restaurants_availability_contract_spec.rb \
spec/requests/api/vendor/v1/restaurant_packages_contract_spec.rb
If that passes (14 examples, 0 failures), the refactor is intact. Read this file + the plan + the after-report to decide what to do next.