dscf-banking 0.1.0 → 0.1.2
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/MIT-LICENSE +0 -0
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/app/controllers/dscf/banking/accounts_controller.rb +152 -0
- data/app/controllers/dscf/banking/application_controller.rb +0 -0
- data/app/controllers/dscf/banking/applications_controller.rb +113 -0
- data/app/controllers/dscf/banking/interest_configurations_controller.rb +0 -0
- data/app/controllers/dscf/banking/interest_rate_tiers_controller.rb +0 -0
- data/app/controllers/dscf/banking/interest_rate_types_controller.rb +0 -0
- data/app/controllers/dscf/banking/product_approvals_controller.rb +0 -0
- data/app/controllers/dscf/banking/product_categories_controller.rb +0 -0
- data/app/controllers/dscf/banking/transaction_types_controller.rb +27 -0
- data/app/controllers/dscf/banking/transactions_controller.rb +275 -0
- data/app/controllers/dscf/banking/virtual_account_products_controller.rb +0 -0
- data/app/jobs/dscf/banking/application_job.rb +0 -0
- data/app/mailers/dscf/banking/application_mailer.rb +0 -0
- data/app/models/concerns/dscf/banking/auditable.rb +0 -0
- data/app/models/dscf/banking/account.rb +110 -0
- data/app/models/dscf/banking/application.rb +70 -0
- data/app/models/dscf/banking/application_record.rb +0 -0
- data/app/models/dscf/banking/interest_configuration.rb +0 -0
- data/app/models/dscf/banking/interest_rate_tier.rb +0 -0
- data/app/models/dscf/banking/interest_rate_type.rb +0 -0
- data/app/models/dscf/banking/product_approval.rb +0 -0
- data/app/models/dscf/banking/product_audit_log.rb +0 -0
- data/app/models/dscf/banking/product_category.rb +0 -0
- data/app/models/dscf/banking/transaction.rb +68 -0
- data/app/models/dscf/banking/transaction_type.rb +20 -0
- data/app/models/dscf/banking/virtual_account_product.rb +0 -0
- data/app/serializers/dscf/banking/account_serializer.rb +11 -0
- data/app/serializers/dscf/banking/application_serializer.rb +16 -0
- data/app/serializers/dscf/banking/interest_configuration_serializer.rb +0 -0
- data/app/serializers/dscf/banking/interest_rate_tier_serializer.rb +0 -0
- data/app/serializers/dscf/banking/interest_rate_type_serializer.rb +0 -0
- data/app/serializers/dscf/banking/product_approval_serializer.rb +0 -0
- data/app/serializers/dscf/banking/product_category_serializer.rb +0 -0
- data/app/serializers/dscf/banking/transaction_serializer.rb +19 -0
- data/app/serializers/dscf/banking/transaction_type_serializer.rb +9 -0
- data/app/serializers/dscf/banking/virtual_account_product_serializer.rb +0 -0
- data/app/services/dscf/banking/account_creation_service.rb +53 -0
- data/app/services/dscf/banking/base_transaction_service.rb +99 -0
- data/app/services/dscf/banking/deposit_service.rb +97 -0
- data/app/services/dscf/banking/transfer_service.rb +78 -0
- data/app/services/dscf/banking/withdrawal_service.rb +100 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20250830211002_create_dscf_banking_product_categories.rb +0 -0
- data/db/migrate/20250830211027_create_dscf_banking_interest_rate_types.rb +0 -0
- data/db/migrate/20250831063605_add_code_and_remove_fields_from_interest_rate_types.rb +0 -0
- data/db/migrate/20250831064917_create_dscf_banking_virtual_account_products.rb +0 -0
- data/db/migrate/20250831072627_create_dscf_banking_interest_configurations.rb +0 -0
- data/db/migrate/20250831080745_create_dscf_banking_interest_rate_tiers.rb +0 -0
- data/db/migrate/20250831082356_create_dscf_banking_product_approvals.rb +0 -0
- data/db/migrate/20250831083907_create_dscf_banking_product_audit_logs.rb +0 -0
- data/db/migrate/20250831084706_allow_null_virtual_account_product_id_in_product_audit_logs.rb +0 -0
- data/db/migrate/20250912193134_create_dscf_banking_applications.rb +20 -0
- data/db/migrate/20250912203527_create_dscf_banking_accounts.rb +21 -0
- data/db/migrate/20250919084147_add_account_type_to_accounts.rb +6 -0
- data/db/migrate/20250919084927_make_account_associations_optional.rb +6 -0
- data/db/migrate/20250919182831_create_dscf_banking_transaction_types.rb +14 -0
- data/db/migrate/20250919184220_create_dscf_banking_transactions.rb +21 -0
- data/db/seeds.rb +125 -0
- data/lib/dscf/banking/engine.rb +0 -0
- data/lib/dscf/banking/version.rb +1 -1
- data/lib/dscf/banking.rb +0 -0
- data/lib/tasks/dscf/banking_tasks.rake +0 -0
- data/spec/factories/dscf/banking/accounts.rb +79 -0
- data/spec/factories/dscf/banking/applications.rb +65 -0
- data/spec/factories/dscf/banking/interest_configurations.rb +0 -0
- data/spec/factories/dscf/banking/interest_rate_tiers.rb +0 -0
- data/spec/factories/dscf/banking/interest_rate_types.rb +0 -0
- data/spec/factories/dscf/banking/product_approvals.rb +0 -0
- data/spec/factories/dscf/banking/product_audit_logs.rb +0 -0
- data/spec/factories/dscf/banking/product_categories.rb +0 -0
- data/spec/factories/dscf/banking/transaction_types.rb +56 -0
- data/spec/factories/dscf/banking/transactions.rb +52 -0
- data/spec/factories/dscf/banking/virtual_account_products.rb +0 -0
- metadata +30 -2
@@ -0,0 +1,110 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class Account < ApplicationRecord
|
3
|
+
belongs_to :virtual_account_product, optional: true
|
4
|
+
belongs_to :application, optional: true
|
5
|
+
|
6
|
+
enum :status, {
|
7
|
+
draft: 0,
|
8
|
+
pending_activation: 1,
|
9
|
+
active: 2,
|
10
|
+
suspended: 3,
|
11
|
+
closed: 4,
|
12
|
+
dormant: 5
|
13
|
+
}
|
14
|
+
|
15
|
+
validates :account_number, presence: true, uniqueness: { case_sensitive: false }
|
16
|
+
validates :name, presence: true
|
17
|
+
validates :currency, presence: true
|
18
|
+
validates :current_balance, :available_balance, :minimum_balance,
|
19
|
+
numericality: { greater_than_or_equal_to: 0 }
|
20
|
+
validates :system_account, inclusion: { in: [ true, false ] }
|
21
|
+
validate :system_account_associations
|
22
|
+
|
23
|
+
before_validation :generate_account_number, on: :create
|
24
|
+
before_validation :set_defaults, on: :create
|
25
|
+
|
26
|
+
scope :active_accounts, -> { where(active: true, status: :active) }
|
27
|
+
scope :by_currency, ->(currency) { where(currency: currency) }
|
28
|
+
scope :customer_accounts, -> { where(system_account: false) }
|
29
|
+
scope :system_accounts, -> { where(system_account: true) }
|
30
|
+
|
31
|
+
def sufficient_funds_for_withdrawal?(amount)
|
32
|
+
available_balance - amount >= minimum_balance
|
33
|
+
end
|
34
|
+
|
35
|
+
def suspend!(reason = nil)
|
36
|
+
update!(status: :suspended)
|
37
|
+
end
|
38
|
+
|
39
|
+
def activate!
|
40
|
+
update!(status: :active, activation_date: Date.current)
|
41
|
+
end
|
42
|
+
|
43
|
+
def close!(reason = nil)
|
44
|
+
update!(status: :closed, active: false, closure_date: Date.current)
|
45
|
+
end
|
46
|
+
|
47
|
+
def formatted_account_number
|
48
|
+
return nil unless account_number
|
49
|
+
account_number.gsub(/(\d{3})(\d{1})(\d{1})(\d{6})/, '\1 \2 \3 \4')
|
50
|
+
end
|
51
|
+
|
52
|
+
def can_be_activated?
|
53
|
+
draft? || pending_activation? || suspended?
|
54
|
+
end
|
55
|
+
|
56
|
+
def can_be_suspended?
|
57
|
+
active?
|
58
|
+
end
|
59
|
+
|
60
|
+
def can_be_closed?
|
61
|
+
active? || suspended? || dormant?
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def generate_account_number
|
67
|
+
return if account_number.present?
|
68
|
+
|
69
|
+
branch_code = "001"
|
70
|
+
product_scheme = "1"
|
71
|
+
voucher_type = "0"
|
72
|
+
|
73
|
+
loop do
|
74
|
+
sequence = SecureRandom.random_number(1000000).to_s.rjust(6, "0")
|
75
|
+
account_num = "#{branch_code}#{product_scheme}#{voucher_type}#{sequence}"
|
76
|
+
|
77
|
+
unless self.class.exists?(account_number: account_num)
|
78
|
+
self.account_number = account_num
|
79
|
+
break
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def set_defaults
|
85
|
+
return unless virtual_account_product
|
86
|
+
|
87
|
+
self.currency ||= "ETB"
|
88
|
+
self.minimum_balance ||= 0
|
89
|
+
self.name ||= "Account Holder"
|
90
|
+
end
|
91
|
+
|
92
|
+
def system_account_associations
|
93
|
+
if system_account?
|
94
|
+
if virtual_account_product.present?
|
95
|
+
errors.add(:virtual_account_product, "must be blank for system accounts")
|
96
|
+
end
|
97
|
+
if application.present?
|
98
|
+
errors.add(:application, "must be blank for system accounts")
|
99
|
+
end
|
100
|
+
else
|
101
|
+
unless virtual_account_product.present?
|
102
|
+
errors.add(:virtual_account_product, "must be present for customer accounts")
|
103
|
+
end
|
104
|
+
unless application.present?
|
105
|
+
errors.add(:application, "must be present for customer accounts")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class Application < ApplicationRecord
|
3
|
+
include Dscf::Banking::Auditable
|
4
|
+
|
5
|
+
belongs_to :user, class_name: "Dscf::Core::User"
|
6
|
+
belongs_to :assigned_kyc_officer, class_name: "Dscf::Core::User", optional: true
|
7
|
+
belongs_to :assigned_branch_manager, class_name: "Dscf::Core::User", optional: true
|
8
|
+
belongs_to :virtual_account_product, class_name: "Dscf::Banking::VirtualAccountProduct"
|
9
|
+
has_one :account, class_name: "Dscf::Banking::Account"
|
10
|
+
|
11
|
+
validates :application_number, presence: true, uniqueness: true, length: { maximum: 50 }
|
12
|
+
validates :applicant_type, presence: true
|
13
|
+
validates :status, presence: true
|
14
|
+
validates :form_data, presence: true
|
15
|
+
validates :rejection_reason, presence: true, if: -> { status == "rejected" }
|
16
|
+
|
17
|
+
enum :applicant_type, { individual: 0, business: 1 }, prefix: :applicant_type
|
18
|
+
enum :status, { draft: 0, submitted: 1, under_review: 2, approved: 3, rejected: 4 }, prefix: :status
|
19
|
+
|
20
|
+
before_validation :generate_application_number, if: -> { new_record? && application_number.blank? }
|
21
|
+
before_update :set_timestamps_on_status_change
|
22
|
+
after_update :create_account_if_approved
|
23
|
+
|
24
|
+
def approve!(approved_by = nil)
|
25
|
+
update!(status: :approved, completed_at: Time.current)
|
26
|
+
end
|
27
|
+
|
28
|
+
def reject!(reason, rejected_by = nil)
|
29
|
+
update!(status: :rejected, rejection_reason: reason, completed_at: Time.current)
|
30
|
+
end
|
31
|
+
|
32
|
+
def can_be_approved_via_api?
|
33
|
+
status_submitted? || status_under_review?
|
34
|
+
end
|
35
|
+
|
36
|
+
def can_be_rejected_via_api?
|
37
|
+
status_submitted? || status_under_review?
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_account?
|
41
|
+
Account.exists?(application: self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_account_if_approved
|
45
|
+
if saved_change_to_status? && status_approved?
|
46
|
+
AccountCreationService.call(self)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def generate_application_number
|
53
|
+
prefix = applicant_type == "individual" ? "IND" : "BUS"
|
54
|
+
timestamp = Time.current.strftime("%Y%m%d")
|
55
|
+
random_suffix = SecureRandom.random_number(10000).to_s.rjust(4, "0")
|
56
|
+
self.application_number = "#{prefix}#{timestamp}#{random_suffix}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_timestamps_on_status_change
|
60
|
+
if status_changed?
|
61
|
+
case status
|
62
|
+
when "submitted"
|
63
|
+
self.submitted_at = Time.current
|
64
|
+
when "completed"
|
65
|
+
self.completed_at = Time.current
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class Transaction < ApplicationRecord
|
3
|
+
belongs_to :account, class_name: "Dscf::Banking::Account"
|
4
|
+
belongs_to :transaction_type, class_name: "Dscf::Banking::TransactionType"
|
5
|
+
belongs_to :debit_account, class_name: "Dscf::Banking::Account"
|
6
|
+
belongs_to :credit_account, class_name: "Dscf::Banking::Account"
|
7
|
+
|
8
|
+
enum :status, {
|
9
|
+
pending: 0,
|
10
|
+
processing: 1,
|
11
|
+
completed: 2,
|
12
|
+
failed: 3,
|
13
|
+
cancelled: 4
|
14
|
+
}
|
15
|
+
|
16
|
+
validates :amount, presence: true, numericality: { greater_than: 0 }
|
17
|
+
validates :currency, presence: true
|
18
|
+
validates :reference_number, presence: true, uniqueness: true
|
19
|
+
|
20
|
+
validate :different_debit_credit_accounts
|
21
|
+
validate :account_currency_consistency
|
22
|
+
|
23
|
+
before_validation :set_defaults, on: :create
|
24
|
+
before_validation :generate_reference_number, if: -> { reference_number.nil? }
|
25
|
+
|
26
|
+
scope :by_account, ->(account_id) { where(account_id: account_id) }
|
27
|
+
scope :by_type, ->(type_code) { joins(:transaction_type).where(dscf_banking_transaction_types: { code: type_code.upcase }) }
|
28
|
+
scope :by_status, ->(status) { where(status: status) }
|
29
|
+
scope :recent, -> { order(created_at: :desc) }
|
30
|
+
|
31
|
+
def settlement_transaction?
|
32
|
+
debit_account.system_account? || credit_account.system_account?
|
33
|
+
end
|
34
|
+
|
35
|
+
def customer_transaction?
|
36
|
+
!settlement_transaction?
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def different_debit_credit_accounts
|
42
|
+
return unless debit_account_id && credit_account_id
|
43
|
+
|
44
|
+
if debit_account_id == credit_account_id
|
45
|
+
errors.add(:credit_account, "must be different from debit account")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def account_currency_consistency
|
50
|
+
return unless account && currency
|
51
|
+
|
52
|
+
if account.currency != currency
|
53
|
+
errors.add(:currency, "must match account currency")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_defaults
|
58
|
+
self.currency ||= account&.currency || "ETB"
|
59
|
+
self.status ||= :pending
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_reference_number
|
63
|
+
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
|
64
|
+
random_suffix = SecureRandom.hex(4).upcase
|
65
|
+
self.reference_number = "TXN#{timestamp}#{random_suffix}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class TransactionType < ApplicationRecord
|
3
|
+
has_many :transactions, class_name: "Dscf::Banking::Transaction", dependent: :restrict_with_error
|
4
|
+
|
5
|
+
validates :code, presence: true, uniqueness: { case_sensitive: false }
|
6
|
+
validates :name, presence: true
|
7
|
+
|
8
|
+
before_save :upcase_code
|
9
|
+
|
10
|
+
def self.find_by_code(code)
|
11
|
+
find_by(code: code.to_s.upcase)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def upcase_code
|
17
|
+
self.code = code.upcase if code.present?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class AccountSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :account_number, :name, :status, :activation_date, :closure_date,
|
4
|
+
:account_properties, :current_balance, :available_balance, :minimum_balance,
|
5
|
+
:currency, :active, :created_at, :updated_at
|
6
|
+
|
7
|
+
belongs_to :virtual_account_product, serializer: VirtualAccountProductSerializer
|
8
|
+
|
9
|
+
attribute :application_id
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class ApplicationSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :application_number, :applicant_type, :status, :form_data,
|
4
|
+
:submitted_at, :completed_at, :rejection_reason, :created_at, :updated_at
|
5
|
+
|
6
|
+
belongs_to :user
|
7
|
+
belongs_to :virtual_account_product, serializer: VirtualAccountProductSerializer
|
8
|
+
belongs_to :assigned_kyc_officer, optional: true
|
9
|
+
belongs_to :assigned_branch_manager, optional: true
|
10
|
+
has_one :account, serializer: AccountSerializer, if: :has_account?
|
11
|
+
|
12
|
+
def has_account?
|
13
|
+
object.has_account?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class TransactionSerializer < ActiveModel::Serializer
|
3
|
+
attributes :id, :reference_number, :amount, :currency, :description,
|
4
|
+
:status, :created_at, :updated_at
|
5
|
+
|
6
|
+
belongs_to :account
|
7
|
+
belongs_to :transaction_type
|
8
|
+
belongs_to :debit_account, serializer: Dscf::Banking::AccountSerializer
|
9
|
+
belongs_to :credit_account, serializer: Dscf::Banking::AccountSerializer
|
10
|
+
|
11
|
+
def status
|
12
|
+
object.status.humanize
|
13
|
+
end
|
14
|
+
|
15
|
+
def amount
|
16
|
+
object.amount.to_f
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
File without changes
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class AccountCreationService
|
3
|
+
def self.call(application)
|
4
|
+
new(application).call
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(application)
|
8
|
+
@application = application
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
return nil unless @application.status_approved?
|
13
|
+
return nil if account_already_exists?
|
14
|
+
|
15
|
+
create_account
|
16
|
+
rescue => e
|
17
|
+
Rails.logger.error "Account creation failed: #{e.message}" if defined?(Rails)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :application
|
24
|
+
|
25
|
+
def account_already_exists?
|
26
|
+
Account.exists?(application: @application)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_account
|
30
|
+
account = Account.new(
|
31
|
+
application: @application,
|
32
|
+
virtual_account_product: @application.virtual_account_product,
|
33
|
+
name: generate_account_name,
|
34
|
+
currency: "ETB",
|
35
|
+
minimum_balance: 0,
|
36
|
+
current_balance: 0,
|
37
|
+
available_balance: 0,
|
38
|
+
status: :draft
|
39
|
+
)
|
40
|
+
|
41
|
+
if account.save
|
42
|
+
account
|
43
|
+
else
|
44
|
+
Rails.logger.error "Account validation failed: #{account.errors.full_messages}" if defined?(Rails)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_account_name
|
50
|
+
"Test Account - #{@application.application_number}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class BaseTransactionService
|
3
|
+
attr_reader :errors
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@errors = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def success?
|
10
|
+
errors.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
def failure?
|
14
|
+
!success?
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def add_error(message)
|
20
|
+
@errors << message
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_account_active(account, account_type = "Account")
|
24
|
+
unless account.active? && account.status == "active"
|
25
|
+
add_error("#{account_type} is not active")
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_sufficient_funds(account, amount)
|
32
|
+
unless account.sufficient_funds_for_withdrawal?(amount)
|
33
|
+
add_error("Insufficient funds. Available balance: #{account.available_balance}, Required: #{amount}")
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_or_create_transaction_type(code)
|
40
|
+
transaction_type = Dscf::Banking::TransactionType.find_by_code(code)
|
41
|
+
unless transaction_type
|
42
|
+
add_error("Transaction type '#{code}' not found")
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
transaction_type
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_transaction(params)
|
49
|
+
transaction = Dscf::Banking::Transaction.new(params)
|
50
|
+
unless transaction.save
|
51
|
+
transaction.errors.full_messages.each { |msg| add_error(msg) }
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
transaction
|
55
|
+
end
|
56
|
+
|
57
|
+
def update_account_balances(debit_account, credit_account, amount)
|
58
|
+
ActiveRecord::Base.transaction do
|
59
|
+
# Update debit account (decrease balance)
|
60
|
+
new_debit_balance = debit_account.current_balance - amount
|
61
|
+
new_debit_available = debit_account.available_balance - amount
|
62
|
+
|
63
|
+
unless debit_account.update(
|
64
|
+
current_balance: new_debit_balance,
|
65
|
+
available_balance: new_debit_available
|
66
|
+
)
|
67
|
+
debit_account.errors.full_messages.each { |msg| add_error("Debit account: #{msg}") }
|
68
|
+
raise ActiveRecord::Rollback
|
69
|
+
end
|
70
|
+
|
71
|
+
# Update credit account (increase balance)
|
72
|
+
new_credit_balance = credit_account.current_balance + amount
|
73
|
+
new_credit_available = credit_account.available_balance + amount
|
74
|
+
|
75
|
+
unless credit_account.update(
|
76
|
+
current_balance: new_credit_balance,
|
77
|
+
available_balance: new_credit_available
|
78
|
+
)
|
79
|
+
credit_account.errors.full_messages.each { |msg| add_error("Credit account: #{msg}") }
|
80
|
+
raise ActiveRecord::Rollback
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
success?
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_transaction(transaction)
|
88
|
+
transaction.update(status: :processing)
|
89
|
+
|
90
|
+
if update_account_balances(transaction.debit_account, transaction.credit_account, transaction.amount)
|
91
|
+
transaction.update(status: :completed)
|
92
|
+
true
|
93
|
+
else
|
94
|
+
transaction.update(status: :failed)
|
95
|
+
false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class DepositService < BaseTransactionService
|
3
|
+
attr_reader :transaction
|
4
|
+
|
5
|
+
def initialize(account:, amount:, description:, transaction_type_code: "DEPOSIT")
|
6
|
+
super()
|
7
|
+
@account = account
|
8
|
+
@amount = amount.to_f
|
9
|
+
@description = description
|
10
|
+
@transaction_type_code = transaction_type_code
|
11
|
+
@transaction = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
return self unless validate_deposit
|
16
|
+
|
17
|
+
ActiveRecord::Base.transaction do
|
18
|
+
create_deposit_transaction
|
19
|
+
return self unless success?
|
20
|
+
|
21
|
+
process_deposit
|
22
|
+
return self unless success?
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_deposit
|
31
|
+
# Validate account
|
32
|
+
return false unless validate_account_active(@account, "Target account")
|
33
|
+
|
34
|
+
# Validate amount
|
35
|
+
if @amount <= 0
|
36
|
+
add_error("Deposit amount must be greater than zero")
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_deposit_transaction
|
44
|
+
transaction_type = find_or_create_transaction_type(@transaction_type_code)
|
45
|
+
return unless transaction_type
|
46
|
+
|
47
|
+
# For deposits, we need a system account as the debit account
|
48
|
+
# This represents the external source (like mobile banking - Abole)
|
49
|
+
system_account = find_or_create_system_account
|
50
|
+
|
51
|
+
@transaction = create_transaction(
|
52
|
+
account: @account, # Primary account for the transaction
|
53
|
+
transaction_type: transaction_type,
|
54
|
+
debit_account: system_account, # External source
|
55
|
+
credit_account: @account, # Customer account receiving the deposit
|
56
|
+
amount: @amount,
|
57
|
+
currency: @account.currency,
|
58
|
+
description: @description,
|
59
|
+
status: :pending
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_deposit
|
64
|
+
@transaction.update(status: :processing)
|
65
|
+
|
66
|
+
# Update account balance (increase for deposit)
|
67
|
+
new_balance = @account.current_balance + @amount
|
68
|
+
new_available = @account.available_balance + @amount
|
69
|
+
|
70
|
+
if @account.update(current_balance: new_balance, available_balance: new_available)
|
71
|
+
@transaction.update(status: :completed)
|
72
|
+
true
|
73
|
+
else
|
74
|
+
@account.errors.full_messages.each { |msg| add_error(msg) }
|
75
|
+
@transaction.update(status: :failed)
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_or_create_system_account
|
81
|
+
# Find or create a system account for external deposits
|
82
|
+
system_account = Dscf::Banking::Account.system_accounts
|
83
|
+
.where(currency: @account.currency)
|
84
|
+
.where("name LIKE ?", "%Deposit%")
|
85
|
+
.first
|
86
|
+
|
87
|
+
unless system_account
|
88
|
+
# In a real system, this would be pre-created during setup
|
89
|
+
# For now, we'll assume it exists or create a placeholder
|
90
|
+
add_error("System deposit account not found for currency #{@account.currency}")
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
system_account
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|