Skip to content

Commit fb2ffed

Browse files
committed
feat: Implement combined PID support in AddDocumentInteractor
This commit updates the document issuance logic to support combined Personal Identification (PID) documents and refactors the interactor's state management. Key changes: - Renamed `AddDocumentInteractorPartialState` to `AddDocumentInteractorScopedPartialState`. - Updated `AddDocumentInteractor` to group and partition PID documents, presenting them as a single "PID Combined" option per issuer. - Improved sorting logic for document options by issuer and display name. - Added `issuance_add_document_pid_combined` string resource. - Enabled `forcePidActivation` in `ConfigLogic`. - Updated `AddDocumentViewModel` and unit tests to reflect the state renaming and the new combined PID logic.
1 parent c792c4c commit fb2ffed

File tree

6 files changed

+100
-58
lines changed

6 files changed

+100
-58
lines changed

business-logic/src/main/java/eu/europa/ec/businesslogic/config/ConfigLogic.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ interface ConfigLogic {
5757
/**
5858
* Set if the wallet requires PID Activation.
5959
*/
60-
val forcePidActivation: Boolean get() = false
60+
val forcePidActivation: Boolean get() = true
6161
}
6262

6363
enum class AppFlavor {

issuance-feature/src/main/java/eu/europa/ec/issuancefeature/interactor/AddDocumentInteractor.kt

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,18 @@ sealed class AddDocumentInteractorIssueDocumentsPartialState {
6464
) : AddDocumentInteractorIssueDocumentsPartialState()
6565
}
6666

67-
sealed class AddDocumentInteractorPartialState {
67+
sealed class AddDocumentInteractorScopedPartialState {
6868
data class Success(val options: List<Pair<String, List<AddDocumentUi>>>) :
69-
AddDocumentInteractorPartialState()
69+
AddDocumentInteractorScopedPartialState()
7070

71-
data class NoOptions(val errorMsg: String) : AddDocumentInteractorPartialState()
72-
data class Failure(val error: String) : AddDocumentInteractorPartialState()
71+
data class NoOptions(val errorMsg: String) : AddDocumentInteractorScopedPartialState()
72+
data class Failure(val error: String) : AddDocumentInteractorScopedPartialState()
7373
}
7474

7575
interface AddDocumentInteractor {
7676
fun getAddDocumentOption(
7777
flowType: IssuanceFlowType,
78-
): Flow<AddDocumentInteractorPartialState>
78+
): Flow<AddDocumentInteractorScopedPartialState>
7979

8080
fun issueDocuments(
8181
issuanceMethod: IssuanceMethod,
@@ -105,72 +105,105 @@ class AddDocumentInteractorImpl(
105105
private val genericErrorMsg
106106
get() = resourceProvider.genericErrorMessage()
107107

108-
// TODO: REWORK TO SUPPORT COMBINED PID
109108
override fun getAddDocumentOption(
110109
flowType: IssuanceFlowType,
111-
): Flow<AddDocumentInteractorPartialState> =
110+
): Flow<AddDocumentInteractorScopedPartialState> =
112111
flow {
113112
val state =
114113
walletCoreDocumentsController.getScopedDocuments(resourceProvider.getLocale())
114+
115115
when (state) {
116116
is FetchScopedDocumentsPartialState.Failure -> emit(
117-
AddDocumentInteractorPartialState.Failure(
117+
AddDocumentInteractorScopedPartialState.Failure(
118118
error = state.errorMessage
119119
)
120120
)
121121

122122
is FetchScopedDocumentsPartialState.Success -> {
123123

124-
val customFormatType: FormatType? =
124+
val formatType: FormatType? =
125125
(flowType as? IssuanceFlowType.ExtraDocument)?.formatType
126126

127127
val options: List<Pair<String, List<AddDocumentUi>>> =
128128
state.documents
129129
.asSequence()
130130
.filter { doc ->
131-
(customFormatType == null || doc.formatType == customFormatType) &&
131+
(formatType == null || doc.formatType == formatType) &&
132132
(flowType !is IssuanceFlowType.NoDocument || doc.isPid)
133133
}
134-
.sortedWith(
135-
compareBy(
136-
{ it.credentialIssuerId },
137-
{ it.name.lowercase() }
138-
))
139-
.map { doc ->
140-
AddDocumentUi(
141-
credentialIssuerId = doc.credentialIssuerId,
142-
configurationIds = listOf(doc.configurationId),
143-
itemData = ListItemDataUi(
144-
itemId = doc.configurationId,
145-
mainContentData = ListItemMainContentDataUi.Text(text = doc.name),
146-
trailingContentData = ListItemTrailingContentDataUi.Icon(
147-
iconData = AppIcons.Add
134+
.groupBy { it.credentialIssuerId }
135+
.map { (issuer, docs) ->
136+
137+
val (pidDocs, otherDocs) = docs.partition { it.isPid }
138+
val pidIds = pidDocs.map { it.configurationId }
139+
140+
val combinedPid: List<AddDocumentUi> =
141+
if (pidDocs.isNotEmpty()) {
142+
listOf(
143+
AddDocumentUi(
144+
credentialIssuerId = issuer,
145+
configurationIds = pidIds,
146+
itemData = ListItemDataUi(
147+
itemId = "${issuer}_${pidIds.joinToString(",")}",
148+
mainContentData = ListItemMainContentDataUi.Text(
149+
text = resourceProvider.getString(
150+
R.string.issuance_add_document_pid_combined
151+
)
152+
),
153+
trailingContentData = ListItemTrailingContentDataUi.Icon(
154+
iconData = AppIcons.Add
155+
)
156+
)
157+
)
158+
)
159+
} else {
160+
emptyList()
161+
}
162+
163+
val mappedOthers: List<AddDocumentUi> =
164+
otherDocs.map { doc ->
165+
AddDocumentUi(
166+
credentialIssuerId = issuer,
167+
configurationIds = listOf(doc.configurationId),
168+
itemData = ListItemDataUi(
169+
itemId = doc.configurationId,
170+
mainContentData = ListItemMainContentDataUi.Text(
171+
text = doc.name
172+
),
173+
trailingContentData = ListItemTrailingContentDataUi.Icon(
174+
iconData = AppIcons.Add
175+
)
176+
)
148177
)
149-
)
150-
)
178+
}
179+
180+
val items = (combinedPid + mappedOthers)
181+
.sortedBy {
182+
(it.itemData.mainContentData as ListItemMainContentDataUi.Text)
183+
.text
184+
.lowercase()
185+
}
186+
issuer to items
151187
}
152-
.groupBy { it.credentialIssuerId }
153-
.entries
154-
.map { (issuer, items) -> issuer to items }
155-
188+
.sortedBy { it.first.lowercase() }
156189

157190
if (options.isEmpty()) {
158191
emit(
159-
AddDocumentInteractorPartialState.NoOptions(
192+
AddDocumentInteractorScopedPartialState.NoOptions(
160193
errorMsg = resourceProvider.getString(R.string.issuance_add_document_no_options)
161194
)
162195
)
163196
} else {
164197
emit(
165-
AddDocumentInteractorPartialState.Success(
198+
AddDocumentInteractorScopedPartialState.Success(
166199
options = options
167200
)
168201
)
169202
}
170203
}
171204
}
172205
}.safeAsync {
173-
AddDocumentInteractorPartialState.Failure(
206+
AddDocumentInteractorScopedPartialState.Failure(
174207
error = it.localizedMessage ?: genericErrorMsg
175208
)
176209
}

issuance-feature/src/main/java/eu/europa/ec/issuancefeature/ui/add/AddDocumentViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import eu.europa.ec.corelogic.controller.IssuanceMethod
3333
import eu.europa.ec.corelogic.di.getOrCreatePresentationScope
3434
import eu.europa.ec.issuancefeature.interactor.AddDocumentInteractor
3535
import eu.europa.ec.issuancefeature.interactor.AddDocumentInteractorIssueDocumentsPartialState
36-
import eu.europa.ec.issuancefeature.interactor.AddDocumentInteractorPartialState
36+
import eu.europa.ec.issuancefeature.interactor.AddDocumentInteractorScopedPartialState
3737
import eu.europa.ec.issuancefeature.ui.add.model.AddDocumentUi
3838
import eu.europa.ec.resourceslogic.R
3939
import eu.europa.ec.resourceslogic.provider.ResourceProvider
@@ -216,7 +216,7 @@ class AddDocumentViewModel(
216216
flowType = viewState.value.issuanceConfig.flowType
217217
).collect { response ->
218218
when (response) {
219-
is AddDocumentInteractorPartialState.Success -> {
219+
is AddDocumentInteractorScopedPartialState.Success -> {
220220
setState {
221221
copy(
222222
error = null,
@@ -232,7 +232,7 @@ class AddDocumentViewModel(
232232
handleDeepLink(deepLinkUri)
233233
}
234234

235-
is AddDocumentInteractorPartialState.Failure -> {
235+
is AddDocumentInteractorScopedPartialState.Failure -> {
236236

237237
val deepLinkAction = getDeepLinkAction(deepLinkUri)
238238

@@ -259,7 +259,7 @@ class AddDocumentViewModel(
259259
}
260260
}
261261

262-
is AddDocumentInteractorPartialState.NoOptions -> {
262+
is AddDocumentInteractorScopedPartialState.NoOptions -> {
263263
setState {
264264
copy(
265265
error = null,

issuance-feature/src/test/java/eu/europa/ec/issuancefeature/interactor/TestAddDocumentInteractor.kt

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import eu.europa.ec.corelogic.controller.IssuanceMethod
2828
import eu.europa.ec.corelogic.controller.IssueDocumentsPartialState
2929
import eu.europa.ec.corelogic.controller.WalletCoreDocumentsController
3030
import eu.europa.ec.issuancefeature.util.mockedAgeOptionItemUi
31+
import eu.europa.ec.issuancefeature.util.mockedCombinedPid
3132
import eu.europa.ec.issuancefeature.util.mockedConfigNavigationTypePopToScreen
3233
import eu.europa.ec.issuancefeature.util.mockedConfigNavigationTypePush
3334
import eu.europa.ec.issuancefeature.util.mockedIssuerId
@@ -130,7 +131,7 @@ class TestAddDocumentInteractor {
130131
// 2. walletCoreDocumentsController.getScopedDocuments() returns a non-empty list
131132

132133
// Case 1 Expected Result:
133-
// AddDocumentInteractorPartialState.Success state, with the following options:
134+
// AddDocumentInteractorScopedPartialState.Success state, with the following options:
134135
// 1. a PID
135136
@Test
136137
fun `Given Case 1, When getAddDocumentOption is called, Then Case 1 Expected Result is returned`() {
@@ -141,13 +142,16 @@ class TestAddDocumentInteractor {
141142
)
142143
mockGetScopedDocumentsResponse(expectedResponse)
143144

145+
whenever(resourceProvider.getString(R.string.issuance_add_document_pid_combined))
146+
.thenReturn(mockedCombinedPid)
147+
144148
// When
145149
interactor.getAddDocumentOption(
146150
flowType = IssuanceFlowType.NoDocument
147151
).runFlowTest {
148152
// Then
149153
assertEquals(
150-
AddDocumentInteractorPartialState.Success(
154+
AddDocumentInteractorScopedPartialState.Success(
151155
options = listOf(
152156
Pair(mockedIssuerId, listOf(mockedPidOptionItemUi))
153157
)
@@ -163,7 +167,7 @@ class TestAddDocumentInteractor {
163167
// 2. walletCoreDocumentsController.getScopedDocuments() returns a non-empty list
164168

165169
// Case 2 Expected Result:
166-
// AddDocumentInteractorPartialState.Success state, with the following options:
170+
// AddDocumentInteractorScopedPartialState.Success state, with the following options:
167171
// 1. an Age Verification,
168172
// 2. a PID,
169173
// 3. an mDL,
@@ -177,6 +181,9 @@ class TestAddDocumentInteractor {
177181
)
178182
mockGetScopedDocumentsResponse(expectedResponse)
179183

184+
whenever(resourceProvider.getString(R.string.issuance_add_document_pid_combined))
185+
.thenReturn(mockedCombinedPid)
186+
180187
// When
181188
interactor.getAddDocumentOption(
182189
flowType = IssuanceFlowType.ExtraDocument(
@@ -185,15 +192,15 @@ class TestAddDocumentInteractor {
185192
).runFlowTest {
186193
// Then
187194
assertEquals(
188-
AddDocumentInteractorPartialState.Success(
195+
AddDocumentInteractorScopedPartialState.Success(
189196
options = listOf(
190197
Pair(
191198
mockedIssuerId,
192199
listOf(
193200
mockedAgeOptionItemUi,
194-
mockedPidOptionItemUi,
195201
mockedMdlOptionItemUi,
196-
mockedPhotoIdOptionItemUi
202+
mockedPhotoIdOptionItemUi,
203+
mockedPidOptionItemUi
197204
)
198205
)
199206
)
@@ -209,7 +216,7 @@ class TestAddDocumentInteractor {
209216
// 2. walletCoreDocumentsController.getScopedDocuments() returns an empty list
210217

211218
// Case 3 Expected Result:
212-
// AddDocumentInteractorPartialState.NoOptions state.
219+
// AddDocumentInteractorScopedPartialState.NoOptions state.
213220
@Test
214221
fun `Given Case 3, When getAddDocumentOption is called, Then Case 3 Expected Result is returned`() {
215222
coroutineRule.runTest {
@@ -229,7 +236,7 @@ class TestAddDocumentInteractor {
229236
).runFlowTest {
230237
// Then
231238
assertEquals(
232-
AddDocumentInteractorPartialState.NoOptions(
239+
AddDocumentInteractorScopedPartialState.NoOptions(
233240
errorMsg = noDocumentsMsg
234241
),
235242
awaitItem()
@@ -243,7 +250,7 @@ class TestAddDocumentInteractor {
243250
// 2. walletCoreDocumentsController.getScopedDocuments() returns a non-empty list
244251

245252
// Case 4 Expected Result:
246-
// AddDocumentInteractorPartialState.NoOptions state.
253+
// AddDocumentInteractorScopedPartialState.NoOptions state.
247254
@Test
248255
fun `Given Case 4, When getAddDocumentOption is called, Then Case 4 Expected Result is returned`() {
249256
coroutineRule.runTest {
@@ -263,7 +270,7 @@ class TestAddDocumentInteractor {
263270
).runFlowTest {
264271
// Then
265272
assertEquals(
266-
AddDocumentInteractorPartialState.NoOptions(
273+
AddDocumentInteractorScopedPartialState.NoOptions(
267274
errorMsg = noDocumentsMsg
268275
),
269276
awaitItem()
@@ -277,7 +284,7 @@ class TestAddDocumentInteractor {
277284
// 2. walletCoreDocumentsController.getScopedDocuments() returns only non-PID documents
278285

279286
// Case 5 Expected Result:
280-
// AddDocumentInteractorPartialState.NoOptions state.
287+
// AddDocumentInteractorScopedPartialState.NoOptions state.
281288
@Test
282289
fun `Given Case 5, When getAddDocumentOption is called, Then Case 5 Expected Result is returned`() {
283290
coroutineRule.runTest {
@@ -298,7 +305,7 @@ class TestAddDocumentInteractor {
298305
).runFlowTest {
299306
// Then
300307
assertEquals(
301-
AddDocumentInteractorPartialState.NoOptions(
308+
AddDocumentInteractorScopedPartialState.NoOptions(
302309
errorMsg = noDocumentsMsg
303310
),
304311
awaitItem()
@@ -313,7 +320,7 @@ class TestAddDocumentInteractor {
313320
// FetchScopedDocumentsPartialState.Failure with an error message.
314321

315322
// Case 6 Expected Result:
316-
// AddDocumentInteractorPartialState.Failure with the same error message.
323+
// AddDocumentInteractorScopedPartialState.Failure with the same error message.
317324
@Test
318325
fun `Given Case 6, When getAddDocumentOption is called, Then Case 6 Expected Result is returned`() {
319326
coroutineRule.runTest {
@@ -329,7 +336,7 @@ class TestAddDocumentInteractor {
329336
).runFlowTest {
330337
// Then
331338
assertEquals(
332-
AddDocumentInteractorPartialState.Failure(
339+
AddDocumentInteractorScopedPartialState.Failure(
333340
error = mockedPlainFailureMessage
334341
),
335342
awaitItem()
@@ -343,7 +350,7 @@ class TestAddDocumentInteractor {
343350
// 2. walletCoreDocumentsController.getScopedDocuments() throws an exception with no error message.
344351

345352
// Case 7 Expected Result:
346-
// AddDocumentInteractorPartialState.Failure with the generic error message.
353+
// AddDocumentInteractorScopedPartialState.Failure with the generic error message.
347354
@Test
348355
fun `Given Case 7, When getAddDocumentOption is called, Then Case 7 Expected Result is returned`() {
349356
coroutineRule.runTest {
@@ -358,7 +365,7 @@ class TestAddDocumentInteractor {
358365
).runFlowTest {
359366
// Then
360367
assertEquals(
361-
AddDocumentInteractorPartialState.Failure(
368+
AddDocumentInteractorScopedPartialState.Failure(
362369
error = mockedGenericErrorMessage
363370
),
364371
awaitItem()
@@ -372,7 +379,7 @@ class TestAddDocumentInteractor {
372379
// 2. walletCoreDocumentsController.getScopedDocuments() throws an exception with an error message.
373380

374381
// Case 8 Expected Result:
375-
// AddDocumentInteractorPartialState.Failure with the exception's error message.
382+
// AddDocumentInteractorScopedPartialState.Failure with the exception's error message.
376383
@Test
377384
fun `Given Case 8, When getAddDocumentOption is called, Then Case 8 Expected Result is returned`() {
378385
coroutineRule.runTest {
@@ -387,7 +394,7 @@ class TestAddDocumentInteractor {
387394
).runFlowTest {
388395
// Then
389396
assertEquals(
390-
AddDocumentInteractorPartialState.Failure(
397+
AddDocumentInteractorScopedPartialState.Failure(
391398
error = mockedExceptionWithMessage.localizedMessage!!
392399
),
393400
awaitItem()

0 commit comments

Comments
 (0)