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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b754b36bf6ebf7d81d2aa27b41afaba55e13243dd6480d181975ead1308d070
4
- data.tar.gz: 4bc4c26afed1d059067b644ff78670fca050c5445d009e7854e70989288c60b9
3
+ metadata.gz: 7fe37bdff67cefd5cc8827eba966f74185e155c3cf627fed75b4e27ad43ebdec
4
+ data.tar.gz: 905268bd1b0f28a2e806fc7a950851b85915d7bab0354f6bd7473b40b196d3d2
5
5
  SHA512:
6
- metadata.gz: c6a6a1d6cae6a552e2051764ea1a8e223b2560db876e708fe83b0b2662f5378e380dfad4d0d6bea261ae8b9ca3e4337297340b41fe9125a46d731394c92fbe6f
7
- data.tar.gz: 8ece386566d110156443f0ef3824878012bd2206965b3b3497e89e7646f5290494d099e0991c19099e0b4e759e8de08a9b33a5f9a00f258e7f65a82a752a68c6
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 DocumentsController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  before_action :find_application
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class InterestConfigurationsController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class InterestRateTiersController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class InterestRateTypesController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class ProductApprovalsController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class ProductCategoriesController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class TransactionTypesController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  private
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Dscf::Banking
2
2
  class TransactionsController < ApplicationController
3
3
  include Dscf::Core::Common
4
+ include Dscf::Banking::DemoPermissionBypass
4
5
 
5
6
  rescue_from ActiveRecord::RecordNotFound do |exception|
6
7
  render json: {
@@ -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
@@ -2,6 +2,7 @@ module Dscf
2
2
  module Banking
3
3
  class VouchersController < ApplicationController
4
4
  include Dscf::Core::Common
5
+ include Dscf::Banking::DemoPermissionBypass
5
6
  skip_before_action :set_object, only: :show
6
7
 
7
8
  def create
@@ -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
@@ -7,6 +7,7 @@ Dscf::Banking::Engine.routes.draw do
7
7
  post :activate
8
8
  post :suspend
9
9
  post :close
10
+ post :interest_simulations
10
11
  get :transactions
11
12
  get :full_name
12
13
  end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Banking
3
- VERSION = "0.5.0"
3
+ VERSION = "0.5.2"
4
4
  end
5
5
  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.5.0
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 00:00:00.000000000 Z
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