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 +4 -4
- data/app/models/bscf/core/address.rb +25 -0
- data/app/models/bscf/core/delivery_order.rb +129 -52
- data/app/models/bscf/core/delivery_order_item.rb +7 -0
- data/app/services/bscf/core/gebeta_maps_service.rb +59 -0
- data/config/master.key +1 -0
- data/db/migrate/20250616134209_update_address_coordinates_type.rb +6 -0
- data/db/migrate/20250616145522_add_position_to_delivery_order_items.rb +6 -0
- data/lib/bscf/core/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac477439e787a6451f0b7d5a92ac75e430ac8b0ddcd7a04bb5f8e57c4bd086dc
|
4
|
+
data.tar.gz: 4bd37eaa9562180049efec4028c438404bce7f08661c27c2c9e4018ba88a724a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
59
|
-
|
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
|
data/lib/bscf/core/version.rb
CHANGED
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.
|
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
|