bscf-core 0.3.93 → 0.3.98
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/business.rb +1 -1
- data/app/models/bscf/core/delivery_order.rb +1 -1
- data/app/models/bscf/core/order.rb +1 -0
- data/app/models/bscf/core/user.rb +3 -0
- data/app/models/bscf/core/user_profile.rb +0 -2
- data/app/models/bscf/core/virtual_account.rb +48 -4
- data/app/models/bscf/core/voucher.rb +134 -0
- data/db/migrate/20250521084935_update_not_null_constraints_in_businesses.rb +7 -0
- data/db/migrate/20250524073441_remove_order_id_from_delivery_orders_and_add_delivery_order_id_to_orders.rb +6 -0
- data/db/migrate/20250524081221_create_bscf_core_vouchers.rb +19 -0
- data/db/migrate/20250524151346_add_locked_amount_to_virtual_accounts.rb +5 -0
- data/lib/bscf/core/version.rb +1 -1
- data/spec/factories/bscf/core/delivery_orders.rb +6 -1
- data/spec/factories/bscf/core/orders.rb +1 -0
- data/spec/factories/bscf/core/virtual_accounts.rb +8 -1
- data/spec/factories/bscf/core/voucher.rb +0 -0
- data/spec/factories/bscf/core/vouchers.rb +51 -0
- metadata +8 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d6e29a6fe52791c63a6ac04626a7b35f11b4648fcd554c8c4d5e8241b4107fa
|
4
|
+
data.tar.gz: 6f5fe2859090c771bd496335e49969fcff65800e3173918a8714a3b1f1eedd5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebd0b053b1456de1e56d3a369441c1d5fb134531f35c70cbab87430063979a6ea0fa9117ed85d4fcad8bb65cb0bdfe00cd85fe34d6ef20afdc0d89aab288c045
|
7
|
+
data.tar.gz: 6265ca3093db1bae9d6205b5e0eee1d97b583b42cc8d13b3d17abadcf9d46e4ead1f202fdacb09320f33e9cdbb82e62485b1ce5ceca46002f11b656b33945183
|
@@ -4,7 +4,7 @@ module Bscf
|
|
4
4
|
belongs_to :user
|
5
5
|
|
6
6
|
validates :business_name, presence: true
|
7
|
-
validates :tin_number,
|
7
|
+
validates :tin_number, uniqueness: { case_sensitive: false }
|
8
8
|
validates :business_type, presence: true
|
9
9
|
validates :verification_status, presence: true
|
10
10
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Bscf::Core
|
2
2
|
class DeliveryOrder < ApplicationRecord
|
3
|
-
|
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
|
@@ -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
|
|
@@ -5,6 +5,9 @@ module Bscf
|
|
5
5
|
|
6
6
|
has_one :user_profile
|
7
7
|
has_one :user_role
|
8
|
+
has_one :business
|
9
|
+
has_one :vehicle, foreign_key: :driver_id
|
10
|
+
has_one :virtual_account
|
8
11
|
has_many :user_roles
|
9
12
|
has_many :roles, through: :user_roles
|
10
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,17 @@ module Bscf
|
|
44
81
|
self.account_number = "#{branch_code}#{product_scheme}#{voucher_type}#{seq}"
|
45
82
|
end
|
46
83
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
97
|
+
|
@@ -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,7 @@
|
|
1
|
+
class UpdateNotNullConstraintsInBusinesses < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
change_column_null :bscf_core_user_profiles, :source_of_funds, false
|
4
|
+
change_column_null :bscf_core_user_profiles, :occupation, false
|
5
|
+
change_column_null :bscf_core_businesses, :tin_number, false
|
6
|
+
end
|
7
|
+
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
|
data/lib/bscf/core/version.rb
CHANGED
@@ -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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
FactoryBot.define do
|
2
2
|
factory :virtual_account, class: 'Bscf::Core::VirtualAccount' do
|
3
3
|
association :user, factory: :user
|
4
|
-
|
4
|
+
|
5
5
|
sequence(:cbs_account_number) { |n| "CBS#{n.to_s.rjust(8, '0')}" }
|
6
6
|
branch_code { "BR001" }
|
7
7
|
product_scheme { "SAVINGS" }
|
@@ -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,51 @@
|
|
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
|
+
|
49
|
+
|
50
|
+
end
|
51
|
+
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.
|
4
|
+
version: 0.3.98
|
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
|
@@ -339,6 +340,10 @@ files:
|
|
339
340
|
- db/migrate/20250410181145_add_actual_delivery_time_to_delivery_orders.rb
|
340
341
|
- db/migrate/20250410181146_modify_user_email_and_password.rb
|
341
342
|
- db/migrate/20250425055419_add_delivery_price_to_delivery_orders.rb
|
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
|
342
347
|
- lib/bscf/core.rb
|
343
348
|
- lib/bscf/core/engine.rb
|
344
349
|
- lib/bscf/core/version.rb
|
@@ -367,6 +372,8 @@ files:
|
|
367
372
|
- spec/factories/bscf/core/vehicles.rb
|
368
373
|
- spec/factories/bscf/core/virtual_account_transactions.rb
|
369
374
|
- spec/factories/bscf/core/virtual_accounts.rb
|
375
|
+
- spec/factories/bscf/core/voucher.rb
|
376
|
+
- spec/factories/bscf/core/vouchers.rb
|
370
377
|
- spec/factories/bscf/core/wholesaler_products.rb
|
371
378
|
homepage: https://mksaddis.com/
|
372
379
|
licenses:
|