bscf-core 0.4.0 → 0.4.1

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: edbe11919e64a7d36377d85759973899a2407eb6417e7d070eb78ee9c2062bf6
4
- data.tar.gz: 37375af1d5dfdf4f4703540d8e3c6d7c2d7496026421b8ae8e4c5c412cb23fd2
3
+ metadata.gz: ac477439e787a6451f0b7d5a92ac75e430ac8b0ddcd7a04bb5f8e57c4bd086dc
4
+ data.tar.gz: 4bd37eaa9562180049efec4028c438404bce7f08661c27c2c9e4018ba88a724a
5
5
  SHA512:
6
- metadata.gz: 2de3ef4fe24d2536031b4f527a5d7ed90f958f736d255157a9e17836c50e8587d00fa06ed2128e14b35102af7b81a9ba74bcc6994047f755e728e9e6cf1d1b9c
7
- data.tar.gz: 80a64d9c90c60baaf9966dee56261e41675a9fcf8e3091059b994e8fa2af36bc607caf192a22504fa74cff29b7d7fca4bc8e6061918c0db991e3556f739c590b
6
+ metadata.gz: 37282dd5fdd0974155a6d4f05000fca63f94d897a61d16d764e3dc047a769957ec7927f99924852e7b6f12e425e71689b18f59a987e984a88d21b31d9fe6ba12
7
+ data.tar.gz: 7c5d5491af64a7f91ed9a951fa04358e74a7e124d6e3f4fd232c9a8ef1461be00aaa11fc296c0fdf673145618f15035756927e6ee75b1a0e2c83f80a9879ada9
@@ -2,6 +2,31 @@ module Bscf
2
2
  module Core
3
3
  class Address < ApplicationRecord
4
4
  has_many :user_profiles
5
+
6
+ # Add associations for delivery orders and items
7
+ has_many :delivery_order_pickups, class_name: "Bscf::Core::DeliveryOrder", foreign_key: "pickup_address_id"
8
+ has_many :delivery_order_dropoffs, class_name: "Bscf::Core::DeliveryOrder", foreign_key: "dropoff_address_id"
9
+ has_many :delivery_order_item_pickups, class_name: "Bscf::Core::DeliveryOrderItem", foreign_key: "pickup_address_id"
10
+ has_many :delivery_order_item_dropoffs, class_name: "Bscf::Core::DeliveryOrderItem", foreign_key: "dropoff_address_id"
11
+
12
+ validates :latitude, :longitude, presence: true, if: :requires_coordinates?
13
+
14
+ def coordinates
15
+ %i[latitude longitude] if latitude.present? && longitude.present?
16
+ end
17
+
18
+ def full_address
19
+ %i[house_number woreda sub_city city].compact.join(", ")
20
+ end
21
+
22
+ private
23
+
24
+ def requires_coordinates?
25
+ # Determine when coordinates are required
26
+ # For delivery addresses, they should always be required
27
+ delivery_order_pickups.any? || delivery_order_dropoffs.any? ||
28
+ delivery_order_item_pickups.any? || delivery_order_item_dropoffs.any?
29
+ end
5
30
  end
6
31
  end
7
32
  end
@@ -1,62 +1,139 @@
1
- module Bscf::Core
2
- class DeliveryOrder < ApplicationRecord
3
- has_many :orders
4
- belongs_to :pickup_address, class_name: "Bscf::Core::Address"
5
- belongs_to :dropoff_address, class_name: "Bscf::Core::Address"
6
- belongs_to :driver, class_name: "Bscf::Core::User", optional: true
7
-
8
- has_many :delivery_order_items, dependent: :destroy
9
- has_many :order_items, through: :delivery_order_items
10
- has_many :products, through: :delivery_order_items
11
-
12
- validates :driver_phone, :status, :estimated_delivery_time, presence: true
13
- validate :end_time_after_start_time, if: -> { delivery_start_time.present? && delivery_end_time.present? }
14
-
15
- before_save :update_delivery_times
16
- before_save :calculate_actual_delivery_time
17
-
18
- # after_save :sync_items_status, if: :saved_change_to_status?
19
-
20
- enum :status, {
21
- pending: 0,
22
- in_transit: 1,
23
- picked_up: 2,
24
- delivered: 3,
25
- received: 4,
26
- paid: 5,
27
- failed: 6,
28
- cancelled: 7
29
- }
30
-
31
- def delivery_duration
32
- return nil unless delivery_start_time && delivery_end_time
33
- ((delivery_end_time - delivery_start_time) / 1.hour).round(2)
34
- end
1
+ module Bscf
2
+ module Core
3
+ class DeliveryOrder < ApplicationRecord
4
+ has_many :orders
5
+ belongs_to :pickup_address, class_name: "Bscf::Core::Address"
6
+ belongs_to :dropoff_address, class_name: "Bscf::Core::Address"
7
+ belongs_to :driver, class_name: "Bscf::Core::User", optional: true
8
+
9
+ has_many :delivery_order_items, dependent: :destroy
10
+ has_many :order_items, through: :delivery_order_items
11
+ has_many :products, through: :delivery_order_items
12
+
13
+ validates :driver_phone, :status, :estimated_delivery_time, presence: true
14
+ validate :end_time_after_start_time, if: -> { delivery_start_time.present? && delivery_end_time.present? }
15
+
16
+ before_save :update_delivery_times
17
+ before_save :calculate_actual_delivery_time
18
+
19
+ # after_save :sync_items_status, if: :saved_change_to_status?
35
20
 
36
- private
21
+ enum :status, {
22
+ pending: 0,
23
+ in_transit: 1,
24
+ picked_up: 2,
25
+ delivered: 3,
26
+ received: 4,
27
+ paid: 5,
28
+ failed: 6,
29
+ cancelled: 7
30
+ }
37
31
 
38
- def update_delivery_times
39
- case status
40
- when "in_transit"
41
- self.delivery_start_time = Time.current if delivery_start_time.nil?
42
- when "delivered", "failed"
43
- self.delivery_end_time = Time.current if delivery_end_time.nil?
32
+ def delivery_duration
33
+ return nil unless delivery_start_time && delivery_end_time
34
+ ((delivery_end_time - delivery_start_time) / 1.hour).round(2)
44
35
  end
45
- end
46
36
 
47
- def calculate_actual_delivery_time
48
- self.actual_delivery_time = delivery_end_time if status == "delivered"
49
- end
37
+ def optimized_route
38
+ return nil unless pickup_address&.coordinates.present?
39
+
40
+ # Get all delivery order items with dropoff addresses
41
+ items_with_dropoffs = delivery_order_items.includes(:dropoff_address)
42
+ .where.not(dropoff_address: nil)
43
+
44
+ dropoff_addresses = items_with_dropoffs.map(&:dropoff_address).compact.uniq
45
+ return nil if dropoff_addresses.empty?
50
46
 
51
- def end_time_after_start_time
52
- return unless delivery_start_time && delivery_end_time
53
- if delivery_end_time <= delivery_start_time
54
- errors.add(:delivery_end_time, "must be after delivery start time")
47
+ # Check if all dropoff addresses have coordinates
48
+ return nil if dropoff_addresses.any? { |addr| addr.coordinates.blank? }
49
+
50
+ # Call Gebeta Maps service
51
+ gebeta_service = Bscf::Core::GebetaMapsService.new
52
+ route_data = gebeta_service.optimize_route(pickup_address, dropoff_addresses)
53
+
54
+ # Return nil if route_data is empty or doesn't have waypoints
55
+ return nil if route_data.blank? || !route_data.key?("waypoints")
56
+
57
+ # Cache the result if needed
58
+ # Rails.cache.write("delivery_order_route_#{id}", route_data, expires_in: 1.hour)
59
+ route_data
55
60
  end
56
- end
57
61
 
58
- def sync_items_status
59
- delivery_order_items.update_all(status: status)
62
+ # Reorder delivery items based on optimized route or provided positions
63
+ def reorder_items_by_route(positions = nil)
64
+ if positions.is_a?(Hash)
65
+ # Use provided positions
66
+ positions.each do |item_id, position|
67
+ if item = delivery_order_items.find_by(id: item_id)
68
+ item.update(position: position)
69
+ end
70
+ end
71
+ true
72
+ else
73
+ return false if positions && !positions.is_a?(Hash)
74
+
75
+ # Use optimized route
76
+ route_data = optimized_route
77
+
78
+ return false unless route_data.present? && route_data["waypoints"].present?
79
+
80
+ # Get the optimized order of waypoints
81
+ waypoint_order = route_data["waypoints"].map { |wp| wp["waypoint_index"] }
82
+
83
+ # Get all delivery order items with dropoff addresses
84
+ items_with_dropoffs = delivery_order_items.includes(:dropoff_address)
85
+ .where.not(dropoff_address: nil)
86
+ .to_a
87
+
88
+ # Skip if we don't have the same number of items as waypoints
89
+ return false if items_with_dropoffs.size != waypoint_order.size
90
+
91
+ # Reorder items based on waypoint order
92
+ waypoint_order.each_with_index do |original_index, new_position|
93
+ if item = items_with_dropoffs[original_index]
94
+ item.update(position: new_position + 1)
95
+ end
96
+ end
97
+
98
+ true
99
+ end
100
+ end
101
+
102
+ # Add this method to the DeliveryOrder class
103
+ def dropoff_addresses
104
+ delivery_order_items.includes(:dropoff_address)
105
+ .where.not(dropoff_address: nil)
106
+ .map(&:dropoff_address)
107
+ .compact
108
+ .uniq
109
+ end
110
+
111
+
112
+ private
113
+
114
+ def update_delivery_times
115
+ case status
116
+ when "in_transit"
117
+ self.delivery_start_time = Time.current if delivery_start_time.nil?
118
+ when "delivered", "failed"
119
+ self.delivery_end_time = Time.current if delivery_end_time.nil?
120
+ end
121
+ end
122
+
123
+ def calculate_actual_delivery_time
124
+ self.actual_delivery_time = delivery_end_time if status == "delivered"
125
+ end
126
+
127
+ def end_time_after_start_time
128
+ return unless delivery_start_time && delivery_end_time
129
+ if delivery_end_time <= delivery_start_time
130
+ errors.add(:delivery_end_time, "must be after delivery start time")
131
+ end
132
+ end
133
+
134
+ def sync_items_status
135
+ delivery_order_items.update_all(status: status)
136
+ end
60
137
  end
61
138
  end
62
139
  end
@@ -11,6 +11,9 @@ module Bscf::Core
11
11
 
12
12
  after_save :update_delivery_order_status
13
13
  before_save :sync_status_with_delivery_order
14
+ before_save :set_default_pickup_address, if: -> { pickup_address.nil? && delivery_order.present? }
15
+ # Add default scope for ordering by position
16
+ default_scope { order(position: :asc) }
14
17
 
15
18
  enum :status, {
16
19
  pending: 0,
@@ -22,6 +25,10 @@ module Bscf::Core
22
25
 
23
26
  private
24
27
 
28
+ def set_default_pickup_address
29
+ self.pickup_address = delivery_order.pickup_address
30
+ end
31
+
25
32
  def quantity_not_exceeding_order_item
26
33
  return unless quantity && order_item&.quantity
27
34
  if quantity > order_item.quantity
@@ -0,0 +1,59 @@
1
+ require "httparty"
2
+
3
+ module Bscf
4
+ module Core
5
+ class GebetaMapsService
6
+ include HTTParty
7
+ base_uri "https://api.gebeta.app/api/v1"
8
+
9
+ def initialize
10
+ @api_key = ENV["GEBETA_API_KEY"]
11
+ end
12
+
13
+ # One-to-Many (ONM) API for route optimization
14
+ def optimize_route(origin_address, destination_addresses)
15
+ return {} if destination_addresses.empty?
16
+
17
+ # Extract coordinates
18
+ origin = origin_address.coordinates
19
+ destinations = destination_addresses.map(&:coordinates).compact
20
+
21
+ return {} if origin.nil? || destinations.empty?
22
+
23
+ # Prepare destinations in the format expected by Gebeta Maps
24
+ destinations_json = destinations.map.with_index do |coords, index|
25
+ {
26
+ id: index,
27
+ point: {
28
+ lat: coords[0],
29
+ lng: coords[1]
30
+ }
31
+ }
32
+ end.to_json
33
+
34
+ # Make API request
35
+ response = self.class.get(
36
+ "/direction/onm",
37
+ query: {
38
+ origin: "#{origin[0]},#{origin[1]}",
39
+ json: destinations_json,
40
+ key: @api_key
41
+ }
42
+ )
43
+
44
+ handle_response(response)
45
+ end
46
+
47
+ private
48
+
49
+ def handle_response(response)
50
+ if response.success?
51
+ JSON.parse(response.body)
52
+ else
53
+ Rails.logger.error("Gebeta Maps API error: #{response.code} - #{response.body}")
54
+ {}
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
data/config/master.key ADDED
@@ -0,0 +1 @@
1
+ 3009aef5af3ca1ddb654f74ad2ac7d5a
@@ -0,0 +1,6 @@
1
+ class UpdateAddressCoordinatesType < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_column :bscf_core_addresses, :latitude, :decimal, precision: 10, scale: 6, using: 'latitude::numeric(10,6)'
4
+ change_column :bscf_core_addresses, :longitude, :decimal, precision: 10, scale: 6, using: 'longitude::numeric(10,6)'
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddPositionToDeliveryOrderItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :bscf_core_delivery_order_items, :position, :integer
4
+ add_index :bscf_core_delivery_order_items, :position
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module Bscf
2
2
  module Core
3
- VERSION = "0.4.0"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bscf-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -310,9 +310,11 @@ files:
310
310
  - app/models/bscf/core/virtual_account_transaction.rb
311
311
  - app/models/bscf/core/voucher.rb
312
312
  - app/models/bscf/core/wholesaler_product.rb
313
+ - app/services/bscf/core/gebeta_maps_service.rb
313
314
  - app/services/bscf/core/token_service.rb
314
315
  - app/services/bscf/core/transaction_service.rb
315
316
  - config/database.yml
317
+ - config/master.key
316
318
  - config/routes.rb
317
319
  - db/migrate/20250326065606_create_bscf_core_users.rb
318
320
  - db/migrate/20250326075111_create_bscf_core_roles.rb
@@ -348,6 +350,8 @@ files:
348
350
  - db/migrate/20250606100158_add_product_to_market_place_listings.rb
349
351
  - db/migrate/20250609180530_add_accounting_fields_to_virtual_account_transactions.rb
350
352
  - db/migrate/20250610120351_update_delivery_tables.rb
353
+ - db/migrate/20250616134209_update_address_coordinates_type.rb
354
+ - db/migrate/20250616145522_add_position_to_delivery_order_items.rb
351
355
  - lib/bscf/core.rb
352
356
  - lib/bscf/core/engine.rb
353
357
  - lib/bscf/core/version.rb