bscf-core 0.3.97 → 0.3.99

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: b5e753307eddd088b9766de57d13afe6cc4314ff0397e6f41cfe52186d590600
4
- data.tar.gz: ef5ae10b4107c6e55a15f5510c9752fed060f72f76507d2f7b1ba6717854838c
3
+ metadata.gz: 7ba4cb08d046b4bcef3b55100146f08c64fa32d7b97f59302ffdf8ef1a804737
4
+ data.tar.gz: da7bd6abdf23ec993e8442e998853118533d2f2d7773de25155aca33c6569c22
5
5
  SHA512:
6
- metadata.gz: c7416e778a80c6d2ff80e84e20bdf1177085eafb1bc720cf1c0184f8116ec6670f8595667040d739064363ddc9afc97fc64745a94eb415866b63af73e9e567e2
7
- data.tar.gz: 0b93e631c442249dd259a10e3bf5be40adabb233504bea4d467e8871d26cbf0e21ccb67d90d9a709ac90751cf81e17ddd41f4318a37b888cb7709e125309f070
6
+ metadata.gz: 7eca0f066d06df32ff1fcbf12c1056331ada9eef7f5f5cd18d37becc26dfad27a3576e9afc97e157bd7bd133a5c28534c70aeb2c74592c325b67b51bcf42f6e4
7
+ data.tar.gz: 35654a8b5fab7cb7b4bfdaa124014cbf80e9c1b640079ec43a2d95800672d34db54e7ee89c3cacf31eff19f6b627b7e56f8a5f335d70a9308939a19b4932b0b3
@@ -1,6 +1,6 @@
1
1
  module Bscf::Core
2
2
  class DeliveryOrder < ApplicationRecord
3
- belongs_to :order
3
+ has_many :orders
4
4
  belongs_to :pickup_address, class_name: "Bscf::Core::Address"
5
5
  belongs_to :dropoff_address, class_name: "Bscf::Core::Address"
6
6
  belongs_to :driver, class_name: "Bscf::Core::User", optional: true
@@ -3,8 +3,9 @@ module Bscf
3
3
  class MarketplaceListing < ApplicationRecord
4
4
  belongs_to :user
5
5
  belongs_to :address
6
+ belongs_to :product
6
7
 
7
- validates :listing_type, :status, :is_active, presence: true
8
+ validates :listing_type, :status, :is_active, :price, presence: true
8
9
 
9
10
  enum :listing_type, { buy: 0, sell: 1 }
10
11
  enum :status, { open: 0, partially_matched: 1, matched: 2, completed: 3, cancelled: 4 }
@@ -4,6 +4,7 @@ module Bscf
4
4
  belongs_to :ordered_by, class_name: "Bscf::Core::User", optional: true
5
5
  belongs_to :ordered_to, class_name: "Bscf::Core::User", optional: true
6
6
  belongs_to :quotation, optional: true
7
+ belongs_to :delivery_order, optional: true
7
8
  has_many :order_items, dependent: :destroy
8
9
  validates :order_type, :status, presence: true
9
10
 
@@ -7,6 +7,7 @@ module Bscf
7
7
  has_one :user_role
8
8
  has_one :business
9
9
  has_one :vehicle, foreign_key: :driver_id
10
+ has_one :virtual_account
10
11
  has_many :user_roles
11
12
  has_many :roles, through: :user_roles
12
13
  has_many :orders_placed, class_name: "Bscf::Core::Order", foreign_key: "ordered_by_id"
@@ -9,12 +9,18 @@ module Bscf
9
9
  validates :account_number, presence: true, uniqueness: true
10
10
  validates :cbs_account_number, presence: true, uniqueness: true
11
11
  validates :balance, presence: true, numericality: { greater_than_or_equal_to: 0 }
12
+ validates :locked_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
12
13
  validates :interest_rate, presence: true, numericality: { greater_than_or_equal_to: 0 }
13
14
  validates :interest_type, presence: true
14
15
  validates :branch_code, presence: true
15
16
  validates :product_scheme, presence: true, inclusion: { in: PRODUCT_SCHEMES }
16
17
  validates :voucher_type, presence: true, inclusion: { in: VOUCHER_TYPES }
17
18
  validates :status, presence: true
19
+ validate :available_balance_sufficient
20
+ has_many :sent_transactions, class_name: "Bscf::Core::VirtualAccountTransaction",
21
+ foreign_key: "from_account_id", dependent: :restrict_with_error
22
+ has_many :received_transactions, class_name: "Bscf::Core::VirtualAccountTransaction",
23
+ foreign_key: "to_account_id", dependent: :restrict_with_error
18
24
 
19
25
  enum :interest_type, {
20
26
  simple: 0,
@@ -34,8 +40,39 @@ module Bscf
34
40
  scope :by_branch, ->(code) { where(branch_code: code) }
35
41
  scope :by_product, ->(scheme) { where(product_scheme: scheme) }
36
42
 
43
+ def available_balance
44
+ balance - locked_amount
45
+ end
46
+
47
+ def lock_amount!(amount)
48
+ return false if amount <= 0
49
+ return false if amount > available_balance
50
+
51
+ transaction do
52
+ self.locked_amount += amount
53
+ save!
54
+ end
55
+ end
56
+
57
+ def unlock_amount!(amount)
58
+ return false if amount <= 0
59
+ return false if amount > locked_amount
60
+
61
+ transaction do
62
+ self.locked_amount -= amount
63
+ save!
64
+ end
65
+ end
66
+
37
67
  private
38
68
 
69
+ def available_balance_sufficient
70
+ return unless balance && locked_amount
71
+ if locked_amount > balance
72
+ errors.add(:locked_amount, "cannot exceed balance")
73
+ end
74
+ end
75
+
39
76
  def generate_account_number
40
77
  return if account_number.present?
41
78
 
@@ -44,10 +81,16 @@ module Bscf
44
81
  self.account_number = "#{branch_code}#{product_scheme}#{voucher_type}#{seq}"
45
82
  end
46
83
 
47
- has_many :sent_transactions, class_name: "Bscf::Core::VirtualAccountTransaction",
48
- foreign_key: "from_account_id", dependent: :restrict_with_error
49
- has_many :received_transactions, class_name: "Bscf::Core::VirtualAccountTransaction",
50
- foreign_key: "to_account_id", dependent: :restrict_with_error
84
+ def transfer_to!(to_account, amount)
85
+ transaction = VirtualAccountTransaction.new(
86
+ from_account: self,
87
+ to_account: to_account,
88
+ amount: amount,
89
+ transaction_type: :transfer
90
+ )
91
+
92
+ transaction.process!
93
+ end
51
94
  end
52
95
  end
53
96
  end
@@ -0,0 +1,134 @@
1
+ module Bscf
2
+ module Core
3
+ class Voucher < ApplicationRecord
4
+ belongs_to :issued_by, class_name: "Bscf::Core::User"
5
+
6
+ validates :full_name, presence: true
7
+ validates :phone_number, presence: true
8
+ validates :amount, presence: true, numericality: { greater_than: 0 }
9
+ validates :code, presence: true, uniqueness: true
10
+ validate :issuer_has_sufficient_balance, on: :create
11
+
12
+ enum :status, {
13
+ pending: 0,
14
+ active: 1,
15
+ redeemed: 2,
16
+ expired: 3,
17
+ returned: 4,
18
+ cancelled: 5
19
+ }
20
+
21
+ before_validation :generate_code, on: :create
22
+ before_create :set_default_expiry
23
+ after_create :lock_issuer_amount
24
+ after_commit :unlock_issuer_amount, on: [ :update ], if: :should_unlock_amount?
25
+
26
+ def redeem!(to_account)
27
+ return false unless active?
28
+ return false if expired? || returned? || cancelled?
29
+
30
+ from_account = issued_by.virtual_account
31
+ success = false
32
+
33
+ ActiveRecord::Base.transaction do
34
+ transaction = VirtualAccountTransaction.new(
35
+ from_account: from_account,
36
+ to_account: to_account,
37
+ amount: amount,
38
+ transaction_type: :transfer
39
+ )
40
+
41
+ unless transaction.process!
42
+ errors.add(:base, "Voucher redemption failed due to transaction processing error.")
43
+ raise ActiveRecord::Rollback
44
+ end
45
+
46
+ unless from_account.unlock_amount!(amount)
47
+ errors.add(:base, "Failed to unlock amount from issuer after successful transfer.")
48
+ raise ActiveRecord::Rollback
49
+ end
50
+
51
+ update!(status: :redeemed, redeemed_at: Time.current)
52
+ success = true
53
+ end
54
+
55
+ success
56
+ rescue ActiveRecord::RecordInvalid => e
57
+ errors.add(:base, "Voucher redemption failed: #{e.message}")
58
+ false
59
+ rescue ActiveRecord::Rollback
60
+ false
61
+ end
62
+
63
+ def return!
64
+ return false unless can_return?
65
+ update!(status: :returned, returned_at: Time.current)
66
+ true
67
+ rescue ActiveRecord::RecordInvalid
68
+ false
69
+ end
70
+
71
+ def cancel!
72
+ return false unless can_cancel?
73
+ update!(status: :cancelled, returned_at: Time.current)
74
+ true
75
+ rescue ActiveRecord::RecordInvalid
76
+ false
77
+ end
78
+
79
+ def can_return?
80
+ !redeemed? && !returned? && !cancelled? && !expired?
81
+ end
82
+
83
+ def can_cancel?
84
+ (pending? || active?) && !expired? && !returned? && !redeemed?
85
+ end
86
+
87
+ private
88
+
89
+ def generate_code
90
+ self.code ||= SecureRandom.hex(8).upcase
91
+ end
92
+
93
+ def set_default_expiry
94
+ self.expires_at ||= 30.days.from_now
95
+ end
96
+
97
+ def lock_issuer_amount
98
+ issuer_account = issued_by.virtual_account
99
+ unless issuer_account.available_balance >= amount
100
+ errors.add(:amount, "exceeds available balance")
101
+ return false
102
+ end
103
+
104
+ if issuer_account.lock_amount!(amount)
105
+ update!(status: :active)
106
+ true
107
+ else
108
+ errors.add(:amount, "could not be locked")
109
+ false
110
+ end
111
+ end
112
+
113
+ def unlock_issuer_amount
114
+ issued_by.virtual_account.unlock_amount!(amount) if amount_should_be_unlocked?
115
+ end
116
+
117
+ def should_unlock_amount?
118
+ (returned? || cancelled?) && status_previously_changed?
119
+ end
120
+
121
+ def amount_should_be_unlocked?
122
+ returned? || cancelled?
123
+ end
124
+
125
+ def issuer_has_sufficient_balance
126
+ return unless amount && issued_by&.virtual_account
127
+ unless issued_by.virtual_account.available_balance >= amount
128
+ errors.add(:amount, "exceeds available balance")
129
+ throw :abort
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,6 @@
1
+ class RemoveOrderIdFromDeliveryOrdersAndAddDeliveryOrderIdToOrders < ActiveRecord::Migration[8.0]
2
+ def change
3
+ remove_reference :bscf_core_delivery_orders, :order, foreign_key: { to_table: :bscf_core_orders }, index: true
4
+ add_reference :bscf_core_orders, :delivery_order, foreign_key: { to_table: :bscf_core_delivery_orders }, index: true, null: true
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ class CreateBscfCoreVouchers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :bscf_core_vouchers do |t|
4
+ t.string :full_name, null: false
5
+ t.string :phone_number, null: false
6
+ t.decimal :amount, null: false
7
+ t.string :reason
8
+ t.string :code, null: false
9
+ t.integer :status, null: false, default: 0
10
+ t.datetime :expires_at
11
+ t.references :issued_by, null: false, foreign_key: { to_table: :bscf_core_users }
12
+ t.datetime :redeemed_at
13
+ t.datetime :returned_at
14
+
15
+ t.timestamps
16
+ end
17
+ add_index :bscf_core_vouchers, :code
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ class AddLockedAmountToVirtualAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :bscf_core_virtual_accounts, :locked_amount, :decimal
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class AddProductToMarketPlaceListings < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :bscf_core_marketplace_listings, :product_id, :bigint
4
+ add_column :bscf_core_marketplace_listings, :price, :float
5
+ change_column_null :bscf_core_marketplace_listings, :product_id, false
6
+ add_foreign_key :bscf_core_marketplace_listings, :bscf_core_products, column: :product_id
7
+ add_index :bscf_core_marketplace_listings, :product_id, name: "p_on_bscf_core_mpl_index"
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module Bscf
2
2
  module Core
3
- VERSION = "0.3.97"
3
+ VERSION = "0.3.99"
4
4
  end
5
5
  end
@@ -1,6 +1,5 @@
1
1
  FactoryBot.define do
2
2
  factory :delivery_order, class: 'Bscf::Core::DeliveryOrder' do
3
- association :order
4
3
  association :pickup_address, factory: :address
5
4
  association :dropoff_address, factory: :address
6
5
  driver { nil }
@@ -30,5 +29,11 @@ FactoryBot.define do
30
29
  delivery_start_time { 2.hours.ago }
31
30
  delivery_end_time { Time.current }
32
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
33
38
  end
34
39
  end
@@ -7,6 +7,8 @@ FactoryBot.define do
7
7
  expires_at { DateTime.now.advance(days: 5) }
8
8
  is_active { true }
9
9
  status { 1 }
10
+ price { Faker::Commerce.price }
11
+ product
10
12
  address
11
13
  end
12
14
  end
@@ -6,5 +6,6 @@ FactoryBot.define do
6
6
  order_type { :order_from_quote }
7
7
  status { :draft }
8
8
  total_amount { Faker::Number.between(from: 100, to: 1000) }
9
+ delivery_order { nil }
9
10
  end
10
11
  end
@@ -10,6 +10,7 @@ FactoryBot.define do
10
10
  interest_rate { 2.5 }
11
11
  interest_type { :simple }
12
12
  active { true }
13
+ locked_amount { 0.0 }
13
14
  status { :pending }
14
15
 
15
16
  trait :active_status do
@@ -34,5 +35,11 @@ FactoryBot.define do
34
35
  product_scheme { "LOAN" }
35
36
  interest_rate { 12.5 }
36
37
  end
38
+
39
+
40
+ trait :with_locked_amount do
41
+ locked_amount { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
42
+ balance { locked_amount + Faker::Number.decimal(l_digits: 3, r_digits: 2) }
43
+ end
37
44
  end
38
45
  end
File without changes
@@ -0,0 +1,49 @@
1
+ FactoryBot.define do
2
+ factory :voucher, class: 'Bscf::Core::Voucher' do
3
+ association :issued_by, factory: :user
4
+
5
+ after(:build) do |voucher|
6
+ create(:virtual_account, user: voucher.issued_by, balance: 1000.0) unless voucher.issued_by.virtual_account
7
+ end
8
+
9
+ full_name { Faker::Name.name }
10
+ phone_number { Faker::PhoneNumber.cell_phone }
11
+ amount { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
12
+ expires_at { 30.days.from_now }
13
+ status { :pending }
14
+
15
+ trait :active do
16
+ status { :active }
17
+ after(:create) do |voucher|
18
+ voucher.issued_by.virtual_account.update!(balance: voucher.amount + 1000)
19
+ voucher.send(:lock_issuer_amount)
20
+ end
21
+ end
22
+
23
+ trait :redeemed do
24
+ status { :redeemed }
25
+ redeemed_at { Time.current }
26
+ end
27
+
28
+ trait :expired do
29
+ status { :expired }
30
+ expires_at { 1.day.ago }
31
+ end
32
+
33
+ trait :returned do
34
+ status { :returned }
35
+ returned_at { Time.current }
36
+ end
37
+
38
+ trait :cancelled do
39
+ status { :cancelled }
40
+ returned_at { Time.current }
41
+ end
42
+
43
+ trait :with_sufficient_balance do
44
+ after(:build) do |voucher|
45
+ voucher.issued_by.virtual_account.update!(balance: voucher.amount + 1000)
46
+ end
47
+ end
48
+ end
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.97
4
+ version: 0.3.99
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -308,6 +308,7 @@ files:
308
308
  - app/models/bscf/core/vehicle.rb
309
309
  - app/models/bscf/core/virtual_account.rb
310
310
  - app/models/bscf/core/virtual_account_transaction.rb
311
+ - app/models/bscf/core/voucher.rb
311
312
  - app/models/bscf/core/wholesaler_product.rb
312
313
  - app/services/bscf/core/token_service.rb
313
314
  - config/database.yml
@@ -340,6 +341,10 @@ files:
340
341
  - db/migrate/20250410181146_modify_user_email_and_password.rb
341
342
  - db/migrate/20250425055419_add_delivery_price_to_delivery_orders.rb
342
343
  - db/migrate/20250521084935_update_not_null_constraints_in_businesses.rb
344
+ - db/migrate/20250524073441_remove_order_id_from_delivery_orders_and_add_delivery_order_id_to_orders.rb
345
+ - db/migrate/20250524081221_create_bscf_core_vouchers.rb
346
+ - db/migrate/20250524151346_add_locked_amount_to_virtual_accounts.rb
347
+ - db/migrate/20250606100158_add_product_to_market_place_listings.rb
343
348
  - lib/bscf/core.rb
344
349
  - lib/bscf/core/engine.rb
345
350
  - lib/bscf/core/version.rb
@@ -368,6 +373,8 @@ files:
368
373
  - spec/factories/bscf/core/vehicles.rb
369
374
  - spec/factories/bscf/core/virtual_account_transactions.rb
370
375
  - spec/factories/bscf/core/virtual_accounts.rb
376
+ - spec/factories/bscf/core/voucher.rb
377
+ - spec/factories/bscf/core/vouchers.rb
371
378
  - spec/factories/bscf/core/wholesaler_products.rb
372
379
  homepage: https://mksaddis.com/
373
380
  licenses: