Skip to content

Commit 19094ed

Browse files
committed
Add Aggregate 5-3 report
1 parent 1aa7314 commit 19094ed

File tree

7 files changed

+225
-0
lines changed

7 files changed

+225
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
ReportId,Type,ReportTable,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"
3+
A53,Aggregate,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"
34
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"
45
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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package hmda.publication.reports.aggregate
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 A53(
14+
year: Int,
15+
msa: MSAReport,
16+
applicantIncomes: List[ApplicantIncome],
17+
total: List[Disposition],
18+
reportDate: String = formattedCurrentDate,
19+
table: String = A53.metaData.reportTable,
20+
description: String = A53.metaData.description
21+
) extends AggregateReport
22+
23+
object A53 {
24+
val metaData = ReportsMetaDataLookup.values("A53")
25+
val dispositions = metaData.dispositions
26+
27+
// Table filters:
28+
// Property Type 1,2
29+
// Purpose of Loan 3
30+
def generate[ec: EC, mat: MAT, as: AS](
31+
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
32+
fipsCode: Int
33+
): Future[A53] = {
34+
val lars = larSource
35+
.filter(lar => lar.msa != "NA")
36+
.filter(lar => lar.msa.toInt == fipsCode)
37+
.filter { lar =>
38+
(lar.propertyType == 1 || lar.propertyType == 2) &&
39+
(lar.purpose == 3)
40+
}
41+
42+
val larsWithIncome = lars.filter(lar => lar.income != "NA")
43+
44+
val msa = msaReport(fipsCode.toString)
45+
46+
val incomeIntervals = calculateMedianIncomeIntervals(fipsCode)
47+
val applicantIncomesF = applicantIncomesWithBorrowerCharacteristics(larsWithIncome, incomeIntervals, dispositions)
48+
49+
val yearF = calculateYear(larSource)
50+
val totalF = calculateDispositions(lars, dispositions)
51+
52+
for {
53+
applicantIncomes <- applicantIncomesF
54+
year <- yearF
55+
total <- totalF
56+
} yield {
57+
58+
A53(
59+
year,
60+
msa,
61+
applicantIncomes,
62+
total
63+
)
64+
}
65+
}
66+
}

publication/src/main/scala/hmda/publication/reports/aggregate/AggregateReports.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ class AggregateReports(val sys: ActorSystem, val mat: ActorMaterializer) extends
2727
a52F.map { a52 =>
2828
println(a52.toJson.prettyPrint)
2929
}
30+
31+
// A53.generate(larSource, fipsCode)
3032
}
3133
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package hmda.publication.reports.protocol.aggregate
2+
3+
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
4+
import hmda.model.publication.reports.ReportTypeEnum._
5+
import hmda.publication.reports.aggregate.A53
6+
import hmda.publication.reports.protocol.{ ApplicantIncomeProtocol, MSAReportProtocol, ReportTypeEnumProtocol }
7+
import spray.json._
8+
9+
object A53Protocol
10+
extends DefaultJsonProtocol
11+
with ReportTypeEnumProtocol
12+
with MSAReportProtocol
13+
with ApplicantIncomeProtocol {
14+
15+
implicit object A53Format extends RootJsonFormat[A53] {
16+
override def write(obj: A53): JsValue = {
17+
JsObject(
18+
"table" -> JsString("5-3"),
19+
"type" -> JsString(Aggregate.toString),
20+
"desc" -> JsString(obj.description),
21+
"year" -> JsNumber(obj.year),
22+
"reportDate" -> JsString(obj.reportDate),
23+
"msa" -> obj.msa.toJson,
24+
"applicantIncomes" -> obj.applicantIncomes.toJson,
25+
"total" -> obj.total.toJson
26+
)
27+
}
28+
29+
override def read(json: JsValue): A53 = json.asJsObject.getFields(
30+
"table",
31+
"type",
32+
"desc",
33+
"year",
34+
"reportDate",
35+
"msa",
36+
"applicantIncomes",
37+
"total"
38+
) match {
39+
case Seq(table, reportType, description, year, reportDate, msa, applicantIncomes, total) =>
40+
A53(
41+
year.convertTo[Int],
42+
msa.convertTo[MSAReport],
43+
applicantIncomes.convertTo[List[ApplicantIncome]],
44+
total.convertTo[List[Disposition]],
45+
reportDate.convertTo[String]
46+
)
47+
}
48+
}
49+
50+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package hmda.publication.reports.aggregate
2+
3+
import hmda.publication.reports.protocol.aggregate.A53Protocol._
4+
import org.scalatest.{ MustMatchers, PropSpec }
5+
import org.scalatest.prop.PropertyChecks
6+
import spray.json._
7+
import AggregateReportGenerators._
8+
9+
class A53ProtocolSpec extends PropSpec with PropertyChecks with MustMatchers {
10+
11+
property("A53 Report must convert to and from JSON") {
12+
forAll(a53Gen) { a53 =>
13+
a53.toJson.convertTo[A53] mustBe a53
14+
}
15+
}
16+
17+
property("A53 Report must serialize to the correct JSON format") {
18+
forAll(a53Gen) { a53 =>
19+
a53.toJson mustBe JsObject(
20+
"table" -> JsString("5-3"),
21+
"type" -> JsString("Aggregate"),
22+
"desc" -> JsString("Disposition of Applications to Refinance Loans on 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"),
23+
"year" -> JsNumber(a53.year),
24+
"reportDate" -> JsString(a53.reportDate),
25+
"msa" -> a53.msa.toJson,
26+
"applicantIncomes" -> a53.applicantIncomes.toJson,
27+
"total" -> a53.total.toJson
28+
)
29+
}
30+
}
31+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package hmda.publication.reports.aggregate
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+
class A53Spec extends AsyncWordSpec with MustMatchers with LarGenerators with BeforeAndAfterAll {
17+
18+
implicit val system = ActorSystem()
19+
implicit val ec = system.dispatcher
20+
implicit val materializer = ActorMaterializer()
21+
22+
override def afterAll(): Unit = {
23+
super.afterAll()
24+
system.terminate()
25+
}
26+
27+
val fips = 18700 //Corvallis, OR
28+
def propType = Gen.oneOf(1, 2).sample.get
29+
30+
val lars = lar100ListGen.sample.get.map { lar: LoanApplicationRegister =>
31+
val geo = lar.geography.copy(msa = fips.toString)
32+
val loan = lar.loan.copy(loanType = 1, propertyType = propType, purpose = 1)
33+
lar.copy(geography = geo, loan = loan)
34+
}
35+
36+
val source: Source[LoanApplicationRegisterQuery, NotUsed] = Source
37+
.fromIterator(() => lars.toIterator)
38+
.map(lar => toLoanApplicationRegisterQuery(lar))
39+
40+
val expectedDispositions = List(ApplicationReceived, LoansOriginated, ApprovedButNotAccepted, ApplicationsDenied, ApplicationsWithdrawn, ClosedForIncompleteness)
41+
42+
"Generate an Aggregate 5-3 report" in {
43+
A53.generate(source, fips).map { result =>
44+
45+
result.msa mustBe MSAReport("18700", "Corvallis, OR", "OR", "Oregon")
46+
result.table mustBe "5-3"
47+
result.applicantIncomes.size mustBe 5
48+
49+
val lowestIncome = result.applicantIncomes.head
50+
lowestIncome.applicantIncome mustBe LessThan50PercentOfMSAMedian
51+
52+
val races = lowestIncome.borrowerCharacteristics.head.asInstanceOf[RaceBorrowerCharacteristic].races
53+
races.size mustBe 8
54+
55+
val ethnicities = lowestIncome.borrowerCharacteristics(1).asInstanceOf[EthnicityBorrowerCharacteristic].ethnicities
56+
ethnicities.size mustBe 4
57+
58+
val minorityStatuses = lowestIncome.borrowerCharacteristics(2).asInstanceOf[MinorityStatusBorrowerCharacteristic].minoritystatus
59+
minorityStatuses.size mustBe 2
60+
61+
races.head.dispositions.map(_.disposition) mustBe expectedDispositions
62+
}
63+
}
64+
65+
}

publication/src/test/scala/hmda/publication/reports/aggregate/AggregateReportGenerators.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,14 @@ object AggregateReportGenerators {
1616
total <- totalDispositionGen
1717
} yield A52(year, msa, applicantIncomes, total, reportDate)
1818
}
19+
20+
implicit def a53Gen: Gen[A53] = {
21+
for {
22+
msa <- msaReportGen
23+
year = Calendar.getInstance().get(Calendar.YEAR)
24+
reportDate = formatDate(Calendar.getInstance().toInstant)
25+
applicantIncomes <- Gen.listOfN(5, applicantIncomeGen)
26+
total <- totalDispositionGen
27+
} yield A53(year, msa, applicantIncomes, total, reportDate)
28+
}
1929
}

0 commit comments

Comments
 (0)