dscf-marketplace 0.7.2 → 0.7.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81c4fc2b3cbb08308f4da22f26a56d9d7a3ca197325c340d491dacfe4adbaa6f
4
- data.tar.gz: 1c89213f1171138ea6efc0c8566a60a6937348c7c934cfacc8ccecd53f35c310
3
+ metadata.gz: 6868f5b7cbade52d287939591c2011a7140b76eda6f3223ba7f66a3a51fb3d08
4
+ data.tar.gz: dd56581029c76743ec281f4de717f00410a38d780b68ad57a13a11cd36e8ddd2
5
5
  SHA512:
6
- metadata.gz: 2f812ce7f37138fbf88cfed6c03233193b12fef8a194f8cd632c8006b86814201af5877122679c9634920bcb4ba73291e7ea2171009be15265a38cd9dad524c5
7
- data.tar.gz: e6ffd5232a4f43d7d4020f789a0ce6b400858ffd2801192a7008dab0ffd4f66f3dcd3488c0364f26d8861c2b451eb2d6f1f2edcfbd9b990363cd121b6905a3b6
6
+ metadata.gz: 1d7737babebba09f37154720fea2502464ad2c71cac4007114a40d3d22529efc664091d8f96a26086140bdd66696cfffa0bf7b1d828b6764f833e1497327940e
7
+ data.tar.gz: a2f2b76706da91f693a76cbe47e6b913b97819777f81ef3dacd354a83e59d8106dfcca11402af054493c1dcda586edca8b998858adbcfaeb11297aea302ffff2
@@ -0,0 +1,44 @@
1
+ module Dscf
2
+ module Marketplace
3
+ module DemoPermissionBypass
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :demo_bypass_permissions!
8
+ end
9
+
10
+ def bypass_permissions_for_demo?
11
+ true
12
+ end
13
+
14
+ def pundit_user
15
+ user = current_user
16
+ return nil unless user
17
+
18
+ bypass_permissions_on_user!(user)
19
+ end
20
+
21
+ def authorize_review_action!
22
+ skip_authorization if respond_to?(:skip_authorization, true)
23
+ end
24
+
25
+ private
26
+
27
+ def demo_bypass_permissions!
28
+ skip_authorization if respond_to?(:skip_authorization, true)
29
+ skip_policy_scope if respond_to?(:skip_policy_scope, true)
30
+ end
31
+
32
+ def bypass_permissions_on_user!(user)
33
+ return user if user.instance_variable_defined?(:@_banking_demo_permission_bypass)
34
+
35
+ user.define_singleton_method(:has_permission?) { |_permission_code| true }
36
+ user.define_singleton_method(:can?) { |permission_code| has_permission?(permission_code) }
37
+ user.define_singleton_method(:super_admin?) { true }
38
+ user.instance_variable_set(:@_banking_demo_permission_bypass, true)
39
+
40
+ user
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,42 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- class ApplicationController < Dscf::Core::ApplicationController
3
+ class ApplicationController < ActionController::API
4
+ include Dscf::Core::Authenticatable
5
+ include Dscf::Core::JsonResponse
6
+ before_action :authenticate_user
7
+ before_action :demo_bypass_permissions!
8
+
9
+ # TEMPORARY DEMO BYPASS:
10
+ # Bypass marketplace authorization checks for authenticated users only.
11
+ # Remove after the demo.
12
+ def bypass_permissions_for_demo?
13
+ true
14
+ end
15
+
16
+ def pundit_user
17
+ user = current_user
18
+ return nil unless user
19
+
20
+ bypass_permissions_on_user!(user)
21
+ end
22
+
23
+ private
24
+
25
+ def demo_bypass_permissions!
26
+ skip_authorization if respond_to?(:skip_authorization, true)
27
+ skip_policy_scope if respond_to?(:skip_policy_scope, true)
28
+ end
29
+
30
+ def bypass_permissions_on_user!(user)
31
+ return user if user.instance_variable_defined?(:@_banking_demo_permission_bypass)
32
+
33
+ user.define_singleton_method(:has_permission?) { |_permission_code| true }
34
+ user.define_singleton_method(:can?) { |permission_code| has_permission?(permission_code) }
35
+ user.define_singleton_method(:super_admin?) { true }
36
+ user.instance_variable_set(:@_banking_demo_permission_bypass, true)
37
+
38
+ user
39
+ end
4
40
  end
5
41
  end
6
42
  end
@@ -3,6 +3,24 @@ module Dscf
3
3
  class OrdersController < ApplicationController
4
4
  include Dscf::Core::Common
5
5
 
6
+ def create
7
+ authorize @clazz.new, :create?
8
+
9
+ return create_direct_listing_order if direct_listing_request?
10
+
11
+ obj = @clazz.new(model_params)
12
+ if obj.save
13
+ obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
14
+ includes = default_serializer_includes[:create] || []
15
+ options = {include: includes} if includes.present?
16
+ render_success(data: obj, serializer_options: options, status: :created)
17
+ else
18
+ render_error(errors: obj.errors.full_messages.join(", "), status: :unprocessable_entity)
19
+ end
20
+ rescue => e
21
+ render_error(error: e.message)
22
+ end
23
+
6
24
  def filter
7
25
  authorize @clazz.new, :filter?
8
26
  orders = @clazz.all
@@ -48,7 +66,7 @@ module Dscf
48
66
  @obj = find_record
49
67
  authorize @obj, :complete?
50
68
  if @obj.can_be_completed? && @obj.update(status: :completed)
51
- @obj.order_items.update_all(status: OrderItem.statuses[:completed])
69
+ @obj.order_items.update_all(status: OrderItem.statuses[:fulfilled])
52
70
  render_success("orders.success.completed", data: @obj)
53
71
  else
54
72
  render_error("orders.errors.complete_failed")
@@ -70,6 +88,67 @@ module Dscf
70
88
 
71
89
  private
72
90
 
91
+ def create_direct_listing_order
92
+ listing = Dscf::Marketplace::Listing.active.find_by(id: model_params[:listing_id])
93
+ return render_error(errors: "Listing is not available", status: :unprocessable_entity) unless listing
94
+
95
+ quantity = direct_listing_quantity
96
+ return render_error(errors: "Quantity must be greater than 0", status: :unprocessable_entity) unless quantity.positive?
97
+
98
+ if quantity > listing.quantity
99
+ return render_error(errors: "Requested quantity exceeds available listing quantity", status: :unprocessable_entity)
100
+ end
101
+
102
+ order = nil
103
+ ActiveRecord::Base.transaction do
104
+ listing.lock!
105
+
106
+ if quantity > listing.quantity
107
+ listing.errors.add(:base, "Requested quantity exceeds available listing quantity")
108
+ raise ActiveRecord::RecordInvalid.new(listing)
109
+ end
110
+
111
+ order = @clazz.new(model_params.except(:order_items_attributes))
112
+ order.order_type = :direct_listing
113
+ order.status = :pending
114
+ order.listing = listing
115
+ order.ordered_to = listing.business
116
+
117
+ product = listing.supplier_product.product
118
+ order.order_items.build(
119
+ listing: listing,
120
+ product: product,
121
+ unit: product.unit,
122
+ quantity: quantity,
123
+ unit_price: listing.price,
124
+ status: :pending
125
+ )
126
+ order.save!
127
+
128
+ new_quantity = listing.quantity - quantity
129
+ listing.update!(quantity: new_quantity, status: (new_quantity.zero? ? :sold_out : listing.status))
130
+ end
131
+
132
+ order = @clazz.includes(eager_loaded_associations).find(order.id) if eager_loaded_associations.present?
133
+ includes = default_serializer_includes[:create] || []
134
+ options = {include: includes} if includes.present?
135
+ render_success(data: order, serializer_options: options, status: :created)
136
+ rescue ActiveRecord::RecordInvalid => e
137
+ errors = e.record&.errors&.full_messages&.presence || [e.message]
138
+ render_error(errors: errors.join(", "), status: :unprocessable_entity)
139
+ rescue => e
140
+ render_error(error: e.message)
141
+ end
142
+
143
+ def direct_listing_request?
144
+ %w[direct_listing 1].include?(model_params[:order_type].to_s)
145
+ end
146
+
147
+ def direct_listing_quantity
148
+ item = model_params[:order_items_attributes]&.first
149
+ (item&.[](:quantity) || item&.[]("quantity")).to_i
150
+ end
151
+
73
152
  def model_params
74
153
  params.require(:order).permit(
75
154
  :quotation_id, :listing_id, :user_id, :ordered_by_id, :ordered_to_id, :delivery_order_id, :dropoff_address_id,
@@ -9,7 +9,7 @@ module Dscf::Marketplace
9
9
  def assign_driver(delivery_order, driver)
10
10
  raise ArgumentError, "Delivery order is required" unless delivery_order
11
11
  raise ArgumentError, "Driver is required" unless driver
12
-
12
+
13
13
  validate_assignment(delivery_order)
14
14
 
15
15
  delivery_order.driver_id = driver.id
@@ -35,9 +35,15 @@ module Dscf::Marketplace
35
35
  delivery_order = create_delivery_order(vehicle_type, pickup_address, delivery_notes)
36
36
  create_delivery_stops_and_items(delivery_order, orders, pickup_address)
37
37
  associate_orders_with_delivery(delivery_order, orders)
38
-
39
- # Optimize route using Gebeta Maps
40
- RouteOptimizationService.new(delivery_order).optimize!
38
+
39
+ # Best-effort route optimization using Gebeta Maps.
40
+ begin
41
+ RouteOptimizationService.new(delivery_order).optimize!
42
+ rescue StandardError => e
43
+ Rails.logger.warn(
44
+ "[DeliveryOrderService] Route optimization skipped for delivery_order=#{delivery_order.id}: #{e.class} - #{e.message}"
45
+ )
46
+ end
41
47
 
42
48
  delivery_order
43
49
  end
@@ -79,7 +85,7 @@ module Dscf::Marketplace
79
85
  def create_delivery_stops_and_items(delivery_order, orders, pickup_address)
80
86
  # Group orders by dropoff address to create stops
81
87
  orders_by_address = orders.group_by(&:dropoff_address_id)
82
-
88
+
83
89
  orders_by_address.each do |dropoff_address_id, address_orders|
84
90
  # Create stop
85
91
  stop = DeliveryStop.create!(
@@ -87,7 +93,7 @@ module Dscf::Marketplace
87
93
  dropoff_address_id: dropoff_address_id,
88
94
  status: :pending
89
95
  )
90
-
96
+
91
97
  # Create items for this stop
92
98
  address_orders.each do |order|
93
99
  order.order_items.reload.each do |order_item|
@@ -10,83 +10,71 @@ module Dscf
10
10
  def optimize!
11
11
  return unless delivery_order.pickup_address && delivery_order.delivery_stops.any?
12
12
 
13
- # 1. Collect Coordinates
14
- # Format: [[pickup_lat, pickup_lng], [stop1_lat, stop1_lng], ...]
15
- pickup_coords = [delivery_order.pickup_address.latitude.to_f, delivery_order.pickup_address.longitude.to_f]
16
-
17
- # We map stops to their dropoff coordinates.
18
- # We need to keep track of the mapping between original index and stop ID to re-order later.
19
- stops = delivery_order.delivery_stops.includes(:dropoff_address).to_a
20
- stop_coords = stops.map { |stop| [stop.dropoff_address.latitude.to_f, stop.dropoff_address.longitude.to_f] }
21
-
22
- all_locations = [pickup_coords] + stop_coords
23
-
24
- # 2. Call Gebeta API
25
- response = GebetaService.new.tsp(all_locations)
26
-
27
- # 3. Process Response
28
-
29
- # New Response Format:
30
- # {
31
- # "best_order": [
32
- # {"lat":..., "lon":...}, # Start (Pickup)
33
- # {"lat":..., "original_index": 2}, # Stop A
34
- # {"lat":..., "original_index": 1} # Stop B
35
- # ],
36
- # "total_distance": 13.474, # In KM? Diagnostic showed 13.474 for small distance. Need to verify unit.
37
- # "time_taken": 1033.263,
38
- # "Direction": [[lat,lon], ...]
39
- # }
40
-
41
- # 'best_order' includes the start point at index 0 (usually without original_index or it's 0/1 based?)
42
- # Based on diagnostic: "original_index": 2 for the 3rd point (index 2).
43
- # So original_index is 0-based index from the input array.
44
-
45
- best_order = response["best_order"]
46
-
47
- # Filter out the pickup location (which should be the first one, or original_index == 0)
48
- # We only want to re-sequence the STOPS.
49
- # Stops in 'stops' array correspond to input indices 1..N.
50
-
51
- ordered_stops_data = best_order.select { |node| node["original_index"].to_i > 0 }
52
-
13
+ stops = []
14
+ locations = []
15
+
16
+ stops = ordered_stops
17
+ locations = build_locations(stops)
18
+
19
+ apply_remote_optimization(stops, locations)
20
+ rescue StandardError => e
21
+ Rails.logger.warn(
22
+ "[RouteOptimizationService] Falling back to local route for delivery_order=#{delivery_order.id}: #{e.class} - #{e.message}"
23
+ )
24
+
25
+ apply_local_fallback(stops, locations)
26
+ end
27
+
28
+ private
29
+
30
+ def apply_remote_optimization(stops, locations)
31
+ response = GebetaService.new.tsp(locations)
32
+ best_order = Array(response["best_order"])
33
+ ordered_stops_data = best_order.select { |node| node["original_index"].to_i.positive? }
34
+
53
35
  ActiveRecord::Base.transaction do
54
36
  ordered_stops_data.each_with_index do |node, seq_num|
55
- original_idx = node["original_index"]
56
- # stops array is 0-indexed, corresponding to input indices 1, 2, 3...
57
- # If original_idx is 1, it means stops[0]. If 2, stops[1].
37
+ original_idx = node["original_index"].to_i
58
38
  stop = stops[original_idx - 1]
59
-
60
- if stop
61
- stop.update!(sequence_number: seq_num + 1)
62
- end
39
+ stop.update!(sequence_number: seq_num + 1) if stop
63
40
  end
64
41
 
65
- # Update DeliveryOrder metrics
66
- # Check units:
67
- # time_taken: 1033.263 (likely seconds ~ 17 mins for decent distance).
68
- # total_distance: 13.474. If this is KM, it's reasonable. (Distance between 9.02,38.80 and 9.028,38.75 is ~ small).
69
- # If meters, 13 meters is too small. 1000 seconds for 13 meters is wrong.
70
- # So total_distance is likely KM.
71
-
72
42
  distance_km = response["total_distance"].to_f
73
-
43
+
74
44
  delivery_order.update!(
75
45
  estimated_delivery_time: Time.current + response["time_taken"].to_f.seconds,
76
- estimated_delivery_price: calculate_price(distance_km * 1000), # pass meters to calc
77
- optimized_route: response["Direction"]
46
+ estimated_delivery_price: calculate_price(distance_km * 1000),
47
+ optimized_route: response["Direction"]
78
48
  )
79
49
  end
80
50
  end
81
51
 
82
- private
52
+ def apply_local_fallback(stops, locations)
53
+ ActiveRecord::Base.transaction do
54
+ stops.each_with_index do |stop, index|
55
+ stop.update!(sequence_number: index + 1)
56
+ end
57
+
58
+ delivery_order.update!(optimized_route: locations)
59
+ end
60
+ end
61
+
62
+ def ordered_stops
63
+ delivery_order.delivery_stops.includes(:dropoff_address).order(:id).to_a
64
+ end
65
+
66
+ def build_locations(stops)
67
+ pickup_coords = [ delivery_order.pickup_address.latitude.to_f, delivery_order.pickup_address.longitude.to_f ]
68
+ stop_coords = stops.map { |stop| [ stop.dropoff_address.latitude.to_f, stop.dropoff_address.longitude.to_f ] }
69
+
70
+ [ pickup_coords ] + stop_coords
71
+ end
83
72
 
84
- # Placeholder pricing logic
85
73
  def calculate_price(distance_meters)
86
74
  base_rate = 50.0
87
- km_rate = 10.0 # 10 ETB per km
75
+ km_rate = 10.0
88
76
  distance_km = distance_meters / 1000.0
89
-
77
+
90
78
  base_rate + (distance_km * km_rate)
91
79
  end
92
80
  end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.7.2".freeze
3
+ VERSION = "0.7.4".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-marketplace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-10 00:00:00.000000000 Z
10
+ date: 2026-04-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -416,6 +416,7 @@ extra_rdoc_files: []
416
416
  files:
417
417
  - MIT-LICENSE
418
418
  - Rakefile
419
+ - app/controllers/concerns/dscf/marketplace/demo_permission_bypass.rb
419
420
  - app/controllers/dscf/marketplace/application_controller.rb
420
421
  - app/controllers/dscf/marketplace/categories_controller.rb
421
422
  - app/controllers/dscf/marketplace/delivery_order_items_controller.rb