What we need to report, per day, how many applications were submitted for review, how many were approved or rejected, and why the rejected ones failed, at the application grain.
Three questions, at the application grain, sliceable by day. The counts are already answerable today; the reason breakdown is not.
| Metric | Definition | Answerable today? |
|---|---|---|
| Submitted for review / day | COUNT(*) by submitted_at::date | Yes |
| Approved / Rejected / day | split by review_status | Yes |
| Approval rate | approved ÷ submitted | Yes |
| Rejection reason breakdown | COUNT(*) by reject_reason_code | No — reason not stored |
silver.credit_t_cs_credit is already at the right grain, one row per credit application (id), a user can have many. Submit time, decision time, and outcome are all present. Outcome is a clean, terminal 4-value enum (verified over the last 30 days for source='easy_cash'):
| status | Rows / 30d | finish_time | amount | Meaning (inferred, needs sign-off) |
|---|---|---|---|---|
| −1 | 337,070 | all set | 0.00 | Rejected — terminal, no credit granted |
| 1 | 17,935 | all set | 100.00 | Approved — terminal, credit line (fixed 100) |
| 2 | 16 | none | 0.00 | In processing (transient) |
| 0 | 1 | none | 0.00 | Initialised (transient) |
Terminal rows (−1 / 1) always carry a finish_time; approved rows carry a fixed amount = 100.00. That is enough to compute every count above — but the enum meaning is inferred from the data, not from a spec, and needs an official mapping (see REQ-2).
Every candidate field on credit_t_cs_credit that might hold a reason is empty or constant — checked over the same 30-day window:
ext — empty for rejected rows (no JSON payload)type — constant 101 across all 355,044 rowsis_pass_general — constant 0 across all rowsThe rejection reason lives in the risk / credit-decision engine and is not landed in Fabric today. Reporting "未通过原因 / why rejected" is impossible until it is. This is the one real ask of this document.
No brand-new grain is needed, credit_t_cs_credit already is application-level. The ask is to expose it as a clean, typed audit table (or a view) with the three decision-reason fields added. Red rows are the fields that don't exist yet.
| Field | Source today | Status | Note |
|---|---|---|---|
| application_id | id | present | Primary key; one row per application |
| user_id | user_id | present | Borrower; id-space vs loan book to be verified |
| product | source | present | 'easy_cash' |
| submitted_at | created_time | present | Submit-for-review timestamp |
| decided_at | finish_time | present | Null while in review |
| review_status | status | map | Normalise int → APPROVED / REJECTED / IN_REVIEW (REQ-2) |
| approved_amount | amount | fix | Currently varchar ('100.00'); cast numeric + confirm currency |
| reject_reason_code | — | missing | Required. Machine code from the decision engine |
| reject_reason_desc | — | missing | Required. Human-readable reason |
| reject_stage | — | missing | Recommended. Which step declined: anti-fraud / bureau / limit=0 / blacklist / KYC |
| risk_score | — | optional | Decision score, if the engine exposes one |
In priority order. REQ-1 is the only true blocker; the rest are hygiene on an otherwise-usable table.
Add reject_reason_code + reject_reason_desc (and ideally reject_stage) from the risk / credit-decision engine, keyed by application_id. Without this, the reason breakdown cannot be built at all.
Confirm the mapping we inferred (−1 = rejected, 1 = approved, 0 / 2 = transient) and document any other values that can occur, so review_status is derived from a spec, not from observed data.
Expose amount as a numeric column and state the currency. Today it is a varchar and approved rows are a fixed '100.00'; confirm whether that is a real credit limit or a placeholder.
What the reporting queries look like once the table above exists.
-- daily: submitted / approved / rejected / approval rate SELECT CAST(submitted_at AS date) d, COUNT(*) submitted, SUM(CASE WHEN review_status='APPROVED' THEN 1 ELSE 0 END) approved, SUM(CASE WHEN review_status='REJECTED' THEN 1 ELSE 0 END) rejected FROM fact_easycash_application GROUP BY CAST(submitted_at AS date); -- rejection reason breakdown (needs REQ-1) SELECT reject_reason_code, reject_stage, COUNT(*) n FROM fact_easycash_application WHERE review_status='REJECTED' GROUP BY reject_reason_code, reject_stage ORDER BY n DESC;