dscf-banking 0.5.1 → 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: f558728cc00fd36f502cc21c0f342a1db0102f7c2b08b92b4fdc224e7a3a4839
4
- data.tar.gz: 47c41262ad655e43365f13a974cd253474e3dbf4ac0abdb46e03564288f20b42
3
+ metadata.gz: 7fe37bdff67cefd5cc8827eba966f74185e155c3cf627fed75b4e27ad43ebdec
4
+ data.tar.gz: 905268bd1b0f28a2e806fc7a950851b85915d7bab0354f6bd7473b40b196d3d2
5
5
  SHA512:
6
- metadata.gz: c2c57799b26eb5bbe257e20c9a044298021268e55689fbb62e32157dc1b674595b0406f603f95e82356bc865c63a313a662d1266588d967700846579341ae465
7
- data.tar.gz: 8d04ca7ec5e9ead3fa0d026999962fd5026a1cfb98825465ee455ba961f4606b406175bfe9152943cb5c2dbc19b1579ea5301b235e642116a34dc2eb0759da47
6
+ metadata.gz: 8f525a5e131f5fa21f717101a98fd7698477e242818489338fca9b94caa6133bc0feb9d2eea3c2854564449d2454d68a493ca1b7e1797fc6cd64df91b826b5b5
7
+ data.tar.gz: 97c43049bf4d9cbb18cce581dcac6e59b5cc219ac5ae03812edb6875426531a7325765e9ff2ad627bd124cb1786fe94ec06ff5a4442a05dc1ffd58fabf67a8d0
@@ -274,6 +274,46 @@ module Dscf::Banking
274
274
  }, status: :not_found
275
275
  end
276
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
+
277
317
  private
278
318
 
279
319
  def transaction_data(transaction)
@@ -375,5 +415,18 @@ module Dscf::Banking
375
415
  update: [ :virtual_account_product, :application ]
376
416
  }
377
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
378
431
  end
379
432
  end
@@ -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.1"
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.1
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-14 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
@@ -472,6 +472,7 @@ files:
472
472
  - app/services/dscf/banking/account_creation_service.rb
473
473
  - app/services/dscf/banking/base_transaction_service.rb
474
474
  - app/services/dscf/banking/deposit_service.rb
475
+ - app/services/dscf/banking/interest_simulation_service.rb
475
476
  - app/services/dscf/banking/transfer_service.rb
476
477
  - app/services/dscf/banking/voucher_service.rb
477
478
  - app/services/dscf/banking/withdrawal_service.rb