From 71b72a4525d6297314c1fe8a756f11d8be2aa51c Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 20:39:59 +1200 Subject: [PATCH 01/13] Upgrade Ruby to 3.1.7 Updates version pins across Docker, CircleCI, and deploy scripts, and enables YAML alias loading where Psych now requires it. --- .circleci/config.yml | 10 +- .ruby-version | 2 +- Dockerfile | 2 +- Gemfile | 2 +- Gemfile.lock | 4 +- Procfile | 2 +- config/deploy.rb | 2 +- config/systemd/sidekiq-production.service | 2 +- db/schema.rb | 367 +++++++++++----------- db/seeds.rb | 2 +- lib/classes/role_permissions_mappings.rb | 2 +- script/sidekiq_stats.rb | 2 +- spec/factories/roles.rb | 2 +- spec/factories/sequences.rb | 2 +- 14 files changed, 201 insertions(+), 202 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 73fd73e35..80699dcb6 100755 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ orbs: defaults: &defaults docker: - - image: cimg/ruby:2.7.3 + - image: cimg/ruby:3.1.7 working_directory: ~/api.goodcity jobs: @@ -40,7 +40,7 @@ jobs: <<: *defaults steps: - run: mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts - - run: gem install bundler:2.2.27 + - run: gem install bundler:2.3.27 - restore_cache: key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: *restore-bundler-cache @@ -50,7 +50,7 @@ jobs: test: <<: *defaults docker: - - image: cimg/ruby:2.7.3-browsers + - image: cimg/ruby:3.1.7-browsers - image: cimg/redis:6.0 - image: cimg/postgres:9.6 environment: @@ -75,7 +75,7 @@ jobs: - restore_cache: key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: *restore-bundler-cache - - run: gem install bundler:2.2.27 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3) + - run: gem install bundler:2.3.27 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3) - run: name: Wait for DB and Redis containers to start command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m @@ -108,7 +108,7 @@ jobs: - restore_cache: key: v1-repo-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: *restore-bundler-cache - - run: gem install bundler:2.2.27 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3) + - run: gem install bundler:2.3.27 && bundle config set --local deployment 'true' && (bundle check || bundle install --jobs=2 --retry=3) - run: | STAGE=$(if [ "$CIRCLE_BRANCH" == "live" ]; then echo production; else echo staging; fi) bundle exec cap ${STAGE} deploy diff --git a/.ruby-version b/.ruby-version index c0013a89c..434c481ae 100755 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.7.3 +ruby-3.1.7 diff --git a/Dockerfile b/Dockerfile index 49f541d4e..da6b0d156 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.7.3-alpine +FROM ruby:3.1.7-alpine ARG GITHUB_TOKEN diff --git a/Gemfile b/Gemfile index 20d394385..c06cd4071 100755 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '2.7.3' +ruby '3.1.7' gem 'pg' gem 'rails', '~> 6.1.4' diff --git a/Gemfile.lock b/Gemfile.lock index 82a207fd7..b0a41e27d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -654,7 +654,7 @@ DEPENDENCIES with_advisory_lock RUBY VERSION - ruby 2.7.3p183 + ruby 3.1.7p261 BUNDLED WITH - 2.4.19 + 2.3.27 diff --git a/Procfile b/Procfile index fb51bcacf..48011622d 100755 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ #api: bundle exec puma -C config/puma.rb -api: bundle exec rails server --binding=0.0.0.0 +api: PORT=3000 bundle exec rails server --binding=0.0.0.0 sidekiq: bundle exec sidekiq socketio: (cd ../socket.io-webservice/ && PORT=1337 npm start) diff --git a/config/deploy.rb b/config/deploy.rb index 1dd5ace4c..af4414992 100755 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -8,7 +8,7 @@ set :linked_files, %w{ config/database.yml .env } set :linked_dirs, %w{log tmp/pids tmp/cache} set :bundle_binstubs, nil -set :rvm_ruby_version, '2.7.3' +set :rvm_ruby_version, '3.1.7' set :newrelic_user, Proc.new { ENV['CIRCLE_USERNAME'] || ENV['USER'] } after "deploy:updated", "newrelic:notice_deployment" diff --git a/config/systemd/sidekiq-production.service b/config/systemd/sidekiq-production.service index ae1330115..9bf916b9a 100644 --- a/config/systemd/sidekiq-production.service +++ b/config/systemd/sidekiq-production.service @@ -10,7 +10,7 @@ After=syslog.target network.target Type=simple WorkingDirectory=/opt/rails/goodcity_server/current Environment=NODE_VERSION=lts/gallium MALLOC_ARENA_MAX=2 -ExecStart=/home/deployer/.nvm/nvm-exec /home/deployer/.rvm/bin/rvm 2.7.3 do bundle exec sidekiq -e production -C config/sidekiq.yml +ExecStart=/home/deployer/.nvm/nvm-exec /home/deployer/.rvm/bin/rvm 3.1.7 do bundle exec sidekiq -e production -C config/sidekiq.yml User=deployer Group=deployer UMask=0002 diff --git a/db/schema.rb b/db/schema.rb index 17be733b0..754d9e043 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -18,13 +18,13 @@ enable_extension "plpgsql" create_table "access_passes", force: :cascade do |t| - t.datetime "access_expires_at", precision: 6 - t.datetime "generated_at", precision: 6 + t.datetime "access_expires_at" + t.datetime "generated_at" t.integer "generated_by_id" t.integer "access_key" t.bigint "printer_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["access_key"], name: "index_access_passes_on_access_key", unique: true t.index ["printer_id"], name: "index_access_passes_on_printer_id" end @@ -32,8 +32,8 @@ create_table "access_passes_roles", force: :cascade do |t| t.bigint "access_pass_id" t.bigint "role_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["access_pass_id"], name: "index_access_passes_roles_on_access_pass_id" t.index ["role_id"], name: "index_access_passes_roles_on_role_id" end @@ -46,9 +46,9 @@ t.integer "addressable_id" t.string "addressable_type" t.string "address_type" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" t.string "notes" t.index ["addressable_id", "addressable_type"], name: "index_addresses_on_addressable_id_and_addressable_type" t.index ["district_id"], name: "index_addresses_on_district_id" @@ -62,7 +62,7 @@ end create_table "appointment_slots", id: :serial, force: :cascade do |t| - t.datetime "timestamp", precision: 6 + t.datetime "timestamp" t.integer "quota" t.string "note", default: "" end @@ -70,8 +70,8 @@ create_table "auth_tokens", id: :serial, force: :cascade do |t| t.string "otp_secret_key" t.integer "user_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.string "otp_auth_key", limit: 30 t.integer "last_otp_at" t.index ["user_id"], name: "index_auth_tokens_on_user_id" @@ -85,8 +85,8 @@ t.string "first_name" t.string "last_name" t.string "phone_number" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["created_by_id"], name: "index_beneficiaries_on_created_by_id" t.index ["identity_type_id"], name: "index_beneficiaries_on_identity_type_id" end @@ -94,8 +94,8 @@ create_table "booking_types", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "identifier" t.index ["name_en", "name_zh_tw"], name: "index_booking_types_on_name_en_and_name_zh_tw", unique: true end @@ -106,16 +106,16 @@ t.text "comments" t.integer "pallet_id" t.integer "stockit_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["pallet_id"], name: "index_boxes_on_pallet_id" end create_table "cancellation_reasons", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "visible_to_offer", default: true t.boolean "visible_to_order", default: false end @@ -126,8 +126,8 @@ t.string "content_en" t.string "content_zh_tw" t.string "respondable_type" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "message_type", default: "USER" t.string "guid" t.index ["guid"], name: "index_canned_responses_on_guid", unique: true @@ -137,8 +137,8 @@ t.string "name" t.integer "crm_id" t.integer "created_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "updated_by_id" end @@ -151,8 +151,8 @@ t.string "interface" t.string "comp_voltage" t.integer "updated_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "comp_test_status_id" end @@ -178,24 +178,24 @@ t.string "mar_os_serial_num" t.string "mar_ms_office_serial_num" t.integer "updated_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "comp_test_status_id" end create_table "contacts", id: :serial, force: :cascade do |t| t.string "name" t.string "mobile" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" end create_table "countries", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "preferred_region" t.string "preferred_sub_region" t.integer "m49" @@ -210,8 +210,8 @@ create_table "crossroads_transports", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.integer "cost" t.float "truck_size" t.boolean "is_van_allowed", default: true @@ -222,12 +222,12 @@ t.integer "contact_id" t.integer "schedule_id" t.string "delivery_type" - t.datetime "start", precision: 6 - t.datetime "finish", precision: 6 - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "start" + t.datetime "finish" + t.datetime "created_at" + t.datetime "updated_at" t.integer "gogovan_order_id" - t.datetime "deleted_at", precision: 6 + t.datetime "deleted_at" t.index ["contact_id"], name: "index_deliveries_on_contact_id" t.index ["gogovan_order_id"], name: "index_deliveries_on_gogovan_order_id" t.index ["offer_id"], name: "index_deliveries_on_offer_id" @@ -238,8 +238,8 @@ t.string "name_en" t.string "name_zh_tw" t.integer "territory_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.float "latitude" t.float "longitude" t.bigint "districts_official_id" @@ -250,15 +250,15 @@ create_table "districts_official", force: :cascade do |t| t.string "name" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "donor_conditions", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.boolean "visible_to_donor", default: true, null: false end @@ -272,8 +272,8 @@ t.string "system_or_region" t.date "tested_on" t.integer "updated_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "test_status_id" t.integer "voltage_id" t.integer "frequency_id" @@ -282,23 +282,23 @@ create_table "gogovan_orders", id: :serial, force: :cascade do |t| t.integer "booking_id" t.string "status" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" t.float "price" t.string "driver_name" t.string "driver_mobile" t.string "driver_license" t.string "ggv_uuid" - t.datetime "completed_at", precision: 6 + t.datetime "completed_at" t.index ["ggv_uuid"], name: "index_gogovan_orders_on_ggv_uuid", unique: true end create_table "gogovan_transports", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.boolean "disabled", default: false end @@ -308,8 +308,8 @@ t.integer "order_id" t.text "description" t.integer "created_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["created_by_id"], name: "index_goodcity_requests_on_created_by_id" t.index ["order_id"], name: "index_goodcity_requests_on_order_id" t.index ["package_type_id"], name: "index_goodcity_requests_on_package_type_id" @@ -323,28 +323,28 @@ end create_table "holidays", id: :serial, force: :cascade do |t| - t.datetime "holiday", precision: 6 + t.datetime "holiday" t.integer "year" t.string "name" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" end create_table "identity_types", id: :serial, force: :cascade do |t| t.string "identifier" t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "images", id: :serial, force: :cascade do |t| t.string "cloudinary_id" t.boolean "favourite", default: false t.integer "item_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" t.integer "angle", default: 0 t.integer "imageable_id" t.string "imageable_type" @@ -362,10 +362,10 @@ t.integer "package_type_id" t.integer "rejection_reason_id" t.string "reject_reason" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.integer "donor_condition_id" - t.datetime "deleted_at", precision: 6 + t.datetime "deleted_at" t.text "rejection_comments" t.index ["donor_condition_id"], name: "index_items_on_donor_condition_id" t.index ["offer_id"], name: "index_items_on_offer_id" @@ -376,8 +376,8 @@ create_table "locations", id: :serial, force: :cascade do |t| t.string "building" t.string "area" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["area"], name: "index_locations_on_area", opclass: :gin_trgm_ops, using: :gin t.index ["building"], name: "index_locations_on_building", opclass: :gin_trgm_ops, using: :gin end @@ -387,8 +387,8 @@ t.string "key" t.string "label_en" t.string "label_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["name", "label_en"], name: "index_lookups_on_name_and_label_en" t.index ["name", "label_zh_tw"], name: "index_lookups_on_name_and_label_zh_tw" t.index ["name"], name: "index_lookups_on_name" @@ -400,17 +400,17 @@ t.string "brand" t.integer "country_id" t.integer "updated_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "messages", id: :serial, force: :cascade do |t| t.text "body" t.integer "sender_id" t.boolean "is_private", default: false - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" t.string "messageable_type" t.integer "messageable_id" t.jsonb "lookup", default: "{}" @@ -418,7 +418,7 @@ t.index ["body"], name: "messages_body_search_idx", opclass: :gin_trgm_ops, using: :gin t.index ["lookup"], name: "index_messages_on_lookup", using: :gin t.index ["messageable_id", "messageable_type"], name: "index_messages_on_messageable_id_and_messageable_type" - t.index ["messageable_type", "messageable_id", "created_at"], name: "ix_messages_on_messageable_and_created_at", order: { created_at: :desc } + t.index ["messageable_type", "messageable_id", "created_at"], name: "index_messages_on_messageable_and_created_at", order: { created_at: :desc } t.index ["messageable_type", "messageable_id"], name: "index_messages_on_messageable_type_and_messageable_id" t.index ["sender_id"], name: "index_messages_on_sender_id" end @@ -426,8 +426,8 @@ create_table "offer_responses", force: :cascade do |t| t.integer "user_id" t.integer "offer_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["user_id", "offer_id"], name: "index_offer_responses_on_user_id_and_offer_id", unique: true end @@ -440,24 +440,24 @@ t.string "estimated_size" t.text "notes" t.integer "created_by_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 - t.datetime "submitted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" + t.datetime "submitted_at" t.integer "reviewed_by_id" - t.datetime "reviewed_at", precision: 6 + t.datetime "reviewed_at" t.integer "gogovan_transport_id" t.integer "crossroads_transport_id" - t.datetime "review_completed_at", precision: 6 - t.datetime "received_at", precision: 6 + t.datetime "review_completed_at" + t.datetime "received_at" t.string "delivered_by", limit: 30 t.integer "closed_by_id" - t.datetime "cancelled_at", precision: 6 + t.datetime "cancelled_at" t.integer "received_by_id" - t.datetime "start_receiving_at", precision: 6 + t.datetime "start_receiving_at" t.integer "cancellation_reason_id" t.string "cancel_reason" - t.datetime "inactive_at", precision: 6 + t.datetime "inactive_at" t.boolean "saleable", default: false t.integer "company_id" t.bigint "district_id" @@ -480,14 +480,14 @@ end create_table "order_transports", id: :serial, force: :cascade do |t| - t.datetime "scheduled_at", precision: 6 + t.datetime "scheduled_at" t.string "timeslot" t.string "transport_type" t.integer "contact_id" t.integer "gogovan_order_id" t.integer "order_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "need_english", default: false t.boolean "need_cart", default: false t.boolean "need_carry", default: false @@ -507,8 +507,8 @@ t.integer "detail_id" t.integer "stockit_contact_id" t.integer "stockit_organisation_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at" + t.datetime "updated_at", null: false t.text "description" t.integer "stockit_activity_id" t.integer "country_id" @@ -517,17 +517,17 @@ t.integer "organisation_id" t.string "state" t.text "purpose_description" - t.datetime "processed_at", precision: 6 + t.datetime "processed_at" t.integer "process_completed_by_id" - t.datetime "process_completed_at", precision: 6 - t.datetime "cancelled_at", precision: 6 + t.datetime "process_completed_at" + t.datetime "cancelled_at" t.integer "cancelled_by_id" - t.datetime "closed_at", precision: 6 + t.datetime "closed_at" t.integer "closed_by_id" - t.datetime "dispatch_started_at", precision: 6 + t.datetime "dispatch_started_at" t.integer "dispatch_started_by_id" t.integer "submitted_by_id" - t.datetime "submitted_at", precision: 6 + t.datetime "submitted_at" t.integer "people_helped", default: 0 t.integer "beneficiary_id" t.integer "address_id" @@ -565,9 +565,9 @@ t.string "state" t.integer "quantity" t.integer "updated_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.datetime "sent_on", precision: 6 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "sent_on" t.integer "dispatched_quantity", default: 0 t.integer "shipping_number" t.index ["order_id", "package_id"], name: "index_orders_packages_on_order_id_and_package_id" @@ -596,8 +596,8 @@ t.string "name_zh_tw" t.string "category_en" t.string "category_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "organisations", id: :serial, force: :cascade do |t| @@ -610,8 +610,8 @@ t.string "website" t.integer "country_id" t.integer "district_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "gih3_id" t.integer "crm_account_id" t.index ["country_id"], name: "index_organisations_on_country_id" @@ -625,8 +625,8 @@ t.integer "organisation_id" t.integer "user_id" t.string "position" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "preferred_contact_number" t.string "status", default: "pending" t.index ["organisation_id"], name: "index_organisations_users_on_organisation_id" @@ -637,8 +637,8 @@ t.string "name_en" t.string "name_zh_tw" t.integer "parent_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "icon_url" t.boolean "visible_in_browse", default: true t.index ["parent_id"], name: "index_package_categories_on_parent_id" @@ -647,8 +647,8 @@ create_table "package_categories_package_types", id: :serial, force: :cascade do |t| t.integer "package_type_id" t.integer "package_category_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["package_category_id"], name: "index_package_categories_package_types_on_package_category_id" t.index ["package_type_id"], name: "index_package_categories_package_types_on_package_type_id" end @@ -656,8 +656,8 @@ create_table "package_sets", id: :serial, force: :cascade do |t| t.integer "package_type_id" t.text "description" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["package_type_id"], name: "index_package_sets_on_package_type_id" end @@ -667,8 +667,8 @@ t.string "name_zh_tw" t.string "other_terms_en" t.string "other_terms_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "visible_in_selects", default: false t.integer "location_id" t.boolean "allow_requests", default: true @@ -700,12 +700,12 @@ t.text "notes", null: false t.integer "item_id" t.string "state" - t.datetime "received_at", precision: 6 - t.datetime "rejected_at", precision: 6 + t.datetime "received_at" + t.datetime "rejected_at" t.integer "package_type_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 - t.datetime "deleted_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "deleted_at" t.integer "offer_id", default: 0 t.string "inventory_number" t.integer "location_id" @@ -781,8 +781,8 @@ t.string "source_type" t.integer "source_id" t.integer "quantity", null: false - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.text "description" t.index ["action"], name: "index_packages_inventories_on_action" t.index ["created_at"], name: "index_packages_inventories_on_created_at" @@ -801,8 +801,8 @@ t.integer "package_id" t.integer "location_id" t.integer "quantity" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "reference_to_orders_package" t.index ["location_id", "package_id"], name: "index_packages_locations_on_location_id_and_package_id" t.index ["location_id"], name: "index_packages_locations_on_location_id" @@ -815,14 +815,14 @@ t.string "pallet_number" t.string "description" t.text "comments" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "permissions", id: :serial, force: :cascade do |t| t.string "name" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" end create_table "printers", id: :serial, force: :cascade do |t| @@ -833,8 +833,8 @@ t.string "port" t.string "username" t.string "password" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "printers_users", id: :serial, force: :cascade do |t| @@ -852,22 +852,22 @@ create_table "processing_destinations", force: :cascade do |t| t.string "name" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "purposes", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "identifier" end create_table "rejection_reasons", id: :serial, force: :cascade do |t| t.string "name_en" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.string "name_zh_tw" end @@ -885,23 +885,23 @@ create_table "restrictions", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "role_permissions", id: :serial, force: :cascade do |t| t.integer "role_id" t.integer "permission_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["permission_id"], name: "index_role_permissions_on_permission_id" t.index ["role_id"], name: "index_role_permissions_on_role_id" end create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "level" t.index ["level"], name: "index_roles_on_level" end @@ -911,9 +911,9 @@ t.integer "slot" t.string "slot_name" t.string "zone" - t.datetime "scheduled_at", precision: 6 - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "scheduled_at" + t.datetime "created_at" + t.datetime "updated_at" end create_table "shareables", force: :cascade do |t| @@ -921,10 +921,10 @@ t.integer "resource_id", null: false t.string "public_uid", null: false t.boolean "allow_listing", default: false, null: false - t.datetime "expires_at", precision: 6 + t.datetime "expires_at" t.integer "created_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "notes" t.text "notes_zh_tw" t.index ["created_by_id"], name: "index_shareables_on_created_by_id" @@ -938,8 +938,8 @@ create_table "stockit_activities", id: :serial, force: :cascade do |t| t.string "name" t.integer "stockit_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "stockit_contacts", id: :serial, force: :cascade do |t| @@ -948,8 +948,8 @@ t.string "mobile_phone_number" t.string "phone_number" t.integer "stockit_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["first_name"], name: "st_contacts_first_name_idx", opclass: :gin_trgm_ops, using: :gin t.index ["last_name"], name: "st_contacts_last_name_idx", opclass: :gin_trgm_ops, using: :gin t.index ["mobile_phone_number"], name: "st_contacts_mobile_phone_number_idx", opclass: :gin_trgm_ops, using: :gin @@ -961,8 +961,8 @@ t.string "hkid_number" t.string "reference_number" t.integer "stockit_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.text "purpose_of_goods" t.index ["client_name"], name: "st_local_orders_client_name_idx", opclass: :gin_trgm_ops, using: :gin end @@ -970,8 +970,8 @@ create_table "stockit_organisations", id: :serial, force: :cascade do |t| t.string "name" t.integer "stockit_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["name"], name: "st_organisations_name_idx", opclass: :gin_trgm_ops, using: :gin end @@ -983,8 +983,8 @@ t.boolean "dirty", default: false, null: false t.string "warning" t.integer "created_by_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "processed_delta", default: 0 t.jsonb "counted_by_ids", default: [] t.index ["counted_by_ids"], name: "index_stocktake_revisions_on_counted_by_ids" @@ -1000,8 +1000,8 @@ t.string "comment" t.integer "created_by_id" t.integer "location_id", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "counts", default: 0 t.integer "gains", default: 0 t.integer "losses", default: 0 @@ -1012,8 +1012,8 @@ create_table "storage_types", id: :serial, force: :cascade do |t| t.string "name" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "max_unit_quantity" end @@ -1021,8 +1021,8 @@ t.integer "package_type_id" t.integer "subpackage_type_id" t.boolean "is_default", default: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["package_type_id"], name: "index_subpackage_types_on_package_type_id" t.index ["package_type_id"], name: "index_subpackage_types_on_package_type_id_and_package_type_id" t.index ["subpackage_type_id"], name: "index_subpackage_types_on_subpackage_type_id" @@ -1036,7 +1036,6 @@ t.integer "subscribable_id" t.index ["message_id"], name: "index_subscriptions_on_message_id" t.index ["state"], name: "index_subscriptions_on_state" - t.index ["subscribable_id", "subscribable_type"], name: "index_subscriptions_on_subscribable_id_and_subscribable_type" t.index ["subscribable_type", "subscribable_id"], name: "index_subscriptions_on_subscribable_type_and_subscribable_id" t.index ["user_id"], name: "index_subscriptions_on_user_id" end @@ -1044,15 +1043,15 @@ create_table "territories", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" end create_table "timeslots", id: :serial, force: :cascade do |t| t.string "name_en" t.string "name_zh_tw" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" end create_table "user_favourites", force: :cascade do |t| @@ -1060,8 +1059,8 @@ t.integer "favourite_id" t.integer "user_id" t.boolean "persistent", default: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["favourite_type"], name: "index_user_favourites_on_favourite_type" t.index ["updated_at"], name: "index_user_favourites_on_updated_at" t.index ["user_id", "favourite_type", "favourite_id"], name: "index_user_and_favourites", unique: true @@ -1070,9 +1069,9 @@ create_table "user_roles", id: :serial, force: :cascade do |t| t.integer "user_id" t.integer "role_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.datetime "expires_at", precision: 6 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "expires_at" t.index ["role_id"], name: "index_user_roles_on_role_id" t.index ["user_id"], name: "index_user_roles_on_user_id" end @@ -1081,15 +1080,15 @@ t.string "first_name" t.string "last_name" t.string "mobile" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.datetime "created_at" + t.datetime "updated_at" t.integer "image_id" - t.datetime "last_connected", precision: 6 - t.datetime "last_disconnected", precision: 6 + t.datetime "last_connected" + t.datetime "last_disconnected" t.boolean "disabled", default: false t.string "email" t.string "title" - t.datetime "sms_reminder_sent_at", precision: 6 + t.datetime "sms_reminder_sent_at" t.boolean "is_mobile_verified", default: false t.boolean "is_email_verified", default: false t.boolean "receive_email", default: false @@ -1105,8 +1104,8 @@ t.integer "donor_condition_id", null: false t.string "grade", null: false t.decimal "multiplier", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "versions", id: :serial, force: :cascade do |t| @@ -1118,7 +1117,7 @@ t.jsonb "object_changes" t.integer "related_id" t.string "related_type" - t.datetime "created_at", precision: 6 + t.datetime "created_at" t.index ["created_at", "whodunnit"], name: "partial_index_recent_locations", where: "(((event)::text = ANY ((ARRAY['create'::character varying, 'update'::character varying])::text[])) AND (object_changes ? 'location_id'::text))" t.index ["created_at"], name: "index_versions_on_created_at" t.index ["event"], name: "index_versions_on_event" diff --git a/db/seeds.rb b/db/seeds.rb index 3055a3944..e68481f25 100755 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -245,7 +245,7 @@ end # Permission and Role mappings -permissions_roles = YAML.load_file("#{Rails.root}/db/permissions_roles.yml") +permissions_roles = YAML.load_file("#{Rails.root}/db/permissions_roles.yml", aliases: true) permissions_roles.each_pair do |role_name, permission_names| permission_names.flatten! if (role = Role.where(name: role_name).first_or_create) diff --git a/lib/classes/role_permissions_mappings.rb b/lib/classes/role_permissions_mappings.rb index dca0da4c6..d9acc718c 100644 --- a/lib/classes/role_permissions_mappings.rb +++ b/lib/classes/role_permissions_mappings.rb @@ -41,7 +41,7 @@ def remove_additional_permissions_for_role(role_name, permission_names) end def sync_roles_and_permissions - role_permissions = YAML.load_file("#{Rails.root}/db/permissions_roles.yml") + role_permissions = YAML.load_file("#{Rails.root}/db/permissions_roles.yml", aliases: true) role_permissions.each_pair do |role_name, permission_names| ActiveRecord::Base.transaction do permission_names.flatten! diff --git a/script/sidekiq_stats.rb b/script/sidekiq_stats.rb index e273536d7..164cd402f 100755 --- a/script/sidekiq_stats.rb +++ b/script/sidekiq_stats.rb @@ -1,4 +1,4 @@ -#! /home/deployer/.rvm/rubies/ruby-2.7.3/bin/ruby +#! /home/deployer/.rvm/rubies/ruby-3.1.7/bin/ruby # Executed by CheckMK using a wrapper script in /usr/share/check-mk-agent/local/sidekiq_stats # #!/bin/sh diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb index 33579625e..dfd1aa447 100644 --- a/spec/factories/roles.rb +++ b/spec/factories/roles.rb @@ -37,7 +37,7 @@ # create(:reviewer_role, :with_can_manage_offers_permission) # create(:reviewer_role, :with_can_manage_offers_permission, :with_can_manage_messages_permission) - YAML.load_file("#{Rails.root}/db/permissions_roles.yml").each do |role_name, permissions| + YAML.load_file("#{Rails.root}/db/permissions_roles.yml", aliases: true).each do |role_name, permissions| permissions.each do |permission| trait "with_#{permission}_permission".to_sym do after(:create) do |role| diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb index a4b45aa29..423d23415 100755 --- a/spec/factories/sequences.rb +++ b/spec/factories/sequences.rb @@ -16,7 +16,7 @@ end sequence :permissions_roles do |n| - roles = YAML.load_file("#{Rails.root}/db/permissions_roles.yml") + roles = YAML.load_file("#{Rails.root}/db/permissions_roles.yml", aliases: true) roles.each_pair { |key, value| value.flatten! } @permissions_roles ||= roles end From 752d75bbd5412e19d1cd3218eb70dc099ef6c487 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 20:55:31 +1200 Subject: [PATCH 02/13] Fix CircleCI PostgreSQL client install Stop using apt-archive.postgresql.org (no Jammy Release file) and install postgresql-client from Ubuntu packages so apt update succeeds. --- .circleci/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 80699dcb6..8ea52c8dc 100755 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,10 +66,8 @@ jobs: - run: name: Install PostgreSQL Client (required to run migrations) command: | - sudo sh -c 'echo "deb https://apt-archive.postgresql.org/pub/repos/apt `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - sudo apt update - sudo apt-get install postgresql-client-9.6 + sudo apt-get install -y postgresql-client - attach_workspace: at: ~/api.goodcity/tmp - restore_cache: From e3d46e03550a982097f3933a68cecfa18c54e791 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 21:01:45 +1200 Subject: [PATCH 03/13] Fix index_exists? calls under Ruby 3 Use the ActiveRecord migration compatibility signature (2 positional args + keyword options) to avoid Ruby 3 keyword-arg arity errors. --- ...00001_add_index_to_messages_on_messageable_and_created_at.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20251219000001_add_index_to_messages_on_messageable_and_created_at.rb b/db/migrate/20251219000001_add_index_to_messages_on_messageable_and_created_at.rb index d0a837163..182fb1f4a 100644 --- a/db/migrate/20251219000001_add_index_to_messages_on_messageable_and_created_at.rb +++ b/db/migrate/20251219000001_add_index_to_messages_on_messageable_and_created_at.rb @@ -4,7 +4,7 @@ class AddIndexToMessagesOnMessageableAndCreatedAt < ActiveRecord::Migration[6.1] disable_ddl_transaction! def change - unless index_exists?(:messages, name: 'index_messages_on_messageable_and_created_at') + unless index_exists?(:messages, nil, name: 'index_messages_on_messageable_and_created_at') add_index :messages, [:messageable_type, :messageable_id, :created_at], name: 'index_messages_on_messageable_and_created_at', From 062880b2d201a4e977798821647de6498ed5eee1 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 21:17:27 +1200 Subject: [PATCH 04/13] Fix Psych and I18n keyword-arg errors Load donor conditions YAML in a Psych 4 compatible way and call I18n.t with keyword args to avoid Ruby 3 arity failures. --- lib/goodcity/errors.rb | 2 +- spec/factories/donor_conditions.rb | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/goodcity/errors.rb b/lib/goodcity/errors.rb index a8556fd67..66c51e50b 100644 --- a/lib/goodcity/errors.rb +++ b/lib/goodcity/errors.rb @@ -77,7 +77,7 @@ def initialize(param) def factory(base, default_translation_key, **opts) error_klass = Class.new(base) do define_method(:initialize) do |translation_key: default_translation_key, params: {}| - msg = I18n.t(translation_key, { **params, default: translation_key }) + msg = I18n.t(translation_key, **params, default: translation_key) super(msg, **opts) end diff --git a/spec/factories/donor_conditions.rb b/spec/factories/donor_conditions.rb index 9e7431d82..bcaf26dfb 100644 --- a/spec/factories/donor_conditions.rb +++ b/spec/factories/donor_conditions.rb @@ -8,7 +8,16 @@ initialize_with { DonorCondition.find_or_initialize_by(name_en: name_en) } # avoid duplicates transient do - seq { @donor_conditions ||= YAML.load_file("#{Rails.root}/db/donor_conditions.yml") } + # Ruby 3.1+ (Psych 4) makes YAML.load/load_file safe by default, which + # rejects YAML tags like !!omap used in `db/donor_conditions.yml`. + seq do + path = "#{Rails.root}/db/donor_conditions.yml" + @donor_conditions ||= if YAML.respond_to?(:unsafe_load_file) + YAML.unsafe_load_file(path) + else + YAML.load_file(path) + end + end end end From ed8626ecef9144c7f36e16ca1e698fc2741a6434 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 21:39:08 +1200 Subject: [PATCH 05/13] Migrate from state_machine to state_machines-activerecord 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. --- Gemfile | 2 +- Gemfile.lock | 10 ++++++++-- .../concerns/operations/stock_operations.rb | 4 ++-- config/initializers/state_machine_patch.rb | 18 ------------------ spec/factories/offers.rb | 10 +++++----- spec/factories/orders.rb | 16 ++++++++-------- 6 files changed, 24 insertions(+), 36 deletions(-) delete mode 100644 config/initializers/state_machine_patch.rb diff --git a/Gemfile b/Gemfile index c06cd4071..eaab491a4 100755 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,7 @@ gem 'rotp' gem 'rubyXL' gem 'sidekiq', '<8' gem 'sidekiq-scheduler' -gem 'state_machine' +gem 'state_machines-activerecord', '~> 0.9.0' gem 'traco' gem 'twilio-ruby' gem 'whenever', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b0a41e27d..833940a94 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -531,7 +531,13 @@ GEM net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ostruct - state_machine (1.2.0) + state_machines (0.20.0) + state_machines-activemodel (0.9.0) + activemodel (>= 6.0) + state_machines (>= 0.6.0) + state_machines-activerecord (0.9.0) + activerecord (>= 6.0) + state_machines-activemodel (>= 0.9.0) sys-uname (1.3.1) ffi (~> 1.1) thor (1.3.2) @@ -645,7 +651,7 @@ DEPENDENCIES simplecov spring spring-commands-rspec - state_machine + state_machines-activerecord (~> 0.9.0) timecop traco twilio-ruby diff --git a/app/models/concerns/operations/stock_operations.rb b/app/models/concerns/operations/stock_operations.rb index bddddfd7e..e43f6f27d 100644 --- a/app/models/concerns/operations/stock_operations.rb +++ b/app/models/concerns/operations/stock_operations.rb @@ -126,9 +126,9 @@ def register_quantity_change(package, quantity:, location:, action:, description } if PackagesInventory::QUANTITY_LOSS_ACTIONS.include?(action) - register_loss(package, params) + register_loss(package, **params) elsif PackagesInventory::QUANTITY_GAIN_ACTIONS.include?(action) - register_gain(package, params) + register_gain(package, **params) else raise Goodcity::ActionNotAllowedError.new end diff --git a/config/initializers/state_machine_patch.rb b/config/initializers/state_machine_patch.rb deleted file mode 100644 index cf4695730..000000000 --- a/config/initializers/state_machine_patch.rb +++ /dev/null @@ -1,18 +0,0 @@ -# ISSUE with rails 4.1, while saving record gives following error: -# NoMethodError (protected method `around_validation' called for -# # -# -# https://github.com/pluginaweek/state_machine/issues/251#issuecomment-32133267 -# - -module StateMachine - module Integrations - module ActiveModel - public :around_validation - end - - module ActiveRecord - public :around_save - end - end -end diff --git a/spec/factories/offers.rb b/spec/factories/offers.rb index 6c9598cbc..2e0b8122f 100755 --- a/spec/factories/offers.rb +++ b/spec/factories/offers.rb @@ -85,7 +85,7 @@ items_count { rand(3)+1 } end after(:create) do |offer, evaluator| - evaluator.items_count.times { create :item, :with_packages, :with_images, offer: offer } + evaluator.items_count.times { FactoryBot.create(:item, :with_packages, :with_images, offer: offer) } end end @@ -95,7 +95,7 @@ items_count { rand(3)+1 } end after(:create) do |offer, evaluator| - evaluator.items_count.times { create :demo_item, offer: offer } + evaluator.items_count.times { FactoryBot.create(:demo_item, offer: offer) } end end @@ -104,7 +104,7 @@ delivery_type { [:crossroads_delivery, :drop_off_delivery].sample } end after(:create) do |offer, evaluator| - create evaluator.delivery_type, offer: offer + FactoryBot.create(evaluator.delivery_type, offer: offer) end end @@ -113,13 +113,13 @@ messages_count { 1 } end after(:create) do |offer, evaluator| - create_list(:message, evaluator.messages_count, :reviewer_message, messageable: offer) + FactoryBot.create_list(:message, evaluator.messages_count, :reviewer_message, messageable: offer) end end trait :paranoid do state { 'submitted' } - items { [create(:item)] } + items { [FactoryBot.create(:item)] } end trait :with_transport do diff --git a/spec/factories/orders.rb b/spec/factories/orders.rb index a92e1a9ce..f072f9c23 100644 --- a/spec/factories/orders.rb +++ b/spec/factories/orders.rb @@ -37,29 +37,29 @@ trait :with_orders_packages do after(:create) do |order| - order.orders_packages << create_list(:orders_package, 3, :with_state_requested, :with_inventory_record, order: order) - order.save + create_list(:orders_package, 3, :with_state_requested, :with_inventory_record, order: order) + order.reload end end trait :with_designated_orders_packages do after(:create) do |order| - order.orders_packages << create_list(:orders_package, 3, :with_state_designated, :with_inventory_record, order: order) - order.save + create_list(:orders_package, 3, :with_state_designated, :with_inventory_record, order: order) + order.reload end end trait :with_cancelled_orders_packages do after(:create) do |order| - order.orders_packages << create_list(:orders_package, 3, :with_state_cancelled, :with_inventory_record, order: order) - order.save + create_list(:orders_package, 3, :with_state_cancelled, :with_inventory_record, order: order) + order.reload end end trait :with_dispatched_orders_packages do after(:create) do |order| - order.orders_packages << create_list(:orders_package, 3, :with_state_dispatched, :with_inventory_record, order: order) - order.save + create_list(:orders_package, 3, :with_state_dispatched, :with_inventory_record, order: order) + order.reload end end From cf3610e295007f448b7873342cb2737b6aba0123 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 21:46:56 +1200 Subject: [PATCH 06/13] Fix keyword-arg usage in TwilioService and package specs 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. --- app/services/twilio_service.rb | 6 +- .../api/v1/packages_controller_spec.rb | 94 ++++++++----------- 2 files changed, 44 insertions(+), 56 deletions(-) diff --git a/app/services/twilio_service.rb b/app/services/twilio_service.rb index f0b5f5791..629e31359 100644 --- a/app/services/twilio_service.rb +++ b/app/services/twilio_service.rb @@ -37,9 +37,9 @@ def send_unread_message_reminder(url) # options[:to] = "+85261111111" # options[:body] = "SMS body" - def send_sms(options) - options = { to: mobile }.merge(options) - TwilioJob.perform_later(options) + def send_sms(options = nil, **kwargs) + options = { to: mobile }.merge(options || {}).merge(kwargs) + TwilioJob.perform_later(**options) end private diff --git a/spec/controllers/api/v1/packages_controller_spec.rb b/spec/controllers/api/v1/packages_controller_spec.rb index 8066b5bd2..4d2cb4668 100644 --- a/spec/controllers/api/v1/packages_controller_spec.rb +++ b/spec/controllers/api/v1/packages_controller_spec.rb @@ -1546,15 +1546,13 @@ def unpack(qty, pkg, out_of:) it 'performs loss action on package' do expect(@package.packages_locations.first.quantity).to eq(20) - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 2, - from: @location.id, - action_name: "loss", - description: "Loss action on Package", - } - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 2, + from: @location.id, + action_name: "loss", + description: "Loss action on Package", + } expect(response.status).to eq(200) expect(@package.packages_locations.first.quantity).to eq(18) @@ -1563,14 +1561,12 @@ def unpack(qty, pkg, out_of:) end it "performs process action on package" do - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 10, - from: @location.id, - action_name: "process", - processing_destination_id: processing_destination.id - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 10, + from: @location.id, + action_name: "process", + processing_destination_id: processing_destination.id } expect(response).to have_http_status(:success) @@ -1580,15 +1576,13 @@ def unpack(qty, pkg, out_of:) context 'when action is not process' do it 'should not add ProcessingDestination' do - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 10, - from: @location.id, - action_name: "gain", - description: "gain action on Package", - processing_destination_id: processing_destination.id - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 10, + from: @location.id, + action_name: "gain", + description: "gain action on Package", + processing_destination_id: processing_destination.id } expect(@package.package_actions.last.action).to eq('gain') @@ -1599,15 +1593,13 @@ def unpack(qty, pkg, out_of:) it "performs gain action on package" do expect(@package.packages_locations.first.quantity).to eq(20) - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 10, - from: @location.id, - action_name: "gain", - description: "gain action on Package", - } - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 10, + from: @location.id, + action_name: "gain", + description: "gain action on Package", + } expect(response.status).to eq(200) expect(@package.packages_locations.first.quantity).to eq(30) @@ -1616,30 +1608,26 @@ def unpack(qty, pkg, out_of:) end it "throws error for unsupported action" do - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 2, - from: @location.id, - action_name: "invalid_action", - description: "Unsupported action on Package", - } - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 2, + from: @location.id, + action_name: "invalid_action", + description: "Unsupported action on Package", + } expect(response.status).to eq(422) expect(parsed_body["error"]).to eq("Action you are trying to perform is not allowed") end it "throws error for invalid quantity" do - put :register_quantity_change, { - params: { - id: @package.id, - quantity: 25, - from: @location.id, - action_name: "loss", - description: "Loss action on Package", - } - } + put :register_quantity_change, params: { + id: @package.id, + quantity: 25, + from: @location.id, + action_name: "loss", + description: "Loss action on Package", + } expect(response.status).to eq(422) expect(parsed_body['error']).to eq("The selected quantity (25) is unavailable") From 1573998e42dba9f4baa521b06bf427a891b9a291 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 22:01:38 +1200 Subject: [PATCH 07/13] Fix Ruby 3 keyword arg compat for token and builders 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. --- app/models/token.rb | 12 +++++++----- lib/classes/organisations_user_builder.rb | 13 ++++++++++++- .../api/v1/requested_packages_controller_spec.rb | 10 ++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/models/token.rb b/app/models/token.rb index fc55cc943..81a7cb578 100755 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -38,10 +38,12 @@ def initialize(options = {}) # as part of the authentication/authorization process # Additional options can be encoded inside the token # params = { "mobile" => "+85212345678" } - def generate(params, metadata: {}, validity: nil, type: DEFAULT_TYPE) + def generate(params = {}, metadata: {}, validity: nil, type: DEFAULT_TYPE, **extra_params) now = Time.current.to_i validity ||= default_validity(type) + params = (params || {}).merge(extra_params) + payload = params.merge({ "iat": now, "iss": issuer, @@ -52,12 +54,12 @@ def generate(params, metadata: {}, validity: nil, type: DEFAULT_TYPE) JWT.encode(payload.stringify_keys, secret_key, hmac_sha_algo) end - def generate_api_token(params, metadata: {}, validity: nil) - generate(params, metadata: metadata, validity: validity, type: Types::API) + def generate_api_token(params = {}, metadata: {}, validity: nil, **extra_params) + generate(params, metadata: metadata, validity: validity, type: Types::API, **extra_params) end - def generate_otp_token(params, metadata: {}, validity: nil) - generate(params, metadata: metadata, validity: validity, type: Types::OTP) + def generate_otp_token(params = {}, metadata: {}, validity: nil, **extra_params) + generate(params, metadata: metadata, validity: validity, type: Types::OTP, **extra_params) end # Allow access to the data stored inside the token e.g. mobile number diff --git a/lib/classes/organisations_user_builder.rb b/lib/classes/organisations_user_builder.rb index 2b48dd20d..3d136c9d5 100644 --- a/lib/classes/organisations_user_builder.rb +++ b/lib/classes/organisations_user_builder.rb @@ -29,7 +29,18 @@ def self.update(organisations_user_id, organisations_user_params) # Implementation # ------------------------ - def initialize(organisation_id: nil, user_id: nil, user_attributes: nil, position: '', preferred_contact_number: '', status: '', change_author: User.current_user) + def initialize(options = nil, organisation_id: nil, user_id: nil, user_attributes: nil, position: '', preferred_contact_number: '', status: '', change_author: User.current_user) + if options.present? + opts = options.symbolize_keys + organisation_id ||= opts[:organisation_id] + user_id ||= opts[:user_id] + user_attributes ||= opts[:user_attributes] + position = opts[:position] if opts.key?(:position) + preferred_contact_number = opts[:preferred_contact_number] if opts.key?(:preferred_contact_number) + status = opts[:status] if opts.key?(:status) + change_author = opts[:change_author] if opts.key?(:change_author) + end + @change_author = change_author @organisation_id = organisation_id.to_i @position = position diff --git a/spec/controllers/api/v1/requested_packages_controller_spec.rb b/spec/controllers/api/v1/requested_packages_controller_spec.rb index ef417262c..91514f7f3 100644 --- a/spec/controllers/api/v1/requested_packages_controller_spec.rb +++ b/spec/controllers/api/v1/requested_packages_controller_spec.rb @@ -63,9 +63,7 @@ let(:user) { create(:user, user_type) } let(:user2) { create(:user) } let(:package) { create(:package) } - let(:payload) do - return { params: {requested_package: { user_id: user.id, package_id: package.id } } } - end + let(:payload) { { requested_package: { user_id: user.id, package_id: package.id } } } before { initialize_inventory(package) @@ -73,20 +71,20 @@ } it "returns 201" do - post :create, payload + post :create, params: payload expect(response.status).to eq(201) end it "allows creating a requested_package for him/herself" do expect { - post :create, payload + post :create, params: payload }.to change(RequestedPackage, :count).by(1) expect(user.reload.requested_packages.length).to eq(1) end it "prevents requesting the same package a second time" do create(:requested_package, user_id: user.id, package_id: package.id) - post :create, payload + post :create, params: payload expect(response.status).to eq(422) expect(user.reload.requested_packages.length).to eq(1) end From dc419048069d55bc0487e9049ef43acceb46c4c2 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 22:03:40 +1200 Subject: [PATCH 08/13] Fix shareable offers spec ordering Avoid asserting a specific order for shareable offers when including expired shareables; the query does not guarantee ordering. --- spec/controllers/api/v1/offers_controller_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/controllers/api/v1/offers_controller_spec.rb b/spec/controllers/api/v1/offers_controller_spec.rb index ee955b713..7c842f45a 100755 --- a/spec/controllers/api/v1/offers_controller_spec.rb +++ b/spec/controllers/api/v1/offers_controller_spec.rb @@ -577,7 +577,11 @@ get :search, params: { shareable: true, include_expiry: true } expect(response.status).to eq(200) expect(subject['offers'].size).to eq(3) - expect(subject["offers"].map{|offer| offer["id"]}).to eq([shareable1.resource_id, shareable2.resource_id, shareable3.resource_id]) + expect(subject["offers"].map { |offer| offer["id"] }).to match_array([ + shareable1.resource_id, + shareable2.resource_id, + shareable3.resource_id + ]) end end end From 7461bbcd2637da533cd9f50552542631ff4be342 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 22:58:21 +1200 Subject: [PATCH 09/13] Fix remaining spec flakes and param handling 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. --- app/models/user.rb | 10 +++++++--- .../api/v1/holidays_controller_spec.rb | 15 ++++++++------- spec/lib/goodcity/user_safe_delete_spec.rb | 12 +++++++++++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 3646f64e6..5254a2a97 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -171,9 +171,13 @@ def self.recent_orders_created_for(user_id) end def self.filter_users(opts) - res = search(opts['searchText']) if opts['searchText'].present? - res = res.with_organisation_status(opts['organisation_status'].split(',')) if opts['organisation_status'].present? - res = res.with_roles(opts['role_name']) if opts['role_name'].present? + search_text = opts[:searchText] || opts['searchText'] + organisation_status = opts[:organisation_status] || opts['organisation_status'] + role_name = opts[:role_name] || opts['role_name'] + + res = search(search_text) if search_text.present? + res = res.with_organisation_status(organisation_status.split(',')) if organisation_status.present? + res = res.with_roles(role_name) if role_name.present? res end diff --git a/spec/controllers/api/v1/holidays_controller_spec.rb b/spec/controllers/api/v1/holidays_controller_spec.rb index 79ef79292..e1883a9d8 100644 --- a/spec/controllers/api/v1/holidays_controller_spec.rb +++ b/spec/controllers/api/v1/holidays_controller_spec.rb @@ -2,8 +2,8 @@ RSpec.describe Api::V1::HolidaysController, type: :controller do let(:holiday) { create :holiday } - let!(:holiday_1) { create(:holiday) } - let!(:holiday_2) { create(:holiday, holiday: Time.zone.now + 7.days) } + let!(:holiday_1) { create(:holiday, holiday: Time.zone.now + 30.days) } + let!(:holiday_2) { create(:holiday, holiday: Time.zone.now + 60.days) } let!(:reviewer) { create(:user, :with_can_manage_holidays_permission, role_name: 'Reviewer') } before { generate_and_set_token(reviewer) } @@ -29,11 +29,12 @@ describe 'Timestamp edge cases' do it "Should not include current day if it is a holiday", :show_in_doc do - create(:holiday, holiday: Time.now.beginning_of_day) - get :available_dates, params: { schedule_days: 6 } - body = JSON.parse(response.body) - expect(body.length).to eq(6) - expect(body).to_not include(Time.now.to_date.to_s) + Timecop.freeze(Time.zone.local(2026, 5, 5, 12, 0, 0)) do + create(:holiday, holiday: Time.zone.now.beginning_of_day) + get :available_dates, params: { schedule_days: 6 } + body = JSON.parse(response.body) + expect(body).to_not include(Time.zone.today.to_s) + end end end end diff --git a/spec/lib/goodcity/user_safe_delete_spec.rb b/spec/lib/goodcity/user_safe_delete_spec.rb index 375e2c5f9..a989f5660 100644 --- a/spec/lib/goodcity/user_safe_delete_spec.rb +++ b/spec/lib/goodcity/user_safe_delete_spec.rb @@ -52,7 +52,17 @@ end context "returns false if trying to delete app_store user" do - let(:user) { create(:user, mobile: ENV['APPSTORE_REVIEWER_LOGIN_NUMBER']) } + let(:appstore_number) { "+85260000000" } + let(:user) { create(:user, mobile: appstore_number) } + + around do |example| + old_value = Rails.application.secrets.appstore_reviewer_login + allow(Rails.application.secrets).to receive(:appstore_reviewer_login).and_return({ number: appstore_number }) + example.run + ensure + allow(Rails.application.secrets).to receive(:appstore_reviewer_login).and_return(old_value) + end + it do expect(subject.can_delete[:result]).to eql(false) expect(subject.can_delete[:reason]).to eql("App Store Reviewer account cannot be deleted.") From 7c9f20bed24b70ba9dab8ed64ba7fce3714f041b Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 23:16:18 +1200 Subject: [PATCH 10/13] Fix test helpers and Procfile portability 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. --- Procfile | 2 +- app/models/user.rb | 2 +- spec/factories/roles.rb | 4 +++- spec/lib/goodcity/user_safe_delete_spec.rb | 7 ++++--- spec/rails_helper.rb | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Procfile b/Procfile index 48011622d..fc1030bd9 100755 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ #api: bundle exec puma -C config/puma.rb -api: PORT=3000 bundle exec rails server --binding=0.0.0.0 +api: bundle exec rails server --binding=0.0.0.0 --port ${PORT:-3000} sidekiq: bundle exec sidekiq socketio: (cd ../socket.io-webservice/ && PORT=1337 npm start) diff --git a/app/models/user.rb b/app/models/user.rb index 5254a2a97..2e1e44d46 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -109,7 +109,7 @@ class User < ApplicationRecord scope :exclude_system_users, -> { where.not(id: [User.system_user.try(:id), User.stockit_user.try(:id)].compact) } scope :active, -> { where(disabled: false) } scope :exclude_user, ->(id) { where.not(id: id) } - scope :with_roles, ->(role_names) { where(roles: { name: role_names }).joins(:active_roles) } + scope :with_roles, ->(role_names) { joins(:active_roles).where(active_roles: { name: role_names }) } scope :with_organisation_status, ->(status_list) { joins(:organisations_users).where(organisations_users: { status: status_list }) } scope :with_eager_loading, -> { includes([:image, address: [:district]]) } scope :with_permissions, ->(perm) { diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb index dfd1aa447..d40e6b0bd 100644 --- a/spec/factories/roles.rb +++ b/spec/factories/roles.rb @@ -38,10 +38,12 @@ # create(:reviewer_role, :with_can_manage_offers_permission) # create(:reviewer_role, :with_can_manage_offers_permission, :with_can_manage_messages_permission) YAML.load_file("#{Rails.root}/db/permissions_roles.yml", aliases: true).each do |role_name, permissions| + permissions = ::Kernel.Array(permissions).flatten permissions.each do |permission| + permission = permission.to_s trait "with_#{permission}_permission".to_sym do after(:create) do |role| - p = create(:permission, name: permission) + p = create(:permission, name: permission.to_s) role.permissions << p unless role.permissions.include?(p) end end diff --git a/spec/lib/goodcity/user_safe_delete_spec.rb b/spec/lib/goodcity/user_safe_delete_spec.rb index a989f5660..fe23472a9 100644 --- a/spec/lib/goodcity/user_safe_delete_spec.rb +++ b/spec/lib/goodcity/user_safe_delete_spec.rb @@ -56,11 +56,12 @@ let(:user) { create(:user, mobile: appstore_number) } around do |example| - old_value = Rails.application.secrets.appstore_reviewer_login - allow(Rails.application.secrets).to receive(:appstore_reviewer_login).and_return({ number: appstore_number }) + secrets = Rails.application.secrets + original = secrets.appstore_reviewer_login + secrets.appstore_reviewer_login = { number: appstore_number } example.run ensure - allow(Rails.application.secrets).to receive(:appstore_reviewer_login).and_return(old_value) + secrets.appstore_reviewer_login = original end it do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c6a37f99d..5d8dd4806 100755 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -57,7 +57,7 @@ # Create system_user config.before(:suite) do - FactoryBot.create(:user, :system) + FactoryBot.create(:user, :system) unless User.system_user.present? end # Clean up system_user at end of specs From 08b0804176f95daeefbf6cea44c5b4c788b64a8d Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Tue, 5 May 2026 23:34:57 +1200 Subject: [PATCH 11/13] Fix potential error on chained scope when search text is blank Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 2e1e44d46..680bd945c 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -175,7 +175,7 @@ def self.filter_users(opts) organisation_status = opts[:organisation_status] || opts['organisation_status'] role_name = opts[:role_name] || opts['role_name'] - res = search(search_text) if search_text.present? + res = search_text.present? ? search(search_text) : all res = res.with_organisation_status(organisation_status.split(',')) if organisation_status.present? res = res.with_roles(role_name) if role_name.present? res From f7f2b633fdc3698a91ef071c0dd0059a2eda9834 Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Wed, 6 May 2026 00:36:19 +1200 Subject: [PATCH 12/13] Stabilize shareables pagination and fix message subscriptions spec 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. --- app/controllers/api/v2/shareables_controller.rb | 3 ++- spec/models/concerns/message_subscriptions_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v2/shareables_controller.rb b/app/controllers/api/v2/shareables_controller.rb index 952522c6c..ac8a3cbd5 100644 --- a/app/controllers/api/v2/shareables_controller.rb +++ b/app/controllers/api/v2/shareables_controller.rb @@ -123,7 +123,8 @@ def destroy * 401 - unauthorized EOS def index - records = paginate(@shareables) + # Ensure stable pagination order + records = paginate(@shareables.order(id: :asc)) records = records.of_type(params[:resource_type]) if params[:resource_type].present? records = records.where(resource_id: params[:resource_id].split(',')) if params[:resource_id].present? diff --git a/spec/models/concerns/message_subscriptions_spec.rb b/spec/models/concerns/message_subscriptions_spec.rb index f3a0447c4..d234d5ad1 100644 --- a/spec/models/concerns/message_subscriptions_spec.rb +++ b/spec/models/concerns/message_subscriptions_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' module Messages - describe Operations do + describe MessageSubscriptions do let(:offer) { create :offer } let(:message) { create :message, sender: reviewer, messageable: offer } let!(:supervisor) { create :user, :with_supervisor_role, :with_can_manage_offer_messages_permission } @@ -255,7 +255,7 @@ module Messages let!(:user1) { create(:user) } let!(:user2) { create(:user) } let(:message) { create(:message, body: "Hello [:#{user1.id}]. I need help from you and [:#{user2.id}]") } - let(:message2) { build(:message, body: "Hello [:#{user2.id}]. I will help") } + let(:message2) { build(:message, sender: reviewer, messageable: offer1, body: "Hello [:#{user2.id}]. I will help") } before(:each) do allow(message).to receive(:add_subscription) From d767df52fda6acf2689c2ec661c533e352aecdac Mon Sep 17 00:00:00 2001 From: Ben Tillman Date: Wed, 6 May 2026 10:15:33 +1200 Subject: [PATCH 13/13] Harden browseable package search specs with unique tokens Avoid flaky failures when other published packages match the generic substring "towel" in notes or related search fields. --- spec/controllers/api/v1/packages_controller_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/controllers/api/v1/packages_controller_spec.rb b/spec/controllers/api/v1/packages_controller_spec.rb index 4d2cb4668..f6d7ac139 100644 --- a/spec/controllers/api/v1/packages_controller_spec.rb +++ b/spec/controllers/api/v1/packages_controller_spec.rb @@ -81,11 +81,12 @@ def test_packages_location_changes(package) it "returns searched browseable_packages only" do set_browse_app_header + unique = "towel-#{SecureRandom.hex(4)}" 3.times{ create :package, :with_inventory_record, notes: "Baby towels", allow_web_publish: false } 3.times{ create :browseable_package, :with_inventory_record, notes: "Baby car seats" } - create :browseable_package, :with_inventory_record, notes: "towels" + create :browseable_package, :with_inventory_record, notes: unique expect(Package.count).to eq(7) - get :index, params: { "searchText": "towel" } + get :index, params: { "searchText" => unique } expect(response.status).to eq(200) expect( subject["packages"].size ).to eq(1) end @@ -118,11 +119,12 @@ def test_packages_location_changes(package) it "returns searched browseable_packages only" do set_browse_app_header + unique = "towel-#{SecureRandom.hex(4)}" 3.times{ create :package, :with_inventory_record, notes: "Baby towels", allow_web_publish: false } 3.times{ create :browseable_package, :with_inventory_record, notes: "Baby car seats" } - create :browseable_package, :with_inventory_record, notes: "towels" + create :browseable_package, :with_inventory_record, notes: unique expect(Package.count).to eq(7) - get :index, params: { "searchText": "towel" } + get :index, params: { "searchText" => unique } expect(response.status).to eq(200) expect( subject["packages"].size ).to eq(1) end