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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +0 -0
  3. data/README.md +0 -0
  4. data/Rakefile +0 -0
  5. data/app/controllers/dscf/banking/accounts_controller.rb +152 -0
  6. data/app/controllers/dscf/banking/application_controller.rb +0 -0
  7. data/app/controllers/dscf/banking/applications_controller.rb +113 -0
  8. data/app/controllers/dscf/banking/interest_configurations_controller.rb +0 -0
  9. data/app/controllers/dscf/banking/interest_rate_tiers_controller.rb +0 -0
  10. data/app/controllers/dscf/banking/interest_rate_types_controller.rb +0 -0
  11. data/app/controllers/dscf/banking/product_approvals_controller.rb +0 -0
  12. data/app/controllers/dscf/banking/product_categories_controller.rb +0 -0
  13. data/app/controllers/dscf/banking/transaction_types_controller.rb +27 -0
  14. data/app/controllers/dscf/banking/transactions_controller.rb +275 -0
  15. data/app/controllers/dscf/banking/virtual_account_products_controller.rb +0 -0
  16. data/app/jobs/dscf/banking/application_job.rb +0 -0
  17. data/app/mailers/dscf/banking/application_mailer.rb +0 -0
  18. data/app/models/concerns/dscf/banking/auditable.rb +0 -0
  19. data/app/models/dscf/banking/account.rb +110 -0
  20. data/app/models/dscf/banking/application.rb +70 -0
  21. data/app/models/dscf/banking/application_record.rb +0 -0
  22. data/app/models/dscf/banking/interest_configuration.rb +0 -0
  23. data/app/models/dscf/banking/interest_rate_tier.rb +0 -0
  24. data/app/models/dscf/banking/interest_rate_type.rb +0 -0
  25. data/app/models/dscf/banking/product_approval.rb +0 -0
  26. data/app/models/dscf/banking/product_audit_log.rb +0 -0
  27. data/app/models/dscf/banking/product_category.rb +0 -0
  28. data/app/models/dscf/banking/transaction.rb +68 -0
  29. data/app/models/dscf/banking/transaction_type.rb +20 -0
  30. data/app/models/dscf/banking/virtual_account_product.rb +0 -0
  31. data/app/serializers/dscf/banking/account_serializer.rb +11 -0
  32. data/app/serializers/dscf/banking/application_serializer.rb +16 -0
  33. data/app/serializers/dscf/banking/interest_configuration_serializer.rb +0 -0
  34. data/app/serializers/dscf/banking/interest_rate_tier_serializer.rb +0 -0
  35. data/app/serializers/dscf/banking/interest_rate_type_serializer.rb +0 -0
  36. data/app/serializers/dscf/banking/product_approval_serializer.rb +0 -0
  37. data/app/serializers/dscf/banking/product_category_serializer.rb +0 -0
  38. data/app/serializers/dscf/banking/transaction_serializer.rb +19 -0
  39. data/app/serializers/dscf/banking/transaction_type_serializer.rb +9 -0
  40. data/app/serializers/dscf/banking/virtual_account_product_serializer.rb +0 -0
  41. data/app/services/dscf/banking/account_creation_service.rb +53 -0
  42. data/app/services/dscf/banking/base_transaction_service.rb +99 -0
  43. data/app/services/dscf/banking/deposit_service.rb +97 -0
  44. data/app/services/dscf/banking/transfer_service.rb +78 -0
  45. data/app/services/dscf/banking/withdrawal_service.rb +100 -0
  46. data/config/routes.rb +27 -0
  47. data/db/migrate/20250830211002_create_dscf_banking_product_categories.rb +0 -0
  48. data/db/migrate/20250830211027_create_dscf_banking_interest_rate_types.rb +0 -0
  49. data/db/migrate/20250831063605_add_code_and_remove_fields_from_interest_rate_types.rb +0 -0
  50. data/db/migrate/20250831064917_create_dscf_banking_virtual_account_products.rb +0 -0
  51. data/db/migrate/20250831072627_create_dscf_banking_interest_configurations.rb +0 -0
  52. data/db/migrate/20250831080745_create_dscf_banking_interest_rate_tiers.rb +0 -0
  53. data/db/migrate/20250831082356_create_dscf_banking_product_approvals.rb +0 -0
  54. data/db/migrate/20250831083907_create_dscf_banking_product_audit_logs.rb +0 -0
  55. data/db/migrate/20250831084706_allow_null_virtual_account_product_id_in_product_audit_logs.rb +0 -0
  56. data/db/migrate/20250912193134_create_dscf_banking_applications.rb +20 -0
  57. data/db/migrate/20250912203527_create_dscf_banking_accounts.rb +21 -0
  58. data/db/migrate/20250919084147_add_account_type_to_accounts.rb +6 -0
  59. data/db/migrate/20250919084927_make_account_associations_optional.rb +6 -0
  60. data/db/migrate/20250919182831_create_dscf_banking_transaction_types.rb +14 -0
  61. data/db/migrate/20250919184220_create_dscf_banking_transactions.rb +21 -0
  62. data/db/seeds.rb +125 -0
  63. data/lib/dscf/banking/engine.rb +0 -0
  64. data/lib/dscf/banking/version.rb +1 -1
  65. data/lib/dscf/banking.rb +0 -0
  66. data/lib/tasks/dscf/banking_tasks.rake +0 -0
  67. data/spec/factories/dscf/banking/accounts.rb +79 -0
  68. data/spec/factories/dscf/banking/applications.rb +65 -0
  69. data/spec/factories/dscf/banking/interest_configurations.rb +0 -0
  70. data/spec/factories/dscf/banking/interest_rate_tiers.rb +0 -0
  71. data/spec/factories/dscf/banking/interest_rate_types.rb +0 -0
  72. data/spec/factories/dscf/banking/product_approvals.rb +0 -0
  73. data/spec/factories/dscf/banking/product_audit_logs.rb +0 -0
  74. data/spec/factories/dscf/banking/product_categories.rb +0 -0
  75. data/spec/factories/dscf/banking/transaction_types.rb +56 -0
  76. data/spec/factories/dscf/banking/transactions.rb +52 -0
  77. data/spec/factories/dscf/banking/virtual_account_products.rb +0 -0
  78. 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
@@ -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
@@ -0,0 +1,9 @@
1
+ module Dscf::Banking
2
+ class TransactionTypeSerializer < ActiveModel::Serializer
3
+ attributes :id, :code, :name, :description, :created_at, :updated_at
4
+
5
+ def code
6
+ object.code.upcase
7
+ end
8
+ end
9
+ end
@@ -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