Skip to content

Commit 28b295a

Browse files
authored
Merge pull request cfpb#1141 from nickgrippin/a52
A52
2 parents e161e3b + b2da068 commit 28b295a

File tree

8 files changed

+320
-1
lines changed

8 files changed

+320
-1
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
ReportId,Type,ReportNumber,Dispositions,Description
2-
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"
2+
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+
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"
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package hmda.publication.reports.aggregate
2+
3+
import java.util.Calendar
4+
5+
import akka.NotUsed
6+
import akka.stream.scaladsl.Source
7+
import hmda.model.publication.reports.ApplicantIncomeEnum._
8+
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
9+
import hmda.publication.reports._
10+
import hmda.publication.reports.util.DateUtil._
11+
import hmda.publication.reports.util.ReportUtil._
12+
import hmda.publication.reports.util.ReportsMetaDataLookup
13+
import hmda.query.model.filing.LoanApplicationRegisterQuery
14+
15+
import scala.concurrent.Future
16+
17+
case class A52(
18+
year: Int,
19+
reportDate: String,
20+
msa: MSAReport,
21+
applicantIncomes: List[ApplicantIncome],
22+
total: List[Disposition],
23+
table: String = A52.metaData.reportTable,
24+
description: String = A52.metaData.description
25+
) extends AggregateReport
26+
27+
object A52 {
28+
val metaData = ReportsMetaDataLookup.values("A52")
29+
val dispositions = metaData.dispositions
30+
31+
// Table filters:
32+
// Loan Type 1
33+
// Property Type 1,2
34+
// Purpose of Loan 1
35+
def generate[ec: EC, mat: MAT, as: AS](
36+
larSource: Source[LoanApplicationRegisterQuery, NotUsed],
37+
fipsCode: Int
38+
): Future[A52] = {
39+
val lars = larSource
40+
.filter(lar => lar.msa != "NA")
41+
.filter(lar => lar.msa.toInt == fipsCode)
42+
.filter { lar =>
43+
(lar.loanType == 1) &&
44+
(lar.propertyType == 1 || lar.propertyType == 2) &&
45+
(lar.purpose == 1)
46+
}
47+
48+
val larsWithIncome = lars.filter(lar => lar.income != "NA")
49+
50+
val msa = msaReport(fipsCode.toString)
51+
52+
val incomeIntervals = calculateMedianIncomeIntervals(fipsCode)
53+
54+
val larsByIncome = larsByIncomeInterval(larsWithIncome, incomeIntervals)
55+
val borrowerCharacteristicsByIncomeF = borrowerCharacteristicsByIncomeInterval(larsByIncome, dispositions)
56+
57+
val yearF = calculateYear(larSource)
58+
val totalF = calculateDispositions(lars, dispositions)
59+
60+
for {
61+
lars50BorrowerCharacteristics <- borrowerCharacteristicsByIncomeF(LessThan50PercentOfMSAMedian)
62+
lars50To79BorrowerCharacteristics <- borrowerCharacteristicsByIncomeF(Between50And79PercentOfMSAMedian)
63+
lars80To99BorrowerCharacteristics <- borrowerCharacteristicsByIncomeF(Between80And99PercentOfMSAMedian)
64+
lars100To120BorrowerCharacteristics <- borrowerCharacteristicsByIncomeF(Between100And119PercentOfMSAMedian)
65+
lars120BorrowerCharacteristics <- borrowerCharacteristicsByIncomeF(GreaterThan120PercentOfMSAMedian)
66+
67+
year <- yearF
68+
total <- totalF
69+
} yield {
70+
val income50 = ApplicantIncome(
71+
LessThan50PercentOfMSAMedian,
72+
lars50BorrowerCharacteristics
73+
)
74+
val income50To79 = ApplicantIncome(
75+
Between50And79PercentOfMSAMedian,
76+
lars50To79BorrowerCharacteristics
77+
)
78+
val income80To99 = ApplicantIncome(
79+
Between80And99PercentOfMSAMedian,
80+
lars80To99BorrowerCharacteristics
81+
)
82+
val income100To120 = ApplicantIncome(
83+
Between100And119PercentOfMSAMedian,
84+
lars100To120BorrowerCharacteristics
85+
)
86+
val income120 = ApplicantIncome(
87+
GreaterThan120PercentOfMSAMedian,
88+
lars120BorrowerCharacteristics
89+
)
90+
91+
val applicantIncomes = List(
92+
income50,
93+
income50To79,
94+
income80To99,
95+
income100To120,
96+
income120
97+
)
98+
99+
A52(
100+
year,
101+
formatDate(Calendar.getInstance().toInstant),
102+
msa,
103+
applicantIncomes,
104+
total
105+
)
106+
}
107+
}
108+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package hmda.publication.reports.aggregate
2+
3+
import hmda.model.publication.reports.{ MSAReport, ReportTypeEnum }
4+
import hmda.model.publication.reports.ReportTypeEnum.Aggregate
5+
6+
trait AggregateReport {
7+
8+
val table: String
9+
val description: String
10+
val year: Int
11+
val msa: MSAReport
12+
13+
val reportType: ReportTypeEnum = Aggregate
14+
}
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 akka.actor.ActorSystem
4+
import akka.stream.ActorMaterializer
5+
import akka.util.Timeout
6+
import com.typesafe.config.ConfigFactory
7+
import hmda.query.repository.filing.FilingCassandraRepository
8+
import hmda.publication.reports.protocol.aggregate.A52Protocol._
9+
10+
import scala.concurrent.Future
11+
import scala.concurrent.duration._
12+
13+
import spray.json._
14+
15+
class AggregateReports(val sys: ActorSystem, val mat: ActorMaterializer) extends FilingCassandraRepository {
16+
17+
override implicit def system: ActorSystem = sys
18+
override implicit def materializer: ActorMaterializer = mat
19+
val config = ConfigFactory.load()
20+
val duration = config.getInt("hmda.actor-lookup-timeout")
21+
implicit val timeout = Timeout(duration.seconds)
22+
23+
val larSource = readData(1000)
24+
25+
def generateReports(fipsCode: Int): Future[Unit] = {
26+
val a52F = A52.generate(larSource, fipsCode)
27+
a52F.map { a52 =>
28+
println(a52.toJson.prettyPrint)
29+
}
30+
}
31+
}
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.A52
6+
import hmda.publication.reports.protocol.{ ApplicantIncomeProtocol, MSAReportProtocol, ReportTypeEnumProtocol }
7+
import spray.json._
8+
9+
object A52Protocol
10+
extends DefaultJsonProtocol
11+
with ReportTypeEnumProtocol
12+
with MSAReportProtocol
13+
with ApplicantIncomeProtocol {
14+
15+
implicit object A52Format extends RootJsonFormat[A52] {
16+
override def write(obj: A52): JsValue = {
17+
JsObject(
18+
"table" -> JsString("5-2"),
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): A52 = 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+
A52(
41+
year.convertTo[Int],
42+
reportDate.convertTo[String],
43+
msa.convertTo[MSAReport],
44+
applicantIncomes.convertTo[List[ApplicantIncome]],
45+
total.convertTo[List[Disposition]]
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.A52Protocol._
4+
import org.scalatest.{ MustMatchers, PropSpec }
5+
import org.scalatest.prop.PropertyChecks
6+
import spray.json._
7+
import AggregateReportGenerators._
8+
9+
class A52ProtocolSpec extends PropSpec with PropertyChecks with MustMatchers {
10+
11+
property("A52 Report must convert to and from JSON") {
12+
forAll(a52Gen) { a52 =>
13+
a52.toJson.convertTo[A52] mustBe a52
14+
}
15+
}
16+
17+
property("A52 Report must serialize to the correct JSON format") {
18+
forAll(a52Gen) { a52 =>
19+
a52.toJson mustBe JsObject(
20+
"table" -> JsString("5-2"),
21+
"type" -> JsString("Aggregate"),
22+
"desc" -> JsString("Disposition of Applications for Conventional Home-Purchase Loans, 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"),
23+
"year" -> JsNumber(a52.year),
24+
"reportDate" -> JsString(a52.reportDate),
25+
"msa" -> a52.msa.toJson,
26+
"applicantIncomes" -> a52.applicantIncomes.toJson,
27+
"total" -> a52.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 A52Spec 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-2 report" in {
43+
A52.generate(source, fips).map { result =>
44+
45+
result.msa mustBe MSAReport("18700", "Corvallis, OR", "OR", "Oregon")
46+
result.table mustBe "5-2"
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+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package hmda.publication.reports.aggregate
2+
3+
import java.util.Calendar
4+
5+
import hmda.publication.reports.ReportGenerators._
6+
import hmda.publication.reports.util.DateUtil._
7+
import org.scalacheck.Gen
8+
9+
object AggregateReportGenerators {
10+
implicit def a52Gen: Gen[A52] = {
11+
for {
12+
msa <- msaReportGen
13+
year = Calendar.getInstance().get(Calendar.YEAR)
14+
reportDate = formatDate(Calendar.getInstance().toInstant)
15+
applicantIncomes <- Gen.listOfN(5, applicantIncomeGen)
16+
total <- totalDispositionGen
17+
} yield A52(year, reportDate, msa, applicantIncomes, total)
18+
}
19+
}

0 commit comments

Comments
 (0)