dscf-banking 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/dscf/banking/demo_permission_bypass.rb +44 -0
- data/app/controllers/dscf/banking/accounts_controller.rb +54 -0
- data/app/controllers/dscf/banking/application_controller.rb +34 -0
- data/app/controllers/dscf/banking/applications_controller.rb +6 -0
- data/app/controllers/dscf/banking/documents_controller.rb +1 -0
- data/app/controllers/dscf/banking/interest_configurations_controller.rb +1 -0
- data/app/controllers/dscf/banking/interest_rate_tiers_controller.rb +1 -0
- data/app/controllers/dscf/banking/interest_rate_types_controller.rb +1 -0
- data/app/controllers/dscf/banking/product_approvals_controller.rb +1 -0
- data/app/controllers/dscf/banking/product_categories_controller.rb +1 -0
- data/app/controllers/dscf/banking/transaction_types_controller.rb +1 -0
- data/app/controllers/dscf/banking/transactions_controller.rb +1 -0
- data/app/controllers/dscf/banking/virtual_account_products_controller.rb +6 -0
- data/app/controllers/dscf/banking/vouchers_controller.rb +1 -0
- data/app/services/dscf/banking/interest_simulation_service.rb +145 -0
- data/config/routes.rb +1 -0
- data/lib/dscf/banking/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fe37bdff67cefd5cc8827eba966f74185e155c3cf627fed75b4e27ad43ebdec
|
|
4
|
+
data.tar.gz: 905268bd1b0f28a2e806fc7a950851b85915d7bab0354f6bd7473b40b196d3d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f525a5e131f5fa21f717101a98fd7698477e242818489338fca9b94caa6133bc0feb9d2eea3c2854564449d2454d68a493ca1b7e1797fc6cd64df91b826b5b5
|
|
7
|
+
data.tar.gz: 97c43049bf4d9cbb18cce581dcac6e59b5cc219ac5ae03812edb6875426531a7325765e9ff2ad627bd124cb1786fe94ec06ff5a4442a05dc1ffd58fabf67a8d0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Dscf
|
|
2
|
+
module Banking
|
|
3
|
+
module DemoPermissionBypass
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_action :demo_bypass_permissions!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def bypass_permissions_for_demo?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pundit_user
|
|
15
|
+
user = current_user
|
|
16
|
+
return nil unless user
|
|
17
|
+
|
|
18
|
+
bypass_permissions_on_user!(user)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def authorize_review_action!
|
|
22
|
+
skip_authorization if respond_to?(:skip_authorization, true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def demo_bypass_permissions!
|
|
28
|
+
skip_authorization if respond_to?(:skip_authorization, true)
|
|
29
|
+
skip_policy_scope if respond_to?(:skip_policy_scope, true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def bypass_permissions_on_user!(user)
|
|
33
|
+
return user if user.instance_variable_defined?(:@_banking_demo_permission_bypass)
|
|
34
|
+
|
|
35
|
+
user.define_singleton_method(:has_permission?) { |_permission_code| true }
|
|
36
|
+
user.define_singleton_method(:can?) { |permission_code| has_permission?(permission_code) }
|
|
37
|
+
user.define_singleton_method(:super_admin?) { true }
|
|
38
|
+
user.instance_variable_set(:@_banking_demo_permission_bypass, true)
|
|
39
|
+
|
|
40
|
+
user
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Dscf::Banking
|
|
2
2
|
class AccountsController < ApplicationController
|
|
3
3
|
include Dscf::Core::Common
|
|
4
|
+
include Dscf::Banking::DemoPermissionBypass
|
|
4
5
|
|
|
5
6
|
def index
|
|
6
7
|
accounts = Dscf::Banking::Account.all
|
|
@@ -273,6 +274,46 @@ module Dscf::Banking
|
|
|
273
274
|
}, status: :not_found
|
|
274
275
|
end
|
|
275
276
|
|
|
277
|
+
def interest_simulations
|
|
278
|
+
account_number = params[:account_number].presence || params[:id]
|
|
279
|
+
account = Dscf::Banking::Account.find_by(account_number: account_number)
|
|
280
|
+
|
|
281
|
+
unless account
|
|
282
|
+
return render json: {
|
|
283
|
+
success: false,
|
|
284
|
+
error: "Account not found"
|
|
285
|
+
}, status: :not_found
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
validation_errors = interest_simulation_validation_errors
|
|
289
|
+
if validation_errors.any?
|
|
290
|
+
return render json: {
|
|
291
|
+
success: false,
|
|
292
|
+
error: "Invalid interest simulation parameters",
|
|
293
|
+
errors: validation_errors
|
|
294
|
+
}, status: :unprocessable_entity
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
simulation = Dscf::Banking::InterestSimulationService.new(
|
|
298
|
+
account: account,
|
|
299
|
+
initial_balance: params[:initial_balance],
|
|
300
|
+
start_date: params[:start_date],
|
|
301
|
+
end_date: params[:end_date],
|
|
302
|
+
transactions: params[:transactions] || []
|
|
303
|
+
).call
|
|
304
|
+
|
|
305
|
+
render json: {
|
|
306
|
+
success: true,
|
|
307
|
+
data: simulation
|
|
308
|
+
}
|
|
309
|
+
rescue ArgumentError => e
|
|
310
|
+
render json: {
|
|
311
|
+
success: false,
|
|
312
|
+
error: "Invalid interest simulation parameters",
|
|
313
|
+
errors: [ e.message ]
|
|
314
|
+
}, status: :unprocessable_entity
|
|
315
|
+
end
|
|
316
|
+
|
|
276
317
|
private
|
|
277
318
|
|
|
278
319
|
def transaction_data(transaction)
|
|
@@ -374,5 +415,18 @@ module Dscf::Banking
|
|
|
374
415
|
update: [ :virtual_account_product, :application ]
|
|
375
416
|
}
|
|
376
417
|
end
|
|
418
|
+
|
|
419
|
+
def interest_simulation_validation_errors
|
|
420
|
+
errors = []
|
|
421
|
+
errors << "initial_balance is required" if params[:initial_balance].blank?
|
|
422
|
+
errors << "start_date is required" if params[:start_date].blank?
|
|
423
|
+
errors << "end_date is required" if params[:end_date].blank?
|
|
424
|
+
|
|
425
|
+
if params[:transactions].present? && !params[:transactions].is_a?(Array)
|
|
426
|
+
errors << "transactions must be an array"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
errors
|
|
430
|
+
end
|
|
377
431
|
end
|
|
378
432
|
end
|
|
@@ -3,6 +3,40 @@ module Dscf
|
|
|
3
3
|
class ApplicationController < ActionController::API
|
|
4
4
|
include Dscf::Core::Authenticatable
|
|
5
5
|
include Dscf::Core::JsonResponse
|
|
6
|
+
before_action :authenticate_user
|
|
7
|
+
before_action :demo_bypass_permissions!
|
|
8
|
+
|
|
9
|
+
# TEMPORARY DEMO BYPASS:
|
|
10
|
+
# Bypass banking authorization checks for authenticated users only.
|
|
11
|
+
# Remove after the demo.
|
|
12
|
+
def bypass_permissions_for_demo?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pundit_user
|
|
17
|
+
user = current_user
|
|
18
|
+
return nil unless user
|
|
19
|
+
|
|
20
|
+
bypass_permissions_on_user!(user)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def demo_bypass_permissions!
|
|
26
|
+
skip_authorization if respond_to?(:skip_authorization, true)
|
|
27
|
+
skip_policy_scope if respond_to?(:skip_policy_scope, true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def bypass_permissions_on_user!(user)
|
|
31
|
+
return user if user.instance_variable_defined?(:@_banking_demo_permission_bypass)
|
|
32
|
+
|
|
33
|
+
user.define_singleton_method(:has_permission?) { |_permission_code| true }
|
|
34
|
+
user.define_singleton_method(:can?) { |permission_code| has_permission?(permission_code) }
|
|
35
|
+
user.define_singleton_method(:super_admin?) { true }
|
|
36
|
+
user.instance_variable_set(:@_banking_demo_permission_bypass, true)
|
|
37
|
+
|
|
38
|
+
user
|
|
39
|
+
end
|
|
6
40
|
end
|
|
7
41
|
end
|
|
8
42
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Dscf::Banking
|
|
2
2
|
class ApplicationsController < ApplicationController
|
|
3
3
|
include Dscf::Core::Common
|
|
4
|
+
include Dscf::Banking::DemoPermissionBypass
|
|
4
5
|
include Dscf::Core::ReviewableController
|
|
5
6
|
|
|
6
7
|
# Custom default context for applications
|
|
@@ -22,6 +23,11 @@ module Dscf::Banking
|
|
|
22
23
|
resubmit: { status: "submitted", update_model: true }
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
# TEMPORARY DEMO BYPASS: remove after demo.
|
|
27
|
+
def authorize_review_action!
|
|
28
|
+
skip_authorization
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
private
|
|
26
32
|
|
|
27
33
|
def application_data(application)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Dscf::Banking
|
|
2
2
|
class VirtualAccountProductsController < ApplicationController
|
|
3
3
|
include Dscf::Core::Common
|
|
4
|
+
include Dscf::Banking::DemoPermissionBypass
|
|
4
5
|
include Dscf::Core::ReviewableController
|
|
5
6
|
|
|
6
7
|
# Custom default context with request_modification status
|
|
@@ -20,6 +21,11 @@ module Dscf::Banking
|
|
|
20
21
|
resubmit: { status: "pending_approval", update_model: true }
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
# TEMPORARY DEMO BYPASS: remove after demo.
|
|
25
|
+
def authorize_review_action!
|
|
26
|
+
skip_authorization
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
def audit_logs
|
|
24
30
|
product = Dscf::Banking::VirtualAccountProduct.find(params[:id])
|
|
25
31
|
audit_logs = Dscf::Banking::ProductAuditLog.by_product(product.id).recent
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "bigdecimal"
|
|
2
|
+
require "date"
|
|
3
|
+
|
|
4
|
+
module Dscf::Banking
|
|
5
|
+
class InterestSimulationService
|
|
6
|
+
DEFAULT_ANNUAL_INTEREST_RATE = BigDecimal("0.05")
|
|
7
|
+
# Fallback tax rate when no product-level tax is configured.
|
|
8
|
+
DEFAULT_TAX_RATE = BigDecimal("0.15")
|
|
9
|
+
|
|
10
|
+
def initialize(account:, initial_balance:, start_date:, end_date:, transactions: [])
|
|
11
|
+
@account = account
|
|
12
|
+
@initial_balance = decimal(initial_balance)
|
|
13
|
+
@start_date = to_date(start_date)
|
|
14
|
+
@end_date = to_date(end_date)
|
|
15
|
+
@transactions = normalize_transactions(transactions || [])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
raise ArgumentError, "start_date must be before or equal to end_date" if @start_date > @end_date
|
|
20
|
+
|
|
21
|
+
daily_rate = annual_interest_rate / BigDecimal(day_basis.to_s)
|
|
22
|
+
balance = @initial_balance
|
|
23
|
+
gross_interest = BigDecimal("0")
|
|
24
|
+
daily_breakdown = []
|
|
25
|
+
|
|
26
|
+
(@start_date..@end_date).each do |date|
|
|
27
|
+
daily_transactions = @transactions_by_date[date] || []
|
|
28
|
+
daily_transaction_sum = daily_transactions.sum(BigDecimal("0")) { |transaction| transaction[:signed_amount] }
|
|
29
|
+
balance += daily_transaction_sum
|
|
30
|
+
|
|
31
|
+
daily_interest = balance * daily_rate
|
|
32
|
+
gross_interest += daily_interest
|
|
33
|
+
|
|
34
|
+
daily_breakdown << {
|
|
35
|
+
date: date.iso8601,
|
|
36
|
+
balance: money(balance),
|
|
37
|
+
transaction_total: money(daily_transaction_sum),
|
|
38
|
+
daily_interest: money(daily_interest)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
tax_amount = gross_interest * tax_rate
|
|
43
|
+
net_interest = gross_interest - tax_amount
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
gross_interest: money(gross_interest),
|
|
47
|
+
tax_amount: money(tax_amount),
|
|
48
|
+
net_interest: money(net_interest),
|
|
49
|
+
daily_breakdown: daily_breakdown,
|
|
50
|
+
accounting_records: accounting_records(gross_interest, tax_amount, net_interest)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def interest_configuration
|
|
57
|
+
@interest_configuration ||= Dscf::Banking::InterestConfiguration
|
|
58
|
+
.active
|
|
59
|
+
.find_by(virtual_account_product_id: @account.virtual_account_product_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def annual_interest_rate
|
|
63
|
+
configured = interest_configuration&.annual_interest_rate
|
|
64
|
+
configured.present? ? decimal(configured) : DEFAULT_ANNUAL_INTEREST_RATE
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def tax_rate
|
|
68
|
+
configured = interest_configuration&.income_tax_rate
|
|
69
|
+
configured.present? ? decimal(configured) : DEFAULT_TAX_RATE
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def day_basis
|
|
73
|
+
case interest_configuration&.interest_basis
|
|
74
|
+
when "actual_360", "thirty_360"
|
|
75
|
+
360
|
|
76
|
+
else
|
|
77
|
+
365
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_transactions(transactions)
|
|
82
|
+
return [] unless transactions.is_a?(Array)
|
|
83
|
+
|
|
84
|
+
normalized = transactions.map do |transaction|
|
|
85
|
+
date = to_date(transaction[:date] || transaction["date"])
|
|
86
|
+
amount = decimal(transaction[:amount] || transaction["amount"])
|
|
87
|
+
type = (transaction[:type] || transaction["type"]).to_s.downcase
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
date: date,
|
|
91
|
+
signed_amount: signed_amount(amount, type)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@transactions_by_date = normalized.group_by { |transaction| transaction[:date] }
|
|
96
|
+
normalized
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def signed_amount(amount, type)
|
|
100
|
+
return amount * -1 if %w[debit withdrawal outflow].include?(type) && amount.positive?
|
|
101
|
+
return amount.abs if %w[credit deposit inflow].include?(type)
|
|
102
|
+
|
|
103
|
+
amount
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_date(value)
|
|
107
|
+
Date.parse(value.to_s)
|
|
108
|
+
rescue Date::Error
|
|
109
|
+
raise ArgumentError, "Invalid date value: #{value}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def decimal(value)
|
|
113
|
+
BigDecimal(value.to_s)
|
|
114
|
+
rescue ArgumentError
|
|
115
|
+
raise ArgumentError, "Invalid numeric value: #{value}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def money(amount)
|
|
119
|
+
decimal(amount).round(2).to_s("F")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def accounting_records(gross_interest, tax_amount, net_interest)
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
date: @end_date.iso8601,
|
|
126
|
+
type: "gross_interest",
|
|
127
|
+
amount: money(gross_interest),
|
|
128
|
+
currency: @account.currency
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
date: @end_date.iso8601,
|
|
132
|
+
type: "tax",
|
|
133
|
+
amount: money(tax_amount),
|
|
134
|
+
currency: @account.currency
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
date: @end_date.iso8601,
|
|
138
|
+
type: "net_interest",
|
|
139
|
+
amount: money(net_interest),
|
|
140
|
+
currency: @account.currency
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
data/config/routes.rb
CHANGED
data/lib/dscf/banking/version.rb
CHANGED
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.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat Efrem
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-03-
|
|
10
|
+
date: 2026-03-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -425,6 +425,7 @@ files:
|
|
|
425
425
|
- MIT-LICENSE
|
|
426
426
|
- README.md
|
|
427
427
|
- Rakefile
|
|
428
|
+
- app/controllers/concerns/dscf/banking/demo_permission_bypass.rb
|
|
428
429
|
- app/controllers/dscf/banking/accounts_controller.rb
|
|
429
430
|
- app/controllers/dscf/banking/application_controller.rb
|
|
430
431
|
- app/controllers/dscf/banking/applications_controller.rb
|
|
@@ -471,6 +472,7 @@ files:
|
|
|
471
472
|
- app/services/dscf/banking/account_creation_service.rb
|
|
472
473
|
- app/services/dscf/banking/base_transaction_service.rb
|
|
473
474
|
- app/services/dscf/banking/deposit_service.rb
|
|
475
|
+
- app/services/dscf/banking/interest_simulation_service.rb
|
|
474
476
|
- app/services/dscf/banking/transfer_service.rb
|
|
475
477
|
- app/services/dscf/banking/voucher_service.rb
|
|
476
478
|
- app/services/dscf/banking/withdrawal_service.rb
|