dscf-banking 0.5.2 → 0.5.4
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2120d4ec544cad8e92b56fdada27d93498dc94e49879ac723ffafb898e0ce59d
|
|
4
|
+
data.tar.gz: 27c308d176f328a1aafec4f0f6dc860cfed388418224ae65cf1440a626bec56f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42473d30dd0ebb03d49193d5beb0852d4499532bd0678297fb54fe80d8994357c5658bcd4e3896cd963aa6874cfcd11e441b7e6e24bf7f538d3a6167ad1e427a
|
|
7
|
+
data.tar.gz: de39ecd9ca3c7c67ea0699f7378fe6d5bbf8b399283e7fe5603389161edf286cec5bbe4f05d69e603dead7727ef9ad35c1c0e4a1d31f72f7c3719807f186fa1f
|
|
@@ -426,7 +426,41 @@ module Dscf::Banking
|
|
|
426
426
|
errors << "transactions must be an array"
|
|
427
427
|
end
|
|
428
428
|
|
|
429
|
+
if params[:initial_balance].present? && !valid_decimal?(params[:initial_balance])
|
|
430
|
+
errors << "initial_balance must be a valid number"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
start_date = nil
|
|
434
|
+
end_date = nil
|
|
435
|
+
|
|
436
|
+
if params[:start_date].present?
|
|
437
|
+
start_date = parse_iso_date(params[:start_date])
|
|
438
|
+
errors << "start_date must be a valid date" unless start_date
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
if params[:end_date].present?
|
|
442
|
+
end_date = parse_iso_date(params[:end_date])
|
|
443
|
+
errors << "end_date must be a valid date" unless end_date
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if start_date && end_date && start_date > end_date
|
|
447
|
+
errors << "start_date must be before or equal to end_date"
|
|
448
|
+
end
|
|
449
|
+
|
|
429
450
|
errors
|
|
430
451
|
end
|
|
452
|
+
|
|
453
|
+
def valid_decimal?(value)
|
|
454
|
+
BigDecimal(value.to_s)
|
|
455
|
+
true
|
|
456
|
+
rescue ArgumentError
|
|
457
|
+
false
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def parse_iso_date(value)
|
|
461
|
+
Date.iso8601(value.to_s)
|
|
462
|
+
rescue Date::Error
|
|
463
|
+
nil
|
|
464
|
+
end
|
|
431
465
|
end
|
|
432
466
|
end
|
|
@@ -4,6 +4,9 @@ module Dscf::Banking
|
|
|
4
4
|
belongs_to :virtual_account_product, class_name: "Dscf::Banking::VirtualAccountProduct"
|
|
5
5
|
has_many :interest_rate_tiers, class_name: "Dscf::Banking::InterestRateTier", dependent: :destroy
|
|
6
6
|
|
|
7
|
+
before_validation :normalize_rate_attributes
|
|
8
|
+
validate :interest_free_configuration_guidance
|
|
9
|
+
|
|
7
10
|
validates :annual_interest_rate, presence: true, numericality: { greater_than: 0, less_than_or_equal_to: 1 }
|
|
8
11
|
validates :income_tax_rate, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
|
|
9
12
|
validates :minimum_balance_for_interest, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
@@ -59,6 +62,45 @@ module Dscf::Banking
|
|
|
59
62
|
|
|
60
63
|
private
|
|
61
64
|
|
|
65
|
+
def interest_free_configuration_guidance
|
|
66
|
+
return unless no_interest_requested?
|
|
67
|
+
return unless required_calculation_fields_missing?
|
|
68
|
+
|
|
69
|
+
errors.add(
|
|
70
|
+
:base,
|
|
71
|
+
"Interest-free products should not have an interest configuration. Skip interest configuration creation for interest-free products."
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def normalize_rate_attributes
|
|
76
|
+
self.annual_interest_rate = normalize_rate_value(annual_interest_rate_before_type_cast)
|
|
77
|
+
self.income_tax_rate = normalize_rate_value(income_tax_rate_before_type_cast)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize_rate_value(value)
|
|
81
|
+
return value if value.blank?
|
|
82
|
+
|
|
83
|
+
sanitized = value.to_s.strip
|
|
84
|
+
return value if sanitized.blank?
|
|
85
|
+
|
|
86
|
+
percentage_input = sanitized.end_with?("%")
|
|
87
|
+
decimal = BigDecimal(sanitized.delete("%").tr(",", "."))
|
|
88
|
+
|
|
89
|
+
percentage_input || decimal > 1 ? decimal / 100 : decimal
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def no_interest_requested?
|
|
95
|
+
annual_interest_rate.blank? || BigDecimal(annual_interest_rate.to_s) <= 0
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def required_calculation_fields_missing?
|
|
101
|
+
calculation_method.blank? || interest_basis.blank? || accrual_frequency.blank? || rounding_rule.blank? || calculation_timing.blank?
|
|
102
|
+
end
|
|
103
|
+
|
|
62
104
|
def promotional_dates_consistency
|
|
63
105
|
return unless promotional_start_date.present? || promotional_end_date.present?
|
|
64
106
|
|
|
@@ -3,24 +3,24 @@ require "date"
|
|
|
3
3
|
|
|
4
4
|
module Dscf::Banking
|
|
5
5
|
class InterestSimulationService
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
DEFAULT_TAX_RATE = BigDecimal("0.15")
|
|
6
|
+
NO_INTEREST_RATE = BigDecimal("0")
|
|
7
|
+
NO_TAX_RATE = BigDecimal("0")
|
|
9
8
|
|
|
10
9
|
def initialize(account:, initial_balance:, start_date:, end_date:, transactions: [])
|
|
11
10
|
@account = account
|
|
12
11
|
@initial_balance = decimal(initial_balance)
|
|
13
12
|
@start_date = to_date(start_date)
|
|
14
13
|
@end_date = to_date(end_date)
|
|
14
|
+
@transactions_by_date = {}
|
|
15
15
|
@transactions = normalize_transactions(transactions || [])
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call
|
|
19
19
|
raise ArgumentError, "start_date must be before or equal to end_date" if @start_date > @end_date
|
|
20
20
|
|
|
21
|
-
daily_rate = annual_interest_rate / BigDecimal(day_basis.to_s)
|
|
22
21
|
balance = @initial_balance
|
|
23
22
|
gross_interest = BigDecimal("0")
|
|
23
|
+
accrued_interest = BigDecimal("0")
|
|
24
24
|
daily_breakdown = []
|
|
25
25
|
|
|
26
26
|
(@start_date..@end_date).each do |date|
|
|
@@ -28,14 +28,25 @@ module Dscf::Banking
|
|
|
28
28
|
daily_transaction_sum = daily_transactions.sum(BigDecimal("0")) { |transaction| transaction[:signed_amount] }
|
|
29
29
|
balance += daily_transaction_sum
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
daily_rate = annual_interest_rate_for(balance) / BigDecimal(day_basis.to_s)
|
|
32
|
+
raw_daily_interest = eligible_for_interest?(date, balance) ? balance * daily_rate : BigDecimal("0")
|
|
33
|
+
accrued_interest += raw_daily_interest
|
|
34
|
+
|
|
35
|
+
posted_daily_interest = BigDecimal("0")
|
|
36
|
+
if accrual_posting_due?(date)
|
|
37
|
+
posted_daily_interest = accrued_interest
|
|
38
|
+
accrued_interest = BigDecimal("0")
|
|
39
|
+
gross_interest += posted_daily_interest
|
|
40
|
+
|
|
41
|
+
# Compound mode credits posted interest into principal for subsequent days.
|
|
42
|
+
balance += posted_daily_interest if compound_calculation? && compounding_due?(date)
|
|
43
|
+
end
|
|
33
44
|
|
|
34
45
|
daily_breakdown << {
|
|
35
46
|
date: date.iso8601,
|
|
36
47
|
balance: money(balance),
|
|
37
48
|
transaction_total: money(daily_transaction_sum),
|
|
38
|
-
daily_interest: money(
|
|
49
|
+
daily_interest: money(posted_daily_interest)
|
|
39
50
|
}
|
|
40
51
|
end
|
|
41
52
|
|
|
@@ -60,13 +71,23 @@ module Dscf::Banking
|
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
def annual_interest_rate
|
|
74
|
+
return NO_INTEREST_RATE unless interest_configuration
|
|
75
|
+
|
|
63
76
|
configured = interest_configuration&.annual_interest_rate
|
|
64
|
-
configured.present? ? decimal(configured) :
|
|
77
|
+
configured.present? ? decimal(configured) : NO_INTEREST_RATE
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def annual_interest_rate_for(balance)
|
|
81
|
+
return annual_interest_rate unless interest_configuration
|
|
82
|
+
|
|
83
|
+
interest_rate_tier_for(balance)&.yield_self { |tier| decimal(tier.interest_rate) } || annual_interest_rate
|
|
65
84
|
end
|
|
66
85
|
|
|
67
86
|
def tax_rate
|
|
87
|
+
return NO_TAX_RATE unless interest_configuration
|
|
88
|
+
|
|
68
89
|
configured = interest_configuration&.income_tax_rate
|
|
69
|
-
configured.present? ? decimal(configured) :
|
|
90
|
+
configured.present? ? decimal(configured) : NO_TAX_RATE
|
|
70
91
|
end
|
|
71
92
|
|
|
72
93
|
def day_basis
|
|
@@ -78,10 +99,76 @@ module Dscf::Banking
|
|
|
78
99
|
end
|
|
79
100
|
end
|
|
80
101
|
|
|
102
|
+
def compound_calculation?
|
|
103
|
+
interest_configuration&.calculation_method == "compound"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def accrual_posting_due?(date)
|
|
107
|
+
return date == date.end_of_month || date == @end_date if monthly_accrual_mode?
|
|
108
|
+
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def compounding_due?(date)
|
|
113
|
+
return date == date.end_of_month || date == @end_date if monthly_compounding_mode?
|
|
114
|
+
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def monthly_accrual_mode?
|
|
119
|
+
interest_configuration&.accrual_frequency == "monthly"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def monthly_compounding_mode?
|
|
123
|
+
interest_configuration&.compounding_period == "monthly"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def eligible_for_interest?(date, balance)
|
|
127
|
+
meets_minimum_balance?(balance) && within_promotional_window?(date)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def meets_minimum_balance?(balance)
|
|
131
|
+
balance >= minimum_balance_for_interest
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def minimum_balance_for_interest
|
|
135
|
+
configured = interest_configuration&.minimum_balance_for_interest
|
|
136
|
+
configured.present? ? decimal(configured) : BigDecimal("0")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def interest_rate_tier_for(balance)
|
|
140
|
+
interest_rate_tiers.find do |tier|
|
|
141
|
+
lower_bound_satisfied = balance >= decimal(tier.balance_min)
|
|
142
|
+
upper_bound_satisfied = tier.balance_max.blank? || balance <= decimal(tier.balance_max)
|
|
143
|
+
|
|
144
|
+
lower_bound_satisfied && upper_bound_satisfied
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def interest_rate_tiers
|
|
149
|
+
@interest_rate_tiers ||= begin
|
|
150
|
+
if interest_configuration
|
|
151
|
+
Dscf::Banking::InterestRateTier.by_interest_config(interest_configuration.id).ordered.to_a
|
|
152
|
+
else
|
|
153
|
+
[]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def within_promotional_window?(date)
|
|
159
|
+
return true unless interest_configuration&.promotional_start_date.present? || interest_configuration&.promotional_end_date.present?
|
|
160
|
+
|
|
161
|
+
date >= interest_configuration.promotional_start_date && date <= interest_configuration.promotional_end_date
|
|
162
|
+
end
|
|
163
|
+
|
|
81
164
|
def normalize_transactions(transactions)
|
|
82
165
|
return [] unless transactions.is_a?(Array)
|
|
83
166
|
|
|
84
|
-
normalized = transactions.map do |transaction|
|
|
167
|
+
normalized = transactions.each_with_index.map do |transaction, index|
|
|
168
|
+
unless transaction.respond_to?(:[]) && (transaction.is_a?(Hash) || transaction.respond_to?(:to_h))
|
|
169
|
+
raise ArgumentError, "Invalid transaction payload at index #{index}"
|
|
170
|
+
end
|
|
171
|
+
|
|
85
172
|
date = to_date(transaction[:date] || transaction["date"])
|
|
86
173
|
amount = decimal(transaction[:amount] || transaction["amount"])
|
|
87
174
|
type = (transaction[:type] || transaction["type"]).to_s.downcase
|
|
@@ -104,7 +191,7 @@ module Dscf::Banking
|
|
|
104
191
|
end
|
|
105
192
|
|
|
106
193
|
def to_date(value)
|
|
107
|
-
Date.
|
|
194
|
+
Date.iso8601(value.to_s)
|
|
108
195
|
rescue Date::Error
|
|
109
196
|
raise ArgumentError, "Invalid date value: #{value}"
|
|
110
197
|
end
|
|
@@ -116,7 +203,7 @@ module Dscf::Banking
|
|
|
116
203
|
end
|
|
117
204
|
|
|
118
205
|
def money(amount)
|
|
119
|
-
decimal(amount).round(2).
|
|
206
|
+
format("%.2f", decimal(amount).round(2).to_f)
|
|
120
207
|
end
|
|
121
208
|
|
|
122
209
|
def accounting_records(gross_interest, tax_amount, net_interest)
|
data/db/seeds.rb
CHANGED
|
@@ -1,115 +1,225 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
user3.roles
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
# This file should contain all the record creation needed to seed the banking engine
|
|
2
|
+
# with deterministic, idempotent sample data.
|
|
3
|
+
|
|
4
|
+
puts "Seeding DSCF Banking data..."
|
|
5
|
+
|
|
6
|
+
seed_timestamp = Time.current
|
|
7
|
+
|
|
8
|
+
roles_data = [
|
|
9
|
+
{ code: "USER", name: "User" },
|
|
10
|
+
{ code: "ADMIN", name: "Administrator" },
|
|
11
|
+
{ code: "KYC_OFFICER", name: "KYC Officer" },
|
|
12
|
+
{ code: "BRANCH_MANAGER", name: "Branch Manager" },
|
|
13
|
+
{ code: "VIRTUAL_ACCOUNT_OFFICER", name: "Virtual Account Officer" },
|
|
14
|
+
{ code: "VIRTUAL_ACCOUNT_MANAGER", name: "Virtual Account Manager" },
|
|
15
|
+
{ code: "REGULAR_USER", name: "Regular User" }
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
users_data = [
|
|
19
|
+
{ email: "admin@dscf.com", phone: "+251911000001", roles: %w[ADMIN], password: "password123" },
|
|
20
|
+
{ email: "kyc1@dscf.com", phone: "+251911000002", roles: %w[KYC_OFFICER], password: "password123" },
|
|
21
|
+
{ email: "kyc2@dscf.com", phone: "+251911000003", roles: %w[KYC_OFFICER], password: "password123" },
|
|
22
|
+
{ email: "kyc3@dscf.com", phone: "+251911000004", roles: %w[KYC_OFFICER], password: "password123" },
|
|
23
|
+
{ email: "manager1@dscf.com", phone: "+251911000005", roles: %w[BRANCH_MANAGER], password: "password123" },
|
|
24
|
+
{ email: "manager2@dscf.com", phone: "+251911000006", roles: %w[BRANCH_MANAGER], password: "password123" },
|
|
25
|
+
{ email: "va_officer1@dscf.com", phone: "+251911000007", roles: %w[VIRTUAL_ACCOUNT_OFFICER], password: "password123" },
|
|
26
|
+
{ email: "va_officer2@dscf.com", phone: "+251911000008", roles: %w[VIRTUAL_ACCOUNT_OFFICER], password: "password123" },
|
|
27
|
+
{ email: "va_manager1@dscf.com", phone: "+251911000009", roles: %w[VIRTUAL_ACCOUNT_MANAGER], password: "password123" },
|
|
28
|
+
{ email: "va_manager2@dscf.com", phone: "+251911000010", roles: %w[VIRTUAL_ACCOUNT_MANAGER], password: "password123" },
|
|
29
|
+
{ email: "user1@example.com", phone: "+251911000011", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
30
|
+
{ email: "user2@example.com", phone: "+251911000012", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
31
|
+
{ email: "user3@example.com", phone: "+251911000013", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
32
|
+
{ email: "user4@example.com", phone: "+251911000014", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
33
|
+
{ email: "user5@example.com", phone: "+251911000015", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
34
|
+
{ email: "user6@example.com", phone: "+251911000016", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
35
|
+
{ email: "user7@example.com", phone: "+251911000017", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
36
|
+
{ email: "user8@example.com", phone: "+251911000018", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
37
|
+
{ email: "user9@example.com", phone: "+251911000019", roles: %w[USER REGULAR_USER], password: "password123" },
|
|
38
|
+
{ email: "user10@example.com", phone: "+251911000020", roles: %w[USER REGULAR_USER], password: "password123" }
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
transaction_types_data = [
|
|
42
|
+
{ code: "DEPOSIT", name: "Deposit", description: "Deposit transaction - money coming into account" },
|
|
43
|
+
{ code: "WITHDRAWAL", name: "Withdrawal", description: "Withdrawal transaction - money going out of account" },
|
|
44
|
+
{ code: "TRANSFER", name: "Transfer", description: "Transfer transaction - money moving between accounts" },
|
|
45
|
+
{ code: "VOUCHER", name: "Voucher", description: "Voucher issuance and redemption transactions" }
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
48
|
+
system_accounts_data = [
|
|
49
|
+
{ name: "System Deposit Account" },
|
|
50
|
+
{ name: "System Withdrawal Account" },
|
|
51
|
+
{ name: "Voucher Settlement Account" }
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
product_categories_data = [
|
|
55
|
+
{ name: "Checking Account", description: "Day-to-day transaction accounts" },
|
|
56
|
+
{ name: "Savings Account", description: "Standard savings accounts for retail customers" },
|
|
57
|
+
{ name: "Fixed Deposit", description: "Time-bound deposit products with fixed returns" },
|
|
58
|
+
{ name: "Business Account", description: "Accounts for small and medium business operations" }
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
interest_rate_types_data = [
|
|
62
|
+
{ code: "STANDARD", name: "Standard Rate", description: "Default interest rate for standard products" },
|
|
63
|
+
{ code: "PROMOTIONAL", name: "Promotional Rate", description: "Temporary promotional rate for campaigns" },
|
|
64
|
+
{ code: "TIERED", name: "Tiered Rate", description: "Interest rate that varies by product tier" }
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
virtual_account_products_data = [
|
|
68
|
+
{
|
|
69
|
+
product_code: "CHK-001",
|
|
70
|
+
product_name: "Standard Checking",
|
|
71
|
+
category_name: "Checking Account",
|
|
72
|
+
description: "Everyday checking account for regular transactions."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
product_code: "SVG-001",
|
|
76
|
+
product_name: "Standard Savings",
|
|
77
|
+
category_name: "Savings Account",
|
|
78
|
+
description: "Savings account for customers who want interest on available balances."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
product_code: "FD-001",
|
|
82
|
+
product_name: "12-Month Fixed Deposit",
|
|
83
|
+
category_name: "Fixed Deposit",
|
|
84
|
+
description: "Fixed-term deposit product with a one-year investment period."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
product_code: "BUS-001",
|
|
88
|
+
product_name: "SME Business Account",
|
|
89
|
+
category_name: "Business Account",
|
|
90
|
+
description: "Business operating account designed for small and medium enterprises."
|
|
91
|
+
}
|
|
92
|
+
].freeze
|
|
93
|
+
|
|
94
|
+
interest_rates_by_category = {
|
|
95
|
+
"Savings Account" => 0.035,
|
|
96
|
+
"Fixed Deposit" => 0.06,
|
|
97
|
+
"Business Account" => 0.025
|
|
98
|
+
}.freeze
|
|
50
99
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
100
|
+
puts "Seeding roles..."
|
|
101
|
+
roles = roles_data.each_with_object({}) do |role_data, memo|
|
|
102
|
+
role = Dscf::Core::Role.find_or_initialize_by(code: role_data[:code])
|
|
103
|
+
role.name = role_data[:name]
|
|
104
|
+
role.active = true
|
|
105
|
+
role.save!
|
|
106
|
+
|
|
107
|
+
memo[role_data[:code]] = role
|
|
108
|
+
puts " Seeded role: #{role.code}"
|
|
56
109
|
end
|
|
57
|
-
puts "Created transaction type: #{deposit_type.name}"
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
puts "Seeding users..."
|
|
112
|
+
users = users_data.each_with_object({}) do |user_data, memo|
|
|
113
|
+
user = Dscf::Core::User.find_or_initialize_by(email: user_data[:email])
|
|
114
|
+
user.phone = user_data[:phone]
|
|
115
|
+
user.password = user_data[:password] if user.new_record? || user.password_digest.blank?
|
|
116
|
+
user.temp_password = false
|
|
117
|
+
user.verified_at ||= seed_timestamp
|
|
118
|
+
user.save!
|
|
119
|
+
|
|
120
|
+
user_data[:roles].each do |role_code|
|
|
121
|
+
Dscf::Core::UserRole.find_or_create_by!(user: user, role: roles.fetch(role_code))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
memo[user_data[:email]] = user
|
|
125
|
+
puts " Seeded user: #{user.email}"
|
|
62
126
|
end
|
|
63
|
-
puts "Created transaction type: #{withdrawal_type.name}"
|
|
64
127
|
|
|
65
|
-
|
|
66
|
-
tt.name = "Transfer"
|
|
67
|
-
tt.description = "Transfer transaction - money moving between accounts"
|
|
68
|
-
end
|
|
69
|
-
puts "Created transaction type: #{transfer_type.name}"
|
|
128
|
+
admin_user = users.fetch("admin@dscf.com")
|
|
70
129
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
130
|
+
puts "Seeding transaction types..."
|
|
131
|
+
transaction_types_data.each do |transaction_type_data|
|
|
132
|
+
transaction_type = Dscf::Banking::TransactionType.find_or_initialize_by(code: transaction_type_data[:code])
|
|
133
|
+
transaction_type.name = transaction_type_data[:name]
|
|
134
|
+
transaction_type.description = transaction_type_data[:description]
|
|
135
|
+
transaction_type.save!
|
|
136
|
+
|
|
137
|
+
puts " Seeded transaction type: #{transaction_type.code}"
|
|
74
138
|
end
|
|
75
|
-
puts "Created transaction type: #{voucher_type.name}"
|
|
76
139
|
|
|
77
|
-
# Seed system accounts for transaction processing
|
|
78
140
|
puts "Seeding system accounts..."
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
system_accounts_data.each do |account_data|
|
|
142
|
+
account = Dscf::Banking::Account.find_or_initialize_by(
|
|
143
|
+
name: account_data[:name],
|
|
144
|
+
system_account: true
|
|
145
|
+
)
|
|
83
146
|
account.currency = "ETB"
|
|
84
147
|
account.status = :active
|
|
85
148
|
account.current_balance = 0
|
|
86
149
|
account.available_balance = 0
|
|
87
150
|
account.minimum_balance = 0
|
|
151
|
+
account.active = true
|
|
152
|
+
account.save!
|
|
153
|
+
|
|
154
|
+
puts " Seeded system account: #{account.name}"
|
|
88
155
|
end
|
|
89
|
-
puts "Created system deposit account: #{system_deposit_account.name}"
|
|
90
156
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
157
|
+
puts "Seeding product categories..."
|
|
158
|
+
product_categories = product_categories_data.each_with_object({}) do |category_data, memo|
|
|
159
|
+
category = Dscf::Banking::ProductCategory.find_or_initialize_by(name: category_data[:name])
|
|
160
|
+
category.description = category_data[:description]
|
|
161
|
+
category.is_active = true
|
|
162
|
+
category.save!
|
|
163
|
+
|
|
164
|
+
memo[category_data[:name]] = category
|
|
165
|
+
puts " Seeded product category: #{category.name}"
|
|
100
166
|
end
|
|
101
|
-
puts "Created system withdrawal account: #{system_withdrawal_account.name}"
|
|
102
167
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
puts "Seeding interest rate types..."
|
|
169
|
+
interest_rate_types = interest_rate_types_data.each_with_object([]) do |rate_type_data, memo|
|
|
170
|
+
rate_type = Dscf::Banking::InterestRateType.find_or_initialize_by(code: rate_type_data[:code])
|
|
171
|
+
rate_type.name = rate_type_data[:name]
|
|
172
|
+
rate_type.description = rate_type_data[:description]
|
|
173
|
+
rate_type.save!
|
|
174
|
+
|
|
175
|
+
memo << rate_type
|
|
176
|
+
puts " Seeded interest rate type: #{rate_type.code}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
puts "Seeding virtual account products..."
|
|
180
|
+
virtual_account_products = virtual_account_products_data.each_with_object([]) do |product_data, memo|
|
|
181
|
+
product = Dscf::Banking::VirtualAccountProduct.find_or_initialize_by(product_code: product_data[:product_code])
|
|
182
|
+
product.product_name = product_data[:product_name]
|
|
183
|
+
product.product_category = product_categories.fetch(product_data[:category_name])
|
|
184
|
+
product.description = product_data[:description]
|
|
185
|
+
product.document_reference = "DOC-#{product_data[:product_code]}"
|
|
186
|
+
product.status = :approved
|
|
187
|
+
product.created_by = admin_user
|
|
188
|
+
product.approved_by = admin_user
|
|
189
|
+
product.approved_at ||= seed_timestamp
|
|
190
|
+
product.is_active = true
|
|
191
|
+
product.save!
|
|
192
|
+
|
|
193
|
+
memo << product
|
|
194
|
+
puts " Seeded virtual account product: #{product.product_code}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
puts "Seeding interest configurations..."
|
|
198
|
+
virtual_account_products.each_with_index do |product, index|
|
|
199
|
+
category_name = product.product_category&.name
|
|
200
|
+
|
|
201
|
+
if category_name == "Checking Account"
|
|
202
|
+
puts " Skipped interest configuration for: #{product.product_code}"
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
interest_configuration = Dscf::Banking::InterestConfiguration.find_or_initialize_by(
|
|
207
|
+
virtual_account_product: product,
|
|
208
|
+
interest_rate_type: interest_rate_types[index % interest_rate_types.length]
|
|
209
|
+
)
|
|
210
|
+
interest_configuration.annual_interest_rate = interest_rates_by_category.fetch(category_name, 0.02)
|
|
211
|
+
interest_configuration.income_tax_rate = 0.05
|
|
212
|
+
interest_configuration.minimum_balance_for_interest = 1000.0
|
|
213
|
+
interest_configuration.calculation_method = :simple
|
|
214
|
+
interest_configuration.compounding_period = :monthly
|
|
215
|
+
interest_configuration.interest_basis = :actual_365
|
|
216
|
+
interest_configuration.accrual_frequency = :monthly
|
|
217
|
+
interest_configuration.rounding_rule = :nearest_cent
|
|
218
|
+
interest_configuration.calculation_timing ||= seed_timestamp
|
|
219
|
+
interest_configuration.is_active = true
|
|
220
|
+
interest_configuration.save!
|
|
221
|
+
|
|
222
|
+
puts " Seeded interest configuration for: #{product.product_code}"
|
|
112
223
|
end
|
|
113
|
-
puts "Created voucher settlement account: #{voucher_settlement_account.name}"
|
|
114
224
|
|
|
115
|
-
puts "
|
|
225
|
+
puts "DSCF Banking seed completed successfully."
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat Efrem
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|