dscf-banking 0.1.15 → 0.1.16
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/controllers/dscf/banking/transaction_types_controller.rb +27 -0
- data/app/controllers/dscf/banking/transactions_controller.rb +275 -0
- data/app/models/dscf/banking/account.rb +24 -2
- data/app/models/dscf/banking/transaction.rb +68 -0
- data/app/models/dscf/banking/transaction_type.rb +20 -0
- data/app/serializers/dscf/banking/transaction_serializer.rb +19 -0
- data/app/serializers/dscf/banking/transaction_type_serializer.rb +9 -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 +14 -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 +115 -41
- data/lib/dscf/banking/version.rb +1 -1
- data/spec/factories/dscf/banking/accounts.rb +28 -0
- data/spec/factories/dscf/banking/applications.rb +6 -5
- data/spec/factories/dscf/banking/transaction_types.rb +56 -0
- data/spec/factories/dscf/banking/transactions.rb +52 -0
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3be6cb234512da6ec3f02e719d9f3a7315c29711a1510541c4f6431bb7c2934a
|
4
|
+
data.tar.gz: 4e1d771356543e224441f1c68b4e8d343c755be3dbc72299e79cec4871e4fabd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b45b884c53a7fa29feb90a0d33aa584b48b961560504eaa97a2802446e32d8508169e180557947195b34fb5d02ee25fe728ae1d881180d45f13238b230ec305
|
7
|
+
data.tar.gz: 2f0828d64d4dee6222e155e97dfeae0f1bcfb66f9162ae7566baf34b82cd5c603821def2de710633662206993cf6055817c766a5a9706a595a069e69e16465a8
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class TransactionTypesController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def model_params
|
8
|
+
params.require(:payload).permit(
|
9
|
+
:code,
|
10
|
+
:name,
|
11
|
+
:description
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def eager_loaded_associations
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
|
19
|
+
def allowed_order_columns
|
20
|
+
%w[id code name created_at updated_at]
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_serializer_includes
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class TransactionsController < ApplicationController
|
3
|
+
include Dscf::Core::Common
|
4
|
+
|
5
|
+
# Transfer money between accounts (from test cases)
|
6
|
+
def transfer
|
7
|
+
debit_account = Dscf::Banking::Account.find_by(account_number: transfer_params[:debit_account_number])
|
8
|
+
credit_account = Dscf::Banking::Account.find_by(account_number: transfer_params[:credit_account_number])
|
9
|
+
|
10
|
+
unless debit_account
|
11
|
+
return render json: {
|
12
|
+
success: false,
|
13
|
+
error: "Debit account not found",
|
14
|
+
errors: [ "Account with number #{transfer_params[:debit_account_number]} not found" ]
|
15
|
+
}, status: :not_found
|
16
|
+
end
|
17
|
+
|
18
|
+
unless credit_account
|
19
|
+
return render json: {
|
20
|
+
success: false,
|
21
|
+
error: "Credit account not found",
|
22
|
+
errors: [ "Account with number #{transfer_params[:credit_account_number]} not found" ]
|
23
|
+
}, status: :not_found
|
24
|
+
end
|
25
|
+
|
26
|
+
result = Dscf::Banking::TransferService.new(
|
27
|
+
debit_account: debit_account,
|
28
|
+
credit_account: credit_account,
|
29
|
+
amount: transfer_params[:amount],
|
30
|
+
description: transfer_params[:description] || "Transfer between accounts",
|
31
|
+
transaction_type_code: transfer_params[:transaction_type_code] || "TRANSFER"
|
32
|
+
).execute
|
33
|
+
|
34
|
+
if result.success?
|
35
|
+
render json: {
|
36
|
+
success: true,
|
37
|
+
data: transaction_data(result.transaction),
|
38
|
+
message: "Transfer completed successfully"
|
39
|
+
}
|
40
|
+
else
|
41
|
+
render json: {
|
42
|
+
success: false,
|
43
|
+
error: "Transfer failed",
|
44
|
+
errors: result.errors
|
45
|
+
}, status: :unprocessable_entity
|
46
|
+
end
|
47
|
+
rescue StandardError => e
|
48
|
+
render json: {
|
49
|
+
success: false,
|
50
|
+
error: "Transfer failed",
|
51
|
+
errors: [ e.message ]
|
52
|
+
}, status: :internal_server_error
|
53
|
+
end
|
54
|
+
|
55
|
+
# Deposit money to account (from mobile banking - Abole)
|
56
|
+
def deposit
|
57
|
+
account = Dscf::Banking::Account.find_by(account_number: deposit_params[:account_number])
|
58
|
+
|
59
|
+
unless account
|
60
|
+
return render json: {
|
61
|
+
success: false,
|
62
|
+
error: "Account not found",
|
63
|
+
errors: [ "Account with number #{deposit_params[:account_number]} not found" ]
|
64
|
+
}, status: :not_found
|
65
|
+
end
|
66
|
+
|
67
|
+
result = Dscf::Banking::DepositService.new(
|
68
|
+
account: account,
|
69
|
+
amount: deposit_params[:amount],
|
70
|
+
description: deposit_params[:description] || "Deposit to account",
|
71
|
+
transaction_type_code: deposit_params[:transaction_type_code] || "DEPOSIT"
|
72
|
+
).execute
|
73
|
+
|
74
|
+
if result.success?
|
75
|
+
render json: {
|
76
|
+
success: true,
|
77
|
+
data: transaction_data(result.transaction),
|
78
|
+
message: "Deposit completed successfully"
|
79
|
+
}
|
80
|
+
else
|
81
|
+
render json: {
|
82
|
+
success: false,
|
83
|
+
error: "Deposit failed",
|
84
|
+
errors: result.errors
|
85
|
+
}, status: :unprocessable_entity
|
86
|
+
end
|
87
|
+
rescue StandardError => e
|
88
|
+
render json: {
|
89
|
+
success: false,
|
90
|
+
error: "Deposit failed",
|
91
|
+
errors: [ e.message ]
|
92
|
+
}, status: :internal_server_error
|
93
|
+
end
|
94
|
+
|
95
|
+
# Withdraw money from account
|
96
|
+
def withdrawal
|
97
|
+
account = Dscf::Banking::Account.find_by(account_number: withdrawal_params[:account_number])
|
98
|
+
|
99
|
+
unless account
|
100
|
+
return render json: {
|
101
|
+
success: false,
|
102
|
+
error: "Account not found",
|
103
|
+
errors: [ "Account with number #{withdrawal_params[:account_number]} not found" ]
|
104
|
+
}, status: :not_found
|
105
|
+
end
|
106
|
+
|
107
|
+
result = Dscf::Banking::WithdrawalService.new(
|
108
|
+
account: account,
|
109
|
+
amount: withdrawal_params[:amount],
|
110
|
+
description: withdrawal_params[:description] || "Withdrawal from account",
|
111
|
+
transaction_type_code: withdrawal_params[:transaction_type_code] || "WITHDRAWAL"
|
112
|
+
).execute
|
113
|
+
|
114
|
+
if result.success?
|
115
|
+
render json: {
|
116
|
+
success: true,
|
117
|
+
data: transaction_data(result.transaction),
|
118
|
+
message: "Withdrawal completed successfully"
|
119
|
+
}
|
120
|
+
else
|
121
|
+
render json: {
|
122
|
+
success: false,
|
123
|
+
error: "Withdrawal failed",
|
124
|
+
errors: result.errors
|
125
|
+
}, status: :unprocessable_entity
|
126
|
+
end
|
127
|
+
rescue StandardError => e
|
128
|
+
render json: {
|
129
|
+
success: false,
|
130
|
+
error: "Withdrawal failed",
|
131
|
+
errors: [ e.message ]
|
132
|
+
}, status: :internal_server_error
|
133
|
+
end
|
134
|
+
|
135
|
+
# Cancel a transaction
|
136
|
+
def cancel
|
137
|
+
transaction = Dscf::Banking::Transaction.find(params[:id])
|
138
|
+
|
139
|
+
unless transaction.status == "pending"
|
140
|
+
return render json: {
|
141
|
+
success: false,
|
142
|
+
error: "Cannot cancel transaction",
|
143
|
+
errors: [ "Only pending transactions can be cancelled" ]
|
144
|
+
}, status: :unprocessable_entity
|
145
|
+
end
|
146
|
+
|
147
|
+
if transaction.update(status: :cancelled)
|
148
|
+
render json: {
|
149
|
+
success: true,
|
150
|
+
data: transaction_data(transaction),
|
151
|
+
message: "Transaction cancelled successfully"
|
152
|
+
}
|
153
|
+
else
|
154
|
+
render json: {
|
155
|
+
success: false,
|
156
|
+
error: "Failed to cancel transaction",
|
157
|
+
errors: transaction.errors.full_messages
|
158
|
+
}, status: :unprocessable_entity
|
159
|
+
end
|
160
|
+
rescue ActiveRecord::RecordNotFound
|
161
|
+
render json: {
|
162
|
+
success: false,
|
163
|
+
error: "Transaction not found"
|
164
|
+
}, status: :not_found
|
165
|
+
end
|
166
|
+
|
167
|
+
# Get transaction details with related account information
|
168
|
+
def details
|
169
|
+
transaction = Dscf::Banking::Transaction.includes(:account, :debit_account, :credit_account, :transaction_type).find(params[:id])
|
170
|
+
|
171
|
+
render json: {
|
172
|
+
success: true,
|
173
|
+
data: transaction_data_with_details(transaction)
|
174
|
+
}
|
175
|
+
rescue ActiveRecord::RecordNotFound
|
176
|
+
render json: {
|
177
|
+
success: false,
|
178
|
+
error: "Transaction not found"
|
179
|
+
}, status: :not_found
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def transfer_params
|
185
|
+
params.require(:transfer).permit(:debit_account_number, :credit_account_number, :amount, :description, :transaction_type_code)
|
186
|
+
end
|
187
|
+
|
188
|
+
def deposit_params
|
189
|
+
params.require(:deposit).permit(:account_number, :amount, :description, :transaction_type_code)
|
190
|
+
end
|
191
|
+
|
192
|
+
def withdrawal_params
|
193
|
+
params.require(:withdrawal).permit(:account_number, :amount, :description, :transaction_type_code)
|
194
|
+
end
|
195
|
+
|
196
|
+
def transaction_data(transaction)
|
197
|
+
{
|
198
|
+
id: transaction.id,
|
199
|
+
reference_number: transaction.reference_number,
|
200
|
+
amount: transaction.amount.to_f,
|
201
|
+
currency: transaction.currency,
|
202
|
+
description: transaction.description,
|
203
|
+
status: transaction.status,
|
204
|
+
account_id: transaction.account_id,
|
205
|
+
debit_account_id: transaction.debit_account_id,
|
206
|
+
credit_account_id: transaction.credit_account_id,
|
207
|
+
transaction_type_id: transaction.transaction_type_id,
|
208
|
+
created_at: transaction.created_at,
|
209
|
+
updated_at: transaction.updated_at
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
def transaction_data_with_details(transaction)
|
214
|
+
{
|
215
|
+
id: transaction.id,
|
216
|
+
reference_number: transaction.reference_number,
|
217
|
+
amount: transaction.amount.to_f,
|
218
|
+
currency: transaction.currency,
|
219
|
+
description: transaction.description,
|
220
|
+
status: transaction.status,
|
221
|
+
account: account_summary(transaction.account),
|
222
|
+
debit_account: account_summary(transaction.debit_account),
|
223
|
+
credit_account: account_summary(transaction.credit_account),
|
224
|
+
transaction_type: {
|
225
|
+
id: transaction.transaction_type.id,
|
226
|
+
code: transaction.transaction_type.code,
|
227
|
+
name: transaction.transaction_type.name
|
228
|
+
},
|
229
|
+
created_at: transaction.created_at,
|
230
|
+
updated_at: transaction.updated_at
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
234
|
+
def account_summary(account)
|
235
|
+
return nil unless account
|
236
|
+
{
|
237
|
+
id: account.id,
|
238
|
+
account_number: account.account_number,
|
239
|
+
name: account.name,
|
240
|
+
currency: account.currency,
|
241
|
+
current_balance: account.current_balance
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
def model_params
|
246
|
+
params.require(:payload).permit(
|
247
|
+
:account_id,
|
248
|
+
:transaction_type_id,
|
249
|
+
:debit_account_id,
|
250
|
+
:credit_account_id,
|
251
|
+
:amount,
|
252
|
+
:currency,
|
253
|
+
:description,
|
254
|
+
:status
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
def eager_loaded_associations
|
259
|
+
[ :account, :transaction_type, :debit_account, :credit_account ]
|
260
|
+
end
|
261
|
+
|
262
|
+
def allowed_order_columns
|
263
|
+
%w[id reference_number amount currency status created_at updated_at]
|
264
|
+
end
|
265
|
+
|
266
|
+
def default_serializer_includes
|
267
|
+
{
|
268
|
+
account: {},
|
269
|
+
transaction_type: {},
|
270
|
+
debit_account: {},
|
271
|
+
credit_account: {}
|
272
|
+
}
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Dscf::Banking
|
2
2
|
class Account < ApplicationRecord
|
3
|
-
belongs_to :virtual_account_product
|
4
|
-
belongs_to :application
|
3
|
+
belongs_to :virtual_account_product, optional: true
|
4
|
+
belongs_to :application, optional: true
|
5
5
|
|
6
6
|
enum :status, {
|
7
7
|
draft: 0,
|
@@ -17,12 +17,16 @@ module Dscf::Banking
|
|
17
17
|
validates :currency, presence: true
|
18
18
|
validates :current_balance, :available_balance, :minimum_balance,
|
19
19
|
numericality: { greater_than_or_equal_to: 0 }
|
20
|
+
validates :system_account, inclusion: { in: [ true, false ] }
|
21
|
+
validate :system_account_associations
|
20
22
|
|
21
23
|
before_validation :generate_account_number, on: :create
|
22
24
|
before_validation :set_defaults, on: :create
|
23
25
|
|
24
26
|
scope :active_accounts, -> { where(active: true, status: :active) }
|
25
27
|
scope :by_currency, ->(currency) { where(currency: currency) }
|
28
|
+
scope :customer_accounts, -> { where(system_account: false) }
|
29
|
+
scope :system_accounts, -> { where(system_account: true) }
|
26
30
|
|
27
31
|
def sufficient_funds_for_withdrawal?(amount)
|
28
32
|
available_balance - amount >= minimum_balance
|
@@ -84,5 +88,23 @@ module Dscf::Banking
|
|
84
88
|
self.minimum_balance ||= 0
|
85
89
|
self.name ||= "Account Holder"
|
86
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
|
87
109
|
end
|
88
110
|
end
|
@@ -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
|
@@ -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,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
|