bscf-core 0.3.99 → 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: 7ba4cb08d046b4bcef3b55100146f08c64fa32d7b97f59302ffdf8ef1a804737
4
- data.tar.gz: da7bd6abdf23ec993e8442e998853118533d2f2d7773de25155aca33c6569c22
3
+ metadata.gz: ac477439e787a6451f0b7d5a92ac75e430ac8b0ddcd7a04bb5f8e57c4bd086dc
4
+ data.tar.gz: 4bd37eaa9562180049efec4028c438404bce7f08661c27c2c9e4018ba88a724a
5
5
  SHA512:
6
- metadata.gz: 7eca0f066d06df32ff1fcbf12c1056331ada9eef7f5f5cd18d37becc26dfad27a3576e9afc97e157bd7bd133a5c28534c70aeb2c74592c325b67b51bcf42f6e4
7
- data.tar.gz: 35654a8b5fab7cb7b4bfdaa124014cbf80e9c1b640079ec43a2d95800672d34db54e7ee89c3cacf31eff19f6b627b7e56f8a5f335d70a9308939a19b4932b0b3
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,63 +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 :buyer_phone, :seller_phone, :driver_phone,
13
- :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?
20
-
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
- }
31
-
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)
35
- 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?
36
20
 
37
- 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
+ }
38
31
 
39
- def update_delivery_times
40
- case status
41
- when "in_transit"
42
- self.delivery_start_time = Time.current if delivery_start_time.nil?
43
- when "delivered", "failed"
44
- 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)
45
35
  end
46
- end
47
36
 
48
- def calculate_actual_delivery_time
49
- self.actual_delivery_time = delivery_end_time if status == "delivered"
50
- 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?
51
46
 
52
- def end_time_after_start_time
53
- return unless delivery_start_time && delivery_end_time
54
- if delivery_end_time <= delivery_start_time
55
- 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
56
60
  end
57
- end
58
61
 
59
- def sync_items_status
60
- 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
61
137
  end
62
138
  end
63
139
  end
@@ -2,7 +2,8 @@ module Bscf::Core
2
2
  class DeliveryOrderItem < ApplicationRecord
3
3
  belongs_to :delivery_order
4
4
  belongs_to :order_item
5
- belongs_to :product
5
+ belongs_to :pickup_address, class_name: "Bscf::Core::Address", optional: true
6
+ belongs_to :dropoff_address, class_name: "Bscf::Core::Address", optional: true
6
7
 
7
8
  validates :quantity, :status, presence: true
8
9
  validate :quantity_not_exceeding_order_item
@@ -10,6 +11,9 @@ module Bscf::Core
10
11
 
11
12
  after_save :update_delivery_order_status
12
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) }
13
17
 
14
18
  enum :status, {
15
19
  pending: 0,
@@ -21,6 +25,10 @@ module Bscf::Core
21
25
 
22
26
  private
23
27
 
28
+ def set_default_pickup_address
29
+ self.pickup_address = delivery_order.pickup_address
30
+ end
31
+
24
32
  def quantity_not_exceeding_order_item
25
33
  return unless quantity && order_item&.quantity
26
34
  if quantity > order_item.quantity
@@ -1,20 +1,27 @@
1
1
  module Bscf
2
2
  module Core
3
3
  class VirtualAccountTransaction < ApplicationRecord
4
- belongs_to :from_account, class_name: "Bscf::Core::VirtualAccount", optional: true
5
- belongs_to :to_account, class_name: "Bscf::Core::VirtualAccount", optional: true
4
+ belongs_to :account, class_name: "Bscf::Core::VirtualAccount"
5
+ belongs_to :paired_transaction, class_name: "Bscf::Core::VirtualAccountTransaction", optional: true
6
6
 
7
- validates :from_account_id, presence: true, if: :requires_from_account?
8
- validates :to_account_id, presence: true, if: :requires_to_account?
7
+ validates :account_id, presence: true
9
8
  validates :amount, presence: true, numericality: { greater_than: 0 }
10
9
  validates :transaction_type, presence: true
10
+ validates :entry_type, presence: true
11
11
  validates :status, presence: true
12
12
  validates :reference_number, presence: true, uniqueness: true
13
13
 
14
14
  enum :transaction_type, {
15
15
  transfer: 0,
16
16
  deposit: 1,
17
- withdrawal: 2
17
+ withdrawal: 2,
18
+ fee: 3,
19
+ adjustment: 4
20
+ }
21
+
22
+ enum :entry_type, {
23
+ debit: 0,
24
+ credit: 1
18
25
  }
19
26
 
20
27
  enum :status, {
@@ -27,122 +34,76 @@ module Bscf
27
34
  before_validation :generate_reference_number, on: :create
28
35
  validate :validate_transaction, on: :create
29
36
 
37
+ scope :debits, -> { where(entry_type: :debit) }
38
+ scope :credits, -> { where(entry_type: :credit) }
39
+ scope :for_account, ->(account_id) { where(account_id: account_id) }
40
+ scope :by_type, ->(type) { where(transaction_type: type) }
41
+ scope :by_status, ->(status) { where(status: status) }
42
+
30
43
  def process!
31
44
  return false unless pending?
32
45
 
33
46
  ActiveRecord::Base.transaction do
34
- process_transaction
47
+ update_account_balance
48
+
35
49
  update!(status: :completed)
50
+ paired_transaction&.update!(status: :completed)
36
51
  end
37
52
  true
38
53
  rescue StandardError => e
39
54
  update(status: :failed)
55
+ paired_transaction&.update(status: :failed)
56
+ Rails.logger.error("Transaction processing failed: #{e.message}")
40
57
  false
41
58
  end
42
59
 
43
60
  def cancel!
44
61
  return false unless pending?
45
- update(status: :cancelled)
46
- end
47
-
48
- private
49
-
50
- def requires_from_account?
51
- return false if transaction_type.nil?
52
- %w[transfer withdrawal].include?(transaction_type)
53
- end
54
62
 
55
- def requires_to_account?
56
- return false if transaction_type.nil?
57
- %w[transfer deposit].include?(transaction_type)
58
- end
59
-
60
- def validate_transaction
61
- case transaction_type.to_sym
62
- when :transfer
63
- validate_transfer
64
- when :withdrawal
65
- validate_withdrawal
66
- when :deposit
67
- validate_deposit
63
+ ActiveRecord::Base.transaction do
64
+ update(status: :cancelled)
65
+ paired_transaction&.update(status: :cancelled)
68
66
  end
69
-
70
- validate_account_requirements
71
- end
72
-
73
- def validate_transfer
74
- return unless from_account && to_account
75
-
76
- errors.add(:from_account, "must be active") unless from_account.active?
77
- errors.add(:to_account, "must be active") unless to_account.active?
78
- errors.add(:from_account, "insufficient balance") if from_account.balance.to_d < amount.to_d
67
+ true
79
68
  end
80
69
 
81
70
  private
82
71
 
83
- def validate_account_requirements
84
- case transaction_type.to_sym
85
- when :transfer
86
- return if from_account_id.present? && to_account_id.present?
87
- errors.add(:base, "Both accounts are required for transfer")
88
- when :withdrawal
89
- return if from_account_id.present?
90
- errors.add(:base, "Source account is required for withdrawal")
91
- errors.add(:to_account_id, "must be blank for withdrawal")
92
- when :deposit
93
- return if to_account_id.present?
94
- errors.add(:base, "Destination account is required for deposit")
95
- errors.add(:from_account_id, "must be blank for deposit")
96
- end
97
- end
98
-
99
- def validate_withdrawal
100
- return unless from_account
101
- errors.add(:from_account, "must be active") unless from_account.active?
102
- errors.add(:from_account, "insufficient balance") if from_account.balance.to_d < amount.to_d
103
- end
72
+ def validate_transaction
73
+ errors.add(:account, "must be active") unless account.active?
104
74
 
105
- def validate_deposit
106
- return unless to_account
107
- errors.add(:to_account, "must be active") unless to_account.active?
108
- end
75
+ if debit? && !adjustment? && account.balance.to_d < amount.to_d
76
+ errors.add(:account, "insufficient balance")
77
+ end
109
78
 
110
- def process_transaction
111
- case transaction_type.to_sym
112
- when :transfer
113
- process_transfer
114
- when :withdrawal
115
- process_withdrawal
116
- when :deposit
117
- process_deposit
79
+ if transfer? && paired_transaction.blank?
80
+ errors.add(:paired_transaction, "must be present for transfers")
118
81
  end
119
82
  end
120
83
 
121
- def process_transfer
122
- ActiveRecord::Base.transaction do
123
- from_account.with_lock do
124
- to_account.with_lock do
125
- new_from_balance = (from_account.balance - amount).round(2)
126
- new_to_balance = (to_account.balance + amount).round(2)
127
-
128
- from_account.update!(balance: new_from_balance)
129
- to_account.update!(balance: new_to_balance)
130
- end
84
+ def update_account_balance
85
+ account.with_lock do
86
+ new_balance = if debit?
87
+ (account.balance - amount).round(2)
88
+ else
89
+ (account.balance + amount).round(2)
131
90
  end
132
- end
133
- end
134
91
 
135
- def process_withdrawal
136
- from_account.with_lock do
137
- new_balance = (from_account.balance - amount).round(2)
138
- from_account.update!(balance: new_balance)
92
+ account.update!(balance: new_balance)
93
+ update!(running_balance: new_balance)
139
94
  end
140
- end
141
95
 
142
- def process_deposit
143
- to_account.with_lock do
144
- new_balance = (to_account.balance + amount).round(2)
145
- to_account.update!(balance: new_balance)
96
+ if paired_transaction.present?
97
+ paired_transaction.account.with_lock do
98
+ new_balance = if paired_transaction.debit?
99
+ (paired_transaction.account.balance - paired_transaction.amount).round(2)
100
+ else
101
+ (paired_transaction.account.balance + paired_transaction.amount).round(2)
102
+ end
103
+
104
+ paired_transaction.account.update!(balance: new_balance)
105
+ paired_transaction.update!(running_balance: new_balance)
106
+ end
146
107
  end
147
108
  end
148
109
 
@@ -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
@@ -0,0 +1,185 @@
1
+ module Bscf
2
+ module Core
3
+ class TransactionService
4
+ # Create a transfer between two accounts
5
+ # @param from_account_id [Integer] Source account ID
6
+ # @param to_account_id [Integer] Destination account ID
7
+ # @param amount [Decimal] Amount to transfer
8
+ # @param description [String] Optional description
9
+ # @return [Array<VirtualAccountTransaction>] The created transaction pair
10
+ def self.create_transfer(from_account_id:, to_account_id:, amount:, description: nil)
11
+ create_paired_transaction(
12
+ from_account_id: from_account_id,
13
+ to_account_id: to_account_id,
14
+ amount: amount,
15
+ transaction_type: :transfer,
16
+ description: description
17
+ )
18
+ end
19
+
20
+ # Create a deposit to an account
21
+ # @param to_account_id [Integer] Destination account ID
22
+ # @param amount [Decimal] Amount to deposit
23
+ # @param description [String] Optional description
24
+ # @return [Array<VirtualAccountTransaction>] The created transaction pair
25
+ def self.create_deposit(to_account_id:, amount:, description: nil)
26
+ system_account = find_or_create_system_account("DEPOSIT_ACCOUNT")
27
+
28
+ # Ensure system account has sufficient balance for deposits
29
+ ensure_system_account_balance(system_account, amount)
30
+
31
+ create_paired_transaction(
32
+ from_account_id: system_account.id,
33
+ to_account_id: to_account_id,
34
+ amount: amount,
35
+ transaction_type: :deposit,
36
+ description: description
37
+ )
38
+ end
39
+
40
+ # Create a withdrawal from an account
41
+ # @param from_account_id [Integer] Source account ID
42
+ # @param amount [Decimal] Amount to withdraw
43
+ # @param description [String] Optional description
44
+ # @return [Array<VirtualAccountTransaction>] The created transaction pair
45
+ def self.create_withdrawal(from_account_id:, amount:, description: nil)
46
+ system_account = find_or_create_system_account("WITHDRAWAL_ACCOUNT")
47
+
48
+ create_paired_transaction(
49
+ from_account_id: from_account_id,
50
+ to_account_id: system_account.id,
51
+ amount: amount,
52
+ transaction_type: :withdrawal,
53
+ description: description
54
+ )
55
+ end
56
+
57
+ # Create a fee transaction
58
+ # @param from_account_id [Integer] Account to charge
59
+ # @param amount [Decimal] Fee amount
60
+ # @param description [String] Optional description
61
+ # @return [Array<VirtualAccountTransaction>] The created transaction pair
62
+ def self.create_fee(from_account_id:, amount:, description: nil)
63
+ fee_account = find_or_create_system_account("FEE_ACCOUNT")
64
+
65
+ create_paired_transaction(
66
+ from_account_id: from_account_id,
67
+ to_account_id: fee_account.id,
68
+ amount: amount,
69
+ transaction_type: :fee,
70
+ description: description
71
+ )
72
+ end
73
+
74
+ # Create an adjustment to an account
75
+ # @param account_id [Integer] Account to adjust
76
+ # @param amount [Decimal] Adjustment amount
77
+ # @param is_debit [Boolean] Whether this is a debit (true) or credit (false)
78
+ # @param description [String] Optional description
79
+ # @return [VirtualAccountTransaction] The created adjustment transaction
80
+ def self.create_adjustment(account_id:, amount:, is_debit: true, description: nil)
81
+ reference_number = generate_reference_number
82
+
83
+ VirtualAccountTransaction.create!(
84
+ transaction_type: :adjustment,
85
+ entry_type: is_debit ? :debit : :credit,
86
+ account_id: account_id,
87
+ amount: amount,
88
+ reference_number: reference_number,
89
+ description: description,
90
+ status: :pending
91
+ )
92
+ end
93
+
94
+ # Process a transaction
95
+ # @param transaction [VirtualAccountTransaction] Transaction to process
96
+ # @return [Boolean] Success or failure
97
+ def self.process(transaction)
98
+ transaction.process!
99
+ end
100
+
101
+ # Cancel a transaction
102
+ # @param transaction [VirtualAccountTransaction] Transaction to cancel
103
+ # @return [Boolean] Success or failure
104
+ def self.cancel(transaction)
105
+ transaction.cancel!
106
+ end
107
+
108
+ private
109
+
110
+ def self.create_paired_transaction(from_account_id:, to_account_id:, amount:, transaction_type:, description: nil)
111
+ reference_number = generate_reference_number
112
+
113
+ ActiveRecord::Base.transaction do
114
+ # Debit entry (from account)
115
+ debit = VirtualAccountTransaction.create!(
116
+ transaction_type: transaction_type,
117
+ entry_type: :debit,
118
+ account_id: from_account_id,
119
+ amount: amount,
120
+ reference_number: reference_number,
121
+ description: description,
122
+ status: :pending
123
+ )
124
+
125
+ # Credit entry (to account)
126
+ credit = VirtualAccountTransaction.create!(
127
+ transaction_type: transaction_type,
128
+ entry_type: :credit,
129
+ account_id: to_account_id,
130
+ amount: amount,
131
+ reference_number: reference_number,
132
+ description: description,
133
+ status: :pending,
134
+ paired_transaction_id: debit.id
135
+ )
136
+
137
+ # Update the paired_transaction_id for the debit entry
138
+ debit.update!(paired_transaction_id: credit.id)
139
+
140
+ [ debit, credit ]
141
+ end
142
+ end
143
+
144
+ def self.find_or_create_system_account(account_identifier)
145
+ # Find or create a system account for external transactions
146
+ account = VirtualAccount.find_or_create_by!(account_number: account_identifier) do |account|
147
+ # Set default values for a new system account
148
+ account.user_id = find_or_create_system_user.id
149
+ account.cbs_account_number = account_identifier
150
+ account.branch_code = "SYSTEM"
151
+ account.product_scheme = "SAVINGS"
152
+ account.voucher_type = "REGULAR"
153
+ account.interest_rate = 0
154
+ account.interest_type = :simple
155
+ account.balance = 100000.00 # Initialize with a large balance
156
+ account.locked_amount = 0
157
+ account.status = :active
158
+ end
159
+
160
+ account
161
+ end
162
+
163
+ def self.ensure_system_account_balance(account, required_amount)
164
+ # Top up the system account if needed
165
+ if account.balance < required_amount
166
+ account.update!(balance: account.balance + required_amount * 2)
167
+ end
168
+ end
169
+
170
+ def self.find_or_create_system_user
171
+ User.find_or_create_by!(phone_number: "SYSTEM_USER") do |user|
172
+ user.first_name = "System"
173
+ user.last_name = "User"
174
+ user.password = SecureRandom.hex(8)
175
+ end
176
+ end
177
+
178
+ def self.generate_reference_number
179
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
180
+ random = SecureRandom.hex(3)
181
+ "TXN#{timestamp}#{random}"
182
+ end
183
+ end
184
+ end
185
+ end
data/config/master.key ADDED
@@ -0,0 +1 @@
1
+ 3009aef5af3ca1ddb654f74ad2ac7d5a
@@ -0,0 +1,20 @@
1
+ class AddAccountingFieldsToVirtualAccountTransactions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ # Add new accounting fields
4
+ add_column :bscf_core_virtual_account_transactions, :entry_type, :integer
5
+ add_reference :bscf_core_virtual_account_transactions, :account, null: false, foreign_key: { to_table: :bscf_core_virtual_accounts }
6
+ add_column :bscf_core_virtual_account_transactions, :running_balance, :decimal, precision: 10, scale: 2
7
+ add_reference :bscf_core_virtual_account_transactions, :paired_transaction, null: true, foreign_key: { to_table: :bscf_core_virtual_account_transactions }
8
+ add_column :bscf_core_virtual_account_transactions, :value_date, :datetime
9
+
10
+ # Add new indexes
11
+ add_index :bscf_core_virtual_account_transactions, [ :account_id, :reference_number ]
12
+ add_index :bscf_core_virtual_account_transactions, :entry_type
13
+
14
+ # Remove old fields and indexes
15
+ remove_index :bscf_core_virtual_account_transactions, [ :from_account_id, :reference_number ], if_exists: true
16
+ remove_index :bscf_core_virtual_account_transactions, [ :to_account_id, :reference_number ], if_exists: true
17
+ remove_reference :bscf_core_virtual_account_transactions, :from_account
18
+ remove_reference :bscf_core_virtual_account_transactions, :to_account
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ class UpdateDeliveryTables < ActiveRecord::Migration[8.0]
2
+ def up
3
+ remove_column :bscf_core_delivery_orders, :buyer_phone
4
+ remove_column :bscf_core_delivery_orders, :seller_phone
5
+
6
+ add_column :bscf_core_delivery_orders, :estimated_delivery_price, :float
7
+
8
+ rename_column :bscf_core_delivery_orders, :delivery_price, :actual_delivery_price
9
+
10
+ add_reference :bscf_core_delivery_order_items, :pickup_address, foreign_key: { to_table: :bscf_core_addresses }
11
+ add_reference :bscf_core_delivery_order_items, :dropoff_address, foreign_key: { to_table: :bscf_core_addresses }
12
+
13
+ remove_reference :bscf_core_delivery_order_items, :product
14
+ end
15
+ end
@@ -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.3.99"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
@@ -2,7 +2,8 @@ FactoryBot.define do
2
2
  factory :delivery_order_item, class: 'Bscf::Core::DeliveryOrderItem' do
3
3
  association :delivery_order
4
4
  association :order_item
5
- association :product
5
+ association :pickup_address, factory: :address
6
+ association :dropoff_address, factory: :address
6
7
  quantity { order_item.quantity }
7
8
  status { :pending }
8
9
  notes { Faker::Lorem.paragraph }
@@ -2,38 +2,15 @@ FactoryBot.define do
2
2
  factory :delivery_order, class: 'Bscf::Core::DeliveryOrder' do
3
3
  association :pickup_address, factory: :address
4
4
  association :dropoff_address, factory: :address
5
- driver { nil }
5
+ association :driver, factory: :user
6
6
 
7
- buyer_phone { Faker::PhoneNumber.phone_number }
8
- seller_phone { Faker::PhoneNumber.phone_number }
9
7
  driver_phone { Faker::PhoneNumber.phone_number }
10
8
  delivery_notes { Faker::Lorem.paragraph }
11
9
  estimated_delivery_time { 2.days.from_now }
10
+ estimated_delivery_price { Faker::Number.decimal(l_digits: 2, r_digits: 2) }
11
+ actual_delivery_price { Faker::Number.decimal(l_digits: 2, r_digits: 2) }
12
12
  delivery_start_time { nil }
13
13
  delivery_end_time { nil }
14
14
  status { :pending }
15
-
16
- trait :with_driver do
17
- association :driver, factory: :user
18
- end
19
-
20
- trait :in_transit do
21
- with_driver
22
- status { :in_transit }
23
- delivery_start_time { Time.current }
24
- end
25
-
26
- trait :delivered do
27
- with_driver
28
- status { :delivered }
29
- delivery_start_time { 2.hours.ago }
30
- delivery_end_time { Time.current }
31
- end
32
-
33
- trait :with_orders do
34
- after(:create) do |delivery_order|
35
- create_list(:order, 2, delivery_order: delivery_order)
36
- end
37
- end
38
15
  end
39
16
  end
@@ -1,34 +1,49 @@
1
1
  FactoryBot.define do
2
2
  factory :virtual_account_transaction, class: 'Bscf::Core::VirtualAccountTransaction' do
3
- association :from_account, factory: :virtual_account, status: :active, balance: 10000
4
- association :to_account, factory: :virtual_account, status: :active, balance: 5000
3
+ association :account, factory: :virtual_account, status: :active, balance: 10000
5
4
  amount { 100.0 }
6
- transaction_type { :transfer }
5
+ transaction_type { :adjustment }
6
+ entry_type { :debit }
7
7
  status { :pending }
8
+ value_date { Time.current }
8
9
  description { Faker::Lorem.sentence }
10
+ running_balance { nil }
9
11
 
10
- trait :completed do
11
- status { :completed }
12
- end
13
-
14
- trait :failed do
15
- status { :failed }
16
- end
12
+ factory :paired_transaction do
13
+ transient do
14
+ paired_account { create(:virtual_account, status: :active, balance: 10000) }
15
+ is_credit { false }
16
+ end
17
17
 
18
- trait :cancelled do
19
- status { :cancelled }
20
- end
18
+ after(:build) do |transaction, evaluator|
19
+ if transaction.transaction_type == 'transfer'
20
+ paired_entry_type = transaction.entry_type == 'debit' ? :credit : :debit
21
+ elsif transaction.transaction_type == 'deposit'
22
+ paired_entry_type = transaction.entry_type == 'debit' ? :credit : :debit
23
+ elsif transaction.transaction_type == 'withdrawal'
24
+ paired_entry_type = transaction.entry_type == 'debit' ? :credit : :debit
25
+ else
26
+ paired_entry_type = evaluator.is_credit ? :credit : :debit
27
+ end
21
28
 
22
- trait :deposit do
23
- transaction_type { :deposit }
24
- end
29
+ paired = build(:virtual_account_transaction,
30
+ transaction_type: transaction.transaction_type,
31
+ entry_type: paired_entry_type,
32
+ account: evaluator.paired_account,
33
+ amount: transaction.amount,
34
+ reference_number: transaction.reference_number,
35
+ value_date: transaction.value_date,
36
+ status: transaction.status
37
+ )
25
38
 
26
- trait :withdrawal do
27
- transaction_type { :withdrawal }
28
- end
39
+ transaction.paired_transaction = paired
40
+ paired.paired_transaction = transaction
41
+ end
29
42
 
30
- trait :with_large_amount do
31
- amount { 5000.0 }
43
+ after(:create) do |transaction, evaluator|
44
+ # Save the paired transaction after the main transaction is created
45
+ transaction.paired_transaction.save! if transaction.paired_transaction&.new_record?
46
+ end
32
47
  end
33
48
  end
34
49
  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.3.99
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -310,8 +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
315
+ - app/services/bscf/core/transaction_service.rb
314
316
  - config/database.yml
317
+ - config/master.key
315
318
  - config/routes.rb
316
319
  - db/migrate/20250326065606_create_bscf_core_users.rb
317
320
  - db/migrate/20250326075111_create_bscf_core_roles.rb
@@ -345,6 +348,10 @@ files:
345
348
  - db/migrate/20250524081221_create_bscf_core_vouchers.rb
346
349
  - db/migrate/20250524151346_add_locked_amount_to_virtual_accounts.rb
347
350
  - db/migrate/20250606100158_add_product_to_market_place_listings.rb
351
+ - db/migrate/20250609180530_add_accounting_fields_to_virtual_account_transactions.rb
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
348
355
  - lib/bscf/core.rb
349
356
  - lib/bscf/core/engine.rb
350
357
  - lib/bscf/core/version.rb