dscf-credit 0.4.47 → 0.4.49

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/dscf/credit/application_controller.rb +1 -3
  3. data/app/controllers/dscf/credit/credit_limit_calculations_controller.rb +2 -0
  4. data/app/controllers/dscf/credit/disbursements_controller.rb +2 -0
  5. data/app/controllers/dscf/credit/repayments_controller.rb +2 -0
  6. data/app/policies/dscf/credit/application_policy.rb +75 -0
  7. data/app/policies/dscf/credit/bank_branch_policy.rb +6 -0
  8. data/app/policies/dscf/credit/bank_policy.rb +6 -0
  9. data/app/policies/dscf/credit/bank_staff_policy.rb +46 -0
  10. data/app/policies/dscf/credit/category_policy.rb +6 -0
  11. data/app/policies/dscf/credit/credit_line_policy.rb +6 -0
  12. data/app/policies/dscf/credit/credit_line_spec_policy.rb +6 -0
  13. data/app/policies/dscf/credit/credit_product_policy.rb +6 -0
  14. data/app/policies/dscf/credit/eligible_credit_line_policy.rb +40 -0
  15. data/app/policies/dscf/credit/facilitator_application_policy.rb +31 -0
  16. data/app/policies/dscf/credit/facilitator_policy.rb +6 -0
  17. data/app/policies/dscf/credit/information_source_policy.rb +6 -0
  18. data/app/policies/dscf/credit/loan_accrual_policy.rb +42 -0
  19. data/app/policies/dscf/credit/loan_application_datum_policy.rb +33 -0
  20. data/app/policies/dscf/credit/loan_application_policy.rb +43 -0
  21. data/app/policies/dscf/credit/loan_policy.rb +36 -0
  22. data/app/policies/dscf/credit/loan_profile_policy.rb +38 -0
  23. data/app/policies/dscf/credit/loan_transaction_policy.rb +34 -0
  24. data/app/policies/dscf/credit/scoring_parameter_policy.rb +6 -0
  25. data/app/policies/dscf/credit/scoring_table_normalizer_policy.rb +6 -0
  26. data/app/policies/dscf/credit/scoring_table_parameter_policy.rb +6 -0
  27. data/app/policies/dscf/credit/scoring_table_policy.rb +6 -0
  28. data/app/policies/dscf/credit/system_config_definition_policy.rb +6 -0
  29. data/app/policies/dscf/credit/system_config_policy.rb +6 -0
  30. data/db/dev_seeds.rb +545 -1221
  31. data/db/migrate/{20260219000029_create_dscf_credit_credit_products.rb → 20260219000040_create_dscf_credit_credit_products.rb} +0 -2
  32. data/db/migrate/{20260219000005_create_dscf_credit_credit_lines.rb → 20260219000060_create_dscf_credit_credit_lines.rb} +1 -1
  33. data/db/migrate/{20260219000013_create_dscf_credit_loan_applications.rb → 20260219000140_create_dscf_credit_loan_applications.rb} +1 -1
  34. data/db/seeds.rb +250 -4
  35. data/lib/dscf/credit/engine.rb +40 -0
  36. data/lib/dscf/credit/version.rb +1 -1
  37. metadata +55 -31
  38. /data/db/migrate/{20260219000001_create_dscf_credit_banks.rb → 20260219000010_create_dscf_credit_banks.rb} +0 -0
  39. /data/db/migrate/{20260219000002_create_dscf_credit_scoring_tables.rb → 20260219000020_create_dscf_credit_scoring_tables.rb} +0 -0
  40. /data/db/migrate/{20260219000003_create_dscf_credit_categories.rb → 20260219000030_create_dscf_credit_categories.rb} +0 -0
  41. /data/db/migrate/{20260219000004_create_dscf_credit_bank_branches.rb → 20260219000050_create_dscf_credit_bank_branches.rb} +0 -0
  42. /data/db/migrate/{20260219000006_create_dscf_credit_information_sources.rb → 20260219000070_create_dscf_credit_information_sources.rb} +0 -0
  43. /data/db/migrate/{20260219000007_create_dscf_credit_scoring_parameters.rb → 20260219000080_create_dscf_credit_scoring_parameters.rb} +0 -0
  44. /data/db/migrate/{20260219000008_create_dscf_credit_credit_line_specs.rb → 20260219000090_create_dscf_credit_credit_line_specs.rb} +0 -0
  45. /data/db/migrate/{20260219000009_create_dscf_credit_scoring_table_parameters.rb → 20260219000100_create_dscf_credit_scoring_table_parameters.rb} +0 -0
  46. /data/db/migrate/{20260219000010_create_dscf_credit_scoring_table_normalizers.rb → 20260219000110_create_dscf_credit_scoring_table_normalizers.rb} +0 -0
  47. /data/db/migrate/{20260219000011_create_dscf_credit_system_config_definitions.rb → 20260219000120_create_dscf_credit_system_config_definitions.rb} +0 -0
  48. /data/db/migrate/{20260219000012_create_dscf_credit_system_configs.rb → 20260219000130_create_dscf_credit_system_configs.rb} +0 -0
  49. /data/db/migrate/{20260219000014_create_dscf_credit_loan_profiles.rb → 20260219000150_create_dscf_credit_loan_profiles.rb} +0 -0
  50. /data/db/migrate/{20260219000015_create_dscf_credit_scoring_results.rb → 20260219000160_create_dscf_credit_scoring_results.rb} +0 -0
  51. /data/db/migrate/{20260219000016_create_dscf_credit_facilitator_applications.rb → 20260219000170_create_dscf_credit_facilitator_applications.rb} +0 -0
  52. /data/db/migrate/{20260219000017_create_dscf_credit_facilitators.rb → 20260219000180_create_dscf_credit_facilitators.rb} +0 -0
  53. /data/db/migrate/{20260219000018_create_dscf_credit_facilitator_performances.rb → 20260219000190_create_dscf_credit_facilitator_performances.rb} +0 -0
  54. /data/db/migrate/{20260219000019_create_dscf_credit_loans.rb → 20260219000200_create_dscf_credit_loans.rb} +0 -0
  55. /data/db/migrate/{20260219000020_create_dscf_credit_loan_transactions.rb → 20260219000210_create_dscf_credit_loan_transactions.rb} +0 -0
  56. /data/db/migrate/{20260219000021_create_dscf_credit_daily_routine_transactions.rb → 20260219000220_create_dscf_credit_daily_routine_transactions.rb} +0 -0
  57. /data/db/migrate/{20260219000022_create_dscf_credit_accounting_audit_requests.rb → 20260219000230_create_dscf_credit_accounting_audit_requests.rb} +0 -0
  58. /data/db/migrate/{20260219000023_create_dscf_credit_failed_operations_logs.rb → 20260219000240_create_dscf_credit_failed_operations_logs.rb} +0 -0
  59. /data/db/migrate/{20260219000024_create_dscf_credit_accounting_entries.rb → 20260219000250_create_dscf_credit_accounting_entries.rb} +0 -0
  60. /data/db/migrate/{20260219000025_create_dscf_credit_bank_staff.rb → 20260219000260_create_dscf_credit_bank_staff.rb} +0 -0
  61. /data/db/migrate/{20260219000026_create_dscf_credit_eligible_credit_lines.rb → 20260219000270_create_dscf_credit_eligible_credit_lines.rb} +0 -0
  62. /data/db/migrate/{20260219000027_create_dscf_credit_loan_accruals.rb → 20260219000280_create_dscf_credit_loan_accruals.rb} +0 -0
  63. /data/db/migrate/{20260219000028_create_dscf_credit_loan_application_data.rb → 20260219000290_create_dscf_credit_loan_application_data.rb} +0 -0
data/db/dev_seeds.rb CHANGED
@@ -1,1258 +1,582 @@
1
- # =============================================================================
2
- # DEV SEEDS — DSCF Credit Engine
3
- # =============================================================================
1
+ # DSCF Credit Engine — DEV Seeds
2
+ # ========================================================
3
+ # Creates rich development data for the DSCF Credit Dev Workflow
4
+ # Postman collection. Depends on db/seeds.rb having already run.
4
5
  #
5
- # PURPOSE: 3 fully-predictable, calculable loan application scenarios for
6
- # manual end-to-end testing of the DSCF Credit scoring and loan pipeline.
6
+ # Usage:
7
+ # RAILS_ENV=development bundle exec rails runner db/dev_seeds.rb
7
8
  #
8
- # HOW TO RUN:
9
- # From engine root (spec/dummy context):
10
- # cd spec/dummy && RAILS_ENV=development bundle exec rails runner \
11
- # "load Rails.root.join('../../db/dev_seeds.rb')"
12
- #
13
- # Or set up a Rake task pointing at this file:
14
- # Rake::Task["db:seed"].enhance do
15
- # load Rails.root.join("../../db/dev_seeds.rb")
16
- # end
17
- #
18
- # -----------------------------------------------------------------------------
19
- # THE 3 SCENARIOS
20
- # -----------------------------------------------------------------------------
21
- #
22
- # Scenario A Sara Kebede (sara.kebede@dev.et)
23
- # avg_monthly_purchase: 380,000 ETB → High Volume → normalized 0.95
24
- # years_in_business: 8 years → Established → normalized 0.80
25
- # business_type: "retail" → normalized 0.80
26
- # payment_track_record: "excellent" → normalized 1.00
27
- #
28
- # MATH: 0.95×0.35 + 0.80×0.25 + 0.80×0.20 + 1.00×0.20
29
- # = 0.3325 + 0.2000 + 0.1600 + 0.2000 = 0.8925
30
- # final_score = (0.8925 / 1.00) × 100 = 89.25 → APPROVED ( 65.0)
31
- #
32
- # Facility limits (base_metric = 380,000 ÷ 1, capped to spec min/max):
33
- # Credit Line A (30D, multiplier 0.50): 380k × 0.50 × 0.8925 = 169,575
34
- # Credit Line B (90D, multiplier 1.00): 380k × 1.00 × 0.8925 = 339,150
35
- # Credit Line C (180D/wholesale, multiplier 2.00): 380k × 2.00 × 0.8925 = 678,300
36
- # total_limit = 1,187,025
37
- #
38
- # Seed state: Full pipeline — LoanProfile BB010001, ScoringResult, 3
39
- # EligibleCreditLine records. 1 active Loan (150k ETB, Credit Line A,
40
- # disbursed 30 days ago, due in 5 days) with 3 LoanAccruals.
41
- #
42
- # Scenario B — Mekdes Tadesse (mekdes.tadesse@dev.et)
43
- # avg_monthly_purchase: 120,000 ETB → Low Volume → normalized 0.40
44
- # years_in_business: 3 years → Emerging → normalized 0.50
45
- # business_type: "startup" → normalized 0.30
46
- # payment_track_record: "good" → normalized 0.75
47
- #
48
- # MATH: 0.40×0.35 + 0.50×0.25 + 0.30×0.20 + 0.75×0.20
49
- # = 0.1400 + 0.1250 + 0.0600 + 0.1500 = 0.4750
50
- # final_score = (0.4750 / 1.00) × 100 = 47.50
51
- # 45.0 ≤ 47.50 < 65.0 → PENDING (manual review required)
52
- #
53
- # Seed state: All data submitted, score=47.50 on LoanApplication, review
54
- # status = "pending". NO LoanProfile yet. Awaiting:
55
- # PATCH /loan_applications/:id/approve
56
- #
57
- # Scenario C — Dawit Alemu (dawit.alemu@dev.et)
58
- # avg_monthly_purchase: 30,000 ETB → Very Low Vol → normalized 0.10
59
- # years_in_business: 1 year → Startup → normalized 0.10
60
- # business_type: "startup" → normalized 0.30
61
- # payment_track_record: "poor" → normalized 0.20
62
- #
63
- # MATH: 0.10×0.35 + 0.10×0.25 + 0.30×0.20 + 0.20×0.20
64
- # = 0.0350 + 0.0250 + 0.0600 + 0.0400 = 0.1600
65
- # final_score = (0.1600 / 1.00) × 100 = 16.00
66
- # 16.00 < 45.0 (pending_threshold) → REJECTED
67
- #
68
- # Seed state: Data submitted, review status = "draft" (not yet scored in
69
- # system). Score the application to see auto-rejection:
70
- # POST /loan_applications/:id/score (or equivalent scoring endpoint)
71
- #
72
- # =============================================================================
73
-
74
- puts ""
75
- puts "=" * 70
76
- puts " DEV SEEDS — DSCF Credit Engine"
77
- puts "=" * 70
78
-
79
- # =============================================================================
80
- # 0. CORE DEPENDENCIES
81
- # =============================================================================
82
-
83
- unless defined?(Dscf::Credit::Bank)
84
- puts "ERROR: Dscf::Credit models not found. Run inside the engine context."
85
- return
86
- end
87
-
88
- # ---------------------------------------------------------------------------
89
- # 0.1 Admin users (must already exist from production seeds)
90
- # ---------------------------------------------------------------------------
91
- admin_user = nil
92
- bank_admin = nil
93
-
94
- if defined?(Dscf::Core::User)
95
- admin_user = Dscf::Core::User.find_or_create_by!(email: "admin@bunna.com") do |u|
96
- u.phone = "+251911123456"
97
- u.verified_at = Time.current
98
- u.temp_password = false
99
- u.password = "SecurePassword123!"
100
- end
101
-
102
- bank_admin = Dscf::Core::User.find_or_create_by!(email: "bank.admin@bunna.com") do |u|
103
- u.phone = "+251922123456"
104
- u.verified_at = Time.current
105
- u.temp_password = false
106
- u.password = "SecurePassword123!"
107
- end
108
-
109
- puts "✓ Admin users resolved (admin_user##{admin_user.id}, bank_admin##{bank_admin.id})"
110
- else
111
- puts "WARNING: Dscf::Core::User not available — skipping user-dependent records."
112
- end
113
-
114
- # ---------------------------------------------------------------------------
115
- # 0.2 Bunna Bank
116
- # ---------------------------------------------------------------------------
117
- bunna_bank = Dscf::Credit::Bank.find_or_create_by!(registration_number: "BUN001") do |b|
118
- b.name = "Bunna Bank"
119
- b.swift_code = "BUNNETAA"
120
- b.headquarters_address = "Churchill Avenue, Addis Ababa"
121
- b.city = "Addis Ababa"
122
- b.country = "Ethiopia"
123
- b.contact_email = "info@bunnabank.com"
124
- b.contact_phone = "+251115510000"
125
- b.status = "active"
126
- end
127
- puts "✓ Bunna Bank resolved (id=#{bunna_bank.id})"
128
-
129
- # ---------------------------------------------------------------------------
130
- # 0.3 Head-office branch (used as review_branch for all dev applications)
131
- # ---------------------------------------------------------------------------
132
- dev_branch = Dscf::Credit::BankBranch.find_or_create_by!(bank: bunna_bank, branch_name: "Head Office") do |br|
133
- br.branch_code = "BUN-HO-001"
134
- br.branch_address = "Churchill Avenue, Addis Ababa"
135
- br.city = "Addis Ababa"
136
- br.country = "Ethiopia"
137
- br.contact_email = "headoffice@bunnabank.com"
138
- br.contact_phone = "+251115510001"
139
- br.status = "active"
140
- end
141
- puts "✓ Head-office branch resolved (id=#{dev_branch.id})"
142
-
143
- # ---------------------------------------------------------------------------
144
- # 0.4 Global information sources (bank_id: nil — created by prod seeds)
145
- # ---------------------------------------------------------------------------
146
- info_source_facilitator = Dscf::Credit::InformationSource.find_or_create_by!(code: "facilitator", bank_id: nil) do |s|
147
- s.name = "Facilitator"
148
- s.description = "Field facilitator who verifies applicant data and provides on-the-ground assessments"
149
- s.active = true
150
- s.created_by = admin_user
151
- end
152
-
153
- info_source_applicant = Dscf::Credit::InformationSource.find_or_create_by!(code: "applicant", bank_id: nil) do |s|
154
- s.name = "Applicant"
155
- s.description = "The loan applicant (borrower/retailer) who provides self-reported data during the application process"
156
- s.active = true
157
- s.created_by = admin_user
158
- end
159
-
160
- info_source_branch_officer = Dscf::Credit::InformationSource.find_or_create_by!(code: "branch_officer", bank_id: nil) do |s|
161
- s.name = "Branch Officer"
162
- s.description = "Bank branch officer who conducts in-person verification and assessment"
163
- s.active = true
164
- s.created_by = admin_user
9
+ # Expected state after running:
10
+ # - DEV Retail category
11
+ # - dev_retailer_v1 scoring table (passing=65, pending=45)
12
+ # - 4 scoring parameters with normalizers
13
+ # - 3 credit lines (30/60/90 day) each with credit_line_specs
14
+ # - 3 loan applications: Sara (auto-approved), Mekdes (pending data), Dawit (pending data)
15
+ # - Sara: fully scored (89.25), loan profile, 3 ECLs, active loan, accruals
16
+ # - Mekdes & Dawit: source data pre-seeded, scoring NOT run (trigger via API)
17
+ # ========================================================
18
+
19
+ puts "=" * 60
20
+ puts "DSCF Credit Engine — DEV Seeds"
21
+ puts "=" * 60
22
+
23
+ # ── Prerequisites ──────────────────────────────────────────
24
+ admin_user = Dscf::Core::User.find_by!(email: "admin@bunna.com")
25
+ bank_admin = Dscf::Core::User.find_by!(email: "bank.admin@bunna.com")
26
+ bunna_bank = Dscf::Credit::Bank.find_by!(registration_number: "BUN001")
27
+ bunna_head_office = Dscf::Credit::BankBranch.find_by!(bank: bunna_bank, branch_name: "Head Office")
28
+ facilitator1 = Dscf::Credit::Facilitator.first!
29
+ info_source_facilitator = Dscf::Credit::InformationSource.find_by!(code: "facilitator")
30
+ info_source_applicant = Dscf::Credit::InformationSource.find_by!(code: "applicant")
31
+ info_source_branch = Dscf::Credit::InformationSource.find_by!(code: "branch_officer")
32
+
33
+ puts "✓ Prerequisites loaded"
34
+
35
+ # ── 1. DEV Category ────────────────────────────────────────
36
+ puts "\n── Seeding DEV category ──"
37
+ dev_category = Dscf::Credit::Category.find_or_create_by!(
38
+ category_type: "credit_line", name: "DEV Retail"
39
+ ) do |c|
40
+ c.description = "Development retail category for testing the scoring workflow"
165
41
  end
42
+ puts "✓ Category: #{dev_category.name} (id=#{dev_category.id})"
166
43
 
167
- puts "✓ Information sources resolved (facilitator##{info_source_facilitator.id}, applicant##{info_source_applicant.id}, branch_officer##{info_source_branch_officer.id})"
168
-
169
- # =============================================================================
170
- # 1. DEV SCORING TABLE
171
- # code: dev_retailer_v1 | is_template: false | bank: Bunna Bank
172
- # =============================================================================
173
- puts ""
174
- puts "--- [1/8] DEV Scoring Table ---"
175
-
44
+ # ── 2. DEV Scoring Table ──────────────────────────────────
45
+ puts "\n── Seeding DEV scoring table ──"
176
46
  dev_scoring_table = Dscf::Credit::ScoringTable.find_or_create_by!(code: "dev_retailer_v1") do |st|
177
- st.name = "DEV Retailer Scoring" # unique per bank_id
178
- st.description = "[DEV] Scoring table for retail applicant dev scenarios do not use in production"
179
- st.document_reference = "DEV-ST-RET-001"
180
- st.passing_score = 65.0
47
+ st.name = "DEV Retailer Scoring Table v1"
48
+ st.description = "Development scoring table with 4 parameters for retailer credit assessment"
49
+ st.document_reference = "DEV-ST-RET-V1"
50
+ st.passing_score = 65.0
181
51
  st.pending_threshold = 45.0
182
- st.active = true
183
- st.is_template = false
184
- st.bank = bunna_bank
185
- st.created_by = admin_user
186
- end
187
-
188
- # Approved review so scoring engine can use this table
189
- if defined?(Dscf::Core::Review)
190
- Dscf::Core::Review.find_or_create_by!(reviewable: dev_scoring_table, context: "default") do |r|
191
- r.status = "approved"
192
- r.reviewed_by = admin_user
193
- r.reviewed_at = Time.current
52
+ st.active = true
53
+ st.is_template = true
54
+ st.bank_id = nil
55
+ st.created_by = admin_user
56
+ end
57
+ Dscf::Core::Review.find_or_create_by!(reviewable: dev_scoring_table, context: "default") do |r|
58
+ r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
59
+ end
60
+ puts "✓ Scoring table: #{dev_scoring_table.code} (passing=#{dev_scoring_table.passing_score}, pending=#{dev_scoring_table.pending_threshold})"
61
+
62
+ # ── 3. DEV Scoring Parameters ─────────────────────────────
63
+ puts "\n── Seeding DEV scoring parameters ──"
64
+
65
+ param_definitions = [
66
+ { code: "avg_monthly_purchase", name: "Average Monthly Purchase", data_type: "decimal",
67
+ description: "Average monthly purchase volume in ETB" },
68
+ { code: "years_in_business", name: "Years in Business", data_type: "integer",
69
+ description: "Number of years the applicant has been in business" },
70
+ { code: "business_type", name: "Business Type", data_type: "string",
71
+ description: "Type/maturity of business (established, startup, micro)" },
72
+ { code: "payment_track_record", name: "Payment Track Record", data_type: "string",
73
+ description: "Historical payment behavior rating" }
74
+ ]
75
+
76
+ dev_params = {}
77
+ param_definitions.each do |defn|
78
+ param = Dscf::Credit::ScoringParameter.find_or_create_by!(bank: bunna_bank, code: defn[:code]) do |p|
79
+ p.name = defn[:name]
80
+ p.description = defn[:description]
81
+ p.data_type = defn[:data_type]
82
+ p.active = true
83
+ p.created_by = admin_user
84
+ p.category = dev_category
85
+ p.document_reference = "DEV-SP-#{defn[:code].upcase}"
86
+ end
87
+ Dscf::Core::Review.find_or_create_by!(reviewable: param, context: "default") do |r|
88
+ r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
194
89
  end
90
+ dev_params[defn[:code]] = param
91
+ puts " ✓ #{param.code} (id=#{param.id}, type=#{param.data_type})"
195
92
  end
196
93
 
197
- puts "✓ DEV Scoring Table created (id=#{dev_scoring_table.id}, passing=#{dev_scoring_table.passing_score}, pending_threshold=#{dev_scoring_table.pending_threshold})"
198
-
199
- # =============================================================================
200
- # 2. DEV CATEGORY
201
- # Must reference the scoring table (scoring_table_id is set here)
202
- # =============================================================================
203
- puts ""
204
- puts "--- [2/8] DEV Category ---"
205
-
206
- dev_category = Dscf::Credit::Category.find_or_create_by!(
207
- category_type: "credit_line",
208
- name: "DEV Retail"
209
- ) do |c|
210
- c.description = "[DEV] Retail category for dev seed scenarios — do not use in production"
211
- c.bank = bunna_bank
212
- end
213
-
214
- puts "✓ DEV Category created (id=#{dev_category.id}, name=#{dev_category.name})"
215
-
216
- # =============================================================================
217
- # 2.5 DEV CREDIT PRODUCT
218
- # Wraps the scoring table — CreditLines belong to CreditProduct, not Category
219
- # Category is now purely an organizer for ScoringParameter records
220
- # =============================================================================
221
- puts ""
222
- puts "--- [2.5] DEV Credit Product ---"
223
-
94
+ # ── 4. DEV Credit Product ─────────────────────────────────
95
+ puts "\n── Seeding DEV credit product ──"
224
96
  dev_credit_product = Dscf::Credit::CreditProduct.find_or_create_by!(
225
- bank: bunna_bank,
226
- name: "DEV Retail Credit Product"
97
+ bank: bunna_bank, name: "DEV Retailer Credit Product"
227
98
  ) do |cp|
228
99
  cp.scoring_table = dev_scoring_table
229
- cp.description = "[DEV] Credit product linking DEV Retailer Scoring to dev credit lines"
100
+ cp.description = "Development credit product for retailer workflow testing"
230
101
  cp.document_reference = "DEV-CP-RET-001"
231
102
  end
232
-
233
- if defined?(Dscf::Core::Review)
234
- Dscf::Core::Review.find_or_create_by!(reviewable: dev_credit_product, context: "default") do |r|
235
- r.status = "approved"
236
- r.reviewed_by = admin_user
237
- r.reviewed_at = Time.current
238
- end
239
- end
240
-
241
- puts "✓ DEV Credit Product created (id=#{dev_credit_product.id}, scoring_table=#{dev_scoring_table.code})"
242
-
243
- # =============================================================================
244
- # 3. DEV SCORING PARAMETERS (4 parameters, all under dev_category)
245
- # Codes are prefixed with dev_ to avoid conflicts with production parameters
246
- # =============================================================================
247
- puts ""
248
- puts "--- [3/8] DEV Scoring Parameters ---"
249
-
250
- # Parameter 1: avg_monthly_purchase — decimal — weight 0.35 — source: facilitator
251
- param_avg_monthly_purchase = Dscf::Credit::ScoringParameter.find_or_create_by!(
252
- code: "dev_avg_monthly_purchase",
253
- bank: bunna_bank
254
- ) do |p|
255
- p.name = "DEV Avg Monthly Purchase"
256
- p.description = "Average monthly purchase volume in ETB"
257
- p.data_type = "decimal"
258
- p.active = true
259
- p.category = dev_category
260
- p.created_by = admin_user
261
- end
262
-
263
- if defined?(Dscf::Core::Review)
264
- Dscf::Core::Review.find_or_create_by!(reviewable: param_avg_monthly_purchase, context: "default") do |r|
265
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
266
- end
267
- end
268
-
269
- # Parameter 2: years_in_business — integer — weight 0.25 — source: applicant
270
- param_years_in_business = Dscf::Credit::ScoringParameter.find_or_create_by!(
271
- code: "dev_years_in_business",
272
- bank: bunna_bank
273
- ) do |p|
274
- p.name = "DEV Years in Business"
275
- p.description = "Number of years the business has been operating"
276
- p.data_type = "integer"
277
- p.active = true
278
- p.category = dev_category
279
- p.created_by = admin_user
280
- end
281
-
282
- if defined?(Dscf::Core::Review)
283
- Dscf::Core::Review.find_or_create_by!(reviewable: param_years_in_business, context: "default") do |r|
284
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
285
- end
286
- end
287
-
288
- # Parameter 3: business_type — string — weight 0.20 — source: applicant
289
- param_business_type = Dscf::Credit::ScoringParameter.find_or_create_by!(
290
- code: "dev_business_type",
291
- bank: bunna_bank
292
- ) do |p|
293
- p.name = "DEV Business Type"
294
- p.description = "Type of business (retail, wholesale, startup, service)"
295
- p.data_type = "string"
296
- p.active = true
297
- p.category = dev_category
298
- p.created_by = admin_user
299
- end
300
-
301
- if defined?(Dscf::Core::Review)
302
- Dscf::Core::Review.find_or_create_by!(reviewable: param_business_type, context: "default") do |r|
303
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
304
- end
305
- end
306
-
307
- # Parameter 4: payment_track_record — string — weight 0.20 source: branch_officer
308
- param_payment_track_record = Dscf::Credit::ScoringParameter.find_or_create_by!(
309
- code: "dev_payment_track_record",
310
- bank: bunna_bank
311
- ) do |p|
312
- p.name = "DEV Payment Track Record"
313
- p.description = "Historical payment reliability rating"
314
- p.data_type = "string"
315
- p.active = true
316
- p.category = dev_category
317
- p.created_by = admin_user
318
- end
319
-
320
- if defined?(Dscf::Core::Review)
321
- Dscf::Core::Review.find_or_create_by!(reviewable: param_payment_track_record, context: "default") do |r|
322
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
323
- end
324
- end
325
-
326
- puts "✓ 4 DEV Scoring Parameters created:"
327
- puts " ##{param_avg_monthly_purchase.id} dev_avg_monthly_purchase (decimal)"
328
- puts " ##{param_years_in_business.id} dev_years_in_business (integer)"
329
- puts " ##{param_business_type.id} dev_business_type (string)"
330
- puts " ##{param_payment_track_record.id} dev_payment_track_record (string)"
331
- puts " Weights: 0.35 + 0.25 + 0.20 + 0.20 = 1.00 ✓"
332
-
333
- # =============================================================================
334
- # 4. DEV SCORING TABLE PARAMETERS + NORMALIZERS
335
- # Links scoring_table scoring_parameter with weight & information_source
336
- # =============================================================================
337
- puts ""
338
- puts "--- [4/8] DEV Scoring Table Parameters & Normalizers ---"
339
-
340
- # ---------------------------------------------------------------------------
341
- # STP 1: avg_monthly_purchase decimal — weight 0.35 — source: facilitator
342
- # ---------------------------------------------------------------------------
343
- stp_avg_purchase = Dscf::Credit::ScoringTableParameter.find_or_create_by!(
344
- scoring_table: dev_scoring_table,
345
- scoring_parameter: param_avg_monthly_purchase
346
- ) do |stp|
347
- stp.weight = 0.35
348
- stp.information_source = info_source_facilitator
349
- stp.required = true
350
- stp.order = 1
351
- stp.created_by = admin_user
352
- end
353
-
354
- # Range-based normalizers for avg_monthly_purchase (decimal)
355
- # 0 – 49,999 → 0.10 "Very Low Volume"
356
- # 50,000 – 149,999 → 0.40 "Low Volume"
357
- # 150,000 – 349,999 → 0.70 "Medium Volume"
358
- # 350,000 – 999,999 → 0.95 "High Volume"
359
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
360
- scoring_table_parameter: stp_avg_purchase,
361
- min_range_value: 0,
362
- max_range_value: 49_999
363
- ) { |n| n.name = "Very Low Volume"; n.normalized_value = 0.10 }
364
-
365
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
366
- scoring_table_parameter: stp_avg_purchase,
367
- min_range_value: 50_000,
368
- max_range_value: 149_999
369
- ) { |n| n.name = "Low Volume"; n.normalized_value = 0.40 }
370
-
371
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
372
- scoring_table_parameter: stp_avg_purchase,
373
- min_range_value: 150_000,
374
- max_range_value: 349_999
375
- ) { |n| n.name = "Medium Volume"; n.normalized_value = 0.70 }
376
-
377
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
378
- scoring_table_parameter: stp_avg_purchase,
379
- min_range_value: 350_000,
380
- max_range_value: 999_999
381
- ) { |n| n.name = "High Volume"; n.normalized_value = 0.95 }
382
-
383
- puts " ✓ stp_avg_purchase (id=#{stp_avg_purchase.id}, weight=0.35) + 4 normalizers"
384
-
385
- # ---------------------------------------------------------------------------
386
- # STP 2: years_in_business — integer — weight 0.25 — source: applicant
387
- # ---------------------------------------------------------------------------
388
- stp_years = Dscf::Credit::ScoringTableParameter.find_or_create_by!(
389
- scoring_table: dev_scoring_table,
390
- scoring_parameter: param_years_in_business
391
- ) do |stp|
392
- stp.weight = 0.25
393
- stp.information_source = info_source_applicant
394
- stp.required = true
395
- stp.order = 2
396
- stp.created_by = admin_user
397
- end
398
-
399
- # Range-based normalizers for years_in_business (integer)
400
- # 0 – 1 → 0.10 "Startup"
401
- # 2 – 4 → 0.50 "Emerging"
402
- # 5 – 9 → 0.80 "Established"
403
- # 10 – 99 → 1.00 "Veteran"
404
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
405
- scoring_table_parameter: stp_years,
406
- min_range_value: 0,
407
- max_range_value: 1
408
- ) { |n| n.name = "Startup"; n.normalized_value = 0.10 }
409
-
410
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
411
- scoring_table_parameter: stp_years,
412
- min_range_value: 2,
413
- max_range_value: 4
414
- ) { |n| n.name = "Emerging"; n.normalized_value = 0.50 }
415
-
416
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
417
- scoring_table_parameter: stp_years,
418
- min_range_value: 5,
419
- max_range_value: 9
420
- ) { |n| n.name = "Established"; n.normalized_value = 0.80 }
421
-
422
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
423
- scoring_table_parameter: stp_years,
424
- min_range_value: 10,
425
- max_range_value: 99
426
- ) { |n| n.name = "Veteran"; n.normalized_value = 1.00 }
427
-
428
- puts " ✓ stp_years (id=#{stp_years.id}, weight=0.25) + 4 normalizers"
429
-
430
- # ---------------------------------------------------------------------------
431
- # STP 3: business_type — string — weight 0.20 — source: applicant
432
- # ---------------------------------------------------------------------------
433
- stp_biz_type = Dscf::Credit::ScoringTableParameter.find_or_create_by!(
434
- scoring_table: dev_scoring_table,
435
- scoring_parameter: param_business_type
436
- ) do |stp|
437
- stp.weight = 0.20
438
- stp.information_source = info_source_applicant
439
- stp.required = true
440
- stp.order = 3
441
- stp.created_by = admin_user
442
- end
443
-
444
- # Text-based normalizers for business_type (string)
445
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
446
- scoring_table_parameter: stp_biz_type,
447
- text_value: "retail"
448
- ) { |n| n.name = "Retail Business"; n.normalized_value = 0.80 }
449
-
450
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
451
- scoring_table_parameter: stp_biz_type,
452
- text_value: "wholesale"
453
- ) { |n| n.name = "Wholesale Business"; n.normalized_value = 0.90 }
454
-
455
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
456
- scoring_table_parameter: stp_biz_type,
457
- text_value: "startup"
458
- ) { |n| n.name = "Startup Business"; n.normalized_value = 0.30 }
459
-
460
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
461
- scoring_table_parameter: stp_biz_type,
462
- text_value: "service"
463
- ) { |n| n.name = "Service Business"; n.normalized_value = 0.60 }
464
-
465
- puts " ✓ stp_biz_type (id=#{stp_biz_type.id}, weight=0.20) + 4 normalizers"
466
-
467
- # ---------------------------------------------------------------------------
468
- # STP 4: payment_track_record — string — weight 0.20 — source: branch_officer
469
- # ---------------------------------------------------------------------------
470
- stp_payment = Dscf::Credit::ScoringTableParameter.find_or_create_by!(
471
- scoring_table: dev_scoring_table,
472
- scoring_parameter: param_payment_track_record
473
- ) do |stp|
474
- stp.weight = 0.20
475
- stp.information_source = info_source_branch_officer
476
- stp.required = true
477
- stp.order = 4
478
- stp.created_by = admin_user
479
- end
480
-
481
- # Text-based normalizers for payment_track_record (string)
482
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
483
- scoring_table_parameter: stp_payment,
484
- text_value: "excellent"
485
- ) { |n| n.name = "Excellent Payment Record"; n.normalized_value = 1.00 }
486
-
487
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
488
- scoring_table_parameter: stp_payment,
489
- text_value: "good"
490
- ) { |n| n.name = "Good Payment Record"; n.normalized_value = 0.75 }
491
-
492
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
493
- scoring_table_parameter: stp_payment,
494
- text_value: "fair"
495
- ) { |n| n.name = "Fair Payment Record"; n.normalized_value = 0.50 }
496
-
497
- Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
498
- scoring_table_parameter: stp_payment,
499
- text_value: "poor"
500
- ) { |n| n.name = "Poor Payment Record"; n.normalized_value = 0.20 }
501
-
502
- puts " ✓ stp_payment (id=#{stp_payment.id}, weight=0.20) + 4 normalizers"
503
- puts " Total weight check: 0.35 + 0.25 + 0.20 + 0.20 = 1.00 ✓"
504
-
505
- # =============================================================================
506
- # 5. DEV CREDIT LINES + SPECS
507
- # =============================================================================
508
- puts ""
509
- puts "--- [5/8] DEV Credit Lines & Specs ---"
510
-
511
- # ---------------------------------------------------------------------------
512
- # Credit Line A: 30-Day Retail Credit (DEV-RET-30D)
513
- # ---------------------------------------------------------------------------
514
- credit_line_30d = Dscf::Credit::CreditLine.find_or_create_by!(
515
- bank: bunna_bank,
516
- code: "DEV-RET-30D"
517
- ) do |cl|
518
- cl.name = "DEV 30-Day Retail Credit"
519
- cl.credit_product = dev_credit_product
520
- cl.description = "[DEV] 30-day short-term retail supplier credit"
521
- cl.created_by = admin_user
522
- end
523
-
524
- if defined?(Dscf::Core::Review)
525
- Dscf::Core::Review.find_or_create_by!(reviewable: credit_line_30d, context: "default") do |r|
526
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
527
- end
528
- end
529
-
530
- Dscf::Credit::CreditLineSpec.find_or_create_by!(credit_line: credit_line_30d) do |s|
531
- s.min_amount = 10_000.00
532
- s.max_amount = 200_000.00
533
- s.interest_rate = 0.15 # 15% per annum
534
- s.penalty_rate = 0.02 # 2%
535
- s.facilitation_fee_rate = 0.01 # 1%
536
- s.tax_rate = 0.05 # 5%
537
- s.max_penalty_days = 15
538
- s.loan_duration = 30
539
- s.interest_frequency = "daily"
540
- s.interest_income_tax = 0.0300
541
- s.vat = 0.15
542
- s.max_interest_calculation_days = 30
543
- s.penalty_frequency = "daily"
544
- s.penalty_income_tax = 0.025
545
- s.minimum_score = 45.0
546
- s.credit_line_multiplier = 0.50
547
- s.credit_line_divider = 1
548
- s.active = true
549
- s.base_scoring_parameter = param_avg_monthly_purchase
550
- s.created_by = admin_user
551
- end
552
-
553
- # ---------------------------------------------------------------------------
554
- # Credit Line B: 90-Day Retail Credit (DEV-RET-90D)
555
- # ---------------------------------------------------------------------------
556
- credit_line_90d = Dscf::Credit::CreditLine.find_or_create_by!(
557
- bank: bunna_bank,
558
- code: "DEV-RET-90D"
559
- ) do |cl|
560
- cl.name = "DEV 90-Day Retail Credit"
561
- cl.credit_product = dev_credit_product
562
- cl.description = "[DEV] 90-day medium-term retail supplier credit"
563
- cl.created_by = admin_user
564
- end
565
-
566
- if defined?(Dscf::Core::Review)
567
- Dscf::Core::Review.find_or_create_by!(reviewable: credit_line_90d, context: "default") do |r|
568
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
569
- end
570
- end
571
-
572
- Dscf::Credit::CreditLineSpec.find_or_create_by!(credit_line: credit_line_90d) do |s|
573
- s.min_amount = 25_000.00
574
- s.max_amount = 500_000.00
575
- s.interest_rate = 0.18 # 18% per annum
576
- s.penalty_rate = 0.025
577
- s.facilitation_fee_rate = 0.015
578
- s.tax_rate = 0.05
579
- s.max_penalty_days = 30
580
- s.loan_duration = 90
581
- s.interest_frequency = "monthly"
582
- s.interest_income_tax = 0.035
583
- s.vat = 0.15
584
- s.max_interest_calculation_days = 90
585
- s.penalty_frequency = "daily"
586
- s.penalty_income_tax = 0.030
587
- s.minimum_score = 55.0
588
- s.credit_line_multiplier = 1.00
589
- s.credit_line_divider = 1
590
- s.active = true
591
- s.base_scoring_parameter = param_avg_monthly_purchase
592
- s.created_by = admin_user
593
- end
594
-
595
- # ---------------------------------------------------------------------------
596
- # Credit Line C: 180-Day Wholesale Credit (DEV-WHO-180D)
597
- # ---------------------------------------------------------------------------
598
- credit_line_180d = Dscf::Credit::CreditLine.find_or_create_by!(
599
- bank: bunna_bank,
600
- code: "DEV-WHO-180D"
601
- ) do |cl|
602
- cl.name = "DEV 180-Day Wholesale Credit"
603
- cl.credit_product = dev_credit_product
604
- cl.description = "[DEV] 180-day long-term wholesale distributor credit"
605
- cl.created_by = admin_user
606
- end
607
-
608
- if defined?(Dscf::Core::Review)
609
- Dscf::Core::Review.find_or_create_by!(reviewable: credit_line_180d, context: "default") do |r|
103
+ Dscf::Core::Review.find_or_create_by!(reviewable: dev_credit_product, context: "default") do |r|
104
+ r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
105
+ end
106
+ puts "✓ Credit product: #{dev_credit_product.name} (scoring_table=#{dev_scoring_table.code})"
107
+
108
+ # ── 5. Scoring Table Parameters (weights) ─────────────────
109
+ puts "\n── Seeding scoring table parameters ──"
110
+
111
+ # Weights: 0.35 + 0.25 + 0.20 + 0.20 = 1.00
112
+ stp_config = [
113
+ { param_key: "avg_monthly_purchase", weight: 0.35, source: info_source_facilitator,
114
+ min_value: 0.0, max_value: 5_000_000.0, order: 1 },
115
+ { param_key: "years_in_business", weight: 0.25, source: info_source_applicant,
116
+ min_value: 0.0, max_value: 50.0, order: 2 },
117
+ { param_key: "business_type", weight: 0.20, source: info_source_applicant,
118
+ min_value: nil, max_value: nil, order: 3 },
119
+ { param_key: "payment_track_record", weight: 0.20, source: info_source_branch,
120
+ min_value: nil, max_value: nil, order: 4 }
121
+ ]
122
+
123
+ dev_stps = {}
124
+ stp_config.each do |cfg|
125
+ param = dev_params[cfg[:param_key]]
126
+ stp = Dscf::Credit::ScoringTableParameter.find_or_create_by!(
127
+ scoring_table: dev_scoring_table, scoring_parameter: param
128
+ ) do |s|
129
+ s.weight = cfg[:weight]
130
+ s.information_source = cfg[:source]
131
+ s.min_value = cfg[:min_value]
132
+ s.max_value = cfg[:max_value]
133
+ s.required = true
134
+ s.order = cfg[:order]
135
+ s.created_by = admin_user
136
+ end
137
+ dev_stps[cfg[:param_key]] = stp
138
+ puts " ✓ #{cfg[:param_key]}: weight=#{cfg[:weight]}, source=#{cfg[:source].code}"
139
+ end
140
+
141
+ # ── 6. Scoring Table Normalizers ──────────────────────────
142
+ puts "\n── Seeding scoring table normalizers ──"
143
+
144
+ # avg_monthly_purchase — range-based (decimal)
145
+ [
146
+ { name: "Very Low Volume", min: 0, max: 49_999.99, nv: 0.10 },
147
+ { name: "Low Volume", min: 50_000, max: 149_999.99, nv: 0.40 },
148
+ { name: "Medium Volume", min: 150_000, max: 349_999.99, nv: 0.70 },
149
+ { name: "High Volume", min: 350_000, max: 999_999.99, nv: 0.95 },
150
+ { name: "Very High Volume", min: 1_000_000, max: 5_000_000.00, nv: 1.00 }
151
+ ].each do |n|
152
+ Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
153
+ scoring_table_parameter: dev_stps["avg_monthly_purchase"],
154
+ min_range_value: n[:min], max_range_value: n[:max]
155
+ ) { |norm| norm.name = n[:name]; norm.normalized_value = n[:nv] }
156
+ end
157
+
158
+ # years_in_business — range-based (integer)
159
+ [
160
+ { name: "New (0-1 years)", min: 0, max: 1.99, nv: 0.10 },
161
+ { name: "Emerging (2-4 years)", min: 2, max: 4.99, nv: 0.50 },
162
+ { name: "Established (5-9 years)", min: 5, max: 9.99, nv: 0.80 },
163
+ { name: "Veteran (10+ years)", min: 10, max: 50.00, nv: 1.00 }
164
+ ].each do |n|
165
+ Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
166
+ scoring_table_parameter: dev_stps["years_in_business"],
167
+ min_range_value: n[:min], max_range_value: n[:max]
168
+ ) { |norm| norm.name = n[:name]; norm.normalized_value = n[:nv] }
169
+ end
170
+
171
+ # business_type — text-based
172
+ [
173
+ { name: "Micro Business", text: "micro", nv: 0.30 },
174
+ { name: "Startup Business", text: "startup", nv: 0.30 },
175
+ { name: "Growing Business", text: "growing", nv: 0.60 },
176
+ { name: "Established Business", text: "established", nv: 0.80 },
177
+ { name: "Enterprise Business", text: "enterprise", nv: 1.00 }
178
+ ].each do |n|
179
+ Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
180
+ scoring_table_parameter: dev_stps["business_type"],
181
+ text_value: n[:text]
182
+ ) { |norm| norm.name = n[:name]; norm.normalized_value = n[:nv] }
183
+ end
184
+
185
+ # payment_track_record — text-based
186
+ [
187
+ { name: "Poor Track Record", text: "poor", nv: 0.20 },
188
+ { name: "Fair Track Record", text: "fair", nv: 0.50 },
189
+ { name: "Good Track Record", text: "good", nv: 0.75 },
190
+ { name: "Excellent Track Record", text: "excellent", nv: 1.00 }
191
+ ].each do |n|
192
+ Dscf::Credit::ScoringTableNormalizer.find_or_create_by!(
193
+ scoring_table_parameter: dev_stps["payment_track_record"],
194
+ text_value: n[:text]
195
+ ) { |norm| norm.name = n[:name]; norm.normalized_value = n[:nv] }
196
+ end
197
+ puts "✓ #{Dscf::Credit::ScoringTableNormalizer.where(scoring_table_parameter: dev_stps.values).count} normalizers seeded"
198
+
199
+ # ── 7. DEV Credit Lines (30 / 60 / 90 day) ───────────────
200
+ puts "\n── Seeding DEV credit lines ──"
201
+
202
+ avg_purchase_param = dev_params["avg_monthly_purchase"]
203
+
204
+ credit_line_defs = [
205
+ { name: "30-Day Retail Credit", code: "DEV-RET-30D", duration: 30,
206
+ min: 10_000, max: 200_000, interest: 0.0008, penalty: 0.0005,
207
+ fee: 0.010, tax: 0.05, multiplier: 5.0, min_score: 45.0 },
208
+ { name: "60-Day Retail Credit", code: "DEV-RET-60D", duration: 60,
209
+ min: 25_000, max: 400_000, interest: 0.0006, penalty: 0.0004,
210
+ fee: 0.015, tax: 0.05, multiplier: 10.0, min_score: 55.0 },
211
+ { name: "90-Day Retail Credit", code: "DEV-RET-90D", duration: 90,
212
+ min: 50_000, max: 800_000, interest: 0.0005, penalty: 0.0003,
213
+ fee: 0.020, tax: 0.05, multiplier: 20.0, min_score: 70.0 }
214
+ ]
215
+
216
+ dev_credit_lines = {}
217
+ credit_line_defs.each do |cl_def|
218
+ cl = Dscf::Credit::CreditLine.find_or_create_by!(bank: bunna_bank, code: cl_def[:code]) do |line|
219
+ line.name = cl_def[:name]
220
+ line.credit_product = dev_credit_product
221
+ line.description = "DEV #{cl_def[:name]} for testing"
222
+ line.created_by = admin_user
223
+ end
224
+ Dscf::Core::Review.find_or_create_by!(reviewable: cl, context: "default") do |r|
610
225
  r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
611
226
  end
612
- end
613
227
 
614
- Dscf::Credit::CreditLineSpec.find_or_create_by!(credit_line: credit_line_180d) do |s|
615
- s.min_amount = 100_000.00
616
- s.max_amount = 1_000_000.00
617
- s.interest_rate = 0.14 # 14% per annum
618
- s.penalty_rate = 0.015
619
- s.facilitation_fee_rate = 0.008
620
- s.tax_rate = 0.05
621
- s.max_penalty_days = 30
622
- s.loan_duration = 180
623
- s.interest_frequency = "monthly"
624
- s.interest_income_tax = 0.030
625
- s.vat = 0.15
626
- s.max_interest_calculation_days = 180
627
- s.penalty_frequency = "weekly"
628
- s.penalty_income_tax = 0.025
629
- s.minimum_score = 70.0
630
- s.credit_line_multiplier = 2.00
631
- s.credit_line_divider = 1
632
- s.active = true
633
- s.base_scoring_parameter = param_avg_monthly_purchase
634
- s.created_by = admin_user
228
+ spec = Dscf::Credit::CreditLineSpec.find_or_create_by!(credit_line: cl) do |s|
229
+ s.min_amount = cl_def[:min]
230
+ s.max_amount = cl_def[:max]
231
+ s.interest_rate = cl_def[:interest]
232
+ s.penalty_rate = cl_def[:penalty]
233
+ s.facilitation_fee_rate = cl_def[:fee]
234
+ s.tax_rate = cl_def[:tax]
235
+ s.credit_line_multiplier = cl_def[:multiplier]
236
+ s.base_scoring_parameter = avg_purchase_param
237
+ s.credit_line_divider = nil
238
+ s.max_penalty_days = 30
239
+ s.loan_duration = cl_def[:duration]
240
+ s.interest_frequency = "daily"
241
+ s.interest_income_tax = 0.03
242
+ s.vat = 0.15
243
+ s.max_interest_calculation_days = cl_def[:duration] + 30
244
+ s.penalty_frequency = "daily"
245
+ s.penalty_income_tax = 0.025
246
+ s.minimum_score = cl_def[:min_score]
247
+ s.active = true
248
+ s.created_by = admin_user
249
+ end
250
+ dev_credit_lines[cl_def[:code]] = { credit_line: cl, spec: spec }
251
+ puts " ✓ #{cl.name}: duration=#{cl_def[:duration]}d, min_score=#{cl_def[:min_score]}, multiplier=#{cl_def[:multiplier]}"
252
+ end
253
+
254
+ # ── 8. DEV Users (loan applicants) ────────────────────────
255
+ puts "\n── Seeding DEV applicant users ──"
256
+
257
+ dev_users = {}
258
+ [
259
+ { email: "sara.kebede@test.et", phone: "+251911001001", first: "Sara", last: "Kebede" },
260
+ { email: "mekdes.tadesse@test.et", phone: "+251911002002", first: "Mekdes", last: "Tadesse" },
261
+ { email: "dawit.alemu@test.et", phone: "+251911003003", first: "Dawit", last: "Alemu" }
262
+ ].each do |u|
263
+ user = Dscf::Core::User.find_or_create_by!(email: u[:email]) do |usr|
264
+ usr.phone = u[:phone]
265
+ usr.verified_at = Time.current
266
+ usr.temp_password = false
267
+ usr.password = "SecurePassword123!"
268
+ end
269
+ Dscf::Core::UserProfile.find_or_create_by!(user: user) do |p|
270
+ p.first_name = u[:first]
271
+ p.last_name = u[:last]
272
+ p.watchlist_hit = false
273
+ end
274
+ user_role = Dscf::Core::Role.find_by!(code: "USER")
275
+ Dscf::Core::UserRole.find_or_create_by!(user: user, role: user_role) do |ur|
276
+ ur.assigned_by = admin_user
277
+ end
278
+ dev_users[u[:first].downcase] = user
279
+ puts " ✓ #{u[:first]} #{u[:last]} (#{u[:email]})"
280
+ end
281
+
282
+ sara_user = dev_users["sara"]
283
+ mekdes_user = dev_users["mekdes"]
284
+ dawit_user = dev_users["dawit"]
285
+
286
+ # ── 9. Loan Applications ─────────────────────────────────
287
+ puts "\n── Seeding DEV loan applications ──"
288
+
289
+ sara_app = Dscf::Credit::LoanApplication.find_or_create_by!(
290
+ bank: bunna_bank, user: sara_user, credit_product: dev_credit_product
291
+ ) do |app|
292
+ app.review_branch = bunna_head_office
293
+ app.bank_statement_source = "internal"
294
+ app.backer = facilitator1
295
+ end
296
+ Dscf::Core::Review.find_or_create_by!(reviewable: sara_app, context: "default") do |r|
297
+ r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
298
+ end
299
+
300
+ mekdes_app = Dscf::Credit::LoanApplication.find_or_create_by!(
301
+ bank: bunna_bank, user: mekdes_user, credit_product: dev_credit_product
302
+ ) do |app|
303
+ app.review_branch = bunna_head_office
304
+ app.bank_statement_source = "internal"
305
+ app.backer = facilitator1
306
+ end
307
+ Dscf::Core::Review.find_or_create_by!(reviewable: mekdes_app, context: "default") do |r|
308
+ r.status = "draft"; r.reviewed_by = admin_user
309
+ end
310
+
311
+ dawit_app = Dscf::Credit::LoanApplication.find_or_create_by!(
312
+ bank: bunna_bank, user: dawit_user, credit_product: dev_credit_product
313
+ ) do |app|
314
+ app.review_branch = bunna_head_office
315
+ app.bank_statement_source = "internal"
316
+ app.backer = facilitator1
317
+ end
318
+ Dscf::Core::Review.find_or_create_by!(reviewable: dawit_app, context: "default") do |r|
319
+ r.status = "draft"; r.reviewed_by = admin_user
320
+ end
321
+
322
+ puts " ✓ Sara's application (id=#{sara_app.id})"
323
+ puts " ✓ Mekdes's application (id=#{mekdes_app.id})"
324
+ puts " ✓ Dawit's application (id=#{dawit_app.id})"
325
+
326
+ # ── 10. Loan Application Data ────────────────────────────
327
+ puts "\n── Seeding loan application source data ──"
328
+
329
+ param_ids = {
330
+ avg_monthly_purchase: dev_params["avg_monthly_purchase"].id.to_s,
331
+ years_in_business: dev_params["years_in_business"].id.to_s,
332
+ business_type: dev_params["business_type"].id.to_s,
333
+ payment_track_record: dev_params["payment_track_record"].id.to_s
334
+ }
335
+
336
+ # ── Sara (score = 89.25) ──
337
+ # avg_monthly_purchase = 450,000 → High Volume (0.95) × 0.35 = 0.3325
338
+ # years_in_business = 8 → Established (0.80) × 0.25 = 0.2000
339
+ # business_type = established → (0.80) × 0.20 = 0.1600
340
+ # payment_track_record = excellent → (1.00) × 0.20 = 0.2000
341
+ # Total: (0.3325 + 0.2000 + 0.1600 + 0.2000) / 1.00 × 100 = 89.25 ✓
342
+ # Sara's raw data values:
343
+ # avg_monthly_purchase = 450,000 (facilitator source)
344
+ # years_in_business = 8 (applicant source)
345
+ # business_type = "established" (applicant source)
346
+ # payment_track_record = "excellent" (branch_officer source)
347
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
348
+ loan_application: sara_app, information_source: info_source_facilitator
349
+ ) do |d|
350
+ d.data = { param_ids[:avg_monthly_purchase] => 450_000 }
351
+ d.submitted_by = admin_user
352
+ d.submitted_at = Time.current
353
+ end
354
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
355
+ loan_application: sara_app, information_source: info_source_applicant
356
+ ) do |d|
357
+ d.data = { param_ids[:years_in_business] => 8, param_ids[:business_type] => "established" }
358
+ d.submitted_by = admin_user
359
+ d.submitted_at = Time.current
360
+ end
361
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
362
+ loan_application: sara_app, information_source: info_source_branch
363
+ ) do |d|
364
+ d.data = { param_ids[:payment_track_record] => "excellent" }
365
+ d.submitted_by = admin_user
366
+ d.submitted_at = Time.current
367
+ end
368
+ puts " ✓ Sara: avg_monthly_purchase=450,000 | years_in_business=8 | business_type=established | payment_track_record=excellent"
369
+
370
+ # ── Mekdes (score = 47.50 when scored) ──
371
+ # avg_monthly_purchase = 120,000 → Low Volume (0.40) × 0.35 = 0.140
372
+ # years_in_business = 3 → Emerging (0.50) × 0.25 = 0.125
373
+ # business_type = startup → (0.30) × 0.20 = 0.060
374
+ # payment_track_record = good → (0.75) × 0.20 = 0.150
375
+ # Total: 0.475 × 100 = 47.50 ✓
376
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
377
+ loan_application: mekdes_app, information_source: info_source_facilitator
378
+ ) do |d|
379
+ d.data = { param_ids[:avg_monthly_purchase] => 120_000 }
380
+ d.submitted_by = admin_user
381
+ d.submitted_at = Time.current
382
+ end
383
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
384
+ loan_application: mekdes_app, information_source: info_source_applicant
385
+ ) do |d|
386
+ d.data = { param_ids[:years_in_business] => 3, param_ids[:business_type] => "startup" }
387
+ d.submitted_by = admin_user
388
+ d.submitted_at = Time.current
389
+ end
390
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
391
+ loan_application: mekdes_app, information_source: info_source_branch
392
+ ) do |d|
393
+ d.data = { param_ids[:payment_track_record] => "good" }
394
+ d.submitted_by = admin_user
395
+ d.submitted_at = Time.current
396
+ end
397
+ puts " ✓ Mekdes: avg_monthly_purchase=120,000 | years_in_business=3 | business_type=startup | payment_track_record=good"
398
+
399
+ # ── Dawit (score = 16.00 when scored) ──
400
+ # avg_monthly_purchase = 30,000 → Very Low (0.10) × 0.35 = 0.035
401
+ # years_in_business = 1 → New (0.10) × 0.25 = 0.025
402
+ # business_type = micro → (0.30) × 0.20 = 0.060
403
+ # payment_track_record = poor → (0.20) × 0.20 = 0.040
404
+ # Total: 0.160 × 100 = 16.00 ✓
405
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
406
+ loan_application: dawit_app, information_source: info_source_facilitator
407
+ ) do |d|
408
+ d.data = { param_ids[:avg_monthly_purchase] => 30_000 }
409
+ d.submitted_by = admin_user
410
+ d.submitted_at = Time.current
411
+ end
412
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
413
+ loan_application: dawit_app, information_source: info_source_applicant
414
+ ) do |d|
415
+ d.data = { param_ids[:years_in_business] => 1, param_ids[:business_type] => "micro" }
416
+ d.submitted_by = admin_user
417
+ d.submitted_at = Time.current
418
+ end
419
+ Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
420
+ loan_application: dawit_app, information_source: info_source_branch
421
+ ) do |d|
422
+ d.data = { param_ids[:payment_track_record] => "poor" }
423
+ d.submitted_by = admin_user
424
+ d.submitted_at = Time.current
425
+ end
426
+ puts " ✓ Dawit: avg_monthly_purchase=30,000 | years_in_business=1 | business_type=micro | payment_track_record=poor"
427
+
428
+ # ── 11. Sara: Run scoring engine manually ────────────────
429
+ puts "\n── Running credit scoring for Sara (expected: 89.25) ──"
430
+
431
+ engine = Dscf::Credit::CreditScoringEngine.new(sara_app)
432
+ result = engine.calculate_score
433
+
434
+ unless result[:success]
435
+ puts " ✗ Scoring FAILED: #{result[:errors].join(', ')}"
436
+ puts " Aborting dev seeds."
437
+ exit 1
438
+ end
439
+
440
+ sara_score = result[:score]
441
+ sara_status = result[:status]
442
+ puts " ✓ Score: #{sara_score} | Status: #{sara_status}"
443
+
444
+ # Update the loan application score
445
+ sara_app.update!(score: sara_score)
446
+
447
+ unless sara_status == "approved"
448
+ puts " ⚠ Expected status 'approved' but got '#{sara_status}'. Check normalizer values."
449
+ end
450
+
451
+ # ── 12. Sara: Create Loan Profile via service ────────────
452
+ puts "\n── Creating Sara's loan profile ──"
453
+
454
+ scoring_result_data = {
455
+ scoring_table_id: dev_scoring_table.id,
456
+ scoring_input_data: result[:breakdown].slice("parameters"),
457
+ breakdown: result[:breakdown]
458
+ }
459
+
460
+ unless sara_app.loan_profile.present?
461
+ profile_service = Dscf::Credit::LoanProfileCreationService.new(sara_app, sara_score, scoring_result_data)
462
+ profile_result = profile_service.create_loan_profile
463
+
464
+ unless profile_result[:success]
465
+ puts " ✗ Loan profile creation FAILED: #{profile_result[:error]}"
466
+ exit 1
467
+ end
468
+ sara_app.reload
469
+ end
470
+
471
+ sara_profile = sara_app.loan_profile
472
+ puts " ✓ Loan Profile: #{sara_profile.code} | Score: #{sara_profile.score} | Total Limit: #{sara_profile.total_limit}"
473
+
474
+ # ── 13. Sara: Verify Eligible Credit Lines ────────────────
475
+ puts "\n── Verifying Sara's eligible credit lines ──"
476
+
477
+ sara_profile.reload
478
+ sara_ecls = sara_profile.eligible_credit_lines.includes(:credit_line)
479
+ puts " ✓ #{sara_ecls.count} eligible credit lines:"
480
+ sara_ecls.each do |ecl|
481
+ puts " - #{ecl.credit_line.name}: limit=#{ecl.credit_limit}, available=#{ecl.available_limit}, locked=#{ecl.locked}"
635
482
  end
636
483
 
637
- puts "✓ 3 DEV Credit Lines + Specs created:"
638
- puts " DEV-RET-30D (id=#{credit_line_30d.id}, min_score=45.0, multiplier=0.50)"
639
- puts " DEV-RET-90D (id=#{credit_line_90d.id}, min_score=55.0, multiplier=1.00)"
640
- puts " DEV-WHO-180D (id=#{credit_line_180d.id}, min_score=70.0, multiplier=2.00)"
641
-
642
- # =============================================================================
643
- # 6. DEV USERS + BUSINESSES + FACILITATOR
644
- # =============================================================================
645
- puts ""
646
- puts "--- [6/8] DEV Users, Businesses & Facilitator ---"
484
+ # ── 14. Sara: Disburse a loan from 30-Day Credit Line ────
485
+ puts "\n── Disbursing loan for Sara from 30-Day Credit Line ──"
647
486
 
648
- if defined?(Dscf::Core::User)
487
+ cl_30d = dev_credit_lines["DEV-RET-30D"][:credit_line]
488
+ sara_ecl_30d = sara_ecls.find { |ecl| ecl.credit_line_id == cl_30d.id }
649
489
 
650
- # DEV applicant users
651
- dev_sara = Dscf::Core::User.find_or_create_by!(email: "sara.kebede@dev.et") do |u|
652
- u.phone = "+251911000001"
653
- u.verified_at = Time.current
654
- u.temp_password = false
655
- u.password = "DevPassword123!"
656
- end
657
-
658
- dev_mekdes = Dscf::Core::User.find_or_create_by!(email: "mekdes.tadesse@dev.et") do |u|
659
- u.phone = "+251911000002"
660
- u.verified_at = Time.current
661
- u.temp_password = false
662
- u.password = "DevPassword123!"
663
- end
664
-
665
- dev_dawit = Dscf::Core::User.find_or_create_by!(email: "dawit.alemu@dev.et") do |u|
666
- u.phone = "+251911000003"
667
- u.verified_at = Time.current
668
- u.temp_password = false
669
- u.password = "DevPassword123!"
670
- end
671
-
672
- # DEV facilitator user
673
- dev_facilitator_user = Dscf::Core::User.find_or_create_by!(email: "facilitator.dev@bunna.com") do |u|
674
- u.phone = "+251911000004"
675
- u.verified_at = Time.current
676
- u.temp_password = false
677
- u.password = "DevPassword123!"
678
- end
679
-
680
- puts "✓ DEV users created: sara##{dev_sara.id}, mekdes##{dev_mekdes.id}, dawit##{dev_dawit.id}, facilitator##{dev_facilitator_user.id}"
681
-
682
- # Optional businesses (only if Dscf::Core::Business is available)
683
- if defined?(Dscf::Core::Business) && defined?(Dscf::Core::BusinessType)
684
- individual_btype = Dscf::Core::BusinessType.find_or_create_by!(name: "individual")
685
-
686
- Dscf::Core::Business.find_or_create_by!(user_id: dev_sara.id, business_type_id: individual_btype.id) do |b|
687
- b.name = "DEV Sara Retail Shop"
688
- b.description = "[DEV] Sara Kebede retail grocery"
689
- b.contact_email = dev_sara.email
690
- b.contact_phone = dev_sara.phone
691
- end
692
-
693
- Dscf::Core::Business.find_or_create_by!(user_id: dev_mekdes.id, business_type_id: individual_btype.id) do |b|
694
- b.name = "DEV Mekdes Startup Trade"
695
- b.description = "[DEV] Mekdes Tadesse startup trader"
696
- b.contact_email = dev_mekdes.email
697
- b.contact_phone = dev_mekdes.phone
698
- end
699
-
700
- Dscf::Core::Business.find_or_create_by!(user_id: dev_dawit.id, business_type_id: individual_btype.id) do |b|
701
- b.name = "DEV Dawit Micro Business"
702
- b.description = "[DEV] Dawit Alemu micro-enterprise"
703
- b.contact_email = dev_dawit.email
704
- b.contact_phone = dev_dawit.phone
705
- end
706
-
707
- puts "✓ DEV businesses created for all 3 applicants"
708
- end
709
-
710
- # DEV Facilitator Application
711
- dev_facilitator_application = Dscf::Credit::FacilitatorApplication.find_or_create_by!(
712
- user: dev_facilitator_user,
713
- bank: bunna_bank
714
- ) do |fa|
715
- fa.facilitator_info = {
716
- business_name: "DEV Field Facilitators Ltd",
717
- contact_person: "Dev Facilitator",
718
- phone: "+251911000004",
719
- email: "facilitator.dev@bunna.com",
720
- address: "123 Dev Street, Addis Ababa",
721
- business_type: "financial_intermediary",
722
- license_number: "DEV-LIC-000001",
723
- years_in_business: 3,
724
- monthly_turnover: 100_000.00,
725
- number_of_employees: 5,
726
- business_registration_date: "2022-01-01",
727
- tax_identification_number: "DEV-TIN-000001",
728
- bank_account_details: {
729
- bank_name: "Bunna Bank",
730
- account_number: "DEV0000000001",
731
- account_holder: "DEV Field Facilitators Ltd"
732
- }
733
- }
734
- end
735
-
736
- if defined?(Dscf::Core::Review)
737
- Dscf::Core::Review.find_or_create_by!(reviewable: dev_facilitator_application, context: "default") do |r|
738
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
739
- end
740
- end
741
-
742
- # DEV Facilitator record
743
- dev_facilitator = Dscf::Credit::Facilitator.find_or_create_by!(
744
- facilitator_application: dev_facilitator_application
745
- ) do |f|
746
- f.total_limit = 2_000_000.00
747
- end
748
-
749
- if defined?(Dscf::Core::Review)
750
- Dscf::Core::Review.find_or_create_by!(reviewable: dev_facilitator, context: "default") do |r|
751
- r.status = "approved"; r.reviewed_by = admin_user; r.reviewed_at = Time.current
752
- end
753
- end
754
-
755
- puts "✓ DEV Facilitator Application (id=#{dev_facilitator_application.id}) + Facilitator (id=#{dev_facilitator.id})"
756
-
757
- else
758
- puts "SKIP: Dscf::Core::User not available — cannot create dev users / facilitator."
759
- puts " Scenarios A/B/C require users to be created manually."
760
- end
761
-
762
- # =============================================================================
763
- # 7. LOAN APPLICATIONS + DATA
764
- # All 3 scenarios share: bank=Bunna Bank, review_branch=head_office,
765
- # backer=dev_facilitator, category=dev_category, bank_statement_source=internal
766
- # =============================================================================
767
- puts ""
768
- puts "--- [7/8] DEV Loan Applications + Application Data ---"
769
-
770
- if !defined?(Dscf::Core::User)
771
- puts "SKIP: Cannot create loan applications without Dscf::Core::User."
490
+ if sara_ecl_30d.nil?
491
+ puts " ✗ No eligible credit line found for 30-Day. Skipping disbursement."
772
492
  else
773
- # We resolve the previously created users (safe to re-find):
774
- dev_sara = Dscf::Core::User.find_by!(email: "sara.kebede@dev.et")
775
- dev_mekdes = Dscf::Core::User.find_by!(email: "mekdes.tadesse@dev.et")
776
- dev_dawit = Dscf::Core::User.find_by!(email: "dawit.alemu@dev.et")
777
-
778
- # ---------------------------------------------------------------------------
779
- # SCENARIO A — Sara Kebede (HIGH SCORER → approved, score = 89.25)
780
- # ---------------------------------------------------------------------------
781
- loan_app_sara = Dscf::Credit::LoanApplication.find_or_create_by!(
782
- bank: bunna_bank,
783
- user: dev_sara,
784
- review_branch: dev_branch,
785
- bank_statement_source: "internal"
786
- ) do |app|
787
- app.backer = dev_facilitator
788
- app.credit_product = dev_credit_product
789
- end
790
-
791
- # Score is set directly (mirrors what scoring service would write)
792
- loan_app_sara.update!(score: 89.25) if loan_app_sara.score != 89.25
793
-
794
- # LoanApplication data — 3 sources, each with the params their source provides
795
- # Facilitator submits: avg_monthly_purchase = 380,000
796
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
797
- loan_application: loan_app_sara,
798
- information_source: info_source_facilitator
799
- ) do |d|
800
- d.data = { param_avg_monthly_purchase.id.to_s => 380_000 }
801
- d.submitted_by = dev_facilitator_user
802
- d.submitted_at = Time.current - 2.days
803
- end
804
-
805
- # Applicant submits: years_in_business = 8, business_type = "retail"
806
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
807
- loan_application: loan_app_sara,
808
- information_source: info_source_applicant
809
- ) do |d|
810
- d.data = {
811
- param_years_in_business.id.to_s => 8,
812
- param_business_type.id.to_s => "retail"
813
- }
814
- d.submitted_by = dev_sara
815
- d.submitted_at = Time.current - 2.days
816
- end
817
-
818
- # Branch officer submits: payment_track_record = "excellent"
819
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
820
- loan_application: loan_app_sara,
821
- information_source: info_source_branch_officer
822
- ) do |d|
823
- d.data = { param_payment_track_record.id.to_s => "excellent" }
824
- d.submitted_by = bank_admin
825
- d.submitted_at = Time.current - 1.day
826
- end
827
-
828
- puts " ✓ Scenario A — Sara (loan_app##{loan_app_sara.id}, score=#{loan_app_sara.score})"
829
-
830
- # ---------------------------------------------------------------------------
831
- # SCENARIO B — Mekdes Tadesse (BORDERLINE → pending, score = 47.50)
832
- # ---------------------------------------------------------------------------
833
- loan_app_mekdes = Dscf::Credit::LoanApplication.find_or_create_by!(
834
- bank: bunna_bank,
835
- user: dev_mekdes,
836
- review_branch: dev_branch,
837
- bank_statement_source: "internal"
838
- ) do |app|
839
- app.backer = dev_facilitator
840
- app.credit_product = dev_credit_product
841
- end
842
-
843
- loan_app_mekdes.update!(score: 47.50) if loan_app_mekdes.score != 47.50
844
-
845
- # Facilitator: avg_monthly_purchase = 120,000
846
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
847
- loan_application: loan_app_mekdes,
848
- information_source: info_source_facilitator
849
- ) do |d|
850
- d.data = { param_avg_monthly_purchase.id.to_s => 120_000 }
851
- d.submitted_by = dev_facilitator_user
852
- d.submitted_at = Time.current - 3.days
853
- end
854
-
855
- # Applicant: years_in_business = 3, business_type = "startup"
856
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
857
- loan_application: loan_app_mekdes,
858
- information_source: info_source_applicant
859
- ) do |d|
860
- d.data = {
861
- param_years_in_business.id.to_s => 3,
862
- param_business_type.id.to_s => "startup"
863
- }
864
- d.submitted_by = dev_mekdes
865
- d.submitted_at = Time.current - 3.days
866
- end
867
-
868
- # Branch officer: payment_track_record = "good"
869
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
870
- loan_application: loan_app_mekdes,
871
- information_source: info_source_branch_officer
872
- ) do |d|
873
- d.data = { param_payment_track_record.id.to_s => "good" }
874
- d.submitted_by = bank_admin
875
- d.submitted_at = Time.current - 2.days
876
- end
877
-
878
- puts " ✓ Scenario B — Mekdes (loan_app##{loan_app_mekdes.id}, score=#{loan_app_mekdes.score})"
879
-
880
- # ---------------------------------------------------------------------------
881
- # SCENARIO C — Dawit Alemu (REJECTED → below pending_threshold, score = 16.00)
882
- # ---------------------------------------------------------------------------
883
- loan_app_dawit = Dscf::Credit::LoanApplication.find_or_create_by!(
884
- bank: bunna_bank,
885
- user: dev_dawit,
886
- review_branch: dev_branch,
887
- bank_statement_source: "internal"
888
- ) do |app|
889
- app.backer = dev_facilitator
890
- app.credit_product = dev_credit_product
891
- # Score deliberately left nil — scoring not yet triggered (draft state)
892
- end
893
-
894
- # Facilitator: avg_monthly_purchase = 30,000
895
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
896
- loan_application: loan_app_dawit,
897
- information_source: info_source_facilitator
898
- ) do |d|
899
- d.data = { param_avg_monthly_purchase.id.to_s => 30_000 }
900
- d.submitted_by = dev_facilitator_user
901
- d.submitted_at = Time.current - 1.day
902
- end
903
-
904
- # Applicant: years_in_business = 1, business_type = "startup"
905
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
906
- loan_application: loan_app_dawit,
907
- information_source: info_source_applicant
908
- ) do |d|
909
- d.data = {
910
- param_years_in_business.id.to_s => 1,
911
- param_business_type.id.to_s => "startup"
912
- }
913
- d.submitted_by = dev_dawit
914
- d.submitted_at = Time.current - 1.day
915
- end
916
-
917
- # Branch officer: payment_track_record = "poor"
918
- Dscf::Credit::LoanApplicationDatum.find_or_create_by!(
919
- loan_application: loan_app_dawit,
920
- information_source: info_source_branch_officer
921
- ) do |d|
922
- d.data = { param_payment_track_record.id.to_s => "poor" }
923
- d.submitted_by = bank_admin
924
- d.submitted_at = Time.current
925
- end
926
-
927
- puts " ✓ Scenario C — Dawit (loan_app##{loan_app_dawit.id}, score=#{loan_app_dawit.score || 'nil (draft)'})"
928
-
929
- # ---------------------------------------------------------------------------
930
- # REVIEW STATUS for each loan application
931
- # ---------------------------------------------------------------------------
932
- if defined?(Dscf::Core::Review)
933
- # Sara — approved (score ≥ passing_score)
934
- Dscf::Core::Review.find_or_create_by!(reviewable: loan_app_sara, context: "default") do |r|
935
- r.status = "approved"
936
- r.reviewed_by = bank_admin
937
- r.reviewed_at = Time.current - 1.day
938
- end
939
-
940
- # Mekdes — pending (pending_threshold ≤ score < passing_score)
941
- Dscf::Core::Review.find_or_create_by!(reviewable: loan_app_mekdes, context: "default") do |r|
942
- r.status = "pending"
943
- r.reviewed_by = bank_admin
944
- r.reviewed_at = Time.current - 1.day
945
- end
946
-
947
- # Dawit — draft (data submitted, not yet scored/reviewed)
948
- Dscf::Core::Review.find_or_create_by!(reviewable: loan_app_dawit, context: "default") do |r|
949
- r.status = "draft"
950
- r.reviewed_by = admin_user
951
- r.reviewed_at = nil
493
+ existing_loan = sara_profile.loans.find_by(credit_line: cl_30d)
494
+ if existing_loan
495
+ puts " Loan already exists (id=#{existing_loan.id}, status=#{existing_loan.status})"
496
+ sara_loan = existing_loan
497
+ else
498
+ disbursement = Dscf::Credit::DisbursementService.new(
499
+ amount: 150_000,
500
+ loan_profile: sara_profile,
501
+ eligible_credit_line: sara_ecl_30d
502
+ )
503
+ disb_result = disbursement.process_disbursement
504
+
505
+ unless disb_result[:success]
506
+ puts " Disbursement FAILED: #{disb_result[:error]}"
507
+ puts " Skipping loan creation."
508
+ else
509
+ sara_loan = disb_result[:loan]
510
+ # Set loan to "active" (DisbursementService creates as "disbursed")
511
+ sara_loan.update!(status: "active")
512
+ puts " ✓ Loan created: id=#{sara_loan.id}, principal=#{sara_loan.principal_amount}, status=#{sara_loan.status}"
952
513
  end
953
-
954
- puts " ✓ Reviews set: Sara=approved, Mekdes=pending, Dawit=draft"
955
514
  end
956
- end # if defined?(Dscf::Core::User)
515
+ end
957
516
 
958
- # =============================================================================
959
- # 8. SCENARIO A FULL PIPELINE
960
- # LoanProfile BB010001 ScoringResult EligibleCreditLines Loan → Accruals
961
- # =============================================================================
962
- puts ""
963
- puts "--- [8/8] Scenario A Full Pipeline (Sara — APPROVED) ---"
517
+ # ── 15. Sara: Generate accruals for her loan ──────────────
518
+ if defined?(sara_loan) && sara_loan.present?
519
+ puts "\n── Generating accruals for Sara's loan ──"
964
520
 
965
- if !defined?(Dscf::Core::User)
966
- puts "SKIP: Cannot build Scenario A pipeline without Dscf::Core::User."
967
- else
968
- loan_app_sara ||= Dscf::Credit::LoanApplication.find_by!(
969
- bank: bunna_bank,
970
- user: Dscf::Core::User.find_by!(email: "sara.kebede@dev.et"),
971
- review_branch: dev_branch,
972
- bank_statement_source: "internal"
973
- )
521
+ cl_spec = cl_30d.credit_line_specs.active.first
974
522
 
975
- # ---------------------------------------------------------------------------
976
- # 8.1 LoanProfile BB010001
977
- # score = 89.25 (final_score from scoring calc)
978
- # total_limit = 169,575 + 339,150 + 678,300 = 1,187,025
979
- # ---------------------------------------------------------------------------
980
- # MATH: total_limit = sum of per-credit-line facility limits (see header)
981
- loan_profile_sara = Dscf::Credit::LoanProfile.find_or_create_by!(code: "BB010001") do |lp|
982
- lp.loan_application = loan_app_sara
983
- lp.score = 89.25
984
- lp.total_limit = 1_187_025.00
985
- end
523
+ # DisbursementService already creates the facilitation_fee accrual.
524
+ # Generate daily interest accrual using the generator service
525
+ accrual_service = Dscf::Credit::LoanAccrualGeneratorService.new(loan_ids: [ sara_loan.id ])
526
+ accrual_result = accrual_service.generate_daily_accruals
986
527
 
987
- if defined?(Dscf::Core::Review)
988
- Dscf::Core::Review.find_or_create_by!(reviewable: loan_profile_sara, context: "default") do |r|
989
- r.status = "approved"
990
- r.reviewed_by = bank_admin
991
- r.reviewed_at = Time.current - 1.day
528
+ # Create a tax accrual on the facilitation fee (VAT)
529
+ fac_fee_accrual = sara_loan.loan_accruals.find_by(accrual_type: "facilitation_fee")
530
+ if fac_fee_accrual
531
+ tax_amount = (fac_fee_accrual.amount * cl_spec.vat).round(2)
532
+ Dscf::Credit::LoanAccrual.find_or_create_by!(
533
+ loan: sara_loan, accrual_type: "tax", applied_on: Date.current
534
+ ) do |a|
535
+ a.amount = tax_amount
536
+ a.status = "pending"
992
537
  end
538
+ puts " ✓ Tax accrual: #{tax_amount} ETB (#{cl_spec.vat * 100}% VAT on facilitation fee)"
993
539
  end
994
540
 
995
- puts " ✓ LoanProfile BB010001 (id=#{loan_profile_sara.id}, score=89.25, total_limit=1,187,025)"
996
-
997
- # ---------------------------------------------------------------------------
998
- # 8.2 ScoringResult breakdown with exact engine output format
999
- # ---------------------------------------------------------------------------
1000
- # MATH (full calculation):
1001
- # avg_monthly_purchase : 380,000 → High Volume → normalized 0.95 × weight 0.35 = 0.3325
1002
- # years_in_business : 8 → Established → normalized 0.80 × weight 0.25 = 0.2000
1003
- # business_type : "retail" → normalized 0.80 × weight 0.20 = 0.1600
1004
- # payment_track_record : "excellent" → normalized 1.00 × weight 0.20 = 0.2000
1005
- # ──────────────────────────────────────────────────────────────────────────
1006
- # total_weighted_sum = 0.3325 + 0.2000 + 0.1600 + 0.2000 = 0.8925
1007
- # total_weight = 0.35 + 0.25 + 0.20 + 0.20 = 1.00
1008
- # final_score = (0.8925 / 1.00) × 100 = 89.25
1009
- # passing_score = 65.0 → 89.25 ≥ 65.0 → status = "approved"
1010
- sara_scoring_result = Dscf::Credit::ScoringResult.find_or_create_by!(
1011
- loan_profile: loan_profile_sara,
1012
- scoring_table: dev_scoring_table
1013
- ) do |sr|
1014
- sr.score = 89.25
1015
- sr.total_limit = 1_187_025.00
1016
- sr.active = true
1017
- sr.created_by = admin_user
1018
-
1019
- sr.scoring_input_data = {
1020
- "facilitator" => { param_avg_monthly_purchase.id.to_s => 380_000 },
1021
- "applicant" => {
1022
- param_years_in_business.id.to_s => 8,
1023
- param_business_type.id.to_s => "retail"
1024
- },
1025
- "branch_officer" => { param_payment_track_record.id.to_s => "excellent" }
1026
- }
1027
-
1028
- sr.breakdown = {
1029
- "parameters" => [
1030
- {
1031
- "parameter_id" => param_avg_monthly_purchase.id,
1032
- "code" => "dev_avg_monthly_purchase",
1033
- "name" => "DEV Avg Monthly Purchase",
1034
- "data_type" => "decimal",
1035
- "information_source" => "Facilitator",
1036
- "raw_value" => 380_000,
1037
- "normalized_value" => 0.95,
1038
- "weight" => 0.35,
1039
- # MATH: 0.95 × 0.35 = 0.3325
1040
- "weighted_contribution" => 0.3325
1041
- },
1042
- {
1043
- "parameter_id" => param_years_in_business.id,
1044
- "code" => "dev_years_in_business",
1045
- "name" => "DEV Years in Business",
1046
- "data_type" => "integer",
1047
- "information_source" => "Applicant",
1048
- "raw_value" => 8,
1049
- "normalized_value" => 0.80,
1050
- "weight" => 0.25,
1051
- # MATH: 0.80 × 0.25 = 0.2000
1052
- "weighted_contribution" => 0.2000
1053
- },
1054
- {
1055
- "parameter_id" => param_business_type.id,
1056
- "code" => "dev_business_type",
1057
- "name" => "DEV Business Type",
1058
- "data_type" => "string",
1059
- "information_source" => "Applicant",
1060
- "raw_value" => "retail",
1061
- "normalized_value" => 0.80,
1062
- "weight" => 0.20,
1063
- # MATH: 0.80 × 0.20 = 0.1600
1064
- "weighted_contribution" => 0.1600
1065
- },
1066
- {
1067
- "parameter_id" => param_payment_track_record.id,
1068
- "code" => "dev_payment_track_record",
1069
- "name" => "DEV Payment Track Record",
1070
- "data_type" => "string",
1071
- "information_source" => "Branch Officer",
1072
- "raw_value" => "excellent",
1073
- "normalized_value" => 1.00,
1074
- "weight" => 0.20,
1075
- # MATH: 1.00 × 0.20 = 0.2000
1076
- "weighted_contribution" => 0.2000
1077
- }
1078
- ],
1079
- # MATH: 0.3325 + 0.2000 + 0.1600 + 0.2000 = 0.8925
1080
- "total_weighted_sum" => 0.8925,
1081
- "total_weight" => 1.0,
1082
- "parameters_processed" => 4,
1083
- "parameters_skipped" => 0,
1084
- "scoring_table_code" => "dev_retailer_v1",
1085
- "passing_score" => 65.0,
1086
- "pending_threshold" => 45.0,
1087
- # MATH: (0.8925 / 1.0) × 100 = 89.25
1088
- "final_score" => 89.25,
1089
- "status" => "approved"
1090
- }
541
+ sara_loan.reload
542
+ puts " ✓ Total accruals: #{sara_loan.loan_accruals.count}"
543
+ sara_loan.loan_accruals.group(:accrual_type).sum(:amount).each do |type, total|
544
+ puts " - #{type}: #{total} ETB"
1091
545
  end
546
+ end
1092
547
 
1093
- puts " ✓ ScoringResult (id=#{sara_scoring_result.id}, score=89.25, status=approved)"
1094
-
1095
- # ---------------------------------------------------------------------------
1096
- # 8.3 Eligible Credit Lines (3 credit lines — all pass Sara's 89.25 score)
1097
- # ---------------------------------------------------------------------------
1098
- # MATH for facility limits (base_metric = avg_monthly_purchase ÷ credit_line_divider):
1099
- # base_metric = 380,000 ÷ 1 = 380,000
1100
- # Credit Line A: 380,000 × 0.50 × (89.25/100) = 169,575 → within [10k, 200k] ✓
1101
- # Credit Line B: 380,000 × 1.00 × (89.25/100) = 339,150 → within [25k, 500k] ✓
1102
- # Credit Line C: 380,000 × 2.00 × (89.25/100) = 678,300 → within [100k, 1M] ✓
1103
-
1104
- # Credit Line A (min_score 45.0 ≤ 89.25 ✅)
1105
- ecl_a = Dscf::Credit::EligibleCreditLine.find_or_create_by!(
1106
- loan_profile: loan_profile_sara,
1107
- credit_line: credit_line_30d
1108
- ) do |ecl|
1109
- # MATH: 380,000 × 0.50 × 0.8925 = 169,575.00
1110
- ecl.credit_limit = 169_575.00
1111
- ecl.available_limit = 169_575.00
1112
- ecl.risk = 0.11 # low risk — high score applicant
1113
- ecl.locked = false
1114
- end
1115
-
1116
- # Credit Line B (min_score 55.0 ≤ 89.25 ✅)
1117
- ecl_b = Dscf::Credit::EligibleCreditLine.find_or_create_by!(
1118
- loan_profile: loan_profile_sara,
1119
- credit_line: credit_line_90d
1120
- ) do |ecl|
1121
- # MATH: 380,000 × 1.00 × 0.8925 = 339,150.00
1122
- ecl.credit_limit = 339_150.00
1123
- ecl.available_limit = 339_150.00
1124
- ecl.risk = 0.11
1125
- ecl.locked = false
1126
- end
1127
-
1128
- # Credit Line C (min_score 70.0 ≤ 89.25 ✅)
1129
- ecl_c = Dscf::Credit::EligibleCreditLine.find_or_create_by!(
1130
- loan_profile: loan_profile_sara,
1131
- credit_line: credit_line_180d
1132
- ) do |ecl|
1133
- # MATH: 380,000 × 2.00 × 0.8925 = 678,300.00
1134
- ecl.credit_limit = 678_300.00
1135
- ecl.available_limit = 678_300.00
1136
- ecl.risk = 0.11
1137
- ecl.locked = false
1138
- end
1139
-
1140
- puts " ✓ 3 EligibleCreditLines:"
1141
- puts " Credit Line A (30D): limit=169,575 (id=#{ecl_a.id})"
1142
- puts " Credit Line B (90D): limit=339,150 (id=#{ecl_b.id})"
1143
- puts " Credit Line C (180D): limit=678,300 (id=#{ecl_c.id})"
1144
- # MATH: 169,575 + 339,150 + 678,300 = 1,187,025
1145
- puts " Total limit = 1,187,025 ✓"
1146
-
1147
- # ---------------------------------------------------------------------------
1148
- # 8.4 Active Loan — Credit Line A, 150,000 ETB, disbursed 30 days ago
1149
- # ---------------------------------------------------------------------------
1150
- dev_loan_sara = Dscf::Credit::Loan.find_or_create_by!(
1151
- loan_profile: loan_profile_sara,
1152
- credit_line: credit_line_30d
1153
- ) do |loan|
1154
- loan.principal_amount = 150_000.00
1155
- loan.remaining_amount = 150_000.00
1156
- loan.status = "active"
1157
- loan.active = true
1158
- loan.disbursed_at = Time.current - 30.days
1159
- loan.due_date = Date.current + 5
1160
- end
1161
-
1162
- puts " ✓ Active Loan (id=#{dev_loan_sara.id}, principal=150,000 ETB, due=#{dev_loan_sara.due_date})"
1163
-
1164
- # Reduce available_limit on Credit Line A by the disbursed amount
1165
- ecl_a.update!(available_limit: [ ecl_a.credit_limit - dev_loan_sara.principal_amount, 0 ].max) \
1166
- if ecl_a.available_limit == ecl_a.credit_limit
1167
-
1168
- # ---------------------------------------------------------------------------
1169
- # 8.5 Loan Accruals (3 accruals — interest, facilitation_fee, VAT on fee)
1170
- # ---------------------------------------------------------------------------
1171
-
1172
- # Accrual 1: Interest (day 1-30)
1173
- # MATH: 150,000 × 0.15 (annual rate) / 365 × 30 = 1,849.32
1174
- Dscf::Credit::LoanAccrual.find_or_create_by!(
1175
- loan: dev_loan_sara,
1176
- accrual_type: "interest"
1177
- ) do |a|
1178
- # 150,000 × 0.15 / 365 × 30 = 1,849.315... ≈ 1,849.32
1179
- a.amount = 1_849.32
1180
- a.applied_on = Date.current - 29
1181
- a.status = "pending"
1182
- end
1183
-
1184
- # Accrual 2: Facilitation fee (one-off at disbursement)
1185
- # MATH: 150,000 × 0.01 = 1,500.00
1186
- Dscf::Credit::LoanAccrual.find_or_create_by!(
1187
- loan: dev_loan_sara,
1188
- accrual_type: "facilitation_fee"
1189
- ) do |a|
1190
- a.amount = 1_500.00
1191
- a.applied_on = Date.current - 30
1192
- a.status = "pending"
1193
- end
1194
-
1195
- # Accrual 3: VAT on facilitation fee
1196
- # MATH: 1,500 × 0.15 = 225.00
1197
- Dscf::Credit::LoanAccrual.find_or_create_by!(
1198
- loan: dev_loan_sara,
1199
- accrual_type: "tax"
1200
- ) do |a|
1201
- a.amount = 225.00
1202
- a.applied_on = Date.current - 30
1203
- a.status = "pending"
1204
- end
1205
-
1206
- puts " ✓ 3 LoanAccruals:"
1207
- puts " interest = 1,849.32 (150,000 × 15% / 365 × 30)"
1208
- puts " facilitation = 1,500.00 (150,000 × 1%)"
1209
- puts " VAT on fee = 225.00 (1,500 × 15%)"
1210
- puts " total pending = #{dev_loan_sara.loan_accruals.pending.sum(:amount)}"
1211
-
1212
- end # if defined?(Dscf::Core::User)
1213
-
1214
- # =============================================================================
1215
- # FINAL SUMMARY
1216
- # =============================================================================
548
+ # ══════════════════════════════════════════════════════════
549
+ # Summary
550
+ # ══════════════════════════════════════════════════════════
551
+ puts "\n" + "=" * 60
552
+ puts "DEV Seeds — Summary"
553
+ puts "=" * 60
554
+ puts " Category: #{dev_category.name} (id=#{dev_category.id})"
555
+ puts " Scoring Table: #{dev_scoring_table.code} (id=#{dev_scoring_table.id})"
556
+ puts " Scoring Parameters: #{dev_params.values.map { |p| "#{p.code}(#{p.id})" }.join(', ')}"
557
+ puts " Credit Product: #{dev_credit_product.name}"
558
+ puts " Credit Lines: #{dev_credit_lines.keys.join(', ')}"
1217
559
  puts ""
1218
- puts "=" * 70
1219
- puts " DEV SEEDS COMPLETE"
1220
- puts "=" * 70
1221
- puts ""
1222
- puts " Scoring Table: DEV Retailer Scoring (dev_retailer_v1)"
1223
- puts " Category: DEV Retail (scoring parameter organizer only)"
1224
- puts " Credit Product: DEV Retail Credit Product (links scoring table to credit lines)"
1225
- puts " Parameters: #{Dscf::Credit::ScoringParameter.where("code LIKE 'dev_%'").count} dev parameters"
1226
- puts " Credit Lines: DEV-RET-30D, DEV-RET-90D, DEV-WHO-180D"
1227
- puts ""
1228
- puts " SCENARIO A — Sara Kebede (sara.kebede@dev.et)"
1229
- puts " score=89.25 → APPROVED"
1230
- puts " LoanProfile BB010001, total_limit=1,187,025 ETB"
1231
- puts " Active loan 150,000 ETB (Credit Line A, due in 5 days)"
1232
- puts ""
1233
- puts " SCENARIO B — Mekdes Tadesse (mekdes.tadesse@dev.et)"
1234
- puts " score=47.50 → PENDING (awaiting manual approval)"
1235
- puts " No LoanProfile — use: PATCH /loan_applications/:id/approve"
560
+ puts " Sara Kebede:"
561
+ puts " Application: id=#{sara_app.id}"
562
+ puts " Score: #{sara_score}"
563
+ if defined?(sara_profile) && sara_profile
564
+ puts " Loan Profile: #{sara_profile.code} (id=#{sara_profile.id})"
565
+ puts " Total Limit: #{sara_profile.total_limit} ETB"
566
+ puts " ECLs: #{sara_ecls.count}"
567
+ end
568
+ if defined?(sara_loan) && sara_loan
569
+ puts " Active Loan: id=#{sara_loan.id}, principal=#{sara_loan.principal_amount}, status=#{sara_loan.status}"
570
+ end
1236
571
  puts ""
1237
- puts " SCENARIO C — Dawit Alemu (dawit.alemu@dev.et)"
1238
- puts " score=nil (draft) — predicted 16.00 → REJECTED"
1239
- puts " Trigger scoring to see auto-rejection"
572
+ puts " Mekdes Tadesse:"
573
+ puts " Application: id=#{mekdes_app.id}"
574
+ puts " Expected Score: 47.50 (run via API: POST /loan_applications/#{mekdes_app.id}/calculate_credit_score)"
1240
575
  puts ""
1241
-
1242
- if defined?(Dscf::Core::User)
1243
- puts " User counts (core): #{Dscf::Core::User.count} total"
1244
- end
1245
- puts " Banks: #{Dscf::Credit::Bank.count}"
1246
- puts " Credit Lines: #{Dscf::Credit::CreditLine.count}"
1247
- puts " Scoring Tables: #{Dscf::Credit::ScoringTable.count}"
1248
- puts " Credit Products: #{Dscf::Credit::CreditProduct.count}"
1249
- puts " Scoring Parameters: #{Dscf::Credit::ScoringParameter.count}"
1250
- puts " Info Sources: #{Dscf::Credit::InformationSource.count}"
1251
- puts " Loan Applications: #{Dscf::Credit::LoanApplication.count}"
1252
- puts " Loan Profiles: #{Dscf::Credit::LoanProfile.count}"
1253
- puts " Scoring Results: #{Dscf::Credit::ScoringResult.count}"
1254
- puts " Eligible CL: #{Dscf::Credit::EligibleCreditLine.count}"
1255
- puts " Loans: #{Dscf::Credit::Loan.count}"
1256
- puts " Loan Accruals: #{Dscf::Credit::LoanAccrual.count}"
1257
- puts " Facilitators: #{Dscf::Credit::Facilitator.count}"
576
+ puts " Dawit Alemu:"
577
+ puts " Application: id=#{dawit_app.id}"
578
+ puts " Expected Score: 16.00 (run via API: POST /loan_applications/#{dawit_app.id}/calculate_credit_score)"
1258
579
  puts ""
580
+ puts "=" * 60
581
+ puts "DEV Seeds completed successfully!"
582
+ puts "=" * 60