Skip to content

Upgrade Rails to 8.1 and Ruby to 3.4.9 - #1338

Open
warp wants to merge 65 commits into
masterfrom
upgrade/rails-8.1
Open

Upgrade Rails to 8.1 and Ruby to 3.4.9#1338
warp wants to merge 65 commits into
masterfrom
upgrade/rails-8.1

Conversation

@warp

@warp warp commented May 5, 2026

Copy link
Copy Markdown
Contributor

Builds on #1337 upgrades Rails to 8.1 and Ruby to 3.4.9

Summary by CodeRabbit

  • New Features

    • Soft-delete support and expanded audit/history fields
    • Token generation accepts extra custom parameters
    • Internal email notifier for system messages
  • Bug Fixes

    • Skip blank fuzzy/search queries to avoid unnecessary work
    • Safer association/reflection handling to prevent errors
    • Stable pagination ordering for list endpoints
  • Chores

    • Upgraded Ruby and Rails; CI, container and deploy configs updated
    • Replaced legacy state machine library with modern alternative
  • Tests

    • In-memory ActiveJob test adapter and widespread spec updates

warp and others added 28 commits May 5, 2026 20:39
Updates version pins across Docker, CircleCI, and deploy scripts, and enables YAML alias loading where Psych now requires it.
Stop using apt-archive.postgresql.org (no Jammy Release file) and install postgresql-client from Ubuntu packages so apt update succeeds.
Use the ActiveRecord migration compatibility signature (2 positional args + keyword options) to avoid Ruby 3 keyword-arg arity errors.
Load donor conditions YAML in a Psych 4 compatible way and call I18n.t with keyword args to avoid Ruby 3 arity failures.
Replace the unmaintained state_machine gem with state_machines-activerecord and remove the legacy initializer patch. Also fix Ruby 3 keyword-arg and factory issues uncovered during the migration.
Enqueue Twilio jobs with keyword args while remaining compatible with callers
passing an options hash, and update package controller specs to use keyword
params for Rails.
Allow Token generation helpers and OrganisationsUserBuilder to accept either a
hash or keyword args, and update requested packages controller specs to use
Rails keyword params.
Avoid asserting a specific order for shareable offers when including expired
shareables; the query does not guarantee ordering.
Make user filtering handle symbol/string param keys, stabilize holidays available
_dates spec by freezing time, and stub appstore reviewer login number in safe
delete specs.
Flatten YAML-derived permissions in role factory, tighten role filtering to
active roles, make the system user setup idempotent, avoid rspec-mocks stubbing
outside the lifecycle, and respect injected PORT in Procfile.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Order shareables by id before paginating to ensure stable page boundaries, and
update the message subscriptions concern spec to reference the correct module
and avoid building unrelated records.
Avoid flaky failures when other published packages match the generic
substring "towel" in notes or related search fields.
Bump Ruby version pins across CI, Docker, and deploy/runtime scripts.
Bump Rails and related dependencies, updating the lockfile for Ruby 3.4.
Add compatibility shims and initializer ordering fixes needed to boot under Ruby 3.4 and Rails 8.1, and update cache key timestamp formatting.
Ruby 3.4 ships CSV as a default gem; add it explicitly so rake tasks that
require csv load correctly in CI.
- OrderCodeGenerator: use POSIX-safe [0-9] for PostgreSQL ~; inline
  SUBSTRING start index in SQL so .maximum does not mis-bind placeholders.
- AutoFavourite: resolve associations via reflect_on_association(name.to_sym)
  because _reflections keys are symbols in Rails 8.
Rails 8 removes Rails.application.secrets; expose secrets from
config/secrets.yml via config_for so JWT and other callers keep working.
- Use alias_method for revisions (Rails 8 restricts alias_attribute to columns).
- Query StocktakeRevision by stocktake_id so processing always sees current DB
  rows; avoids stale association cache skipping revisions and closing incorrectly.
…e.mail

Rails 8 removes the old mail helper API from ActionMailer::Base.
Add InternalNotificationMailer.plain and update Twilio and safe-delete jobs.
Blank searchText produced SIMILARITY(..., NULL) and matched no rows.
Return an unrestricted relation when there is nothing to search.
Fixes Relation#union failing with undefined method arel_table for nil.
Drop have_db_column(...).of_type(:datetime) assertions that conflict with
timestamptz; keep presence checks only.
Avoid brittle global Package/PackagesInventory counts when the test DB
carries data from other examples.
- Match admin designations by the six seeded order ids instead of a fixed count.
- Compare Package.count to a baseline before creating browse/search fixtures.
Disable transactional tests for this file and truncate packages-related
tables before each example so low-sequence next_code tests stay valid.
Replace shoulda uniqueness matcher with an explicit duplicate-code example.
Deleting User.system hit FK violations from packages_inventories; suite
cleanup is unnecessary with transactional examples.
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@warp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 52 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 633d7921-0302-4357-82de-c2c2565f0717

📥 Commits

Reviewing files that changed from the base of the PR and between 744f686 and 0469d7a.

📒 Files selected for processing (1)
  • .circleci/config.yml
📝 Walkthrough

Walkthrough

Bumps Ruby to 3.4.9 and Rails to ~> 8.1, updates CI/Docker/deploy/Bundler, adds small compatibility shims, introduces InternalNotificationMailer and replaces ad-hoc mail calls with it, adjusts several job/service signatures and model/concern logic, broad schema/timestamp/soft-delete changes, and extensive test and seed/factory updates.

Changes

Upgrade, compatibility, wiring, and tests (single cohesive DAG)

Layer / File(s) Summary
Versioning & CI images
.ruby-version, .circleci/config.yml, Dockerfile, script/sidekiq_stats.rb, config/deploy.rb, config/systemd/sidekiq-production.service
Ruby bumped 2.7.3 → 3.4.9; CircleCI images and Bundler updated; Docker base image and script shebang updated; RVM/sysd ExecStart updated.
Dependencies
Gemfile
Rails upgraded to ~> 8.1 and multiple gems added/updated (stdlib entries, active_record_union, state_machines-activerecord, paper_trail, paranoia, puma, rails-i18n, cucumber updates, etc.).
Compatibility shims & initializers
config/boot.rb, config/application.rb, config/initializers/new_framework_defaults_5_2.rb, config/initializers/authentication.rb, config/initializers/state_machine_patch.rb
Added File.exists? shim; added Application#secrets shim; removed legacy ActiveSupport default line; required authentication_service; removed explicit public-exposure lines in state_machine patch.
Server / Procfile
Procfile
Replaced Puma command with bundle exec rails server --binding=0.0.0.0 --port ${PORT:-3000}.
Mailer & notification wiring
app/mailers/internal_notification_mailer.rb, app/jobs/twilio_job.rb, app/jobs/user_safe_delete_job.rb
Added InternalNotificationMailer with plain(to:, subject:, body:); staging/error paths switched from ActionMailer::Base.mail to InternalNotificationMailer.plain(...).deliver_now.
Service / job API changes
app/services/twilio_service.rb, app/models/token.rb, spec/services/twilio_service_spec.rb, spec/jobs/twilio_job_spec.rb
TwilioService#send_sms now accepts optional hash + kwargs and merges them; Token.generate accepts **extra_params with API/OTP wrappers; tests updated for call-shape changes and ActiveJob enqueue assertions added.
Model/concern refactors & small behavior fixes
app/models/concerns/*, app/models/*, lib/classes/*
Safer reflection lookup in AutoFavourite; cache_key uses to_fs(:nsec); fuzzy_search short-circuits on blank input; order code SQL refactor; stocktake processing uses explicit scopes and update_all; alias_attribute → alias_method in Stocktake; User.filter_users robust to symbol/string keys; OrganisationsUserBuilder initializer accepts options hash; InventoryNumber SQL tightened.
Time & SQL formatting / bindings
lib/*, app/models/appointment_slot.rb, lib/classes/packages_inventories_importer.rb, migrations
Replaced many to_s(...) timestamp serializations with to_fs(...) or bound Time objects; updated SQL timestamp handling in inserts and migrations; appointment slot/date handling timezone-normalized to Asia/Hong_Kong.
Schema & seeds
db/schema.rb, db/migrate/*, db/seeds.rb
Added many created_at/updated_at, deleted_at and audit fields across tables; normalized timestamp precision; made index_exists? lookup explicit by name; introduced seed_yaml_file using safe_load with aliases enabled; seeds updated.
Tests & factories
spec/**/*, spec/factories/*, spec/rails_helper.rb, spec/support/transactional_test_isolation.rb, config/environments/test.rb
Relaxed many datetime-type assertions to presence-only; modernized controller param passing to params: style; prefixed FactoryBot calls; enabled YAML aliases in factories; added non-transactional test opt-out support; guarded system_user creation; set ActiveJob test adapter; numerous date/time-related spec updates and test scaffolding improvements.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as API Controller
    participant Service as TwilioService
    participant Job as TwilioJob
    participant Mailer as InternalNotificationMailer
    participant SMTP as Email Service

    rect rgba(100,150,200,0.5)
    Note over Client,SMTP: Old flow (pre-change)
    Client->>Controller: send SMS request
    Controller->>Service: send_sms(options)
    Service->>Job: enqueue TwilioJob with options/hash
    Job->>Job: env check (staging)
    Job->>SMTP: ActionMailer::Base.mail(...).deliver
    end

    rect rgba(100,200,150,0.5)
    Note over Client,SMTP: New flow (post-change)
    Client->>Controller: send SMS request
    Controller->>Service: send_sms(options, **kwargs)
    Service->>Job: enqueue TwilioJob with merged options
    Job->>Job: env check (staging)
    Job->>Mailer: InternalNotificationMailer.plain(to:, subject:, body:)
    Mailer->>SMTP: mail(...).deliver_now
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped from two-seven to three-four-nine,
Bundler and CI in a neat little line.
Mailer sings plain, jobs hum along,
Tests grew wiser, schemas grown strong.
A carrot patch—code fresh as springtime.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upgrade/rails-8.1

@guardrails

guardrails Bot commented May 5, 2026

Copy link
Copy Markdown

All previously detected findings have been fixed. Good job! 👍🎉

We will keep this comment up-to-date as you go along and notify you of any security issues that we identify.


👉 Go to the dashboard for detailed results.

📥 Happy? Share your feedback with us.

Clear persisted AppointmentSlots in the 2018 range before supervisor examples so
for_date is not skewed by leftover rows; resolve calendar rows by date instead of
array index; assert slot sets/timestamps without relying on sort order; scrub Dec
2018 before the timezone locking example.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
spec/models/concerns/share_support_spec.rb (2)

25-27: 💤 Low value

expect assertion inside before block produces opaque failures

Placing expect(...) inside a before block is not idiomatic RSpec. When such an assertion fails, the error surfaces as an around hook / before failure with no example name, making CI output harder to diagnose. The intent here — ensuring exactly 3 new rows were created — would be clearer as a dedicated it example or as a shared subject/context guard.

That said, this is a pragmatic trade-off for stabilising count baselines across pre-seeded data, and the same pattern is applied consistently across all three before blocks (lines 25-27, 49-53, 69-72), so this is a minor concern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/models/concerns/share_support_spec.rb` around lines 25 - 27, Move the
expect assertion out of the before hook into a dedicated example to avoid opaque
failures: replace the assertion inside the before block that calls
touch(location_1, location_2, location_3) and checks ShareableLocation.count
with a new it-block (e.g., "creates three ShareableLocation rows") that runs
after the before setup and asserts expect(ShareableLocation.count).to
eq(baseline + 3); update the other two similar before blocks (the ones
surrounding lines with touch and ShareableLocation.count) the same way so all
count checks are performed in explicit examples rather than inside before hooks.

69-76: 💤 Low value

@share_support_location_total instance variable approach is correct but baseline is redundant when stored directly

The pattern correctly uses an instance variable so the value is accessible across before and it blocks. One small clarity nit: baseline is a throwaway local; the assignment could be inlined.

♻️ Optional simplification
-      baseline = ShareableLocation.count
-      touch(location_1, location_2, location_3)
-      `@share_support_location_total` = baseline + 3
-      expect(ShareableLocation.count).to eq(`@share_support_location_total`)
+      `@share_support_location_total` = ShareableLocation.count + 3
+      touch(location_1, location_2, location_3)
+      expect(ShareableLocation.count).to eq(`@share_support_location_total`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/models/concerns/share_support_spec.rb` around lines 69 - 76, The test
creates a throwaway local `baseline` just to compute
`@share_support_location_total`; simplify by eliminating `baseline` and assign
`@share_support_location_total` directly using the current count from
ShareableLocation (e.g., set `@share_support_location_total` =
ShareableLocation.count + 3 before calling touch(location_1, location_2,
location_3)), then assert ShareableLocation.count and base_model.count against
`@share_support_location_total`; update the block containing touch,
`@share_support_location_total`, and the subsequent expect assertions accordingly.
Gemfile (1)

43-43: ⚡ Quick win

Add an upper bound to the paper_trail constraint.

The gemspec for paper_trail 17.0.0 declares its ActiveRecord compatibility only as a lower bound (>= 7.1) with an internal upper limit (< 8.2) that is used only to warn users, not to enforce installation. With '>= 17.0.0' and no upper bound in the Gemfile, a future major release (18.x) could be silently pulled by bundle update. Use a pessimistic operator to make upgrades intentional.

♻️ Suggested change
-gem 'paper_trail', '>= 17.0.0'
+gem 'paper_trail', '~> 17.0'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gemfile` at line 43, The Gemfile currently allows automatic upgrades of
paper_trail beyond major version 17 via the loose constraint "gem 'paper_trail',
'>= 17.0.0'"; update the constraint for the gem entry for paper_trail in the
Gemfile (the line declaring gem 'paper_trail') to use a
pessimistic/upper-bounded requirement (e.g., restrict to the 17.x series) so
future major releases (18.x) aren't pulled by bundle update unintentionally;
modify the gem declaration to include that upper bound and run bundle
install/update to verify.
app/controllers/api/v1/deliveries_controller.rb (1)

157-173: 💤 Low value

Consider memoizing the normalized delivery hash to eliminate double computation and reduce ordering fragility.

get_hash(delivery_attrs.to_h) is computed twice per request: once here in scheduled_date (called via validate_schedule at line 102) and again inside get_delivery_details at line 149. Additionally, get_delivery_details mutates params["delivery"] with the result — meaning if call order were ever reversed, scheduled_date would operate on the already-mutated (and double-underscore-normalized) hash, silently producing wrong results. The comment on lines 158–160 acknowledges this, but a memoized accessor would make the constraint structural rather than documentary.

♻️ Suggested refactor: memoize the normalized delivery hash
+      def normalized_delivery_attrs
+        `@normalized_delivery_attrs` ||= get_hash(delivery_attrs.to_h)
+      end
+
       def scheduled_date
-        # Use the same underscore normalization as get_delivery_details (get_hash), but do not
-        # rely on get_delivery_details — its permit step can differ by Rails version and must not
-        # mutate params during validation. Client sends scheduleAttributes.scheduledAt.
-        raw_delivery = get_hash(delivery_attrs.to_h)
-        sched = raw_delivery["schedule_attributes"]
+        sched = normalized_delivery_attrs["schedule_attributes"]
         return nil unless sched.is_a?(Hash)

         scheduled_at = sched["scheduled_at"]
         ...
       end

       def get_delivery_details
-        params["delivery"] = get_hash(delivery_attrs.to_h)
+        params["delivery"] = normalized_delivery_attrs
         params.require(:delivery).permit(...)
       end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/api/v1/deliveries_controller.rb` around lines 157 - 173,
scheduled_date calls get_hash(delivery_attrs.to_h) and get_delivery_details
calls get_hash again and mutates params["delivery"], causing double computation
and ordering fragility; introduce a memoized accessor (e.g.,
cached_normalized_delivery or normalized_delivery_hash) that computes
get_hash(delivery_attrs.to_h) once and returns the cached hash for both
scheduled_date and get_delivery_details (and any caller such as
validate_schedule), update scheduled_date to use the memoized accessor instead
of calling get_hash directly, and ensure get_delivery_details uses the same
accessor rather than re-normalizing or mutating params["delivery"] so the
normalization is performed exactly once per request.
spec/controllers/api/v1/packages_controller_spec.rb (1)

425-435: 💤 Low value

Minor inconsistency: map still used to generate it blocks here while each was applied at lines 439–448.

Both work for dynamically defining examples, but mixing them in adjacent contexts is inconsistent. Consider aligning to each throughout.

♻️ Proposed fix
-          [true, false].map do |val|
+          [true, false].each do |val|
             it "creates package with saleble value #{val}" do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/packages_controller_spec.rb` around lines 425 - 435,
The spec uses [true, false].map to define dynamic it blocks which is
inconsistent with nearby examples that use each; change the iterator from map to
each for the block that defines the "creates package with saleable value"
examples (the block containing item.offer.update(saleable: false),
package_params[:saleable] = val, post :create, and the expectations) so it
matches the adjacent style and avoids the unnecessary map return value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/jobs/twilio_job_spec.rb`:
- Line 82: The test uses ENV["EMAIL_FROM"] which can be nil in CI and makes the
assertion meaningless; change the spec to set or stub the ENV value before
invoking InternalNotificationMailer.plain so the recipient is deterministic
(e.g. set ENV["EMAIL_FROM"] = "test@example.com" in the example or use
allow(ENV).to receive(:[]).with("EMAIL_FROM").and_return("test@example.com")),
then assert expect(mail.to).to eq(["test@example.com"]); reference the example
that calls InternalNotificationMailer.plain (which uses ENV.fetch("EMAIL_FROM"))
and the local mail variable in the spec when making this change.

---

Nitpick comments:
In `@app/controllers/api/v1/deliveries_controller.rb`:
- Around line 157-173: scheduled_date calls get_hash(delivery_attrs.to_h) and
get_delivery_details calls get_hash again and mutates params["delivery"],
causing double computation and ordering fragility; introduce a memoized accessor
(e.g., cached_normalized_delivery or normalized_delivery_hash) that computes
get_hash(delivery_attrs.to_h) once and returns the cached hash for both
scheduled_date and get_delivery_details (and any caller such as
validate_schedule), update scheduled_date to use the memoized accessor instead
of calling get_hash directly, and ensure get_delivery_details uses the same
accessor rather than re-normalizing or mutating params["delivery"] so the
normalization is performed exactly once per request.

In `@Gemfile`:
- Line 43: The Gemfile currently allows automatic upgrades of paper_trail beyond
major version 17 via the loose constraint "gem 'paper_trail', '>= 17.0.0'";
update the constraint for the gem entry for paper_trail in the Gemfile (the line
declaring gem 'paper_trail') to use a pessimistic/upper-bounded requirement
(e.g., restrict to the 17.x series) so future major releases (18.x) aren't
pulled by bundle update unintentionally; modify the gem declaration to include
that upper bound and run bundle install/update to verify.

In `@spec/controllers/api/v1/packages_controller_spec.rb`:
- Around line 425-435: The spec uses [true, false].map to define dynamic it
blocks which is inconsistent with nearby examples that use each; change the
iterator from map to each for the block that defines the "creates package with
saleable value" examples (the block containing item.offer.update(saleable:
false), package_params[:saleable] = val, post :create, and the expectations) so
it matches the adjacent style and avoids the unnecessary map return value.

In `@spec/models/concerns/share_support_spec.rb`:
- Around line 25-27: Move the expect assertion out of the before hook into a
dedicated example to avoid opaque failures: replace the assertion inside the
before block that calls touch(location_1, location_2, location_3) and checks
ShareableLocation.count with a new it-block (e.g., "creates three
ShareableLocation rows") that runs after the before setup and asserts
expect(ShareableLocation.count).to eq(baseline + 3); update the other two
similar before blocks (the ones surrounding lines with touch and
ShareableLocation.count) the same way so all count checks are performed in
explicit examples rather than inside before hooks.
- Around line 69-76: The test creates a throwaway local `baseline` just to
compute `@share_support_location_total`; simplify by eliminating `baseline` and
assign `@share_support_location_total` directly using the current count from
ShareableLocation (e.g., set `@share_support_location_total` =
ShareableLocation.count + 3 before calling touch(location_1, location_2,
location_3)), then assert ShareableLocation.count and base_model.count against
`@share_support_location_total`; update the block containing touch,
`@share_support_location_total`, and the subsequent expect assertions accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6afc161e-e869-45cb-8531-c6c98c285f39

📥 Commits

Reviewing files that changed from the base of the PR and between 20062e9 and e4d0269.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • Gemfile
  • app/controllers/api/v1/deliveries_controller.rb
  • app/models/appointment_slot.rb
  • app/models/package.rb
  • app/services/subscriptions_reminder.rb
  • app/services/twilio_service.rb
  • config/environments/test.rb
  • db/migrate/20181121040221_change_order_transport_scheduled_at_to_datetime.rb
  • lib/classes/packages_inventories_importer.rb
  • lib/goodcity/image_archiver.rb
  • spec/controllers/api/v1/appointment_slots_controller_spec.rb
  • spec/controllers/api/v1/organisations_controller_spec.rb
  • spec/controllers/api/v1/package_types_controller_spec.rb
  • spec/controllers/api/v1/packages_controller_spec.rb
  • spec/jobs/poll_gogovan_order_status_job_spec.rb
  • spec/jobs/twilio_job_spec.rb
  • spec/lib/classes/packages_inventories_importer_spec.rb
  • spec/migrations/change_order_transport_scheduled_at_to_datetime_spec.rb
  • spec/models/concerns/share_support_spec.rb
  • spec/rails_helper.rb
  • spec/services/twilio_service_spec.rb
  • spec/support/migration_helpers.rb
💤 Files with no reviewable changes (1)
  • spec/jobs/poll_gogovan_order_status_job_spec.rb


TwilioJob.new.perform(options)
mail = ActionMailer::Base.deliveries.last
expect(mail.to).to eq([ENV["EMAIL_FROM"]])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ENV["EMAIL_FROM"] may be nil in test environments, making the assertion vacuous.

If EMAIL_FROM is not set in CI or the local test environment, ENV["EMAIL_FROM"] returns nil, so the expectation becomes expect(mail.to).to eq([nil]). The mail delivery from InternalNotificationMailer.plain(to: ENV.fetch("EMAIL_FROM"), ...) would raise a KeyError at runtime (since fetch raises on a missing key), so the test would actually fail with an error rather than pass silently — but the assertion itself provides no confidence that the correct recipient was used.

Consider stubbing ENV or using a fixed test value:

🛡️ Proposed fix
+    before { allow(ENV).to receive(:fetch).with("EMAIL_FROM").and_return("test@example.com") }
+
     it "should send an email instead of an SMS" do
       expect {
         TwilioJob.new.perform(options)
       }.to change { ActionMailer::Base.deliveries.size }.by(1)

       mail = ActionMailer::Base.deliveries.last
-      expect(mail.to).to eq([ENV["EMAIL_FROM"]])
+      expect(mail.to).to eq(["test@example.com"])
       expect(mail.subject).to eq("SMS to #{options[:to]}")
       expect(mail.body.raw_source).to include(options[:body])
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mail.to).to eq([ENV["EMAIL_FROM"]])
before { allow(ENV).to receive(:fetch).with("EMAIL_FROM").and_return("test@example.com") }
it "should send an email instead of an SMS" do
expect {
TwilioJob.new.perform(options)
}.to change { ActionMailer::Base.deliveries.size }.by(1)
mail = ActionMailer::Base.deliveries.last
expect(mail.to).to eq(["test@example.com"])
expect(mail.subject).to eq("SMS to #{options[:to]}")
expect(mail.body.raw_source).to include(options[:body])
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/jobs/twilio_job_spec.rb` at line 82, The test uses ENV["EMAIL_FROM"]
which can be nil in CI and makes the assertion meaningless; change the spec to
set or stub the ENV value before invoking InternalNotificationMailer.plain so
the recipient is deterministic (e.g. set ENV["EMAIL_FROM"] = "test@example.com"
in the example or use allow(ENV).to
receive(:[]).with("EMAIL_FROM").and_return("test@example.com")), then assert
expect(mail.to).to eq(["test@example.com"]); reference the example that calls
InternalNotificationMailer.plain (which uses ENV.fetch("EMAIL_FROM")) and the
local mail variable in the spec when making this change.

warp added 3 commits May 6, 2026 12:46
- Parse nested schedule params into plain hashes before computing scheduled_date
  so public-holiday checks work with ActionController::Parameters in Rails 8.
- Appointment slots specs: avoid memoized JSON, align timezone cleanup, and use
  flexible time assertions.
- Package filtering specs: scope expectations to package IDs created in each
  example to ignore unrelated rows.
- Deliveries: normalize nested ActionController::Parameters in get_hash; derive
  scheduled_at from schedule_attributes plus a params fallback for camelCase;
  parse dates with Time.zone and treat blank scheduledAt as absent.
- Deliveries spec: resilient holiday setup; fix bad-data examples to use
  scheduledAt and empty string for missing date.
- Appointment slots spec: clear AppointmentSlotPreset rows before seeding
  presets to match CI without duplicate weekdays.
- Donor conditions spec: create rows with unique name_en so the factory’s
  find_or_initialize_by does not collide with seeded donor conditions.
- Orders: use case/when in epoch_ms and day_epoch_ms so Date/DateTime use
  in_time_zone (Ruby 3 removed Date#to_i).
- Appointment slots: build Oct/Mar fixture times with Time.zone.parse; clear
  stray HKT 2018-12-19 quota-0 rows before the UTC closure POST example.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
spec/controllers/api/v1/donor_conditions_controller_spec.rb (1)

45-49: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ordering assumption in visible_to_donor assertion may cause flakiness.

The before block (now shared with this example) inserts two new rows with visible_to_donor: true, so both the response and the pluck call will include them. However, DonorCondition.pluck(:visible_to_donor) returns values in database-default (typically insertion) order, while the controller's index action may apply a different sort. If the orderings ever diverge the assertion will produce a false failure.

Consider asserting on sets/multisets instead of ordered arrays, or explicitly sort both sides:

🛡️ Proposed fix to make the assertion order-independent
-      expect( parsed_body['donor_conditions'].map { |condition| condition['visible_to_donor']} ).to eq(DonorCondition.pluck(:visible_to_donor))
+      expect( parsed_body['donor_conditions'].map { |condition| condition['visible_to_donor'] }.sort ).to eq(DonorCondition.pluck(:visible_to_donor).map { |v| v ? true : false }.sort)

Or, if you only care that every returned record has a visible_to_donor key with a boolean value:

-      expect( parsed_body['donor_conditions'].map { |condition| condition['visible_to_donor']} ).to eq(DonorCondition.pluck(:visible_to_donor))
+      returned_values = parsed_body['donor_conditions'].map { |condition| condition['visible_to_donor'] }
+      expect(returned_values).to all(be(true).or(be(false)))
+      expect(returned_values.sort).to eq(DonorCondition.pluck(:visible_to_donor).sort)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/donor_conditions_controller_spec.rb` around lines 45
- 49, The test in donor_conditions_controller_spec.rb is assuming order by
comparing parsed_body['donor_conditions'].map { |condition|
condition['visible_to_donor'] } to DonorCondition.pluck(:visible_to_donor) which
can be flaky if controller index ordering differs; update the assertion in the
"returns 'visible_to_donor' in serialized response" example to be
order-independent by either using a multiset comparison (e.g. match_array)
between the two arrays or by explicitly sorting both arrays before comparing, or
alternatively assert each returned condition has a boolean visible_to_donor key
(using parsed_body and enumerating values) so the test no longer depends on
record order.
🧹 Nitpick comments (5)
spec/models/concerns/package_filtering_spec.rb (1)

21-22: ⚡ Quick win

Global count assertions are partially migrated — consider completing ID-scoping.

Lines 21–22 still assert on Package.count / Package.apply_filter.count without .where(id: @fil_pkg_ids), while every other assertion in this file was converted to ID-scoped counts. The @package_filter_baseline snapshot mitigates most of the risk, but these assertions will silently drift if anything else in the suite ever creates a Package via a before(:suite) or before(:all) block (which runs outside the transactional rollback window).

♻️ Proposed fix to apply consistent ID-scoping
 it 'does not filter out anything if no explicit arguments are provided' do
-  expect(Package.count).to eq(`@package_filter_baseline` + 5)
-  expect(Package.apply_filter.count).to eq(`@package_filter_baseline` + 5)
+  expect(Package.apply_filter.where(id: `@fil_pkg_ids`).count).to eq(5)
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/models/concerns/package_filtering_spec.rb` around lines 21 - 22, The two
assertions use global counts (Package.count and Package.apply_filter.count)
instead of ID-scoped counts like the rest of the file; update both to scope to
the filtered package ids by replacing Package.count and
Package.apply_filter.count with Package.where(id: `@fil_pkg_ids`).count and
Package.apply_filter.where(id: `@fil_pkg_ids`).count respectively so the
expectations remain stable against external test-suite package creation while
still using `@package_filter_baseline` in the arithmetic.
spec/controllers/api/v1/deliveries_controller_spec.rb (2)

165-172: 💤 Low value

LGTM — robust holiday seeding.

Idempotent seeding with a unique name, plus the RecordInvalid rescue guarded by a re-check of Holiday.is_holiday?, neatly handles the case where another fixture/spec already seeded the same date. One tiny redundancy: Date.parse(...) already returns a Date, so .to_date on line 165 is a no-op and can be dropped.

♻️ Tiny cleanup
-          date = Date.parse(schedule["scheduledAt"]).to_date
+          date = Date.parse(schedule["scheduledAt"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/deliveries_controller_spec.rb` around lines 165 -
172, The Date.parse(...).to_date call is redundant because Date.parse already
returns a Date; update the spec to remove the unnecessary .to_date by parsing
schedule["scheduledAt"] into date with Date.parse(schedule["scheduledAt"]) and
keep the rest (the Holiday.is_holiday? check and the Holiday.create! / rescue
block) unchanged; ensure references remain to the local variable date and
existing methods Holiday.is_holiday? and Holiday.create!.

191-204: 💤 Low value

Nit: re-parsing response.body instead of using subject.

subject is already defined as JSON.parse(response.body) at the top of this spec file (line 5) and is used in adjacent tests (e.g., the not a date test below). Using subject here would keep the style consistent and remove the local parsed variable. The actual coverage of the blank-scheduledAt path looks correct.

♻️ Suggested cleanup
           expect(response.status).to eq(422)
-          parsed = JSON.parse(response.body)
-          expect(parsed["errors"].length).to eq(1)
-          expect(parsed["errors"][0]["message"]).to eq(
-            "The selected date is either missing or invalid, please try again."
-          )
+          expect(subject["errors"].length).to eq(1)
+          expect(subject["errors"][0]["message"]).to eq(
+            "The selected date is either missing or invalid, please try again."
+          )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/deliveries_controller_spec.rb` around lines 191 -
204, The test re-parses response.body into a local variable `parsed` even though
`subject` is already defined as `JSON.parse(response.body)` for this spec;
replace uses of the local `parsed` with `subject` in the "should fail to modify
the delivery if scheduled_at is nil" example (keep the existing expects against
`response.status` and the Gogovan/GogovanOrder mocks), i.e. remove the `parsed =
JSON.parse(response.body)` line and update subsequent assertions to reference
`subject["errors"]` and `subject["errors"][0]["message"]` so the style matches
adjacent tests.
app/controllers/api/v1/deliveries_controller.rb (2)

175-191: 💤 Low value

Optional: the second fallback branch looks effectively redundant.

delivery_attrs is params.require(:delivery).permit!, so it preserves every nested key from params[:delivery], including scheduleAttributes. Branch 1 then runs get_hash, which underscores keys to schedule_attributes / scheduled_at. If branch 1 doesn't yield a value, the same lookup against params.to_unsafe_h against camelCase keys will not find anything different, since both originate from the same params payload. Consider collapsing to the single (already exhaustive) branch for clarity.

♻️ Possible simplification
       def scheduled_at_from_delivery_params
         raw_delivery = get_hash(parameters_to_plain_hash(delivery_attrs.to_unsafe_h))
         sched = raw_delivery["schedule_attributes"]
-        if sched.is_a?(Hash)
-          at = sched["scheduled_at"].presence || sched[:scheduled_at].presence
-          return at if at.present?
-        end
-
-        plain = parameters_to_plain_hash(params.to_unsafe_h)
-        d = plain["delivery"] || plain[:delivery]
-        return nil unless d.is_a?(Hash)
-
-        sa = d["schedule_attributes"] || d["scheduleAttributes"] || d[:schedule_attributes] || d[:scheduleAttributes]
-        return nil unless sa.is_a?(Hash)
-
-        (sa["scheduled_at"] || sa["scheduledAt"] || sa[:scheduled_at] || sa[:scheduledAt]).presence
+        return nil unless sched.is_a?(Hash)
+
+        sched["scheduled_at"].presence
       end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/api/v1/deliveries_controller.rb` around lines 175 - 191, The
second fallback branch in scheduled_at_from_delivery_params is redundant because
delivery_attrs (params.require(:delivery).permit!) already contains the same
nested keys and get_hash(parameters_to_plain_hash(delivery_attrs.to_unsafe_h))
will underscore camelCase keys (via get_hash), so remove the whole fallback that
reads from parameters_to_plain_hash(params.to_unsafe_h) and its subsequent
lookups; instead, rely solely on raw_delivery (from delivery_attrs) to extract
schedule_attributes and scheduled_at (using the existing checks for
string/symbol keys), keeping the early return behaviour in
scheduled_at_from_delivery_params and preserving presence checks.

215-226: ⚖️ Poor tradeoff

Optional: parameters_to_plain_hash may be redundant on Rails 8.

ActionController::Parameters#to_unsafe_h already performs deep recursive conversion of nested Parameters into HashWithIndifferentAccess, so this helper's recursion mostly converts HashWithIndifferentAccess to plain Hash. Since downstream code works with both types (including defensive key access like plain["delivery"] || plain[:delivery]), you could remove this helper and pass to_unsafe_h result directly to get_hash, which already handles defensive Parameter conversion and key normalization. Not a defect, just a chance to reduce the param-shaping surface area.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/api/v1/deliveries_controller.rb` around lines 215 - 226, The
parameters_to_plain_hash helper is redundant on Rails 8 because
ActionController::Parameters#to_unsafe_h already deeply converts nested params;
remove the parameters_to_plain_hash method and update any callers to pass
params.to_unsafe_h (or the specific ActionController::Parameters instance
.to_unsafe_h) directly into get_hash so get_hash continues to handle key
normalization and defensive access (ensure references to
parameters_to_plain_hash are replaced with .to_unsafe_h and delete the
parameters_to_plain_hash definition).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/controllers/api/v1/appointment_slots_controller_spec.rb`:
- Around line 330-334: The test currently allows "timestamp with time zone";
tighten the assertion in the spec for AppointmentSlot.columns (the example named
'Should use a timestamp column for slot times') to explicitly require the
"without time zone" form while still permitting precision, e.g. change the
sql_type expectation to match a pattern like "timestamp" optionally followed by
precision and then " without time zone" (use a regex such as
/\Atimestamp(?:\(\d+\))? without time zone/ on the column.sql_type).

---

Outside diff comments:
In `@spec/controllers/api/v1/donor_conditions_controller_spec.rb`:
- Around line 45-49: The test in donor_conditions_controller_spec.rb is assuming
order by comparing parsed_body['donor_conditions'].map { |condition|
condition['visible_to_donor'] } to DonorCondition.pluck(:visible_to_donor) which
can be flaky if controller index ordering differs; update the assertion in the
"returns 'visible_to_donor' in serialized response" example to be
order-independent by either using a multiset comparison (e.g. match_array)
between the two arrays or by explicitly sorting both arrays before comparing, or
alternatively assert each returned condition has a boolean visible_to_donor key
(using parsed_body and enumerating values) so the test no longer depends on
record order.

---

Nitpick comments:
In `@app/controllers/api/v1/deliveries_controller.rb`:
- Around line 175-191: The second fallback branch in
scheduled_at_from_delivery_params is redundant because delivery_attrs
(params.require(:delivery).permit!) already contains the same nested keys and
get_hash(parameters_to_plain_hash(delivery_attrs.to_unsafe_h)) will underscore
camelCase keys (via get_hash), so remove the whole fallback that reads from
parameters_to_plain_hash(params.to_unsafe_h) and its subsequent lookups;
instead, rely solely on raw_delivery (from delivery_attrs) to extract
schedule_attributes and scheduled_at (using the existing checks for
string/symbol keys), keeping the early return behaviour in
scheduled_at_from_delivery_params and preserving presence checks.
- Around line 215-226: The parameters_to_plain_hash helper is redundant on Rails
8 because ActionController::Parameters#to_unsafe_h already deeply converts
nested params; remove the parameters_to_plain_hash method and update any callers
to pass params.to_unsafe_h (or the specific ActionController::Parameters
instance .to_unsafe_h) directly into get_hash so get_hash continues to handle
key normalization and defensive access (ensure references to
parameters_to_plain_hash are replaced with .to_unsafe_h and delete the
parameters_to_plain_hash definition).

In `@spec/controllers/api/v1/deliveries_controller_spec.rb`:
- Around line 165-172: The Date.parse(...).to_date call is redundant because
Date.parse already returns a Date; update the spec to remove the unnecessary
.to_date by parsing schedule["scheduledAt"] into date with
Date.parse(schedule["scheduledAt"]) and keep the rest (the Holiday.is_holiday?
check and the Holiday.create! / rescue block) unchanged; ensure references
remain to the local variable date and existing methods Holiday.is_holiday? and
Holiday.create!.
- Around line 191-204: The test re-parses response.body into a local variable
`parsed` even though `subject` is already defined as `JSON.parse(response.body)`
for this spec; replace uses of the local `parsed` with `subject` in the "should
fail to modify the delivery if scheduled_at is nil" example (keep the existing
expects against `response.status` and the Gogovan/GogovanOrder mocks), i.e.
remove the `parsed = JSON.parse(response.body)` line and update subsequent
assertions to reference `subject["errors"]` and
`subject["errors"][0]["message"]` so the style matches adjacent tests.

In `@spec/models/concerns/package_filtering_spec.rb`:
- Around line 21-22: The two assertions use global counts (Package.count and
Package.apply_filter.count) instead of ID-scoped counts like the rest of the
file; update both to scope to the filtered package ids by replacing
Package.count and Package.apply_filter.count with Package.where(id:
`@fil_pkg_ids`).count and Package.apply_filter.where(id: `@fil_pkg_ids`).count
respectively so the expectations remain stable against external test-suite
package creation while still using `@package_filter_baseline` in the arithmetic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1051b39-d158-4455-a37d-bbbca3e15f69

📥 Commits

Reviewing files that changed from the base of the PR and between e4d0269 and 723040d.

📒 Files selected for processing (6)
  • app/controllers/api/v1/deliveries_controller.rb
  • spec/controllers/api/v1/appointment_slots_controller_spec.rb
  • spec/controllers/api/v1/deliveries_controller_spec.rb
  • spec/controllers/api/v1/donor_conditions_controller_spec.rb
  • spec/controllers/api/v1/orders_controller_spec.rb
  • spec/models/concerns/package_filtering_spec.rb

Comment on lines +330 to 334
it 'Should use a timestamp column for slot times' do
column = AppointmentSlot.columns.find { |col| col.name == 'timestamp' }
expect(column).to_not be_nil
expect(column.sql_type).to eq("timestamp(6) with time zone")
expect(column.sql_type).to match(/\Atimestamp/)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten the column-type assertion

match(/\Atimestamp/) also passes for timestamp with time zone, so this example no longer guards the schema behavior it is trying to pin. Please assert the without time zone form explicitly, while still allowing precision.

Suggested change
     column = AppointmentSlot.columns.find { |col| col.name == 'timestamp' }
     expect(column).to_not be_nil
-    expect(column.sql_type).to match(/\Atimestamp/)
+    expect(column.sql_type).to match(/\Atimestamp(?:\(\d+\))? without time zone\z/)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/appointment_slots_controller_spec.rb` around lines
330 - 334, The test currently allows "timestamp with time zone"; tighten the
assertion in the spec for AppointmentSlot.columns (the example named 'Should use
a timestamp column for slot times') to explicitly require the "without time
zone" form while still permitting precision, e.g. change the sql_type
expectation to match a pattern like "timestamp" optionally followed by precision
and then " without time zone" (use a regex such as /\Atimestamp(?:\(\d+\))?
without time zone/ on the column.sql_type).

warp added 5 commits May 6, 2026 13:52
- Clear all AppointmentSlot rows in calendar-related before hooks so examples
  are not affected by committed data from other specs.
- In deliveries modify-existing examples, use old_offer.created_by and align
  ggv_order offerId with old_offer so confirm_delivery expectations match.
- Add :non_transactional metadata helper and use it in inventory_number_spec;
  document transactional fixtures and merge suite Time.zone with system user.
first_missing_code and max_code cast codes to integer; alphanumeric
inventory_numbers rows (allowed by validation) caused PG errors when
mixed with auto-generated numeric codes. Filter inventory_numbers with
the same ^\d+$ pattern as packages.
Reset holidays for the scheduled HKT date before creating the fixture so
full-suite order cannot leave validate_schedule passing and trigger
Gogovan.cancel_order. Align the test date with Time.zone.parse like the controller.
Calendar expectations depend on AppointmentSlot.for_date and wrap_slot,
which can be affected by committed 2018 HKT slots/holidays and appointment
order_transports from other specs. Centralize a reset helper that clears
2018 fixture data and ensure the timezone example is self-contained.
- Orders controller: relax admin designations to include baseline IDs;
  summary matches controller merge; stock app allows non-GoodCity drafts.
- Order model: clear orders in count-sensitive examples.
- Organisation populator: assert fixture registrations only.
- Organisation types index: include created fixtures vs exact table match.
- ManageLocation and ValuationCalculator: use unique PackageType rows
  instead of factory first_or_create by code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
spec/controllers/api/v1/orders_controller_spec.rb (1)

359-366: 💤 Low value

day_epoch_ms timezone handling is asymmetric with epoch_ms.

For a Date, Date#beginning_of_day is materialized in the system local time zone before .in_time_zone is applied — at that point the epoch is already fixed, so .in_time_zone is effectively a no-op for to_i. If Time.zone differs from the system TZ (common in CI vs. local dev), epoch_ms and day_epoch_ms will disagree about the same Date. Consider zoning first, then taking beginning_of_day:

♻️ Proposed adjustment
 def day_epoch_ms(time)
   case time
-  when Date, DateTime
-    time.beginning_of_day.in_time_zone.to_i * 1000
+  when Date, DateTime
+    time.in_time_zone.beginning_of_day.to_i * 1000
   else
     time.beginning_of_day.to_i * 1000
   end
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/orders_controller_spec.rb` around lines 359 - 366,
day_epoch_ms currently materializes beginning_of_day on a Date/DateTime in
system local time then calls .in_time_zone, which can yield a different epoch
than epoch_ms when Time.zone differs; change day_epoch_ms to apply the app
timezone first (e.g. call time.in_time_zone or Time.zone.local conversion) and
then call beginning_of_day and to_i so Date, DateTime and Time paths use the
same zoning logic as epoch_ms (update the day_epoch_ms helper to zone-first for
Date/DateTime and keep the else branch consistent).
spec/controllers/api/v1/deliveries_controller_spec.rb (1)

198-212: 💤 Low value

Minor — reuse subject for consistency.

Lines 188-192 and 222-225 use the file-level subject { JSON.parse(response.body) }; the new branch reintroduces parsed = JSON.parse(response.body). Switching to subject['errors'] keeps this assertion shape consistent with its siblings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/deliveries_controller_spec.rb` around lines 198 -
212, The test re-parses response.body into a local parsed variable instead of
using the file-level subject { JSON.parse(response.body) }; change the spec
inside the "should fail to modify the delivery if scheduled_at is nil" example
to use subject['errors'] (and subject directly) for assertions instead of
parsed, keeping the existing expectations (Gogovan/GogovanOrder not to receive,
post :confirm_delivery, response.status eq 422) but replace
parsed["errors"].length and parsed["errors"][0]["message"] with
subject['errors'].length and subject['errors'][0]['message'] to match
surrounding tests that rely on subject.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/controllers/api/v1/deliveries_controller_spec.rb`:
- Around line 170-180: The WHERE clause using "date(holiday AT TIME ZONE 'HKT')"
is wrong because holidays.holiday is a timestamp without time zone; remove the
AT TIME ZONE cast and compare the stored timestamp's date directly (e.g. use
"date(holiday) = ?") or explicitly convert the parsed schedule date into the
same timezone as the stored value before comparing; update the
Holiday.where(...) call in the spec that builds date =
Time.zone.parse(schedule["scheduledAt"]).to_date so the delete_all matches the
Holiday.create!(..., holiday: date, ...) row by comparing date(holiday) = date
(or by normalizing both sides to UTC) instead of using AT TIME ZONE 'HKT'.

In `@spec/controllers/api/v1/organisation_types_controller_spec.rb`:
- Around line 23-25: The test uses include(*expected) which requires exact hash
equality and will fail if the serializer adds extra keys; update the assertion
in organisation_types_controller_spec (the expect on
response_body['organisation_types']) to match each expected hash as a subset
using RSpec's a_hash_including (i.e. transform each element of expected into
a_hash_including(expected_elem) and pass those to include) so the test asserts
presence of the specified keys/values without requiring exact equality.

In `@spec/lib/goodcity/organisation_populator_spec.rb`:
- Around line 18-19: The test reads fixture org_ids into the local variable
registrations but compares DB rows to the raw array length which can include
duplicates; change the assertion to use a deduplicated set (e.g., call uniq on
registrations) so the comparison uses unique org_ids when checking
Organisation.where(registration: registrations).count against
registrations.uniq.count.

---

Nitpick comments:
In `@spec/controllers/api/v1/deliveries_controller_spec.rb`:
- Around line 198-212: The test re-parses response.body into a local parsed
variable instead of using the file-level subject { JSON.parse(response.body) };
change the spec inside the "should fail to modify the delivery if scheduled_at
is nil" example to use subject['errors'] (and subject directly) for assertions
instead of parsed, keeping the existing expectations (Gogovan/GogovanOrder not
to receive, post :confirm_delivery, response.status eq 422) but replace
parsed["errors"].length and parsed["errors"][0]["message"] with
subject['errors'].length and subject['errors'][0]['message'] to match
surrounding tests that rely on subject.

In `@spec/controllers/api/v1/orders_controller_spec.rb`:
- Around line 359-366: day_epoch_ms currently materializes beginning_of_day on a
Date/DateTime in system local time then calls .in_time_zone, which can yield a
different epoch than epoch_ms when Time.zone differs; change day_epoch_ms to
apply the app timezone first (e.g. call time.in_time_zone or Time.zone.local
conversion) and then call beginning_of_day and to_i so Date, DateTime and Time
paths use the same zoning logic as epoch_ms (update the day_epoch_ms helper to
zone-first for Date/DateTime and keep the else branch consistent).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f8438cf-a2ae-4d5a-9db6-c57b59211e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 723040d and 119b22d.

📒 Files selected for processing (12)
  • app/models/inventory_number.rb
  • spec/controllers/api/v1/appointment_slots_controller_spec.rb
  • spec/controllers/api/v1/deliveries_controller_spec.rb
  • spec/controllers/api/v1/orders_controller_spec.rb
  • spec/controllers/api/v1/organisation_types_controller_spec.rb
  • spec/lib/classes/manage_location_spec.rb
  • spec/lib/goodcity/organisation_populator_spec.rb
  • spec/models/concerns/valuation_calculator_spec.rb
  • spec/models/inventory_number_spec.rb
  • spec/models/order_spec.rb
  • spec/rails_helper.rb
  • spec/support/transactional_test_isolation.rb

Comment thread spec/controllers/api/v1/deliveries_controller_spec.rb
Comment on lines +23 to +25
expected = organisation_types.map { |o| { 'name' => o.name_en, 'id' => o.id, 'category' => o.category_en } }
# The table may already contain seeded organisation types; assert our fixtures are present.
expect(response_body['organisation_types']).to include(*expected)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

include(*expected) performs exact hash equality — use a_hash_including for subset matching

RSpec's array include matcher checks membership via ==, so each hash in expected must be exactly equal (same keys and values) to an element in the response. If the serializer returns any additional fields beyond name, id, and category (e.g. timestamps, extra attributes), none of the expected hashes will match and the test will produce a confusing false negative.

The intent of this change — tolerating extra seeded records — is correct, but the element-level comparison should use a_hash_including to be robust against serializer output changes:

🛠️ Proposed fix using a_hash_including
-      expected = organisation_types.map { |o| { 'name' => o.name_en, 'id' => o.id, 'category' => o.category_en } }
-      # The table may already contain seeded organisation types; assert our fixtures are present.
-      expect(response_body['organisation_types']).to include(*expected)
+      # The table may already contain seeded organisation types; assert our fixtures are present.
+      expected = organisation_types.map { |o| a_hash_including('name' => o.name_en, 'id' => o.id, 'category' => o.category_en) }
+      expect(response_body['organisation_types']).to include(*expected)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/organisation_types_controller_spec.rb` around lines
23 - 25, The test uses include(*expected) which requires exact hash equality and
will fail if the serializer adds extra keys; update the assertion in
organisation_types_controller_spec (the expect on
response_body['organisation_types']) to match each expected hash as a subset
using RSpec's a_hash_including (i.e. transform each element of expected into
a_hash_including(expected_elem) and pass those to include) so the test asserts
presence of the specified keys/values without requiring exact equality.

Comment on lines +18 to +19
registrations = JSON.parse(file).map { |d| d["org_id"] }
expect(Organisation.where(registration: registrations).count).to eq(registrations.count)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Deduplicate fixture registrations before asserting count

Line 18 currently keeps duplicates, but Line 19 compares DB row count to raw array length. If the fixture ever includes repeated org_id, this test can fail even when behavior is correct.

Suggested change
-      registrations = JSON.parse(file).map { |d| d["org_id"] }
-      expect(Organisation.where(registration: registrations).count).to eq(registrations.count)
+      registrations = JSON.parse(file).map { |d| d["org_id"] }.uniq
+      expect(Organisation.where(registration: registrations).count).to eq(registrations.length)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
registrations = JSON.parse(file).map { |d| d["org_id"] }
expect(Organisation.where(registration: registrations).count).to eq(registrations.count)
registrations = JSON.parse(file).map { |d| d["org_id"] }.uniq
expect(Organisation.where(registration: registrations).count).to eq(registrations.length)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/lib/goodcity/organisation_populator_spec.rb` around lines 18 - 19, The
test reads fixture org_ids into the local variable registrations but compares DB
rows to the raw array length which can include duplicates; change the assertion
to use a deduplicated set (e.g., call uniq on registrations) so the comparison
uses unique org_ids when checking Organisation.where(registration:
registrations).count against registrations.uniq.count.

warp added 5 commits May 6, 2026 16:14
CircleCI parallelism: 3 runs three containers against one Postgres;
shared goodcity_server_test caused races (appointment_slots and other
DB-heavy specs). Set DATABASE_URL with CIRCLE_NODE_INDEX for db:migrate,
cucumber, and rspec so each node has an isolated database.
Remove the 2018 HKT reset helper and OrderTransport cleanup; parallel CI
containers use separate test databases, so cross-worker slot/booking
races are handled in .circleci without this extra before-hook.
Stub Holiday.is_holiday? for the fixture date after seeding so validate_schedule
always rejects holiday bookings before delete_old_associations runs (avoids
Gogovan.cancel_order when DB ordering leaves Holiday lookup flaky).
…cleanup

- Add describe-level after Timecop.return so nested freezes do not leak between
  sibling contexts (Order.priority uses Time.zone.now).
- Clear awaiting_dispatch orders before awaiting-dispatch examples.
- Drop redundant Timecop.return from processing-only context.
- Add reset_hkt_2018_calendar_test_data to clear presets, slots, HKT 2018
  holidays, and appointment order_transports that skew for_date/wrap_slot
- Reuse helper in Supervisor and timezone contexts; make Dec 19 example
  self-contained with full slot reset + presets after POST setup

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
spec/controllers/api/v1/deliveries_controller_spec.rb (1)

173-174: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unresolved: AT TIME ZONE 'HKT' on a timestamp without time zone column still misaligns the cleanup query.

The past review on this hunk pointed out that holidays.holiday is timestamp without time zone, so AT TIME ZONE 'HKT' reinterprets the naive value as HKT and converts it to UTC, shifting the extracted date by up to one day relative to the row inserted on lines 175–179 (where holiday: date writes midnight in the app/DB default zone). The result is that the delete_all will not match the row this same before block just created in many sessions, so the "reset only this date" guard against pre-existing rows from other specs is not actually doing what the comment claims. The downstream Holiday.is_holiday? stub (lines 182–186) hides this in the example under test, but any code path that hits the DB for that date still sees stale rows.

Either drop the cast and compare on the stored value directly, or normalize both sides to the same zone:

🛠️ Suggested fix
-          date = Time.zone.parse(schedule["scheduledAt"]).to_date
-          Holiday.where("date(holiday AT TIME ZONE 'HKT') = ?", date).delete_all
+          date = Time.zone.parse(schedule["scheduledAt"]).to_date
+          Holiday.where("date(holiday) = ?", date).delete_all
#!/bin/bash
# Confirm the column type for holidays.holiday is still `timestamp without time zone`
# (i.e., the past-review premise still holds in the current schema).
fd -t f schema.rb db
rg -nP -C2 'create_table\s+["\:]holidays' db
rg -nP -C2 't\.(datetime|timestamp|date)\s+["\:]holiday\b' db
rg -nP -C2 'change_column\s+:holidays\s*,\s*:holiday' db
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/deliveries_controller_spec.rb` around lines 173 -
174, The cleanup query misuses "AT TIME ZONE 'HKT'" against a timestamp without
time zone and thus shifts the date; change the Holiday.where(...) call to
compare the stored timestamp's date directly (e.g. use "date(holiday) = ?" or
"holiday::date = ?") or normalize both sides to the same zone before comparing
so the newly created row (in this before block) is actually matched; update the
Holiday.where("date(holiday AT TIME ZONE 'HKT') = ?", date).delete_all
invocation accordingly and keep the Holiday.is_holiday? stub as-is.
🧹 Nitpick comments (2)
.circleci/config.yml (2)

56-58: 💤 Low value

POSTGRES_DB: goodcity_server_test is now dead configuration.

With the per-node scheme, every node connects to goodcity_server_test_<index> and creates it via rails db:create. The goodcity_server_test DB that the Postgres image auto-creates from this env var is never used, which is mildly misleading for anyone reading the config. Consider dropping it (and optionally documenting the per-node naming next to the comment block on lines 62–64).

♻️ Proposed cleanup
       - image: cimg/postgres:9.6
         environment:
           POSTGRES_USER: postgres
-          POSTGRES_DB: goodcity_server_test
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.circleci/config.yml around lines 56 - 58, Remove the dead POSTGRES_DB env
var entry (POSTGRES_DB: goodcity_server_test) from the CircleCI job environment
and, in its place or nearby, add a brief comment explaining the per-node naming
scheme (goodcity_server_test_<index>) and that each node creates its own DB via
rails db:create so the auto-created goodcity_server_test is unused; update any
related references to avoid confusion.

82-100: ⚡ Quick win

DRY the per-node DATABASE_URL via BASH_ENV instead of re-exporting in every step.

The same export DATABASE_URL=... line is repeated in the new Setup test database, Cucumber tests, and Rspec tests steps. If the suffix scheme ever changes (e.g., adding a _$RAILS_ENV segment, switching ports, or adding credentials), three places must stay in sync. CircleCI sources $BASH_ENV for every run step, so setting it once eliminates the duplication and reduces drift risk.

♻️ Proposed refactor
       - run:
           name: Wait for DB and Redis containers to start
           command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
+      - run:
+          name: Configure per-node DATABASE_URL
+          command: |
+            echo 'export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"' >> "$BASH_ENV"
       - run:
           name: Setup test database (per parallel container)
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             bundle exec rails db:create db:migrate
       - run:
           name: Cucumber tests
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             bundle exec cucumber
       - run:
           name: Rspec tests
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             mkdir -p ~/rspec
             bundle exec rspec --format progress \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.circleci/config.yml around lines 82 - 100, Replace the repeated per-step
export of DATABASE_URL by defining it once in the CircleCI environment that is
sourced for every run step: write the computed DATABASE_URL into $BASH_ENV (so
all run steps inherit it) and remove the duplicate `export
DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"`
lines from the "Setup test database", "Cucumber tests", and "Rspec tests" run
steps; reference the variable name DATABASE_URL, the special file $BASH_ENV, and
the three run step names to locate and update the config.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.circleci/config.yml:
- Line 43: Replace the pinned Bundler version used in CI install steps so it
matches Gemfile.lock: update each instance of the step string "- run: gem
install bundler:2.3.27" to install bundler 2.6.9 instead (there are three
occurrences of that run command in the CI config); ensure all CI install steps
use "gem install bundler:2.6.9" so Bundler version aligns with BUNDLED WITH
2.6.9.
- Line 55: Replace the outdated CI PostgreSQL image reference
"cimg/postgres:9.6" in the .circleci/config.yml with a supported version that
matches the project's README and Rails 8.1 requirements (for example
"cimg/postgres:14"); update the image tag value where "image: cimg/postgres:9.6"
appears so CI runs against PostgreSQL 12+ (preferably 14 to match the
doc/production version).

In `@spec/controllers/api/v1/appointment_slots_controller_spec.rb`:
- Around line 335-337: This test duplicates default presets by creating weekday
presets again; either remove the (1..7).each FactoryBot.create
:appointment_slot_preset loop or clear existing presets first (e.g.
AppointmentSlotPreset.unscoped.delete_all) before creating new ones so the
example doesn't change shared describe-level state—locate the block using
AppointmentSlot.unscoped.delete_all and the FactoryBot.create
:appointment_slot_preset loop and apply one of these fixes.
- Around line 339-340: The test currently posts to create an appointment slot
via post :create but never verifies it succeeded; update the spec to assert the
creation before calling get :calendar by checking the POST response (e.g.
expect(response).to have_http_status(:created) or :success) or by asserting a
record change around the call to post :create (e.g. expect { post :create,
params: ... }.to change(AppointmentSlot, :count).by(1)), then proceed to call
get :calendar and assert the calendar contents; reference the existing post
:create and get :calendar calls to locate where to add the assertion.

In `@spec/models/order_spec.rb`:
- Line 328: The test uses Order.delete_all which bypasses callbacks and
dependent cleanup and can leave orphaned rows or FK failures; replace
Order.delete_all with Order.destroy_all or, better, scope the cleanup to only
the records this example group created (e.g., use the same factory IDs or
created_at range used in this spec) so associated models like orders_packages,
goodcity_requests, and order_transport are cleaned via their callbacks; also
update the other occurrences noted (the similar calls referenced near the other
examples) to follow the same pattern.
- Around line 63-71: The spec removed type checks for timestamp columns; update
the expectations in spec/models/order_spec.rb to assert the columns are datetime
by adding .of_type(:datetime) to each timestamp column check (created_at,
updated_at, dispatch_started_at, cancelled_at, process_completed_at,
processed_at) so each it{ is_expected.to have_db_column(:created_at) } style
line becomes an assertion that includes .of_type(:datetime); leave the integer
checks for *_by_id columns unchanged (dispatch_started_by_id, cancelled_by_id,
process_completed_by_id).

---

Duplicate comments:
In `@spec/controllers/api/v1/deliveries_controller_spec.rb`:
- Around line 173-174: The cleanup query misuses "AT TIME ZONE 'HKT'" against a
timestamp without time zone and thus shifts the date; change the
Holiday.where(...) call to compare the stored timestamp's date directly (e.g.
use "date(holiday) = ?" or "holiday::date = ?") or normalize both sides to the
same zone before comparing so the newly created row (in this before block) is
actually matched; update the Holiday.where("date(holiday AT TIME ZONE 'HKT') =
?", date).delete_all invocation accordingly and keep the Holiday.is_holiday?
stub as-is.

---

Nitpick comments:
In @.circleci/config.yml:
- Around line 56-58: Remove the dead POSTGRES_DB env var entry (POSTGRES_DB:
goodcity_server_test) from the CircleCI job environment and, in its place or
nearby, add a brief comment explaining the per-node naming scheme
(goodcity_server_test_<index>) and that each node creates its own DB via rails
db:create so the auto-created goodcity_server_test is unused; update any related
references to avoid confusion.
- Around line 82-100: Replace the repeated per-step export of DATABASE_URL by
defining it once in the CircleCI environment that is sourced for every run step:
write the computed DATABASE_URL into $BASH_ENV (so all run steps inherit it) and
remove the duplicate `export
DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"`
lines from the "Setup test database", "Cucumber tests", and "Rspec tests" run
steps; reference the variable name DATABASE_URL, the special file $BASH_ENV, and
the three run step names to locate and update the config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4fcb40da-fc70-4348-911c-bb4f916c25e1

📥 Commits

Reviewing files that changed from the base of the PR and between 119b22d and 8db3255.

📒 Files selected for processing (4)
  • .circleci/config.yml
  • spec/controllers/api/v1/appointment_slots_controller_spec.rb
  • spec/controllers/api/v1/deliveries_controller_spec.rb
  • spec/models/order_spec.rb

Comment thread .circleci/config.yml Outdated
Comment thread .circleci/config.yml Outdated
Comment thread spec/controllers/api/v1/appointment_slots_controller_spec.rb
Comment on lines 339 to 340
post :create, params: { appointment_slot: { quota: 0, timestamp: "2018-12-19T16:00:00.000Z", notes: "Closed on the 20th of december" } }
get :calendar, params: { from: '2018-12-19', to: '2018-12-21' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the slot creation succeeded before checking the calendar.

Right now this spec never proves the POST created the closing slot, so the calendar assertions are less diagnostic if the request starts returning 4xx/5xx.

Suggested fix
       post :create, params: { appointment_slot: { quota: 0, timestamp: "2018-12-19T16:00:00.000Z", notes: "Closed on the 20th of december" } }
+      expect(response).to have_http_status(:created)
       get :calendar, params: { from: '2018-12-19', to: '2018-12-21' }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
post :create, params: { appointment_slot: { quota: 0, timestamp: "2018-12-19T16:00:00.000Z", notes: "Closed on the 20th of december" } }
get :calendar, params: { from: '2018-12-19', to: '2018-12-21' }
post :create, params: { appointment_slot: { quota: 0, timestamp: "2018-12-19T16:00:00.000Z", notes: "Closed on the 20th of december" } }
expect(response).to have_http_status(:created)
get :calendar, params: { from: '2018-12-19', to: '2018-12-21' }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/controllers/api/v1/appointment_slots_controller_spec.rb` around lines
339 - 340, The test currently posts to create an appointment slot via post
:create but never verifies it succeeded; update the spec to assert the creation
before calling get :calendar by checking the POST response (e.g.
expect(response).to have_http_status(:created) or :success) or by asserting a
record change around the call to post :create (e.g. expect { post :create,
params: ... }.to change(AppointmentSlot, :count).by(1)), then proceed to call
get :calendar and assert the calendar contents; reference the existing post
:create and get :calendar calls to locate where to add the assertion.

Comment thread spec/models/order_spec.rb Outdated
Comment thread spec/models/order_spec.rb
warp added 6 commits May 6, 2026 17:30
- Run rails db:reset between Cucumber and RSpec so each parallel node gets a
  clean schema plus seeds before RSpec (avoids committed feature data leaking).
- Load seed YAML with safe_load (Psych::Omap, Symbol, aliases) so db:seed works
  under strict Psych.
- Upsert CannedResponse by guid so db:seed is idempotent (unique guid validation).
Use db:drop db:create db:migrate instead of db:reset so we get a clean schema
without running db:seed in the test job.
Use db:schema:load when recreating the per-node test database after Cucumber
for a clean, fast reset before RSpec.
- Convert stored UTC timestamps to HK local date in SQL for AppointmentSlot.for_date
  and Holiday.is_holiday? so afternoon slots don't bucket to the prior day.
- Update specs to use the same UTC→HK conversion when cleaning 2018 data and make
  the PUT timestamp-conflict example resilient to prior created slots.
Avoid assuming user id 1 is missing; derive a guaranteed-missing id from
(User.maximum(:id) + 1) so the job spec is stable in full-suite runs.
- Avoid assuming a clean DB: clear Packages in the simple index serialization
  example, use change(...).by(1) for split_package.
- Avoid assuming package_id=1 is missing in print_barcode; compute a guaranteed
  missing id.
- Make box/pallet GoodcitySetting fixtures idempotent by deleting existing keys
  before creating them (prevents uniqueness failures in randomized runs).
@warp
warp requested a review from steveyken May 6, 2026 10:03
warp added 2 commits May 6, 2026 22:04
Assert Order timestamp columns are datetime types (created_at, updated_at,
dispatch_started_at, cancelled_at, process_completed_at, processed_at) while
leaving *_by_id integer expectations unchanged.
@warp
warp force-pushed the upgrade/rails-8.1 branch from 59bc389 to 744f686 Compare May 6, 2026 10:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
.circleci/config.yml (1)

43-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bundler 2.3.27 still does not match Gemfile.lock (BUNDLED WITH 2.6.9).

All three install sites (lines 43, 78, 123) still pin bundler:2.3.27 while the lockfile in this PR is BUNDLED WITH 2.6.9. Installing an older Bundler than what generated the lockfile will trigger a Bundler downgrade warning at minimum, and on Ruby 3.4.9 can fail the bundle install outright. Bump all three to 2.6.9 (or whatever value Gemfile.lock records on this branch).

🔧 Proposed fix
-      - run: gem install bundler:2.3.27
+      - run: gem install bundler:2.6.9
-      - run: gem install bundler:2.3.27 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3)
+      - run: gem install bundler:2.6.9 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3)

(applies to lines 43, 78, and 123)

Also applies to: 78-78, 123-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.circleci/config.yml at line 43, Update all occurrences of the hardcoded
Bundler version in the CI steps that currently run "gem install bundler:2.3.27"
to match the Gemfile.lock's BUNDLED WITH version (2.6.9); specifically replace
each "gem install bundler:2.3.27" invocation (the three run steps that install
bundler) with "gem install bundler:2.6.9" (or the exact version shown in
Gemfile.lock) so the CI installs the same Bundler used to generate the lockfile.
🧹 Nitpick comments (4)
.circleci/config.yml (2)

82-105: 💤 Low value

Optional: DRY the per-node DATABASE_URL by exporting it once to BASH_ENV.

The same export DATABASE_URL=... line is repeated in four steps. CircleCI persists anything written to $BASH_ENV across subsequent run steps in the same job, so a single setup step keeps the per-node URL in one place and avoids drift if the naming scheme changes later.

♻️ Proposed refactor
       - run:
           name: Wait for DB and Redis containers to start
           command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
+      - run:
+          name: Configure per-node DATABASE_URL
+          command: |
+            echo 'export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"' >> "$BASH_ENV"
       - run:
           name: Setup test database (per parallel container)
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             bundle exec rails db:create db:migrate
       - run:
           name: Cucumber tests
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             bundle exec cucumber
       - run:
           name: Recreate test database after Cucumber
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             bundle exec rails db:drop db:create db:schema:load
       - run:
           name: Rspec tests
           command: |
-            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
             mkdir -p ~/rspec
             bundle exec rspec --format progress \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.circleci/config.yml around lines 82 - 105, Create a single preparatory run
step that writes the per-node DATABASE_URL into $BASH_ENV (using the same
postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}
expression) so subsequent steps inherit it, then remove the repeated export
lines from the steps named "Setup test database (per parallel container)",
"Cucumber tests", "Recreate test database after Cucumber", and "Rspec tests";
ensure the new setup step runs before those four steps so the environment
variable is available throughout the job.

82-96: ⚡ Quick win

Consider using consistent database reset strategy: both should use db:schema:load or both should use db:migrate.

The setup step uses db:create db:migrate while the recreate step uses db:drop db:create db:schema:load. Currently db/schema.rb is in sync with migrations, so both approaches load an identical schema state. However, using the same method for both maintains consistency and prevents any risk of schema drift if a migration is added without regenerating schema.rb. Using db:schema:load for both is slightly faster and more explicit about testing against the committed schema state.

Optional refactoring suggestion
      - run:
          name: Setup test database (per parallel container)
          command: |
            export DATABASE_URL="postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}"
-           bundle exec rails db:create db:migrate
+           bundle exec rails db:create db:schema:load
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.circleci/config.yml around lines 82 - 96, The setup step uses "bundle exec
rails db:create db:migrate" while the recreate step uses "bundle exec rails
db:drop db:create db:schema:load", causing inconsistency; change the setup
command used in the "Setup test database (per parallel container)" run to use
db:schema:load instead of db:migrate (keeping the same DATABASE_URL export) so
both setup and recreate use db:schema:load and ensure tests run against the
committed schema.rb.
spec/models/order_spec.rb (1)

362-364: 💤 Low value

Redundant after { Timecop.return } — the top-level hook already covers this.

The after { Timecop.return } at line 23 applies to every example in the entire RSpec.describe Order block, including all examples nested inside 'priority rules'. The duplicate hook at line 364 is a harmless no-op, but the comment on lines 362–363 ("without a top-level return, frozen time leaks") is now misleading — the top-level return exists.

🧹 Suggested cleanup
-    # Several nested contexts call Timecop.freeze; without a top-level return, frozen time leaks
-    # between sibling examples and breaks Order.priority (last_6pm / one_day_ago use Time.zone.now).
-    after { Timecop.return }
-
     context 'A submitted order' do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spec/models/order_spec.rb` around lines 362 - 364, Remove the redundant after
{ Timecop.return } hook inside the 'priority rules' context in
spec/models/order_spec.rb: the top-level RSpec.describe Order already calls
Timecop.return for every example, so delete this duplicate hook and update or
remove the misleading comment about needing a top-level return (ensure
references to Timecop.freeze/return and Order.priority remain accurate).
db/seeds.rb (1)

41-47: 💤 Low value

Nice idempotency improvement for canned responses.

Switching to find_or_initialize_by(guid:) + assign_attributes + save! makes re-running db:seed safe for this collection, matches the PR's "make db seeding idempotent" goal, and surfaces validation errors via save!. Note that several other blocks in this file (e.g. DonorCondition.create, RejectionReason.create, BookingType.create, Holiday.create at lines 140–141, IdentityType.create, Country.create) still use plain create and will keep duplicating rows on re-seed — consider extending the same pattern there in a follow-up if full idempotency is the goal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@db/seeds.rb` around lines 41 - 47, Several seed blocks still use plain create
(e.g., DonorCondition.create, RejectionReason.create, BookingType.create,
Holiday.create, IdentityType.create, Country.create) which will duplicate rows
on re-seed; update each of these sections to follow the canned_responses pattern
by finding or initializing by the unique identifier (e.g., guid or name),
calling assign_attributes(...) with the sanitized attrs hash (like h =
attrs.stringify_keys), and then save! so seeding becomes idempotent and
validation errors surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/appointment_slot.rb`:
- Line 12: The upcoming scope uses DateTime.now which ignores Time.zone; update
the boundary to use Rails time so the day boundary respects the app timezone —
in appointment_slot.rb change the scope :upcoming lambda to compute the cutoff
with Time.current.beginning_of_day (e.g.,
Time.current.beginning_of_day.utc.to_fs(:db) or
Time.zone.now.beginning_of_day.utc.to_fs(:db)) instead of
DateTime.now.beginning_of_day.utc.to_fs(:db) so the scope uses the configured
Time.zone.

In `@app/models/holiday.rb`:
- Around line 13-18: The is_holiday? class method on Holiday currently uses
Holiday.where(...).count > 0 which is inefficient; change it to use
Holiday.where("date((holiday AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Hong_Kong')
= ?", date.to_date).exists? to perform an existence check without counting all
rows, keeping the existing timezone SQL expression and the date.to_date
conversion in place.

---

Duplicate comments:
In @.circleci/config.yml:
- Line 43: Update all occurrences of the hardcoded Bundler version in the CI
steps that currently run "gem install bundler:2.3.27" to match the
Gemfile.lock's BUNDLED WITH version (2.6.9); specifically replace each "gem
install bundler:2.3.27" invocation (the three run steps that install bundler)
with "gem install bundler:2.6.9" (or the exact version shown in Gemfile.lock) so
the CI installs the same Bundler used to generate the lockfile.

---

Nitpick comments:
In @.circleci/config.yml:
- Around line 82-105: Create a single preparatory run step that writes the
per-node DATABASE_URL into $BASH_ENV (using the same
postgres://postgres@localhost:5432/goodcity_server_test_${CIRCLE_NODE_INDEX:-0}
expression) so subsequent steps inherit it, then remove the repeated export
lines from the steps named "Setup test database (per parallel container)",
"Cucumber tests", "Recreate test database after Cucumber", and "Rspec tests";
ensure the new setup step runs before those four steps so the environment
variable is available throughout the job.
- Around line 82-96: The setup step uses "bundle exec rails db:create
db:migrate" while the recreate step uses "bundle exec rails db:drop db:create
db:schema:load", causing inconsistency; change the setup command used in the
"Setup test database (per parallel container)" run to use db:schema:load instead
of db:migrate (keeping the same DATABASE_URL export) so both setup and recreate
use db:schema:load and ensure tests run against the committed schema.rb.

In `@db/seeds.rb`:
- Around line 41-47: Several seed blocks still use plain create (e.g.,
DonorCondition.create, RejectionReason.create, BookingType.create,
Holiday.create, IdentityType.create, Country.create) which will duplicate rows
on re-seed; update each of these sections to follow the canned_responses pattern
by finding or initializing by the unique identifier (e.g., guid or name),
calling assign_attributes(...) with the sanitized attrs hash (like h =
attrs.stringify_keys), and then save! so seeding becomes idempotent and
validation errors surface.

In `@spec/models/order_spec.rb`:
- Around line 362-364: Remove the redundant after { Timecop.return } hook inside
the 'priority rules' context in spec/models/order_spec.rb: the top-level
RSpec.describe Order already calls Timecop.return for every example, so delete
this duplicate hook and update or remove the misleading comment about needing a
top-level return (ensure references to Timecop.freeze/return and Order.priority
remain accurate).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc01e213-f4ce-4a1c-93b6-d11df4e521c8

📥 Commits

Reviewing files that changed from the base of the PR and between 119b22d and 744f686.

📒 Files selected for processing (9)
  • .circleci/config.yml
  • app/models/appointment_slot.rb
  • app/models/holiday.rb
  • db/seeds.rb
  • spec/controllers/api/v1/appointment_slots_controller_spec.rb
  • spec/controllers/api/v1/deliveries_controller_spec.rb
  • spec/controllers/api/v1/packages_controller_spec.rb
  • spec/jobs/user_safe_delete_job_spec.rb
  • spec/models/order_spec.rb

push_targets [ Channel::STOCK_CHANNEL ]

scope :upcoming, -> { where("timestamp >= ?", DateTime.now.beginning_of_day.utc.to_s(:db)) }
scope :upcoming, -> { where("timestamp >= ?", DateTime.now.beginning_of_day.utc.to_fs(:db)) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

fd -t f "appointment_slot.rb" | head -5

Repository: crossroads/api.goodcity

Length of output: 98


🏁 Script executed:

cat -n app/models/appointment_slot.rb | head -50

Repository: crossroads/api.goodcity

Length of output: 2158


🏁 Script executed:

rg "Time\.zone|Time\.current|DateTime\.now" app/models/ -A 2 -B 2

Repository: crossroads/api.goodcity

Length of output: 4014


🌐 Web query:

Rails DateTime.now vs Time.current timezone behavior

💡 Result:

Rails time zone behavior differs primarily because Time.current and DateTime.current are Rails time-zone aware, while Time.now/DateTime.now are not. 1) Time.current (Rails) vs Time.now (Ruby) - In Rails, Time.current returns Time.zone.now when Time.zone / config.time_zone is set; otherwise it falls back to Time.now. [1][2] - Time.now is standard Ruby time and uses the system/server environment timezone, not Rails’ configured Time.zone. (By contrast with Rails’ definition of Time.current.) [1] 2) DateTime.current vs DateTime.now - In Rails, DateTime.current returns Time.zone.now.to_datetime when Time.zone / config.time_zone is set; otherwise it falls back to Time.now.to_datetime. [3][4] - DateTime.now is standard Ruby and is not Rails time-zone aware (it does not use Time.zone). (This is implied by Rails’ explicit conditional implementation of DateTime.current.) [3] 3) Practical implications for “timezone behavior” - If your Rails app sets config.time_zone (so Time.zone is present), then: - Time.current and DateTime.current reflect the Rails application time zone (including DST rules for that zone). [1][3] - Time.now and DateTime.now reflect the system timezone, which can differ from the Rails app’s configured zone. [1][3] - If Time.zone is not set in Rails, then Time.current and DateTime.current reduce to the non-zone-aware versions (Time.now / Time.now.to_datetime). [1][3] 4) UTC vs local zone - Calling .utc on any of these values will convert the instant to UTC for that object, but the key difference is what zone-aware instant you started from (Rails zone via *.current vs system zone via *.now). The Rails APIs explicitly base *.current on Time.zone.now. [1][3] Recommendation - Prefer Time.current (and DateTime.current / Time.zone.now) for “current time” in a Rails app where you rely on config.time_zone and DST-correct calculations. [1][3] - Prefer Time.now / DateTime.now only when you specifically want the system’s notion of “now,” not Rails’ configured Time.zone. [1][3]

Citations:


Use Rails time for the upcoming scope boundary.

DateTime.now ignores Time.zone, so this boundary flips at system midnight instead of the application's configured timezone (e.g., Hong Kong). This can hide same-day morning slots for several hours when the server runs in UTC. The codebase consistently uses Time.current elsewhere (e.g., holiday.rb uses Time.zone.now.beginning_of_day for similar logic).

Suggested fix
-  scope :upcoming, -> { where("timestamp >= ?", DateTime.now.beginning_of_day.utc.to_fs(:db)) }
+  scope :upcoming, -> { where("timestamp >= ?", Time.current.beginning_of_day.utc) }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/appointment_slot.rb` at line 12, The upcoming scope uses
DateTime.now which ignores Time.zone; update the boundary to use Rails time so
the day boundary respects the app timezone — in appointment_slot.rb change the
scope :upcoming lambda to compute the cutoff with Time.current.beginning_of_day
(e.g., Time.current.beginning_of_day.utc.to_fs(:db) or
Time.zone.now.beginning_of_day.utc.to_fs(:db)) instead of
DateTime.now.beginning_of_day.utc.to_fs(:db) so the scope uses the configured
Time.zone.

Comment thread app/models/holiday.rb
Comment on lines 13 to +18
def self.is_holiday?(date)
Holiday.where(" date(holiday AT TIME ZONE 'HKT') = ?", date.to_date).count > 0
# Use IANA TZ name instead of abbreviation (Postgres 9.6 on CI can treat 'HKT' inconsistently).
Holiday.where(
"date((holiday AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Hong_Kong') = ?",
date.to_date
).count > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's read the holiday.rb file to confirm the current state
cat -n app/models/holiday.rb | head -30

Repository: crossroads/api.goodcity

Length of output: 894


🏁 Script executed:

# Search for usages of is_holiday? to understand how it's called
rg "is_holiday\?" --type ruby -B 2 -A 2

Repository: crossroads/api.goodcity

Length of output: 2020


🏁 Script executed:

# Check how scheduled_date is defined/used in the deliveries controller
rg "scheduled_date" app/controllers/api/v1/deliveries_controller.rb -B 3 -A 3

Repository: crossroads/api.goodcity

Length of output: 766


🏁 Script executed:

# Check Rails time zone configuration
rg "config\.time_zone\|Time\.zone" config/ app/ --type ruby | head -20

Repository: crossroads/api.goodcity

Length of output: 49


🏁 Script executed:

# Verify the test setup to understand what time zones are being passed
rg "is_holiday\?" spec/controllers/api/v1/deliveries_controller_spec.rb -B 5 -A 10

Repository: crossroads/api.goodcity

Length of output: 1356


🏁 Script executed:

# Get the full scheduled_date method definition
rg "def scheduled_date" app/controllers/api/v1/deliveries_controller.rb -A 20

Repository: crossroads/api.goodcity

Length of output: 971


🏁 Script executed:

# Check what Rails time zone is configured
rg "config\.time_zone" --type ruby -r '$0' | head -10

Repository: crossroads/api.goodcity

Length of output: 124


🏁 Script executed:

# Also check initializers and environment configs
fd -e rb config/ | xargs grep -l "time_zone\|Time\.zone" 2>/dev/null

Repository: crossroads/api.goodcity

Length of output: 403


🏁 Script executed:

# Comprehensive search for all calls to is_holiday? to verify all usage patterns
rg "\.is_holiday\?" --type ruby -B 1 -A 1

Repository: crossroads/api.goodcity

Length of output: 960


Replace .count > 0 with .exists? for efficiency.

The method signature accepts a flexible date argument that calls .to_date on it. However, in current usage (scheduled_date from the controller), the argument is already a Date object extracted via Time.zone.parse(...).to_date where Time.zone is configured as 'Hong Kong'. The timezone normalization concern does not apply to actual callers in this codebase.

Consider the performance improvement:

 def self.is_holiday?(date)
   Holiday.where(
     "date((holiday AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Hong_Kong') = ?",
     date.to_date
-  ).count > 0
+  ).exists?
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def self.is_holiday?(date)
Holiday.where(" date(holiday AT TIME ZONE 'HKT') = ?", date.to_date).count > 0
# Use IANA TZ name instead of abbreviation (Postgres 9.6 on CI can treat 'HKT' inconsistently).
Holiday.where(
"date((holiday AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Hong_Kong') = ?",
date.to_date
).count > 0
def self.is_holiday?(date)
# Use IANA TZ name instead of abbreviation (Postgres 9.6 on CI can treat 'HKT' inconsistently).
Holiday.where(
"date((holiday AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Hong_Kong') = ?",
date.to_date
).exists?
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/holiday.rb` around lines 13 - 18, The is_holiday? class method on
Holiday currently uses Holiday.where(...).count > 0 which is inefficient; change
it to use Holiday.where("date((holiday AT TIME ZONE 'UTC') AT TIME ZONE
'Asia/Hong_Kong') = ?", date.to_date).exists? to perform an existence check
without counting all rows, keeping the existing timezone SQL expression and the
date.to_date conversion in place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant