Skip to content

Commit 3ee1f7d

Browse files
authored
PIR: Update completeRequestData to support scans and email extracted data (#8583)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213993026941873?focus=true ### Description Updates completeRequestData() to support scans and email extracted data ### Steps to test this PR Will be testable later when feature is complete. ### UI changes No UI changes <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes broker opt-out/scan form-fill payload assembly and retry behavior after email polling, which can affect automation success if edge cases are wrong. > > **Overview** > PIR broker automation now builds richer **extracted profile** payloads for JS `FillForm` actions that use `EXTRACTED_PROFILE`, and clears action retries after email polling succeeds. > > `completeRequestData` now takes full runner **state** instead of separate profile/email args. It still fills opt-out and email-confirmation steps from `profileToOptOut`, but **scan** steps only attach an extracted profile when generated email exists (minimal params, not a broker profile). Generated email and polled **email extracted data** (e.g. verification codes) are merged into `ExtractedProfileParams`; empty maps omit `emailExtractedData`. `EmailDataReceived` resets **`actionRetryCount`** to 0 when advancing after `GetEmailData`. Tests cover scan vs opt-out combinations and the retry reset. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit aab3e52. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 72b1281 commit 3ee1f7d

5 files changed

Lines changed: 237 additions & 28 deletions

File tree

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/EmailDataReceivedEventHandler.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class EmailDataReceivedEventHandler @Inject constructor() : EventHandler {
4242
return Next(
4343
nextState = state.copy(
4444
currentActionIndex = state.currentActionIndex + 1,
45+
actionRetryCount = 0,
4546
emailExtractedData = actualEvent.emailExtractedData,
4647
),
4748
nextEvent = ExecuteBrokerStepAction(

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/common/actions/ExecuteBrokerStepActionEventHandler.kt

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEf
4040
import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.SideEffect.PushJsAction
4141
import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngine.State
4242
import com.duckduckgo.pir.impl.common.toParams
43-
import com.duckduckgo.pir.impl.models.ProfileQuery
4443
import com.duckduckgo.pir.impl.pixels.PirStage
4544
import com.duckduckgo.pir.impl.scripts.models.BrokerAction
4645
import com.duckduckgo.pir.impl.scripts.models.BrokerAction.Click
@@ -52,10 +51,10 @@ import com.duckduckgo.pir.impl.scripts.models.BrokerAction.GetCaptchaInfo
5251
import com.duckduckgo.pir.impl.scripts.models.BrokerAction.GetEmailData
5352
import com.duckduckgo.pir.impl.scripts.models.BrokerAction.SolveCaptcha
5453
import com.duckduckgo.pir.impl.scripts.models.DataSource.EXTRACTED_PROFILE
54+
import com.duckduckgo.pir.impl.scripts.models.ExtractedProfileParams
5555
import com.duckduckgo.pir.impl.scripts.models.PirError
5656
import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData
5757
import com.duckduckgo.pir.impl.scripts.models.PirScriptRequestData.UserProfile
58-
import com.duckduckgo.pir.impl.store.PirRepository.GeneratedEmailData
5958
import com.squareup.anvil.annotations.ContributesMultibinding
6059
import javax.inject.Inject
6160
import kotlin.reflect.KClass
@@ -283,7 +282,7 @@ class ExecuteBrokerStepActionEventHandler @Inject constructor(
283282
actionToExecute.id,
284283
actionToExecute,
285284
pushDelay,
286-
completeRequestData(currentBrokerStep, actionToExecute, state.profileQuery, requestData, state.generatedEmailData),
285+
completeRequestData(currentBrokerStep, actionToExecute, state, requestData),
287286
),
288287
)
289288
}
@@ -294,35 +293,36 @@ class ExecuteBrokerStepActionEventHandler @Inject constructor(
294293
private fun completeRequestData(
295294
brokerStep: BrokerStep,
296295
actionToExecute: BrokerAction,
297-
profileQuery: ProfileQuery,
296+
state: State,
298297
requestData: PirScriptRequestData,
299-
generatedEmailData: GeneratedEmailData?,
300298
): PirScriptRequestData {
301-
val extractedProfile = if (brokerStep is OptOutStep && actionToExecute.dataSource == EXTRACTED_PROFILE &&
302-
(requestData as UserProfile).extractedProfile == null
303-
) {
304-
brokerStep.profileToOptOut
305-
} else if (brokerStep is EmailConfirmationStep && actionToExecute.dataSource == EXTRACTED_PROFILE &&
306-
(requestData as UserProfile).extractedProfile == null
307-
) {
308-
brokerStep.profileToOptOut
309-
} else {
310-
null
299+
if (requestData !is UserProfile || requestData.extractedProfile != null) {
300+
return requestData
301+
}
302+
if (actionToExecute.dataSource != EXTRACTED_PROFILE) {
303+
return requestData
311304
}
312305

313-
return if (extractedProfile != null && requestData is UserProfile) {
314-
val params = extractedProfile.toParams(profileQuery.fullName)
315-
UserProfile(
316-
userProfile = requestData.userProfile,
317-
extractedProfile = if (generatedEmailData != null) {
318-
params.copy(email = generatedEmailData.emailAddress)
319-
} else {
320-
params
321-
},
322-
)
306+
val baseParams: ExtractedProfileParams = when (brokerStep) {
307+
is OptOutStep -> brokerStep.profileToOptOut.toParams(state.profileQuery.fullName)
308+
is EmailConfirmationStep -> brokerStep.profileToOptOut.toParams(state.profileQuery.fullName)
309+
is ScanStep -> if (state.generatedEmailData != null) ExtractedProfileParams() else return requestData
310+
}
311+
312+
val withEmail = state.generatedEmailData?.let {
313+
baseParams.copy(email = it.emailAddress)
314+
} ?: baseParams
315+
316+
val withEmailExtractedData = if (state.emailExtractedData.isNotEmpty()) {
317+
withEmail.copy(emailExtractedData = state.emailExtractedData)
323318
} else {
324-
requestData
319+
withEmail
325320
}
321+
322+
return UserProfile(
323+
userProfile = requestData.userProfile,
324+
extractedProfile = withEmailExtractedData,
325+
)
326326
}
327327

328328
companion object {

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/scripts/models/PirScriptRequestParams.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ data class ExtractedProfileParams(
4444
val profileUrl: String? = null,
4545
val email: String? = null,
4646
val fullName: String? = null,
47+
val emailExtractedData: Map<String, String>? = null,
4748
)

pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/EmailDataReceivedEventHandlerTest.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,23 @@ class EmailDataReceivedEventHandlerTest {
124124
fun whenEmailDataReceivedThenPreservesOtherStateFields() = runTest {
125125
val state = baseState.copy(
126126
currentBrokerStepIndex = 4,
127-
actionRetryCount = 1,
128127
attemptId = "test-attempt",
129128
)
130129
val event = EmailDataReceived(emailExtractedData = mapOf("verificationCode" to "abc"))
131130

132131
val result = testee.invoke(state, event)
133132

134133
assertEquals(4, result.nextState.currentBrokerStepIndex)
135-
assertEquals(1, result.nextState.actionRetryCount)
136134
assertEquals("test-attempt", result.nextState.attemptId)
137135
}
136+
137+
@Test
138+
fun whenEmailDataReceivedThenResetsActionRetryCount() = runTest {
139+
val state = baseState.copy(actionRetryCount = 3)
140+
val event = EmailDataReceived(emailExtractedData = mapOf("verificationCode" to "abc"))
141+
142+
val result = testee.invoke(state, event)
143+
144+
assertEquals(0, result.nextState.actionRetryCount)
145+
}
138146
}

pir/pir-impl/src/test/kotlin/com/duckduckgo/pir/impl/common/actions/ExecuteBrokerStepActionEventHandlerTest.kt

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,4 +1061,203 @@ class ExecuteBrokerStepActionEventHandlerTest {
10611061
val sideEffect = result.sideEffect as AwaitEmailData
10621062
assertEquals("", sideEffect.emailAddress)
10631063
}
1064+
1065+
@Test
1066+
fun whenScanStepFillFormWithGeneratedEmailDataThenIncludesEmailInRequestData() = runTest {
1067+
val action = BrokerAction.FillForm(
1068+
id = "action-fill",
1069+
elements = emptyList(),
1070+
selector = "form",
1071+
dataSource = DataSource.EXTRACTED_PROFILE,
1072+
)
1073+
val scanStep = ScanStep(
1074+
broker = testBroker,
1075+
step = ScanStepActions(
1076+
stepType = "scan",
1077+
actions = listOf(action),
1078+
scanType = "initial",
1079+
),
1080+
)
1081+
val state = State(
1082+
runType = RunType.MANUAL,
1083+
brokerStepsToExecute = listOf(scanStep),
1084+
profileQuery = testProfileQuery,
1085+
currentBrokerStepIndex = 0,
1086+
currentActionIndex = 0,
1087+
generatedEmailData = GeneratedEmailData(
1088+
emailAddress = "scan-generated@example.com",
1089+
pattern = "pattern-123",
1090+
),
1091+
stageStatus = PirStageStatus(
1092+
currentStage = PirStage.OTHER,
1093+
stageStartMs = 0,
1094+
),
1095+
)
1096+
val event = ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery))
1097+
1098+
val result = testee.invoke(state, event)
1099+
1100+
val sideEffect = result.sideEffect as PushJsAction
1101+
val userData = sideEffect.requestParamsData as UserProfile
1102+
assertEquals(testProfileQuery, userData.userProfile)
1103+
assertEquals("scan-generated@example.com", userData.extractedProfile?.email)
1104+
assertNull(userData.extractedProfile?.name)
1105+
assertNull(userData.extractedProfile?.emailExtractedData)
1106+
}
1107+
1108+
@Test
1109+
fun whenScanStepFillFormWithoutGeneratedEmailDataThenExtractedProfileIsNull() = runTest {
1110+
val action = BrokerAction.FillForm(
1111+
id = "action-fill",
1112+
elements = emptyList(),
1113+
selector = "form",
1114+
dataSource = DataSource.EXTRACTED_PROFILE,
1115+
)
1116+
val scanStep = ScanStep(
1117+
broker = testBroker,
1118+
step = ScanStepActions(
1119+
stepType = "scan",
1120+
actions = listOf(action),
1121+
scanType = "initial",
1122+
),
1123+
)
1124+
val state = State(
1125+
runType = RunType.MANUAL,
1126+
brokerStepsToExecute = listOf(scanStep),
1127+
profileQuery = testProfileQuery,
1128+
currentBrokerStepIndex = 0,
1129+
currentActionIndex = 0,
1130+
generatedEmailData = null,
1131+
stageStatus = PirStageStatus(
1132+
currentStage = PirStage.OTHER,
1133+
stageStartMs = 0,
1134+
),
1135+
)
1136+
val event = ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery))
1137+
1138+
val result = testee.invoke(state, event)
1139+
1140+
val sideEffect = result.sideEffect as PushJsAction
1141+
val userData = sideEffect.requestParamsData as UserProfile
1142+
assertNull(userData.extractedProfile)
1143+
}
1144+
1145+
@Test
1146+
fun whenOptOutFillFormWithEmailExtractedDataThenIncludesItInRequestData() = runTest {
1147+
val action = BrokerAction.FillForm(
1148+
id = "action-fill",
1149+
elements = emptyList(),
1150+
selector = "form",
1151+
dataSource = DataSource.EXTRACTED_PROFILE,
1152+
)
1153+
val optOutStep = OptOutStep(
1154+
broker = testBroker,
1155+
step = OptOutStepActions(
1156+
stepType = "optout",
1157+
actions = listOf(action),
1158+
optOutType = "form",
1159+
),
1160+
profileToOptOut = testExtractedProfile,
1161+
)
1162+
val state = State(
1163+
runType = RunType.OPTOUT,
1164+
brokerStepsToExecute = listOf(optOutStep),
1165+
profileQuery = testProfileQuery,
1166+
currentBrokerStepIndex = 0,
1167+
currentActionIndex = 0,
1168+
emailExtractedData = mapOf("verificationCode" to "123456"),
1169+
stageStatus = PirStageStatus(
1170+
currentStage = PirStage.OTHER,
1171+
stageStartMs = 0,
1172+
),
1173+
)
1174+
val event = ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery))
1175+
1176+
val result = testee.invoke(state, event)
1177+
1178+
val sideEffect = result.sideEffect as PushJsAction
1179+
val userData = sideEffect.requestParamsData as UserProfile
1180+
assertEquals("John Doe", userData.extractedProfile?.name)
1181+
assertEquals(mapOf("verificationCode" to "123456"), userData.extractedProfile?.emailExtractedData)
1182+
}
1183+
1184+
@Test
1185+
fun whenScanStepFillFormWithGeneratedEmailAndEmailExtractedDataThenIncludesBoth() = runTest {
1186+
val action = BrokerAction.FillForm(
1187+
id = "action-fill",
1188+
elements = emptyList(),
1189+
selector = "form",
1190+
dataSource = DataSource.EXTRACTED_PROFILE,
1191+
)
1192+
val scanStep = ScanStep(
1193+
broker = testBroker,
1194+
step = ScanStepActions(
1195+
stepType = "scan",
1196+
actions = listOf(action),
1197+
scanType = "initial",
1198+
),
1199+
)
1200+
val state = State(
1201+
runType = RunType.MANUAL,
1202+
brokerStepsToExecute = listOf(scanStep),
1203+
profileQuery = testProfileQuery,
1204+
currentBrokerStepIndex = 0,
1205+
currentActionIndex = 0,
1206+
generatedEmailData = GeneratedEmailData(
1207+
emailAddress = "scan-generated@example.com",
1208+
pattern = "pattern-123",
1209+
),
1210+
emailExtractedData = mapOf("verificationCode" to "654321"),
1211+
stageStatus = PirStageStatus(
1212+
currentStage = PirStage.OTHER,
1213+
stageStartMs = 0,
1214+
),
1215+
)
1216+
val event = ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery))
1217+
1218+
val result = testee.invoke(state, event)
1219+
1220+
val sideEffect = result.sideEffect as PushJsAction
1221+
val userData = sideEffect.requestParamsData as UserProfile
1222+
assertEquals("scan-generated@example.com", userData.extractedProfile?.email)
1223+
assertEquals(mapOf("verificationCode" to "654321"), userData.extractedProfile?.emailExtractedData)
1224+
}
1225+
1226+
@Test
1227+
fun whenEmailExtractedDataIsEmptyThenFieldIsNull() = runTest {
1228+
val action = BrokerAction.FillForm(
1229+
id = "action-fill",
1230+
elements = emptyList(),
1231+
selector = "form",
1232+
dataSource = DataSource.EXTRACTED_PROFILE,
1233+
)
1234+
val optOutStep = OptOutStep(
1235+
broker = testBroker,
1236+
step = OptOutStepActions(
1237+
stepType = "optout",
1238+
actions = listOf(action),
1239+
optOutType = "form",
1240+
),
1241+
profileToOptOut = testExtractedProfile,
1242+
)
1243+
val state = State(
1244+
runType = RunType.OPTOUT,
1245+
brokerStepsToExecute = listOf(optOutStep),
1246+
profileQuery = testProfileQuery,
1247+
currentBrokerStepIndex = 0,
1248+
currentActionIndex = 0,
1249+
emailExtractedData = emptyMap(),
1250+
stageStatus = PirStageStatus(
1251+
currentStage = PirStage.OTHER,
1252+
stageStartMs = 0,
1253+
),
1254+
)
1255+
val event = ExecuteBrokerStepAction(UserProfile(userProfile = testProfileQuery))
1256+
1257+
val result = testee.invoke(state, event)
1258+
1259+
val sideEffect = result.sideEffect as PushJsAction
1260+
val userData = sideEffect.requestParamsData as UserProfile
1261+
assertNull(userData.extractedProfile?.emailExtractedData)
1262+
}
10641263
}

0 commit comments

Comments
 (0)