Skip to content

Commit 2e0786f

Browse files
committed
Add Disclosure 5-3 report and protocol
1 parent 6c8669b commit 2e0786f

File tree

7 files changed

+254
-0
lines changed

7 files changed

+254
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
ReportId,Type,ReportNumber,Dispositions,Description
22
A52,Aggregate,5-2,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of Applications for Conventional Home-Purchase Loans, 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"
33
D51,Disclosure,5-1,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of applications for FHA, FSA/RHS, and VA home-purchase loans, 1- to 4-family and manufactured home dwellings, by income, race and ethnicity of applicant"
4+
D53,Disclosure,5-3,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of Applications to Refinance Loans on 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package hmda.publication.reports.disclosure
2+
3+
import akka.NotUsed
4+
import akka.stream.scaladsl.Source
5+
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
6+
import hmda.publication.reports._
7+
import hmda.publication.reports.util.ReportUtil._
8+
import hmda.publication.reports.util.ReportsMetaDataLookup
9+
import hmda.query.model.filing.LoanApplicationRegisterQuery
10+
11+
import scala.concurrent.Future
12+
13+
case class D53(
14+
respondentId: String,
15+
institutionName: String,
16+
year: Int,
17+
msa: MSAReport,
18+
applicantIncomes: List[ApplicantIncome],
19+
total: List[Disposition],
20+
reportDate: String = formattedCurrentDate,
21+
table: String = D53.metaData.reportTable,
22+
description: String = D53.metaData.description
23+
) extends DisclosureReport
24+
25+
object D53 {
26+
val metaData = ReportsMetaDataLookup.values("D53")
27+
val dispositions = metaData.dispositions
28+
29+
// Table filters:
30+
// Property Type: 1, 2
31+
// Purpose of Loan: 3
32+
def generate[ec: EC, mat: MAT, as: AS](
33+
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
34+
fipsCode: Int,
35+
respondentId: String,
36+
institutionNameF: Future[String]
37+
): Future[D53] = {
38+
39+
val lars = larSource
40+
.filter(lar => lar.respondentId == respondentId)
41+
.filter(lar => lar.msa != "NA")
42+
.filter(lar => lar.msa.toInt == fipsCode)
43+
.filter { lar =>
44+
(lar.loanType == 2 || lar.loanType == 3 || lar.loanType == 4) &&
45+
(lar.propertyType == 1 || lar.propertyType == 2) &&
46+
(lar.purpose == 1)
47+
}
48+
val larsWithIncome = lars.filter(lar => lar.income != "NA")
49+
50+
val msa = msaReport(fipsCode.toString)
51+
52+
val incomeIntervals = calculateMedianIncomeIntervals(fipsCode)
53+
val applicantIncomesF = applicantIncomesWithBorrowerCharacteristics(larsWithIncome, incomeIntervals, dispositions)
54+
55+
val yearF = calculateYear(larSource)
56+
val totalF = calculateDispositions(lars, dispositions)
57+
58+
for {
59+
institutionName <- institutionNameF
60+
year <- yearF
61+
applicantIncomes <- applicantIncomesF
62+
total <- totalF
63+
} yield {
64+
65+
D53(
66+
respondentId,
67+
institutionName,
68+
year,
69+
msa,
70+
applicantIncomes,
71+
total
72+
)
73+
}
74+
75+
}
76+
77+
}

publication/src/main/scala/hmda/publication/reports/disclosure/DisclosureReports.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class DisclosureReports(val sys: ActorSystem, val mat: ActorMaterializer) extend
3333
d51F.map { d51 =>
3434
println(d51.toJson.prettyPrint)
3535
}
36+
37+
//val d53F = D53.generate(larSource, fipsCode, respId, institutionNameF)
3638
}
3739

3840
private def institutionName(respondentId: String): Future[String] = {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package hmda.publication.reports.protocol.disclosure
2+
3+
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
4+
import hmda.model.publication.reports.ReportTypeEnum.Disclosure
5+
import hmda.publication.reports.disclosure.D53
6+
import hmda.publication.reports.protocol.{ ApplicantIncomeProtocol, MSAReportProtocol, ReportTypeEnumProtocol }
7+
8+
import spray.json._
9+
import spray.json.DefaultJsonProtocol
10+
11+
object D53Protocol
12+
extends DefaultJsonProtocol
13+
with ReportTypeEnumProtocol
14+
with MSAReportProtocol
15+
with ApplicantIncomeProtocol {
16+
17+
implicit object D53Format extends RootJsonFormat[D53] {
18+
19+
override def write(obj: D53): JsValue = {
20+
JsObject(
21+
"respondentId" -> JsString(obj.respondentId),
22+
"institutionName" -> JsString(obj.institutionName),
23+
"table" -> JsString("5-3"),
24+
"type" -> JsString(Disclosure.toString),
25+
"desc" -> JsString(obj.description),
26+
"year" -> JsNumber(obj.year),
27+
"reportDate" -> JsString(obj.reportDate),
28+
"msa" -> obj.msa.toJson,
29+
"applicantIncomes" -> obj.applicantIncomes.toJson,
30+
"total" -> obj.total.toJson
31+
)
32+
}
33+
34+
override def read(json: JsValue): D53 = json.asJsObject.getFields(
35+
"respondentId",
36+
"institutionName",
37+
"table",
38+
"type",
39+
"desc",
40+
"year",
41+
"reportDate",
42+
"msa",
43+
"applicantIncomes",
44+
"total"
45+
) match {
46+
case Seq(respondentId, institutionName, table, reportType, description, year, reportDate, msa, applicantIncomes, total) =>
47+
D53(
48+
respondentId.convertTo[String],
49+
institutionName.convertTo[String],
50+
year.convertTo[Int],
51+
msa.convertTo[MSAReport],
52+
applicantIncomes.convertTo[List[ApplicantIncome]],
53+
total.convertTo[List[Disposition]],
54+
reportDate.convertTo[String]
55+
)
56+
}
57+
}
58+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package hmda.publication.reports.disclosure
2+
3+
import hmda.publication.reports.protocol.disclosure.D53Protocol._
4+
import org.scalatest.prop.PropertyChecks
5+
import org.scalatest.{ MustMatchers, PropSpec }
6+
import spray.json._
7+
import DisclosureReportGenerators._
8+
9+
class D53ProtocolSpec extends PropSpec with PropertyChecks with MustMatchers {
10+
11+
property("D53 Report must convert to and from JSON") {
12+
forAll(d53Gen) { d53 =>
13+
d53.toJson.convertTo[D53] mustBe d53
14+
}
15+
}
16+
17+
property("D53 Report must serialize to the correct JSON format") {
18+
forAll(d53Gen) { d53 =>
19+
d53.toJson mustBe JsObject(
20+
"respondentId" -> JsString(d53.respondentId),
21+
"institutionName" -> JsString(d53.institutionName),
22+
"table" -> JsString("5-3"),
23+
"type" -> JsString("Disclosure"),
24+
"desc" -> JsString("Disposition of Applications to Refinance Loans on 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"),
25+
"year" -> JsNumber(d53.year),
26+
"reportDate" -> JsString(d53.reportDate),
27+
"msa" -> d53.msa.toJson,
28+
"applicantIncomes" -> d53.applicantIncomes.toJson,
29+
"total" -> d53.total.toJson
30+
)
31+
}
32+
}
33+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package hmda.publication.reports.disclosure
2+
3+
import akka.NotUsed
4+
import akka.actor.ActorSystem
5+
import akka.stream.ActorMaterializer
6+
import akka.stream.scaladsl.Source
7+
import hmda.model.fi.lar.{ LarGenerators, LoanApplicationRegister }
8+
import hmda.model.publication.reports.ActionTakenTypeEnum._
9+
import hmda.model.publication.reports.ApplicantIncomeEnum.LessThan50PercentOfMSAMedian
10+
import hmda.model.publication.reports.{ EthnicityBorrowerCharacteristic, MSAReport, MinorityStatusBorrowerCharacteristic, RaceBorrowerCharacteristic }
11+
import hmda.query.model.filing.LoanApplicationRegisterQuery
12+
import hmda.query.repository.filing.LarConverter._
13+
import org.scalacheck.Gen
14+
import org.scalatest.{ AsyncWordSpec, BeforeAndAfterAll, MustMatchers }
15+
16+
import scala.concurrent.Future
17+
18+
class D53Spec extends AsyncWordSpec with MustMatchers with LarGenerators with BeforeAndAfterAll {
19+
20+
implicit val system = ActorSystem()
21+
implicit val ec = system.dispatcher
22+
implicit val materializer = ActorMaterializer()
23+
24+
override def afterAll(): Unit = {
25+
super.afterAll()
26+
system.terminate()
27+
}
28+
29+
val respId = "98765"
30+
val fips = 18700 //Corvallis, OR
31+
def loanType = Gen.oneOf(2, 3, 4).sample.get
32+
def propType = Gen.oneOf(1, 2).sample.get
33+
34+
val lars = lar100ListGen.sample.get.map { lar: LoanApplicationRegister =>
35+
val geo = lar.geography.copy(msa = fips.toString)
36+
val loan = lar.loan.copy(loanType = loanType, propertyType = propType, purpose = 1)
37+
lar.copy(respondentId = respId, geography = geo, loan = loan)
38+
}
39+
40+
val source: Source[LoanApplicationRegisterQuery, NotUsed] = Source
41+
.fromIterator(() => lars.toIterator)
42+
.map(lar => toLoanApplicationRegisterQuery(lar))
43+
44+
val expectedDispositions = List(ApplicationReceived, LoansOriginated, ApprovedButNotAccepted, ApplicationsDenied, ApplicationsWithdrawn, ClosedForIncompleteness)
45+
46+
"Generate a Disclosure 5-3 report" in {
47+
D53.generate(source, fips, respId, Future("Corvallis Test Bank")).map { result =>
48+
49+
result.msa mustBe MSAReport("18700", "Corvallis, OR", "OR", "Oregon")
50+
result.table mustBe "5-3"
51+
result.respondentId mustBe "98765"
52+
result.institutionName mustBe "Corvallis Test Bank"
53+
result.applicantIncomes.size mustBe 5
54+
55+
val lowestIncome = result.applicantIncomes.head
56+
lowestIncome.applicantIncome mustBe LessThan50PercentOfMSAMedian
57+
58+
val races = lowestIncome.borrowerCharacteristics.head.asInstanceOf[RaceBorrowerCharacteristic].races
59+
races.size mustBe 8
60+
61+
val ethnicities = lowestIncome.borrowerCharacteristics(1).asInstanceOf[EthnicityBorrowerCharacteristic].ethnicities
62+
ethnicities.size mustBe 4
63+
64+
val minorityStatuses = lowestIncome.borrowerCharacteristics(2).asInstanceOf[MinorityStatusBorrowerCharacteristic].minoritystatus
65+
minorityStatuses.size mustBe 2
66+
67+
races.head.dispositions.map(_.disposition) mustBe expectedDispositions
68+
}
69+
}
70+
71+
}

publication/src/test/scala/hmda/publication/reports/disclosure/DisclosureReportGenerators.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,16 @@ object DisclosureReportGenerators {
1919
} yield D51(respId, instName, year, msa, applicantIncomes, total, reportDate)
2020
}
2121

22+
implicit def d53Gen: Gen[D53] = {
23+
for {
24+
respId <- Gen.alphaStr
25+
instName <- Gen.alphaStr
26+
year = Calendar.getInstance().get(Calendar.YEAR)
27+
reportDate = formatDate(Calendar.getInstance().toInstant)
28+
msa <- msaReportGen
29+
applicantIncomes <- Gen.listOfN(5, applicantIncomeGen)
30+
total <- totalDispositionGen
31+
} yield D53(respId, instName, year, msa, applicantIncomes, total, reportDate)
32+
}
33+
2234
}

0 commit comments

Comments
 (0)