dscf-banking 0.1.14 → 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 +125 -0
- 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 +19 -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
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class TransferService < BaseTransactionService
|
3
|
+
attr_reader :transaction
|
4
|
+
|
5
|
+
def initialize(debit_account:, credit_account:, amount:, description:, transaction_type_code: "TRANSFER")
|
6
|
+
super()
|
7
|
+
@debit_account = debit_account
|
8
|
+
@credit_account = credit_account
|
9
|
+
@amount = amount.to_f
|
10
|
+
@description = description
|
11
|
+
@transaction_type_code = transaction_type_code
|
12
|
+
@transaction = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
return self unless validate_transfer
|
17
|
+
|
18
|
+
ActiveRecord::Base.transaction do
|
19
|
+
create_transfer_transaction
|
20
|
+
return self unless success?
|
21
|
+
|
22
|
+
process_transaction(@transaction)
|
23
|
+
return self unless success?
|
24
|
+
end
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_transfer
|
32
|
+
# Validate debit account
|
33
|
+
return false unless validate_account_active(@debit_account, "Debit account")
|
34
|
+
|
35
|
+
# Validate credit account
|
36
|
+
return false unless validate_account_active(@credit_account, "Credit account")
|
37
|
+
|
38
|
+
# Validate amount
|
39
|
+
if @amount <= 0
|
40
|
+
add_error("Transfer amount must be greater than zero")
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
|
44
|
+
# Validate sufficient funds
|
45
|
+
return false unless validate_sufficient_funds(@debit_account, @amount)
|
46
|
+
|
47
|
+
# Validate different accounts
|
48
|
+
if @debit_account.id == @credit_account.id
|
49
|
+
add_error("Cannot transfer to the same account")
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validate currency match
|
54
|
+
if @debit_account.currency != @credit_account.currency
|
55
|
+
add_error("Currency mismatch between accounts")
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_transfer_transaction
|
63
|
+
transaction_type = find_or_create_transaction_type(@transaction_type_code)
|
64
|
+
return unless transaction_type
|
65
|
+
|
66
|
+
@transaction = create_transaction(
|
67
|
+
account: @debit_account, # Primary account for the transaction
|
68
|
+
transaction_type: transaction_type,
|
69
|
+
debit_account: @debit_account,
|
70
|
+
credit_account: @credit_account,
|
71
|
+
amount: @amount,
|
72
|
+
currency: @debit_account.currency,
|
73
|
+
description: @description,
|
74
|
+
status: :pending
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Dscf::Banking
|
2
|
+
class WithdrawalService < BaseTransactionService
|
3
|
+
attr_reader :transaction
|
4
|
+
|
5
|
+
def initialize(account:, amount:, description:, transaction_type_code: "WITHDRAWAL")
|
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_withdrawal
|
16
|
+
|
17
|
+
ActiveRecord::Base.transaction do
|
18
|
+
create_withdrawal_transaction
|
19
|
+
return self unless success?
|
20
|
+
|
21
|
+
process_withdrawal
|
22
|
+
return self unless success?
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate_withdrawal
|
31
|
+
# Validate account
|
32
|
+
return false unless validate_account_active(@account, "Source account")
|
33
|
+
|
34
|
+
# Validate amount
|
35
|
+
if @amount <= 0
|
36
|
+
add_error("Withdrawal amount must be greater than zero")
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
# Validate sufficient funds
|
41
|
+
return false unless validate_sufficient_funds(@account, @amount)
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_withdrawal_transaction
|
47
|
+
transaction_type = find_or_create_transaction_type(@transaction_type_code)
|
48
|
+
return unless transaction_type
|
49
|
+
|
50
|
+
# For withdrawals, we need a system account as the credit account
|
51
|
+
# This represents the external destination
|
52
|
+
system_account = find_or_create_system_account
|
53
|
+
|
54
|
+
@transaction = create_transaction(
|
55
|
+
account: @account, # Primary account for the transaction
|
56
|
+
transaction_type: transaction_type,
|
57
|
+
debit_account: @account, # Customer account being debited
|
58
|
+
credit_account: system_account, # External destination
|
59
|
+
amount: @amount,
|
60
|
+
currency: @account.currency,
|
61
|
+
description: @description,
|
62
|
+
status: :pending
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def process_withdrawal
|
67
|
+
@transaction.update(status: :processing)
|
68
|
+
|
69
|
+
# Update account balance (decrease for withdrawal)
|
70
|
+
new_balance = @account.current_balance - @amount
|
71
|
+
new_available = @account.available_balance - @amount
|
72
|
+
|
73
|
+
if @account.update(current_balance: new_balance, available_balance: new_available)
|
74
|
+
@transaction.update(status: :completed)
|
75
|
+
true
|
76
|
+
else
|
77
|
+
@account.errors.full_messages.each { |msg| add_error(msg) }
|
78
|
+
@transaction.update(status: :failed)
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_or_create_system_account
|
84
|
+
# Find or create a system account for external withdrawals
|
85
|
+
system_account = Dscf::Banking::Account.system_accounts
|
86
|
+
.where(currency: @account.currency)
|
87
|
+
.where("name LIKE ?", "%Withdrawal%")
|
88
|
+
.first
|
89
|
+
|
90
|
+
unless system_account
|
91
|
+
# In a real system, this would be pre-created during setup
|
92
|
+
# For now, we'll assume it exists or create a placeholder
|
93
|
+
add_error("System withdrawal account not found for currency #{@account.currency}")
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
|
97
|
+
system_account
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/config/routes.rb
CHANGED
@@ -22,4 +22,18 @@ Dscf::Banking::Engine.routes.draw do
|
|
22
22
|
post :reject
|
23
23
|
end
|
24
24
|
end
|
25
|
+
|
26
|
+
resources :transaction_types
|
27
|
+
|
28
|
+
resources :transactions do
|
29
|
+
collection do
|
30
|
+
post :transfer
|
31
|
+
post :deposit
|
32
|
+
post :withdrawal
|
33
|
+
end
|
34
|
+
member do
|
35
|
+
post :cancel
|
36
|
+
get :details
|
37
|
+
end
|
38
|
+
end
|
25
39
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateDscfBankingTransactionTypes < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_banking_transaction_types do |t|
|
4
|
+
t.string :code, null: false, limit: 20
|
5
|
+
t.string :name, null: false, limit: 100
|
6
|
+
t.text :description
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index :dscf_banking_transaction_types, :code, unique: true
|
12
|
+
add_index :dscf_banking_transaction_types, :name, unique: true
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateDscfBankingTransactions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :dscf_banking_transactions do |t|
|
4
|
+
t.references :account, null: false, foreign_key: { to_table: :dscf_banking_accounts }
|
5
|
+
t.references :transaction_type, null: false, foreign_key: { to_table: :dscf_banking_transaction_types }
|
6
|
+
t.decimal :amount, precision: 20, scale: 4, null: false
|
7
|
+
t.string :currency, null: false, default: "ETB"
|
8
|
+
t.string :reference_number, null: false
|
9
|
+
t.text :description
|
10
|
+
t.integer :status, default: 0, null: false
|
11
|
+
t.references :debit_account, null: false, foreign_key: { to_table: :dscf_banking_accounts }
|
12
|
+
t.references :credit_account, null: false, foreign_key: { to_table: :dscf_banking_accounts }
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index :dscf_banking_transactions, :status
|
18
|
+
add_index :dscf_banking_transactions, :reference_number, unique: true
|
19
|
+
add_index :dscf_banking_transactions, [ :account_id, :created_at ]
|
20
|
+
end
|
21
|
+
end
|
data/db/seeds.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# db/seeds.rb
|
2
|
+
|
3
|
+
# DEBUG: Confirm seed file execution and model availability
|
4
|
+
puts "Seed file started"
|
5
|
+
puts "Dscf::Core::User loaded? => #{defined?(Dscf::Core::User)}"
|
6
|
+
puts "Dscf::Core::Role loaded? => #{defined?(Dscf::Core::Role)}"
|
7
|
+
|
8
|
+
# DSCF Banking Engine Seed Data
|
9
|
+
# This file seeds the database with sample data for all models in the correct dependency order
|
10
|
+
|
11
|
+
puts "Starting DSCF Banking Engine seed data..."
|
12
|
+
|
13
|
+
|
14
|
+
# 1. Seed roles (if model exists)
|
15
|
+
puts "Seeding roles..."
|
16
|
+
if defined?(Dscf::Core::Role)
|
17
|
+
user_role = Dscf::Core::Role.find_or_create_by(code: 'user') do |role|
|
18
|
+
role.name = 'User'
|
19
|
+
end
|
20
|
+
officer_role = Dscf::Core::Role.find_or_create_by(code: 'virtual_account_officer') do |role|
|
21
|
+
role.name = 'Virtual Account Officer'
|
22
|
+
end
|
23
|
+
manager_role = Dscf::Core::Role.find_or_create_by(code: 'virtual_account_manager') do |role|
|
24
|
+
role.name = 'Virtual Account Manager'
|
25
|
+
end
|
26
|
+
kyc_officer_role = Dscf::Core::Role.find_or_create_by(code: 'kyc_officer') do |role|
|
27
|
+
role.name = 'KYC Officer'
|
28
|
+
end
|
29
|
+
branch_manager_role = Dscf::Core::Role.find_or_create_by(code: 'branch_manager') do |role|
|
30
|
+
role.name = 'Branch Manager'
|
31
|
+
end
|
32
|
+
puts "✓ Roles created/found"
|
33
|
+
else
|
34
|
+
puts "Warning: Dscf::Core::Role model not found. Skipping role seeding."
|
35
|
+
end
|
36
|
+
# 2. Seed users with roles (if model exists)
|
37
|
+
puts "Seeding users..."
|
38
|
+
if defined?(Dscf::Core::User)
|
39
|
+
user1 = Dscf::Core::User.find_or_create_by(email: "user@banking.com") do |u|
|
40
|
+
u.phone = "+251911123456"
|
41
|
+
u.password = "SecurePassword123!"
|
42
|
+
u.verified_at = Time.current
|
43
|
+
u.temp_password = false
|
44
|
+
end
|
45
|
+
user1.roles << user_role if defined?(user_role)
|
46
|
+
puts "Created user: #{user1.email} (user@banking.com)"
|
47
|
+
|
48
|
+
user2 = Dscf::Core::User.find_or_create_by(email: "virtual_account_officer@banking.com") do |u|
|
49
|
+
u.phone = "+251922123456"
|
50
|
+
u.password = "SecurePassword123!"
|
51
|
+
u.verified_at = Time.current
|
52
|
+
u.temp_password = false
|
53
|
+
end
|
54
|
+
user2.roles << officer_role if defined?(officer_role)
|
55
|
+
puts "Created user: #{user2.email} (virtual_account_officer@banking.com)"
|
56
|
+
|
57
|
+
user3 = Dscf::Core::User.find_or_create_by(email: "virtual_account_manager@banking.com") do |u|
|
58
|
+
u.phone = "+251933123456"
|
59
|
+
u.password = "SecurePassword123!"
|
60
|
+
u.verified_at = Time.current
|
61
|
+
u.temp_password = false
|
62
|
+
end
|
63
|
+
user3.roles << manager_role if defined?(manager_role)
|
64
|
+
puts "Created user: #{user3.email} (virtual_account_manager@banking.com)"
|
65
|
+
|
66
|
+
user4 = Dscf::Core::User.find_or_create_by(email: "user_virtual_account_officer@banking.com") do |u|
|
67
|
+
u.phone = "+251944123456"
|
68
|
+
u.password = "SecurePassword123!"
|
69
|
+
u.verified_at = Time.current
|
70
|
+
u.temp_password = false
|
71
|
+
end
|
72
|
+
user4.roles << user_role if defined?(user_role)
|
73
|
+
user4.roles << officer_role if defined?(officer_role)
|
74
|
+
puts "Created user: #{user4.email} (user_virtual_account_officer@banking.com)"
|
75
|
+
|
76
|
+
user5 = Dscf::Core::User.find_or_create_by(email: "virtual_account_manager2@banking.com") do |u|
|
77
|
+
u.phone = "+251955123456"
|
78
|
+
u.password = "SecurePassword123!"
|
79
|
+
u.verified_at = Time.current
|
80
|
+
u.temp_password = false
|
81
|
+
end
|
82
|
+
user5.roles << manager_role if defined?(manager_role)
|
83
|
+
puts "Created user: #{user5.email} (virtual_account_manager2@banking.com)"
|
84
|
+
|
85
|
+
user6 = Dscf::Core::User.find_or_create_by(email: "kyc_officer@banking.com") do |u|
|
86
|
+
u.phone = "+251966123456"
|
87
|
+
u.password = "SecurePassword123!"
|
88
|
+
u.verified_at = Time.current
|
89
|
+
u.temp_password = false
|
90
|
+
end
|
91
|
+
user6.roles << kyc_officer_role if defined?(kyc_officer_role)
|
92
|
+
puts "Created user: #{user6.email} (kyc_officer@banking.com)"
|
93
|
+
|
94
|
+
user7 = Dscf::Core::User.find_or_create_by(email: "branch_manager@banking.com") do |u|
|
95
|
+
u.phone = "+251977123456"
|
96
|
+
u.password = "SecurePassword123!"
|
97
|
+
u.verified_at = Time.current
|
98
|
+
u.temp_password = false
|
99
|
+
end
|
100
|
+
user7.roles << branch_manager_role if defined?(branch_manager_role)
|
101
|
+
puts "Created user: #{user7.email} (branch_manager@banking.com)"
|
102
|
+
else
|
103
|
+
puts "Warning: Dscf::Core::User model not found. Skipping user seeding."
|
104
|
+
end# 3. Seed transaction types (engine model)
|
105
|
+
puts "Seeding transaction types..."
|
106
|
+
if defined?(Dscf::Banking::TransactionType)
|
107
|
+
transaction_types = [
|
108
|
+
{ name: "Deposit", code: "DEP" },
|
109
|
+
{ name: "Withdrawal", code: "WDL" },
|
110
|
+
{ name: "Transfer", code: "TRF" },
|
111
|
+
{ name: "Interest Credit", code: "INTC" },
|
112
|
+
{ name: "Fee Debit", code: "FEED" }
|
113
|
+
]
|
114
|
+
|
115
|
+
transaction_types.each do |attrs|
|
116
|
+
Dscf::Banking::TransactionType.find_or_create_by(code: attrs[:code]) do |tt|
|
117
|
+
tt.name = attrs[:name]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
puts "✓ Transaction types created/found"
|
121
|
+
else
|
122
|
+
puts "Warning: Dscf::Banking::TransactionType not found. Skipping transaction type seeding."
|
123
|
+
end
|
124
|
+
|
125
|
+
puts "DSCF Banking Engine seed data completed successfully!"
|
data/lib/dscf/banking/version.rb
CHANGED
@@ -10,6 +10,7 @@ FactoryBot.define do
|
|
10
10
|
minimum_balance { 0 }
|
11
11
|
status { :draft }
|
12
12
|
active { true }
|
13
|
+
system_account { false }
|
13
14
|
account_properties { {} }
|
14
15
|
|
15
16
|
trait :active do
|
@@ -32,6 +33,33 @@ FactoryBot.define do
|
|
32
33
|
available_balance { 1000 }
|
33
34
|
minimum_balance { 100 }
|
34
35
|
end
|
36
|
+
|
37
|
+
trait :system_account do
|
38
|
+
system_account { true }
|
39
|
+
virtual_account_product { nil }
|
40
|
+
application { nil }
|
41
|
+
end
|
42
|
+
|
43
|
+
trait :settlement_account do
|
44
|
+
system_account { true }
|
45
|
+
name { "Core Banking Settlement Account" }
|
46
|
+
virtual_account_product { nil }
|
47
|
+
application { nil }
|
48
|
+
end
|
49
|
+
|
50
|
+
trait :interest_expense_account do
|
51
|
+
system_account { true }
|
52
|
+
name { "Interest Expense Account" }
|
53
|
+
virtual_account_product { nil }
|
54
|
+
application { nil }
|
55
|
+
end
|
56
|
+
|
57
|
+
trait :tax_liability_account do
|
58
|
+
system_account { true }
|
59
|
+
name { "Interest Tax Payable Account" }
|
60
|
+
virtual_account_product { nil }
|
61
|
+
application { nil }
|
62
|
+
end
|
35
63
|
end
|
36
64
|
|
37
65
|
# Alias for shared spec compatibility
|
@@ -4,6 +4,7 @@ FactoryBot.define do
|
|
4
4
|
association :virtual_account_product
|
5
5
|
applicant_type { :individual }
|
6
6
|
status { :draft }
|
7
|
+
sequence(:application_number) { |n| "IND#{Date.current.strftime('%Y%m%d')}#{n.to_s.rjust(4, '0')}" }
|
7
8
|
|
8
9
|
form_data do
|
9
10
|
{
|
@@ -23,7 +24,7 @@ FactoryBot.define do
|
|
23
24
|
|
24
25
|
trait :business do
|
25
26
|
applicant_type { :business }
|
26
|
-
|
27
|
+
sequence(:application_number) { |n| "BUS#{Date.current.strftime('%Y%m%d')}#{n.to_s.rjust(4, '0')}" }
|
27
28
|
form_data do
|
28
29
|
{
|
29
30
|
business_name: Faker::Company.name,
|
@@ -44,21 +45,21 @@ FactoryBot.define do
|
|
44
45
|
trait :under_review do
|
45
46
|
status { :under_review }
|
46
47
|
submitted_at { 2.days.ago }
|
47
|
-
association :assigned_kyc_officer, factory:
|
48
|
+
association :assigned_kyc_officer, factory: :user
|
48
49
|
end
|
49
50
|
|
50
51
|
trait :approved do
|
51
52
|
status { :approved }
|
52
53
|
submitted_at { 3.days.ago }
|
53
|
-
association :assigned_kyc_officer, factory:
|
54
|
-
association :assigned_branch_manager, factory:
|
54
|
+
association :assigned_kyc_officer, factory: :user
|
55
|
+
association :assigned_branch_manager, factory: :user
|
55
56
|
end
|
56
57
|
|
57
58
|
trait :rejected do
|
58
59
|
status { :rejected }
|
59
60
|
submitted_at { 3.days.ago }
|
60
61
|
rejection_reason { "Incomplete documentation" }
|
61
|
-
association :assigned_kyc_officer, factory:
|
62
|
+
association :assigned_kyc_officer, factory: :user
|
62
63
|
end
|
63
64
|
end
|
64
65
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_banking_transaction_type, class: "Dscf::Banking::TransactionType" do
|
3
|
+
sequence(:code) { |n| "TXN_TYPE_#{n}" }
|
4
|
+
sequence(:name) { |n| "Transaction Type #{n}" }
|
5
|
+
description { "Test transaction type description" }
|
6
|
+
|
7
|
+
trait :deposit do
|
8
|
+
code { "DEPOSIT" }
|
9
|
+
name { "Deposit" }
|
10
|
+
description { "Customer deposit from external source" }
|
11
|
+
end
|
12
|
+
|
13
|
+
trait :withdrawal do
|
14
|
+
code { "WITHDRAWAL" }
|
15
|
+
name { "Withdrawal" }
|
16
|
+
description { "Customer withdrawal to external destination" }
|
17
|
+
end
|
18
|
+
|
19
|
+
trait :transfer do
|
20
|
+
code { "TRANSFER" }
|
21
|
+
name { "Transfer" }
|
22
|
+
description { "Transfer between customer accounts" }
|
23
|
+
end
|
24
|
+
|
25
|
+
trait :interest_credit do
|
26
|
+
code { "INTEREST_CREDIT" }
|
27
|
+
name { "Interest Credit" }
|
28
|
+
description { "Interest payment to customer account" }
|
29
|
+
end
|
30
|
+
|
31
|
+
trait :interest_expense do
|
32
|
+
code { "INTEREST_EXPENSE" }
|
33
|
+
name { "Interest Expense" }
|
34
|
+
description { "Interest expense booking" }
|
35
|
+
end
|
36
|
+
|
37
|
+
trait :tax_withholding do
|
38
|
+
code { "TAX_WITHHOLDING" }
|
39
|
+
name { "Tax Withholding" }
|
40
|
+
description { "Tax withholding on interest payments" }
|
41
|
+
end
|
42
|
+
|
43
|
+
trait :settlement do
|
44
|
+
code { "SETTLEMENT" }
|
45
|
+
name { "Settlement" }
|
46
|
+
description { "Settlement transaction with core banking system" }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Alias for shared spec compatibility
|
51
|
+
factory :transaction_type, class: "Dscf::Banking::TransactionType" do
|
52
|
+
sequence(:code) { |n| "TXN_TYPE_#{n}" }
|
53
|
+
sequence(:name) { |n| "Transaction Type #{n}" }
|
54
|
+
description { "Test transaction type description" }
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :dscf_banking_transaction, class: "Dscf::Banking::Transaction" do
|
3
|
+
association :account, factory: :dscf_banking_account
|
4
|
+
association :transaction_type, factory: [ :dscf_banking_transaction_type, :deposit ]
|
5
|
+
association :debit_account, factory: :dscf_banking_account
|
6
|
+
association :credit_account, factory: :dscf_banking_account
|
7
|
+
|
8
|
+
amount { 1000.00 }
|
9
|
+
currency { "ETB" }
|
10
|
+
description { "Test transaction" }
|
11
|
+
status { :pending }
|
12
|
+
|
13
|
+
trait :deposit do
|
14
|
+
association :transaction_type, factory: [ :dscf_banking_transaction_type, :deposit ]
|
15
|
+
|
16
|
+
after(:build) do |transaction|
|
17
|
+
transaction.credit_account = transaction.account
|
18
|
+
transaction.debit_account = create(:dscf_banking_account, :settlement_account, currency: transaction.currency)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
trait :withdrawal do
|
23
|
+
association :transaction_type, factory: [ :dscf_banking_transaction_type, :withdrawal ]
|
24
|
+
|
25
|
+
after(:build) do |transaction|
|
26
|
+
transaction.debit_account = transaction.account
|
27
|
+
transaction.credit_account = create(:dscf_banking_account, :settlement_account, currency: transaction.currency)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
trait :completed do
|
32
|
+
status { :completed }
|
33
|
+
end
|
34
|
+
|
35
|
+
trait :failed do
|
36
|
+
status { :failed }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Alias for shared spec compatibility
|
41
|
+
factory :transaction, class: "Dscf::Banking::Transaction" do
|
42
|
+
association :account, factory: :dscf_banking_account
|
43
|
+
association :transaction_type, factory: [ :dscf_banking_transaction_type, :deposit ]
|
44
|
+
association :debit_account, factory: :dscf_banking_account
|
45
|
+
association :credit_account, factory: :dscf_banking_account
|
46
|
+
|
47
|
+
amount { 1000.00 }
|
48
|
+
currency { "ETB" }
|
49
|
+
description { "Test transaction" }
|
50
|
+
status { :pending }
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dscf-banking
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eyosiyas Mekbib
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-09-
|
10
|
+
date: 2025-09-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -447,6 +447,8 @@ files:
|
|
447
447
|
- app/controllers/dscf/banking/interest_rate_types_controller.rb
|
448
448
|
- app/controllers/dscf/banking/product_approvals_controller.rb
|
449
449
|
- app/controllers/dscf/banking/product_categories_controller.rb
|
450
|
+
- app/controllers/dscf/banking/transaction_types_controller.rb
|
451
|
+
- app/controllers/dscf/banking/transactions_controller.rb
|
450
452
|
- app/controllers/dscf/banking/virtual_account_products_controller.rb
|
451
453
|
- app/jobs/dscf/banking/application_job.rb
|
452
454
|
- app/mailers/dscf/banking/application_mailer.rb
|
@@ -460,6 +462,8 @@ files:
|
|
460
462
|
- app/models/dscf/banking/product_approval.rb
|
461
463
|
- app/models/dscf/banking/product_audit_log.rb
|
462
464
|
- app/models/dscf/banking/product_category.rb
|
465
|
+
- app/models/dscf/banking/transaction.rb
|
466
|
+
- app/models/dscf/banking/transaction_type.rb
|
463
467
|
- app/models/dscf/banking/virtual_account_product.rb
|
464
468
|
- app/serializers/dscf/banking/account_serializer.rb
|
465
469
|
- app/serializers/dscf/banking/application_serializer.rb
|
@@ -468,8 +472,14 @@ files:
|
|
468
472
|
- app/serializers/dscf/banking/interest_rate_type_serializer.rb
|
469
473
|
- app/serializers/dscf/banking/product_approval_serializer.rb
|
470
474
|
- app/serializers/dscf/banking/product_category_serializer.rb
|
475
|
+
- app/serializers/dscf/banking/transaction_serializer.rb
|
476
|
+
- app/serializers/dscf/banking/transaction_type_serializer.rb
|
471
477
|
- app/serializers/dscf/banking/virtual_account_product_serializer.rb
|
472
478
|
- app/services/dscf/banking/account_creation_service.rb
|
479
|
+
- app/services/dscf/banking/base_transaction_service.rb
|
480
|
+
- app/services/dscf/banking/deposit_service.rb
|
481
|
+
- app/services/dscf/banking/transfer_service.rb
|
482
|
+
- app/services/dscf/banking/withdrawal_service.rb
|
473
483
|
- config/routes.rb
|
474
484
|
- db/migrate/20250830211002_create_dscf_banking_product_categories.rb
|
475
485
|
- db/migrate/20250830211027_create_dscf_banking_interest_rate_types.rb
|
@@ -482,6 +492,11 @@ files:
|
|
482
492
|
- db/migrate/20250831084706_allow_null_virtual_account_product_id_in_product_audit_logs.rb
|
483
493
|
- db/migrate/20250912193134_create_dscf_banking_applications.rb
|
484
494
|
- db/migrate/20250912203527_create_dscf_banking_accounts.rb
|
495
|
+
- db/migrate/20250919084147_add_account_type_to_accounts.rb
|
496
|
+
- db/migrate/20250919084927_make_account_associations_optional.rb
|
497
|
+
- db/migrate/20250919182831_create_dscf_banking_transaction_types.rb
|
498
|
+
- db/migrate/20250919184220_create_dscf_banking_transactions.rb
|
499
|
+
- db/seeds.rb
|
485
500
|
- lib/dscf/banking.rb
|
486
501
|
- lib/dscf/banking/engine.rb
|
487
502
|
- lib/dscf/banking/version.rb
|
@@ -494,6 +509,8 @@ files:
|
|
494
509
|
- spec/factories/dscf/banking/product_approvals.rb
|
495
510
|
- spec/factories/dscf/banking/product_audit_logs.rb
|
496
511
|
- spec/factories/dscf/banking/product_categories.rb
|
512
|
+
- spec/factories/dscf/banking/transaction_types.rb
|
513
|
+
- spec/factories/dscf/banking/transactions.rb
|
497
514
|
- spec/factories/dscf/banking/virtual_account_products.rb
|
498
515
|
homepage: https://mksaddis.com/
|
499
516
|
licenses: []
|