Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add required information to DBP NA<->FE API for removal timeline support #3268

Merged
merged 18 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1c82d0f
Add new fields to DBP->FE API, and associated constructors
THISISDINOSAUR Sep 12, 2024
6388873
Make UIMapper use new constructors
THISISDINOSAUR Sep 12, 2024
24d3fc9
Remove unused data field
THISISDINOSAUR Sep 12, 2024
19ba1c8
Merge branch 'main' into elle/add-removal-timeline-support
THISISDINOSAUR Sep 13, 2024
2f45004
Refactor DBPUIDateBrokerProfileMatch inits to reduce duplication
THISISDINOSAUR Sep 16, 2024
9e326ef
Fix DBPUIDataBrokerProfileMatch creation when created date is default
THISISDINOSAUR Sep 16, 2024
fa5f675
Add unit tests for DBPUIDataBrokerProfileMatch creation
THISISDINOSAUR Sep 16, 2024
59988dd
Change estimatedRemovalDate to be calculated from optOutSubmittedDate…
THISISDINOSAUR Sep 18, 2024
0865a78
Add match testing function to Extracted profile
THISISDINOSAUR Sep 18, 2024
9aa5b41
Add unit tests for matching function
THISISDINOSAUR Sep 18, 2024
1af1ebe
Add parent matching to DBPUIDataBrokerProfileMatch
THISISDINOSAUR Sep 18, 2024
29c8bb8
Add UIMapper tests for profile matching
THISISDINOSAUR Sep 18, 2024
c2d4d9f
Add parent URL field to UIDataBroker
THISISDINOSAUR Sep 18, 2024
06b1020
Fix incorrect data being given when looking for parent matches
THISISDINOSAUR Sep 20, 2024
88fb01b
Simplify parent match logic
THISISDINOSAUR Sep 24, 2024
608bd9f
Fix estimated removal date not being propagated to completed opt outs
THISISDINOSAUR Sep 25, 2024
b389b6b
bump native - FE api version number
THISISDINOSAUR Sep 25, 2024
cea7624
Merge branch 'main' into elle/add-removal-timeline-support
THISISDINOSAUR Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,10 @@ extension InMemoryDataCache: DBPUICommunicationDelegate {
// 2. We map the brokers to the UI model
.flatMap { dataBroker -> [DBPUIDataBroker] in
var result: [DBPUIDataBroker] = []
result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url))
result.append(DBPUIDataBroker(name: dataBroker.name, url: dataBroker.url, parentURL: dataBroker.parent))

for mirrorSite in dataBroker.mirrorSites {
result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url))
result.append(DBPUIDataBroker(name: mirrorSite.name, url: mirrorSite.url, parentURL: dataBroker.parent))
}
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ struct DBPUIUserProfileAddress: Codable {
let zipCode: String?
}

extension DBPUIUserProfileAddress {
init(addressCityState: AddressCityState) {
self.init(street: addressCityState.fullAddress,
city: addressCityState.city,
state: addressCityState.state,
zipCode: nil)
}
}

/// Message Object representing a user profile containing one or more names and addresses
/// also contains the user profile's birth year
struct DBPUIUserProfile: Codable {
Expand Down Expand Up @@ -105,11 +114,13 @@ struct DBPUIDataBroker: Codable, Hashable {
let name: String
let url: String
let date: Double?
let parentURL: String?

init(name: String, url: String, date: Double? = nil) {
init(name: String, url: String, date: Double? = nil, parentURL: String?) {
self.name = name
self.url = url
self.date = date
self.parentURL = parentURL
}

func hash(into hasher: inout Hasher) {
Expand All @@ -135,7 +146,79 @@ struct DBPUIDataBrokerProfileMatch: Codable {
let addresses: [DBPUIUserProfileAddress]
let alternativeNames: [String]
let relatives: [String]
let date: Double? // Used in some methods to set the removedDate or found date
let foundDate: Double
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼

let optOutSubmittedDate: Double?
let estimatedRemovalDate: Double?
let removedDate: Double?
let hasMatchingRecordOnParentBroker: Bool
}

extension DBPUIDataBrokerProfileMatch {
init(optOutJobData: OptOutJobData,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like how long this construct got, but didn't have any nice ideas to simplify it, so taking suggestions!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried rewriting this a different way, but it was mostly just different, not necessarily better. However, leaving one suggestion.

dataBrokerName: String,
dataBrokerURL: String,
dataBrokerParentURL: String?,
parentBrokerOptOutJobData: [OptOutJobData]?) {
let extractedProfile = optOutJobData.extractedProfile

/*
createdDate used to not exist in the DB, so in the migration we defaulted it to Unix Epoch zero (i.e. 1970)
If that's the case, we should rely on the events instead
We don't do that all the time since it's unnecssarily expensive trawling through events, and
this is involved in some already heavy endpoints

optOutSubmittedDate also used to not exist, but instead defaults to nil
However, it could be nil simply because the opt out hasn't been submitted yet. So since we don't want to
look through events unneccesarily, we instead only look for it if the createdDate is 1970
*/
var foundDate = optOutJobData.createdDate
var optOutSubmittedDate = optOutJobData.submittedSuccessfullyDate
if foundDate == Date(timeIntervalSince1970: 0) {
let foundEvents = optOutJobData.historyEvents.filter { $0.isMatchesFoundEvent() }
let firstFoundEvent = foundEvents.min(by: { $0.date < $1.date })
if let firstFoundEventDate = firstFoundEvent?.date {
foundDate = firstFoundEventDate
} else {
assertionFailure("No matching MatchFound event for an extract profile found")
}

let optOutSubmittedEvents = optOutJobData.historyEvents.filter { $0.type == .optOutRequested }
let firstOptOutEvent = optOutSubmittedEvents.min(by: { $0.date < $1.date })
optOutSubmittedDate = firstOptOutEvent?.date
}
let estimatedRemovalDate = Calendar.current.date(byAdding: .day, value: 14, to: optOutSubmittedDate ?? foundDate)

// Check for any matching records on the parent broker
var hasFoundParentMatch = false
if let parentBrokerOptOutJobData = parentBrokerOptOutJobData {
for parentOptOut in parentBrokerOptOutJobData {
let parentProfile = parentOptOut.extractedProfile
if extractedProfile.doesMatchExtractedProfile(parentProfile) {
hasFoundParentMatch = true
break
}
}
}
THISISDINOSAUR marked this conversation as resolved.
Show resolved Hide resolved

self.init(dataBroker: DBPUIDataBroker(name: dataBrokerName, url: dataBrokerURL, parentURL: dataBrokerParentURL),
name: extractedProfile.fullName ?? "No name",
addresses: extractedProfile.addresses?.map {DBPUIUserProfileAddress(addressCityState: $0) } ?? [],
alternativeNames: extractedProfile.alternativeNames ?? [String](),
relatives: extractedProfile.relatives ?? [String](),
foundDate: foundDate.timeIntervalSince1970,
optOutSubmittedDate: optOutSubmittedDate?.timeIntervalSince1970,
estimatedRemovalDate: estimatedRemovalDate?.timeIntervalSince1970,
removedDate: extractedProfile.removedDate?.timeIntervalSince1970,
hasMatchingRecordOnParentBroker: hasFoundParentMatch)
}

init(optOutJobData: OptOutJobData, dataBroker: DataBroker, parentBrokerOptOutJobData: [OptOutJobData]?) {
self.init(optOutJobData: optOutJobData,
dataBrokerName: dataBroker.name,
dataBrokerURL: dataBroker.url,
dataBrokerParentURL: dataBroker.parent,
parentBrokerOptOutJobData: parentBrokerOptOutJobData)
}
}

/// Protocol to represent a message that can be passed from the host to the UI
Expand All @@ -156,6 +239,27 @@ struct DBPUIOptOutMatch: DBPUISendableMessage {
let alternativeNames: [String]
let addresses: [DBPUIUserProfileAddress]
let date: Double
let foundDate: Double
let optOutSubmittedDate: Double?
let estimatedRemovalDate: Double?
let removedDate: Double?
}

extension DBPUIOptOutMatch {
init?(profileMatch: DBPUIDataBrokerProfileMatch, matches: Int) {
guard let removedDate = profileMatch.removedDate else { return nil }
let dataBroker = profileMatch.dataBroker
self.init(dataBroker: dataBroker,
matches: matches,
name: profileMatch.name,
alternativeNames: profileMatch.alternativeNames,
addresses: profileMatch.addresses,
date: removedDate,
foundDate: profileMatch.foundDate,
optOutSubmittedDate: profileMatch.optOutSubmittedDate,
estimatedRemovalDate: nil,
removedDate: removedDate)
}
}

/// Data representing the initial scan progress
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct ExtractProfileSelectors: Codable, Sendable {
}
}

struct AddressCityState: Codable {
struct AddressCityState: Codable, Hashable {
let city: String
let state: String

Expand Down Expand Up @@ -166,10 +166,51 @@ struct ExtractedProfile: Codable, Sendable {
identifier: self.identifier
)
}

/*
Matching records are:
1/ Completely identical records (same name, addresses, ages, etc)
2/ Records that overlap completely (record A has all the data of record B, but might have
extra information as well (e.g. an extra address, a middle name where record B doesn't)
I.e. B is a subset of A, or vice versa
However, we ignore some of the properties
So, basically age == age, we ignore phone numbers and email, and then everything else one should be a subset of the other
*/
func doesMatchExtractedProfile(_ extractedProfile: ExtractedProfile) -> Bool {
if age != extractedProfile.age {
return false
}

if name != extractedProfile.name {
return false
}

if !(alternativeNames ?? []).isASubSetOrSuperSetOf(extractedProfile.alternativeNames ?? []) {
return false
}

if !(addresses ?? []).isASubSetOrSuperSetOf(extractedProfile.addresses ?? []) {
return false
}

if !(relatives ?? []).isASubSetOrSuperSetOf(extractedProfile.relatives ?? []) {
return false
}
Comment on lines +180 to +198
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prob just a preference, but guard statements seem to fit this pattern better IMO (where we return true at the end if all conditions pass). Again, maybe just subjective preference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this and I think I prefer not using guards because to me guard suggests something went wrong or some prerequisite weren't satisfied. I think that is a biased view on my part though and probably not shared universally

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, one subjective view -vs- another 😅! Fine with me to stick with what you have.


return true
}
}

extension ExtractedProfile: Equatable {
static func == (lhs: ExtractedProfile, rhs: ExtractedProfile) -> Bool {
lhs.name == rhs.name
}
}

private extension Sequence where Element: Hashable {
func isASubSetOrSuperSetOf<Settable>(_ sequence: Settable) -> Bool where Settable: Sequence, Element == Settable.Element {
let setA = Set(self)
let setB = Set(sequence)
return setA.isSubset(of: setB) || setB.isSubset(of: setA)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,13 @@ public struct HistoryEvent: Identifiable, Sendable {
return false
}
}

func isMatchesFoundEvent() -> Bool {
switch type {
case .matchesFound:
return true
default:
return false
}
}
}
Loading
Loading