Skip to content

Commit 9fc1dca

Browse files
author
Nick Grippin
authored
Merge pull request cfpb#1165 from schbetsy/A53
A53
2 parents 4653a45 + d818e71 commit 9fc1dca

File tree

12 files changed

+213
-98
lines changed

12 files changed

+213
-98
lines changed

publication/src/main/resources/reports-metadata.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
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
D52,Disclosure,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"
56
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"

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

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,21 @@ package hmda.publication.reports.aggregate
22

33
import akka.NotUsed
44
import akka.stream.scaladsl.Source
5-
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
65
import hmda.publication.reports._
7-
import hmda.publication.reports.util.ReportUtil._
8-
import hmda.publication.reports.util.ReportsMetaDataLookup
96
import hmda.query.model.filing.LoanApplicationRegisterQuery
107

118
import scala.concurrent.Future
129

13-
case class A52(
14-
year: Int,
15-
msa: MSAReport,
16-
applicantIncomes: List[ApplicantIncome],
17-
total: List[Disposition],
18-
reportDate: String = formattedCurrentDate,
19-
table: String = A52.metaData.reportTable,
20-
description: String = A52.metaData.description
21-
) extends AggregateReport
22-
2310
object A52 {
24-
val metaData = ReportsMetaDataLookup.values("A52")
25-
val dispositions = metaData.dispositions
2611

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ trait AggregateReport {
99
val description: String
1010
val year: Int
1111
val msa: MSAReport
12+
val reportDate: String
1213

1314
val reportType: ReportTypeEnum = Aggregate
1415
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import akka.stream.ActorMaterializer
55
import akka.util.Timeout
66
import com.typesafe.config.ConfigFactory
77
import hmda.query.repository.filing.FilingCassandraRepository
8-
import hmda.publication.reports.protocol.aggregate.A52Protocol._
8+
import hmda.publication.reports.protocol.aggregate.A5XProtocol._
99

1010
import scala.concurrent.Future
1111
import scala.concurrent.duration._
@@ -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
}

publication/src/main/scala/hmda/publication/reports/national/N52.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import akka.stream.scaladsl.Source
55
import hmda.census.model.MsaIncomeLookup
66
import hmda.model.publication.reports._
77
import hmda.publication.reports._
8-
import hmda.publication.reports.aggregate.A52
8+
import hmda.publication.reports.aggregate.{ A52, A5X }
99
import hmda.publication.reports.util.ReportUtil._
1010
import hmda.publication.reports.util.ReportsMetaDataLookup
1111
import hmda.query.model.filing.LoanApplicationRegisterQuery
@@ -20,11 +20,11 @@ case class N52(
2020
table: String = N52.metaData.reportTable,
2121
description: String = N52.metaData.description
2222
) extends NationalAggregateReport {
23-
def +(a52: A52): N52 = {
23+
def +(a5X: A5X): N52 = {
2424
if (applicantIncomes.isEmpty) {
25-
N52(a52.year, a52.reportDate, a52.applicantIncomes, a52.total)
25+
N52(a5X.year, a5X.reportDate, a5X.applicantIncomes, a5X.total)
2626
} else {
27-
val combinedIncomes = a52.applicantIncomes.map(income => {
27+
val combinedIncomes = a5X.applicantIncomes.map(income => {
2828
val newC = applicantIncomes.find(i => i.applicantIncome == income.applicantIncome).get.borrowerCharacteristics
2929
val originalC = income.borrowerCharacteristics
3030

@@ -43,7 +43,7 @@ case class N52(
4343
ApplicantIncome(income.applicantIncome, List(cR, cE, cM))
4444
})
4545

46-
val combinedDispositions = a52.total.map(disposition => {
46+
val combinedDispositions = a5X.total.map(disposition => {
4747
val originalDisposition = total.find(d => d.disposition == disposition.disposition).get
4848
disposition + originalDisposition
4949
})

publication/src/main/scala/hmda/publication/reports/protocol/aggregate/A52Protocol.scala renamed to publication/src/main/scala/hmda/publication/reports/protocol/aggregate/A5XProtocol.scala

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ package hmda.publication.reports.protocol.aggregate
22

33
import hmda.model.publication.reports.{ ApplicantIncome, Disposition, MSAReport }
44
import hmda.model.publication.reports.ReportTypeEnum._
5-
import hmda.publication.reports.aggregate.A52
5+
import hmda.publication.reports.aggregate.A5X
66
import hmda.publication.reports.protocol.{ ApplicantIncomeProtocol, MSAReportProtocol, ReportTypeEnumProtocol }
77
import spray.json._
88

9-
object A52Protocol
9+
object A5XProtocol
1010
extends DefaultJsonProtocol
1111
with ReportTypeEnumProtocol
1212
with MSAReportProtocol
1313
with ApplicantIncomeProtocol {
1414

15-
implicit object A52Format extends RootJsonFormat[A52] {
16-
override def write(obj: A52): JsValue = {
15+
implicit object A5XFormat extends RootJsonFormat[A5X] {
16+
override def write(obj: A5X): JsValue = {
1717
JsObject(
18-
"table" -> JsString("5-2"),
18+
"table" -> JsString(obj.table),
1919
"type" -> JsString(Aggregate.toString),
2020
"desc" -> JsString(obj.description),
2121
"year" -> JsNumber(obj.year),
@@ -26,7 +26,7 @@ object A52Protocol
2626
)
2727
}
2828

29-
override def read(json: JsValue): A52 = json.asJsObject.getFields(
29+
override def read(json: JsValue): A5X = json.asJsObject.getFields(
3030
"table",
3131
"type",
3232
"desc",
@@ -37,11 +37,13 @@ object A52Protocol
3737
"total"
3838
) match {
3939
case Seq(table, reportType, description, year, reportDate, msa, applicantIncomes, total) =>
40-
A52(
40+
A5X(
4141
year.convertTo[Int],
4242
msa.convertTo[MSAReport],
4343
applicantIncomes.convertTo[List[ApplicantIncome]],
4444
total.convertTo[List[Disposition]],
45+
table.convertTo[String],
46+
description.convertTo[String],
4547
reportDate.convertTo[String]
4648
)
4749
}

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

Lines changed: 0 additions & 31 deletions
This file was deleted.
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(propertyType = propType, purpose = 3)
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+
}

0 commit comments

Comments
 (0)