spree_cm_commissioner 2.5.1 → 2.5.2.pre.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +2 -2
  3. data/Gemfile.lock +1 -1
  4. data/app/controllers/spree/admin/inventory_items_controller.rb +36 -56
  5. data/app/controllers/spree/admin/stock_managements_controller.rb +14 -3
  6. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +2 -2
  7. data/app/controllers/spree/api/v2/storefront/transit/draft_orders_controller.rb +4 -4
  8. data/app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb +60 -0
  9. data/app/controllers/spree/api/v2/tenant/routes_controller.rb +50 -0
  10. data/app/controllers/spree/api/v2/tenant/transit/draft_orders_controller.rb +46 -0
  11. data/app/controllers/spree/transit/trips_controller.rb +3 -3
  12. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +12 -12
  13. data/app/finders/spree_cm_commissioner/route_metrics/find_popular.rb +44 -0
  14. data/app/finders/spree_cm_commissioner/routes/find.rb +94 -0
  15. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +19 -35
  16. data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +11 -4
  17. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +10 -1
  18. data/app/interactors/spree_cm_commissioner/trip_clone_creator.rb +4 -3
  19. data/app/interactors/spree_cm_commissioner/trip_stops_creator.rb +2 -2
  20. data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +10 -0
  21. data/app/jobs/spree_cm_commissioner/route_metrics/increase_fulfilled_order_count_job.rb +10 -0
  22. data/app/jobs/spree_cm_commissioner/route_metrics/increase_order_count_job.rb +10 -0
  23. data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +10 -0
  24. data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +2 -2
  25. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +2 -2
  26. data/app/models/spree_cm_commissioner/place.rb +5 -8
  27. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  28. data/app/models/spree_cm_commissioner/route.rb +45 -5
  29. data/app/models/spree_cm_commissioner/route_metric.rb +21 -0
  30. data/app/models/spree_cm_commissioner/route_photo.rb +12 -0
  31. data/app/models/spree_cm_commissioner/trip.rb +8 -33
  32. data/app/models/spree_cm_commissioner/trip_stop.rb +16 -2
  33. data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -1
  34. data/app/queries/spree_cm_commissioner/trip_query.rb +2 -2
  35. data/app/serializers/spree/v2/tenant/transit_cart_serializer.rb +11 -0
  36. data/app/serializers/spree_cm_commissioner/v2/storefront/route_metric_serializer.rb +13 -0
  37. data/app/serializers/spree_cm_commissioner/v2/storefront/route_serializer.rb +2 -1
  38. data/app/serializers/spree_cm_commissioner/v2/storefront/transit_line_item_serializer.rb +17 -0
  39. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_stop_serializer.rb +1 -1
  40. data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +31 -0
  41. data/app/services/spree_cm_commissioner/{routes/increment_fulfilled_order_count.rb → route_metrics/increase_fulfilled_order_count.rb} +3 -3
  42. data/app/services/spree_cm_commissioner/{routes/increment_order_count.rb → route_metrics/increase_order_count.rb} +3 -3
  43. data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +31 -0
  44. data/app/services/spree_cm_commissioner/{routes/base_update_order_metrics.rb → route_metrics/update_route_metrics.rb} +11 -16
  45. data/app/services/spree_cm_commissioner/routes/create.rb +51 -0
  46. data/app/services/spree_cm_commissioner/routes/update.rb +25 -0
  47. data/app/{interactors/spree_cm_commissioner/transit/draft_order_creator.rb → services/spree_cm_commissioner/transit_order/create.rb} +13 -16
  48. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +123 -0
  49. data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +54 -0
  50. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +88 -0
  51. data/app/services/spree_cm_commissioner/trips/variants/create.rb +103 -0
  52. data/app/views/spree/admin/inventory_items/prices.html.erb +45 -0
  53. data/app/views/spree/admin/inventory_items/stocks.html.erb +36 -0
  54. data/app/views/spree/admin/stock_managements/calendar.html.erb +105 -12
  55. data/app/views/spree/admin/stock_managements/index.html.erb +9 -8
  56. data/app/views/spree/transit/trip_stops/index.html.erb +4 -2
  57. data/app/views/spree_cm_commissioner/guest_mailer/send_ticket_to_guest.html.erb +0 -1
  58. data/config/initializers/spree_permitted_attributes.rb +11 -0
  59. data/config/routes.rb +12 -7
  60. data/db/migrate/20251224033103_migrate_cm_routes_to_cm_route_metrics.rb +17 -0
  61. data/db/migrate/20251224033910_migrate_cm_vendor_routes_to_cm_routes.rb +30 -0
  62. data/db/migrate/20260105072450_migrate_cm_trip_stops_to_support_trip_connection.rb +12 -0
  63. data/db/migrate/20260108101406_add_allow_booking_to_cm_trips.rb +5 -0
  64. data/lib/spree_cm_commissioner/test_helper/factories/route_factory.rb +7 -6
  65. data/lib/spree_cm_commissioner/test_helper/factories/route_metric_factory.rb +12 -0
  66. data/lib/spree_cm_commissioner/test_helper/factories/route_photo_factory.rb +5 -0
  67. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +4 -1
  68. data/lib/spree_cm_commissioner/test_helper/factories/trip_stop_factory.rb +3 -1
  69. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +2 -0
  70. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +22 -0
  71. data/lib/spree_cm_commissioner/transit/route_stop.rb +61 -0
  72. data/lib/spree_cm_commissioner/transit/route_stop_collection.rb +175 -0
  73. data/lib/spree_cm_commissioner/transit/trip_form.rb +81 -0
  74. data/lib/spree_cm_commissioner/transit/trip_stop_form.rb +65 -0
  75. data/lib/spree_cm_commissioner/version.rb +1 -1
  76. data/lib/spree_cm_commissioner.rb +4 -0
  77. metadata +42 -21
  78. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +0 -10
  79. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +0 -10
  80. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +0 -13
  81. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +0 -10
  82. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +0 -10
  83. data/app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb +0 -48
  84. data/app/models/spree_cm_commissioner/trip_connection.rb +0 -36
  85. data/app/models/spree_cm_commissioner/vendor_route.rb +0 -9
  86. data/app/services/spree_cm_commissioner/routes/decrement_previous_trip_count.rb +0 -30
  87. data/app/services/spree_cm_commissioner/routes/decrement_trip_count.rb +0 -33
  88. data/app/services/spree_cm_commissioner/routes/increment_trip_count.rb +0 -33
  89. data/app/views/spree/admin/inventory_items/show.html.erb +0 -72
  90. data/lib/spree_cm_commissioner/test_helper/factories/trip_connection_factory.rb +0 -6
@@ -1,9 +1,50 @@
1
1
  <%= turbo_frame_tag "calendar" do %>
2
- <%= form_with url: admin_product_stock_managements_path, method: :get, class: "mt-4" do %>
3
- <%= select_year(params[:year]&.to_i || @year, { start_year: 2025, end_year: @year + 3 }, { name: "year", onchange: "this.form.submit()" }) %>
4
- <% end %>
2
+ <div class="flex-wrap gap-3 mt-4 d-flex align-items-center justify-content-between">
3
+ <%= form_with url: admin_product_stock_managements_path, method: :get, class: "d-flex flex-wrap align-items-center gap-2" do |f| %>
4
+ <%= select_year(params[:year]&.to_i || @year, { start_year: 2025, end_year: @year + 3 }, { class: 'form-control', style: "width: auto;", name: "year", onchange: "this.form.submit()" }) %>
5
+ <%= f.select :selected_variant_id,
6
+ options_for_select([["All Variants", ""]] + @variants.map { |v| [v.options_text.presence || "N/A", v.id] }, params[:selected_variant_id]), {},
7
+ { class: "form-control", style: "width: auto;", onchange: "this.form.submit()" } %>
8
+ <% end %>
5
9
 
6
- <div class="c-annual-calendar-container mt-2">
10
+ <div id="bulkActionsBar" class="flex-wrap gap-2 align-items-center" style="display:none;">
11
+ <button class="btn btn-primary"
12
+ onclick="setModalUrl('prices')"
13
+ data-toggle="modal"
14
+ data-target="#inventoryItemModal">
15
+ <%= svg_icon name: "money.svg", width: '18', height: '18' %>
16
+ <span class="ml-1">Bulk Update Prices</span>
17
+ </button>
18
+ <button class="btn btn-success"
19
+ onclick="setModalUrl('stocks')"
20
+ data-toggle="modal"
21
+ data-target="#inventoryItemModal">
22
+ <%= svg_icon name: "box-seam.svg", width: '18', height: '18' %>
23
+ <span class="ml-1">Bulk Update Stocks</span>
24
+ </button>
25
+ <button class="btn btn-outline-secondary" onclick="clearSelection()">
26
+ Clear (<span id="selectedCount">0</span>)
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <style>
32
+ .gap-2 {
33
+ gap: 0.5rem;
34
+ }
35
+
36
+ .gap-3 {
37
+ gap: 1rem;
38
+ }
39
+
40
+ .day-selected {
41
+ background-color: #e3f2fd !important;
42
+ border: 2px solid #2196F3 !important;
43
+ box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
44
+ }
45
+ </style>
46
+
47
+ <div class="mt-2 c-annual-calendar-container">
7
48
  <div class="c-annual-calendar">
8
49
  <% (1..12).each do |month| %>
9
50
  <%= month_calendar(start_date: Date.new(@year, month, 1), attribute: :from_date, end_attribute: :to_date, events: @events) do |date, events| %>
@@ -20,10 +61,8 @@
20
61
  <% classes += 'badge-success ' if inventory_item.prices.any? %>
21
62
 
22
63
  <li class="<%= classes %>"
23
- data-toggle="modal"
24
- data-target="#inventoryItemModal"
25
- role="button"
26
- onclick="updateModalTurboFrameSrc('<%= admin_product_variant_inventory_item_path(@product, inventory_item.variant_id, inventory_item.id) %>')">
64
+ data-variant-id="<%= inventory_item.variant_id %>"
65
+ data-inventory-id="<%= inventory_item.id %>">
27
66
  <%= inventory_item.quantity_available %>
28
67
  <%= inventory_item.prices.first.display_amount if inventory_item.prices.any? %>
29
68
  </li>
@@ -46,11 +85,65 @@
46
85
  </div>
47
86
 
48
87
  <script>
49
- function updateModalTurboFrameSrc(url) {
88
+ let selectedInventoryIds = new Set();
89
+
90
+ // Use event delegation for day clicks - works after Turbo updates
91
+ document.addEventListener('click', function(e) {
92
+ const dayElement = e.target.closest('.c-annual-day');
93
+ if (dayElement) {
94
+ handleDayClick(e, dayElement);
95
+ }
96
+ });
97
+
98
+ function handleDayClick(event, dayElement) {
99
+ event.preventDefault();
100
+ event.stopPropagation();
101
+
102
+ const badges = dayElement.querySelectorAll('.badge[data-inventory-id]');
103
+ const inventoryIds = Array.from(badges).map(b => b.getAttribute('data-inventory-id'));
104
+
105
+ if (inventoryIds.length === 0) return;
106
+
107
+ const allSelected = inventoryIds.every(id => selectedInventoryIds.has(id));
108
+
109
+ if (allSelected) {
110
+ // Deselect
111
+ inventoryIds.forEach(id => selectedInventoryIds.delete(id));
112
+ dayElement.parentElement.classList.remove('day-selected');
113
+ updateBulkButton();
114
+ } else {
115
+ // Select
116
+ inventoryIds.forEach(id => selectedInventoryIds.add(id));
117
+ dayElement.parentElement.classList.add('day-selected');
118
+ updateBulkButton();
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ function updateBulkButton() {
125
+ const count = selectedInventoryIds.size;
126
+ document.getElementById('selectedCount').textContent = count;
127
+ const bar = document.getElementById('bulkActionsBar');
128
+ bar.style.display = count > 0 ? 'flex' : 'none';
129
+ }
130
+
131
+ function clearSelection() {
132
+ selectedInventoryIds.clear();
133
+ document.querySelectorAll('.day-selected').forEach(element => {
134
+ element.classList.remove('day-selected');
135
+ });
136
+ updateBulkButton();
137
+ }
138
+
139
+ function setModalUrl(type) {
140
+ const ids = Array.from(selectedInventoryIds).join(',');
141
+ const url = type === 'prices'
142
+ ? '<%= prices_admin_product_inventory_items_path(@product) %>?inventory_ids=' + ids
143
+ : '<%= stocks_admin_product_inventory_items_path(@product) %>?inventory_ids=' + ids;
50
144
  let frame = document.getElementById('inventory_item');
51
- frame.src = null;
52
- frame.innerHTML = `<div class="spinner-border mt-2" role="status"></div>`;
53
- frame.src = url
145
+ frame.innerHTML = `<div class="mt-2 spinner-border" role="status"></div>`;
146
+ frame.setAttribute('src', url);
54
147
  }
55
148
  </script>
56
149
  <% end %>
@@ -2,23 +2,24 @@
2
2
  <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @product } %>
3
3
 
4
4
  <% if false && can?(:create, Spree::StockMovement) %>
5
- <div id="add_stock_form" class="card mb-3">
5
+ <div id="add_stock_form" class="mb-3 card">
6
6
  <%= render 'add_stock_form' %>
7
7
  </div>
8
8
  <% end %>
9
9
 
10
- <div class="table-responsive border rounded bg-white">
10
+ <div class="bg-white border rounded table-responsive">
11
11
  <table class="table" id="listing_product_stock">
12
12
  <thead class="text-muted">
13
13
  <tr data-hook="admin_product_stock_management_index_headers">
14
14
  <th colspan="2"><%= Spree.t(:variant) %></th>
15
15
  <th colspan="3"><%= Spree.t(:stock) %></th>
16
+ <th colspan="2"><%= Spree.t(:price) %></th>
16
17
  </tr>
17
18
  </thead>
18
19
  <tbody>
19
20
  <% @variants.each do |variant| %>
20
21
  <tr id="<%= spree_dom_id variant %>" data-hook="admin_product_stock_management_index_rows">
21
- <td class="image text-center">
22
+ <td class="text-center image">
22
23
  <%= small_image(variant) %>
23
24
  </td>
24
25
  <td>
@@ -40,7 +41,7 @@
40
41
  <div>
41
42
  <%= svg_icon name: "cart-check.svg", width: '14', height: '14' %>
42
43
  <%= label_tag "reserved_stock#{variant.id}", "Reserved Stock: #{@reserved_stocks[variant.id] || 0}", class: "m-0" %>
43
- <%= link_to_with_icon('capture.svg', "Create Inventory Item", admin_product_variant_inventory_items_path(@product.id, variant.id),
44
+ <%= link_to_with_icon('capture.svg', "Create Inventory Item", admin_product_inventory_items_path(@product.slug, variant_id: variant.id),
44
45
  method: :post,
45
46
  remote: false,
46
47
  class: 'icon_link btn btn-sm btn-outline-primary ml-2',
@@ -77,7 +78,7 @@
77
78
  <% end %>
78
79
  </span>
79
80
 
80
- <%= link_to_with_icon('arrow-counterclockwise.svg', "Reset Inventory Item", reset_admin_product_variant_inventory_item_path(@product, inventory_item.variant_id, inventory_item.id),
81
+ <%= link_to_with_icon('arrow-counterclockwise.svg', "Reset Inventory Item", reset_admin_product_inventory_item_path(@product.slug, inventory_item.id),
81
82
  method: :patch,
82
83
  remote: false,
83
84
  class: 'icon_link btn btn-sm outline text-dark',
@@ -87,18 +88,18 @@
87
88
  <% end %>
88
89
  <% end %>
89
90
  </td>
90
-
91
91
  <td colspan="3" class="stock_location_info">
92
92
  <%= render partial: 'variant_stock_items', locals: { variant: variant } if variant.vendor.stock_locations.any? %>
93
93
  </td>
94
+ <td><%= variant.display_price %></td>
94
95
  </tr>
95
96
  <% end %>
96
97
  </tbody>
97
98
  </table>
98
99
  </div>
99
100
 
100
- <%= turbo_frame_tag "calendar", src: calendar_admin_product_stock_managements_path(@product.id, year: params[:year]) do %>
101
- <div class="spinner-border mt-2" role="status">
101
+ <%= turbo_frame_tag "calendar", src: calendar_admin_product_stock_managements_path(@product.slug, year: params[:year], selected_variant_id: params[:selected_variant_id]) do %>
102
+ <div class="mt-2 spinner-border" role="status">
102
103
  <span class="sr-only">Loading...</span>
103
104
  </div>
104
105
  <% end if @product.permanent_stock? %>
@@ -9,7 +9,8 @@
9
9
  <tr data-hook="option_header">
10
10
  <th class="no-border handel-head"></th>
11
11
  <th><%= Spree.t(:name) %></th>
12
- <th><%= Spree.t(:stop_type) %></th>
12
+ <th><%= Spree.t(:allow_boarding) %></th>
13
+ <th><%= Spree.t(:allow_drop_off) %></th>
13
14
  <th><%= Spree.t(:sequence) %></th>
14
15
  <th><%= Spree.t(:created_at) %></th>
15
16
  </tr>
@@ -21,7 +22,8 @@
21
22
  <%= svg_icon name: "grip-vertical.svg", width: '18', height: '18' %>
22
23
  </td>
23
24
  <td><%= stop.stop_name %></td>
24
- <td> <%= stop.stop_type %></td>
25
+ <td> <%= stop.allow_boarding ? 'Yes' : 'No' %></td>
26
+ <td> <%= stop.allow_drop_off ? 'Yes' : 'No' %></td>
25
27
  <td> <%= stop.sequence %></td>
26
28
  <td> <%= stop.created_at.to_date %></td>
27
29
  </tr>
@@ -151,7 +151,6 @@
151
151
  <% @trip.trip_stops.order(:sequence).each do |stop| %>
152
152
  <tr>
153
153
  <td>
154
- <span class="stop-type <%= stop.stop_type %>"><%= stop.stop_type.to_s.titleize %></span>
155
154
  <div class="stop-name"><strong><%= stop.stop_name %></strong></div>
156
155
  <% if stop.respond_to?(:location_place) && stop.location_place.present? %>
157
156
  <% if stop.location_place.respond_to?(:address) && stop.location_place.address.present? %>
@@ -1,5 +1,8 @@
1
1
  module Spree
2
2
  module PermittedAttributes
3
+ ATTRIBUTES << :route_attributes unless ATTRIBUTES.include?(:route_attributes)
4
+ mattr_reader :route_attributes
5
+
3
6
  @@vendor_attributes << :logo
4
7
 
5
8
  # Permitted all guest attributes for now as permitting only some guest attributes is not working by design
@@ -86,5 +89,13 @@ module Spree
86
89
  originator_type
87
90
  originator_id
88
91
  ]
92
+
93
+ @@route_attributes = %i[
94
+ route_name
95
+ short_name
96
+ vendor_id
97
+ route_type
98
+ route_stops
99
+ ]
89
100
  end
90
101
  end
data/config/routes.rb CHANGED
@@ -149,13 +149,12 @@ Spree::Core::Engine.add_routes do
149
149
  end
150
150
  end
151
151
 
152
- resources :variants do
153
- resources :inventory_items, only: %i[show create] do
154
- member do
155
- patch :reset
156
- patch :update_prices
157
- delete :delete_prices
158
- end
152
+ resources :inventory_items, only: [] do
153
+ collection do
154
+ get :prices
155
+ get :stocks
156
+ patch :bulk_update_prices
157
+ patch :bulk_update_stocks
159
158
  end
160
159
  end
161
160
 
@@ -560,6 +559,12 @@ Spree::Core::Engine.add_routes do
560
559
  resources :trip_places, only: :index
561
560
  resources :trip_search, only: [:index]
562
561
  resources :trips, only: %i[show]
562
+ resources :popular_route_places, only: [:index]
563
+ resources :routes, only: [:index]
564
+
565
+ namespace :transit do
566
+ resources :draft_orders, only: %i[create]
567
+ end
563
568
 
564
569
  namespace :intercity_taxi do
565
570
  resource :draft_orders, only: %i[create update]
@@ -0,0 +1,17 @@
1
+ class MigrateCmRoutesToCmRouteMetrics < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Rename cm_routes which we use to track order, trip count to cm_route_metrics instead.
4
+ remove_index :cm_routes, [:origin_place_id, :destination_place_id] if index_exists?(:cm_routes, [:origin_place_id, :destination_place_id])
5
+ rename_table :cm_routes, :cm_route_metrics unless table_exists?(:cm_route_metrics)
6
+ add_column :cm_route_metrics, :route_type, :integer, default: 0, null: false unless column_exists?(:cm_route_metrics, :route_type)
7
+
8
+ # 2. Drop the old index before renaming table to avoid index name length issues & re-add the index with a shorter name
9
+ unless index_exists?(:cm_route_metrics, [:route_type, :origin_place_id, :destination_place_id], name: 'index_cm_route_metrics_on_origin_dest')
10
+ add_index :cm_route_metrics, [:route_type, :origin_place_id, :destination_place_id], unique: true, name: 'index_cm_route_metrics_on_origin_dest'
11
+ end
12
+
13
+ # 3. Remove old route_id column from trips.
14
+ # it has route_metrics value, and trip don't need to have association with metric directly.
15
+ remove_column :cm_trips, :route_id if column_exists?(:cm_trips, :route_id)
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ class MigrateCmVendorRoutesToCmRoutes < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Remove cm_vendor_routes and add cm_routes instead for routes that associated with vendor.
4
+ drop_table :cm_vendor_routes, if_exists: true
5
+ create_table :cm_routes, if_not_exists: true do |t|
6
+ t.string :route_name
7
+ t.string :short_name
8
+ t.integer :order_count
9
+ t.integer :fulfilled_order_count
10
+ t.integer :route_type
11
+ t.jsonb :route_stops, default: "[]"
12
+ t.references :vendor, foreign_key: { to_table: :spree_vendors }, null: false
13
+ t.references :tenant, foreign_key: { to_table: :cm_tenants }, null: true
14
+
15
+ t.references :origin_place, foreign_key: { to_table: :cm_places }, null: false
16
+ t.references :destination_place, foreign_key: { to_table: :cm_places }, null: false
17
+
18
+ t.integer :lock_version, default: 0, null: false
19
+ t.timestamps
20
+ end
21
+
22
+ # 2. Add indexes for popular_routes association (vendor_id + order by fulfilled_order_count, order_count)
23
+ add_index :cm_routes, [:vendor_id, :route_type, :fulfilled_order_count, :order_count],
24
+ order: { fulfilled_order_count: :desc, order_count: :desc },
25
+ name: 'index_cm_routes_on_vendor_id_route_type_and_popular' unless index_exists?(:cm_routes, [:vendor_id, :route_type, :fulfilled_order_count, :order_count])
26
+
27
+ # 3. Add new route reference to cm_trips
28
+ add_reference :cm_trips, :route, foreign_key: { to_table: :cm_routes }, null: true unless column_exists?(:cm_trips, :route_id)
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ class MigrateCmTripStopsToSupportTripConnection < ActiveRecord::Migration[7.0]
2
+ def change
3
+ # 1. Replace stop_type with allow_boarding & allow_drop_off instead, it is more flexible.
4
+ # Useful for middle stops where both boarding & drop off can be allowed or both can be disallowed.
5
+ remove_column :cm_trip_stops, :stop_type if column_exists?(:cm_trip_stops, :stop_type)
6
+ add_column :cm_trip_stops, :allow_boarding, :boolean unless column_exists?(:cm_trip_stops, :allow_boarding)
7
+ add_column :cm_trip_stops, :allow_drop_off, :boolean unless column_exists?(:cm_trip_stops, :allow_drop_off)
8
+
9
+ # 2. Add references to link trip stops to trips for boarding, this will enable trip connections. Where current trip is main trip.
10
+ add_reference :cm_trip_stops, :board_to_trip, foreign_key: { to_table: :cm_trips }, null: true unless column_exists?(:cm_trip_stops, :board_to_trip_id)
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class AddAllowBookingToCmTrips < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :cm_trips, :allow_booking, :boolean, default: true, if_not_exists: true
4
+ end
5
+ end
@@ -1,10 +1,11 @@
1
1
  FactoryBot.define do
2
- factory :cm_route, class: 'SpreeCmCommissioner::Route' do
2
+ factory :cm_route, class: SpreeCmCommissioner::Route do
3
+ association :vendor, factory: :vendor
4
+ association :tenant, factory: :cm_tenant
5
+ association :origin_place, factory: :cm_place
6
+ association :destination_place, factory: :cm_place
7
+
3
8
  sequence(:route_name) { |n| "Route #{n}" }
4
- origin_place_id { create(:cm_place).id }
5
- destination_place_id { create(:cm_place).id }
6
- trip_count { 0 }
7
- order_count { 0 }
8
- fulfilled_order_count { 0 }
9
+ sequence(:short_name) { |n| "R#{n}" }
9
10
  end
10
11
  end
@@ -0,0 +1,12 @@
1
+ FactoryBot.define do
2
+ factory :cm_route_metric, class: SpreeCmCommissioner::RouteMetric do
3
+ association :origin_place, factory: :cm_place
4
+ association :destination_place, factory: :cm_place
5
+
6
+ route_name { "#{origin_place.name} - #{destination_place.name}" }
7
+ route_type { :bus }
8
+ trip_count { 0 }
9
+ order_count { 0 }
10
+ fulfilled_order_count { 0 }
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :cm_route_photo, class: SpreeCmCommissioner::RoutePhoto do
3
+ attachment { Rack::Test::UploadedFile.new(SpreeMultiVendor::Engine.root.join('spec', 'fixtures', 'thinking-cat.jpg'), 'image/jpeg') }
4
+ end
5
+ end
@@ -11,7 +11,10 @@ FactoryBot.define do
11
11
  association :vehicle, factory: :cm_vehicle
12
12
  association :origin_place, factory: :cm_place
13
13
  association :destination_place, factory: :cm_place
14
- association :route, factory: :cm_route
14
+
15
+ # Don't create route by default to avoid uniqueness conflicts
16
+ # Tests that need these should create them explicitly
17
+ route { nil }
15
18
  end
16
19
 
17
20
  factory :cm_trip_with_seat_counts, parent: :cm_trip do
@@ -4,7 +4,9 @@ FactoryBot.define do
4
4
  association :stop_place, factory: :cm_place
5
5
  association :location_place, factory: :cm_place
6
6
 
7
- stop_type { :boarding }
7
+ allow_boarding { true }
8
+ allow_drop_off { true }
9
+
8
10
  stop_name { stop_place.name }
9
11
 
10
12
  arrival_time { Time.current }
@@ -1,5 +1,6 @@
1
1
  FactoryBot.define do
2
2
  factory :cm_transit_vendor, parent: :vendor do
3
+ from_email { FFaker::Internet.email } # required only when tenant exist.
3
4
  sequence(:name) { |n| "#{FFaker::Company.name} #{n}#{Kernel.rand(9999)}" }
4
5
  state { :active }
5
6
  primary_product_type { :transit }
@@ -7,6 +8,7 @@ FactoryBot.define do
7
8
 
8
9
  factory :cm_vendor, parent: :vendor do
9
10
  sequence(:name) { |n| "#{FFaker::Company.name} #{n}#{Kernel.rand(9999)}" }
11
+ from_email { FFaker::Internet.email } # required only when tenant exist.
10
12
  state { :active }
11
13
  default_state_id { Spree::State.first&.id }
12
14
  primary_product_type { :ecommerce }
@@ -7,6 +7,17 @@ FactoryBot.define do
7
7
  position { FFaker::Number.number }
8
8
 
9
9
  place_type { :location }
10
+
11
+ transient do
12
+ place_name { nil }
13
+ end
14
+
15
+ before :create do |vendor_place, evaluator|
16
+ if evaluator.place_name.present?
17
+ vendor_place.place.name = evaluator.place_name
18
+ vendor_place.place.save!
19
+ end
20
+ end
10
21
  end
11
22
 
12
23
  factory :cm_vendor_place, class: SpreeCmCommissioner::VendorPlace do
@@ -30,5 +41,16 @@ FactoryBot.define do
30
41
  place_type { :location }
31
42
  location { nil }
32
43
  end
44
+
45
+ transient do
46
+ place_name { nil }
47
+ end
48
+
49
+ before :create do |vendor_place, evaluator|
50
+ if evaluator.place_name.present?
51
+ vendor_place.place.name = evaluator.place_name
52
+ vendor_place.place.save!
53
+ end
54
+ end
33
55
  end
34
56
  end
@@ -0,0 +1,61 @@
1
+ module SpreeCmCommissioner::Transit
2
+ class RouteStop
3
+ attr_accessor :location_id, :vendor_place_id, :type, :sequence, :vendor_place
4
+
5
+ STOP_TYPES = %i[branch stop].freeze
6
+
7
+ def initialize(options = {})
8
+ @location_id = options[:location_id]
9
+ @type = options[:type]&.to_sym
10
+ @sequence = options[:sequence]
11
+
12
+ # vendor place is either stop or branch
13
+ @vendor_place_id = options[:vendor_place_id]
14
+ end
15
+
16
+ def self.from_hash(hash)
17
+ hash = hash.symbolize_keys
18
+
19
+ new(
20
+ location_id: hash[:location_id],
21
+ vendor_place_id: hash[:vendor_place_id],
22
+ type: hash[:type],
23
+ sequence: hash[:sequence]
24
+ )
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ location_id: @location_id,
30
+ vendor_place_id: @vendor_place_id,
31
+ type: @type,
32
+ sequence: @sequence
33
+ }
34
+ end
35
+
36
+ def branch?
37
+ @type == :branch
38
+ end
39
+
40
+ def stop?
41
+ @type == :stop
42
+ end
43
+
44
+ def valid?
45
+ errors.empty?
46
+ end
47
+
48
+ def errors
49
+ @errors ||= []
50
+ @errors.clear
51
+
52
+ @errors << 'location_id is required' if @location_id.blank?
53
+ @errors << 'vendor_place_id is required' if @vendor_place_id.blank?
54
+ @errors << 'type is required' if @type.blank?
55
+ @errors << 'type must be :branch or :stop' if @type.present? && STOP_TYPES.exclude?(@type)
56
+ @errors << 'sequence is required' if @sequence.nil?
57
+
58
+ @errors
59
+ end
60
+ end
61
+ end