diff --git a/Makefile b/Makefile index ff6edecb3c..729bcfa838 100644 --- a/Makefile +++ b/Makefile @@ -210,6 +210,23 @@ emit-object-tags-added-without-virus: ##@events emits a ObjectTagging:Put event --function-name event-received text \ --payload '{"Records":[{"eventSource":"aws:s3","eventTime":"2023-10-23T15:58:33.081Z","eventName":"ObjectTagging:Put","s3":{"bucket":{"name":"uploads-opg-modernising-lpa-eu-west-1"},"object":{"key":"$(key)"}}}]}' +emit-immaterial-change-confirmed: ##@events emits a immaterial-change-confirmed event with the given LpaUID, actor type an actor UID e.g. emit-immaterial-change-confirmed uid=abc-123 actorType=donor actorUid=def-456 + $(eval BODY := $(shell echo '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"immaterial-change-confirmed","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"uid":"$(uid)","actorType":"$(actorType)","actorUid":"$(actorUid)","sentDate":"2024-01-02T12:13:14.000006Z"}}' | sed 's/"/\\"/g')) + + docker compose -f docker/docker-compose.yml exec localstack awslocal lambda invoke \ + --endpoint-url=http://localhost:4566 \ + --region eu-west-1 \ + --function-name event-received text \ + --payload '{"Records": [{"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", "body": "$(BODY)"}]}' + +emit-material-change-confirmed: ##@events emits a material-change-confirmed event with the given LpaUID, actor type an actor UID e.g. emit-material-change-confirmed uid=abc-123 actorType=donor actorUid=def-456 + $(eval BODY := $(shell echo '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"material-change-confirmed","source":"opg.poas.sirius","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"uid":"$(uid)","actorType":"$(actorType)","actorUid":"$(actorUid)","sentDate":"2024-01-02T12:13:14.000006Z"}}' | sed 's/"/\\"/g')) + + docker compose -f docker/docker-compose.yml exec localstack awslocal lambda invoke \ + --endpoint-url=http://localhost:4566 \ + --region eu-west-1 \ + --function-name event-received text \ + --payload '{"Records": [{"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", "body": "$(BODY)"}]}' set-uploads-clean: ##@events calls emit-object-tags-added-without-virus for all documents on a given lpa e.g. set-uploads-clean lpaId=abc for k in $$(docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb --region eu-west-1 query --table-name lpas --key-condition-expression 'PK = :pk and begins_with(SK, :sk)' --expression-attribute-values '{":pk": {"S": "LPA#$(lpaId)"}, ":sk": {"S": "DOCUMENT#"}}' | jq -c -r '.Items[] | .Key[]'); do \ diff --git a/cmd/event-received/handlers.go b/cmd/event-received/handlers.go index 87e67876f8..2faa92ba0a 100644 --- a/cmd/event-received/handlers.go +++ b/cmd/event-received/handlers.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-lambda-go/events" + "github.com/ministryofjustice/opg-modernising-lpa/internal/certificateprovider/certificateproviderdata" "github.com/ministryofjustice/opg-modernising-lpa/internal/donor/donordata" "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" ) @@ -78,3 +79,27 @@ func getDonorByLpaUID(ctx context.Context, client dynamodbClient, uid string) (* return &donor, nil } + +func putCertificateProvider(ctx context.Context, certificateProvider *certificateproviderdata.Provided, now func() time.Time, client dynamodbClient) error { + certificateProvider.UpdatedAt = now() + + return client.Put(ctx, certificateProvider) +} + +func getCertificateProviderByLpaUID(ctx context.Context, client dynamodbClient, uid string) (*certificateproviderdata.Provided, error) { + var key dynamo.Keys + if err := client.OneByUID(ctx, uid, &key); err != nil { + return nil, fmt.Errorf("failed to resolve uid: %w", err) + } + + if key.PK == nil { + return nil, fmt.Errorf("PK missing from LPA in response") + } + + var certificateProvider certificateproviderdata.Provided + if err := client.OneByPartialSK(ctx, key.PK, dynamo.CertificateProviderKey(""), &certificateProvider); err != nil { + return nil, fmt.Errorf("failed to get certificate provider: %w", err) + } + + return &certificateProvider, nil +} diff --git a/cmd/event-received/mock_test.go b/cmd/event-received/mock_test.go index 449965bfd8..ca07fecdeb 100644 --- a/cmd/event-received/mock_test.go +++ b/cmd/event-received/mock_test.go @@ -15,14 +15,21 @@ func (c *mockDynamodbClient_OneByUID_Call) SetData(data any) { } func (c *mockDynamodbClient_One_Call) SetData(data any) { - c.Run(func(_ context.Context, _ dynamo.PK, _ dynamo.SK, v interface{}) { + c.Run(func(_ context.Context, _ dynamo.PK, _ dynamo.SK, v any) { b, _ := attributevalue.Marshal(data) attributevalue.Unmarshal(b, v) }) } func (c *mockDynamodbClient_AllByLpaUIDAndPartialSK_Call) SetData(data any) { - c.Run(func(_ context.Context, _ string, _ dynamo.SK, v interface{}) { + c.Run(func(_ context.Context, _ string, _ dynamo.SK, v any) { + b, _ := attributevalue.Marshal(data) + attributevalue.Unmarshal(b, v) + }) +} + +func (c *mockDynamodbClient_OneByPartialSK_Call) SetData(data any) { + c.Run(func(_ context.Context, _ dynamo.PK, _ dynamo.SK, v any) { b, _ := attributevalue.Marshal(data) attributevalue.Unmarshal(b, v) }) diff --git a/cmd/event-received/sirius_event_handler.go b/cmd/event-received/sirius_event_handler.go index 2c44fb9288..8afab2b60d 100644 --- a/cmd/event-received/sirius_event_handler.go +++ b/cmd/event-received/sirius_event_handler.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-lambda-go/events" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" "github.com/ministryofjustice/opg-modernising-lpa/internal/certificateprovider/certificateproviderdata" "github.com/ministryofjustice/opg-modernising-lpa/internal/donor/donordata" @@ -76,6 +77,12 @@ func (h *siriusEventHandler) Handle(ctx context.Context, factory factory, cloudW case "priority-correspondence-sent": return handlePriorityCorrespondenceSent(ctx, factory.DynamoClient(), cloudWatchEvent, factory.Now()) + case "immaterial-change-confirmed": + return handleChangeConfirmed(ctx, factory.DynamoClient(), cloudWatchEvent, factory.Now(), false) + + case "material-change-confirmed": + return handleChangeConfirmed(ctx, factory.DynamoClient(), cloudWatchEvent, factory.Now(), true) + default: return fmt.Errorf("unknown sirius event") } @@ -120,7 +127,7 @@ func handleFeeApproved( donor, err := getDonorByLpaUID(ctx, client, v.UID) if err != nil { - return err + return fmt.Errorf("failed to get donor: %w", err) } if donor.Tasks.PayForLpa.IsCompleted() || donor.Tasks.PayForLpa.IsApproved() { @@ -168,7 +175,7 @@ func handleFurtherInfoRequested(ctx context.Context, client dynamodbClient, even donor, err := getDonorByLpaUID(ctx, client, v.UID) if err != nil { - return err + return fmt.Errorf("failed to get donor: %w", err) } if donor.Tasks.PayForLpa.IsMoreEvidenceRequired() { @@ -193,7 +200,7 @@ func handleFeeDenied(ctx context.Context, client dynamodbClient, event *events.C donor, err := getDonorByLpaUID(ctx, client, v.UID) if err != nil { - return err + return fmt.Errorf("failed to get donor: %w", err) } if donor.Tasks.PayForLpa.IsDenied() { @@ -333,13 +340,67 @@ func handlePriorityCorrespondenceSent(ctx context.Context, client dynamodbClient donor, err := getDonorByLpaUID(ctx, client, v.UID) if err != nil { - return fmt.Errorf("failed to get lpa: %w", err) + return fmt.Errorf("failed to get donor: %w", err) } donor.PriorityCorrespondenceSentAt = v.SentDate if err := putDonor(ctx, donor, now, client); err != nil { - return fmt.Errorf("failed to update lpa: %w", err) + return fmt.Errorf("failed to update donor: %w", err) + } + + return nil +} + +type changeConfirmedEvent struct { + UID string `json:"uid"` + ActorType actor.Type `json:"actorType"` + ActorUID string `json:"actorUID"` +} + +func handleChangeConfirmed(ctx context.Context, client dynamodbClient, event *events.CloudWatchEvent, now func() time.Time, materialChange bool) error { + var v changeConfirmedEvent + if err := json.Unmarshal(event.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + switch v.ActorType { + case actor.TypeDonor: + donor, err := getDonorByLpaUID(ctx, client, v.UID) + if err != nil { + return fmt.Errorf("failed to get donor: %w", err) + } + + if donor.Tasks.ConfirmYourIdentity.IsProblem() { + if materialChange { + donor.MaterialChangeConfirmedAt = now() + } else { + donor.Tasks.ConfirmYourIdentity = task.IdentityStateCompleted + } + + if err := putDonor(ctx, donor, now, client); err != nil { + return fmt.Errorf("failed to update donor: %w", err) + } + } + case actor.TypeCertificateProvider: + certificateProvider, err := getCertificateProviderByLpaUID(ctx, client, v.UID) + if err != nil { + return fmt.Errorf("failed to get certificate provider: %w", err) + } + + if certificateProvider.Tasks.ConfirmYourIdentity.IsProblem() { + if materialChange { + certificateProvider.MaterialChangeConfirmedAt = now() + } else { + certificateProvider.Tasks.ConfirmYourIdentity = task.IdentityStateCompleted + } + + if err := putCertificateProvider(ctx, certificateProvider, now, client); err != nil { + return fmt.Errorf("failed to update certificate provider: %w", err) + } + } + default: + return fmt.Errorf("invalid actorType, got %s, want donor or certificateProvider", v.ActorType.String()) } return nil diff --git a/cmd/event-received/sirius_event_handler_test.go b/cmd/event-received/sirius_event_handler_test.go index bd3076f44f..202595549f 100644 --- a/cmd/event-received/sirius_event_handler_test.go +++ b/cmd/event-received/sirius_event_handler_test.go @@ -1406,3 +1406,310 @@ func TestHandlePriorityCorrespondenceSentWhenPutError(t *testing.T) { err := handlePriorityCorrespondenceSent(ctx, client, event, testNowFn) assert.ErrorIs(t, err, expectedError) } + +func TestHandleImmaterialChangeConfirmed(t *testing.T) { + testcases := map[string]struct { + setupDynamoClient func() *mockDynamodbClient + }{ + "donor": { + setupDynamoClient: func() *mockDynamodbClient { + updated := &donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStateCompleted}, + UpdatedAt: testNow, + } + updated.UpdateHash() + + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + One(ctx, dynamo.LpaKey("123"), dynamo.DonorKey("456"), mock.Anything). + Return(nil). + SetData(donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(ctx, updated). + Return(nil) + + return c + }, + }, + "certificateProvider": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + OneByPartialSK(ctx, dynamo.LpaKey("123"), dynamo.CertificateProviderKey(""), mock.Anything). + Return(nil). + SetData(&certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(ctx, &certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStateCompleted}, + UpdatedAt: testNow, + }). + Return(nil) + + return c + }, + }, + } + + for actorType, tc := range testcases { + t.Run(actorType, func(t *testing.T) { + event := &events.CloudWatchEvent{ + DetailType: "immaterial-change-confirmed", + Detail: json.RawMessage(fmt.Sprintf(`{"uid":"M-1111-2222-3333","actorUID":"740e5834-3a29-46b4-9a6f-16142fde533a","actorType":"%s"}`, actorType)), + } + + factory := newMockFactory(t) + factory.EXPECT(). + DynamoClient(). + Return(tc.setupDynamoClient()) + factory.EXPECT(). + Now(). + Return(testNowFn) + + handler := &siriusEventHandler{} + err := handler.Handle(ctx, factory, event) + + assert.Nil(t, err) + }) + } +} + +func TestHandleChangeConfirmedWhenIdentityTaskNotProblem(t *testing.T) { + testcases := map[string]struct { + setupDynamoClient func() *mockDynamodbClient + }{ + "donor": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + One(ctx, dynamo.LpaKey("123"), dynamo.DonorKey("456"), mock.Anything). + Return(nil). + SetData(donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStatePending}, + }) + + return c + }, + }, + "certificateProvider": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + OneByPartialSK(ctx, dynamo.LpaKey("123"), dynamo.CertificateProviderKey(""), mock.Anything). + Return(nil). + SetData(&certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStatePending}, + }) + + return c + }, + }, + } + + for actorType, tc := range testcases { + t.Run(actorType, func(t *testing.T) { + event := &events.CloudWatchEvent{ + DetailType: "immaterial-change-confirmed", + Detail: json.RawMessage(fmt.Sprintf(`{"uid":"M-1111-2222-3333","actorUID":"740e5834-3a29-46b4-9a6f-16142fde533a","actorType":"%s"}`, actorType)), + } + + err := handleChangeConfirmed(ctx, tc.setupDynamoClient(), event, testNowFn, false) + + assert.Nil(t, err) + }) + } +} + +func TestHandleChangeConfirmedWhenDynamoClientPutError(t *testing.T) { + testcases := map[string]struct { + setupDynamoClient func() *mockDynamodbClient + }{ + "donor": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(mock.Anything, mock.Anything, mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + One(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + SetData(donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(mock.Anything, mock.Anything). + Return(expectedError) + + return c + }, + }, + "certificateProvider": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(mock.Anything, mock.Anything, mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + OneByPartialSK(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil). + SetData(&certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(ctx, mock.Anything). + Return(expectedError) + + return c + }, + }, + } + + for actorType, tc := range testcases { + t.Run(actorType, func(t *testing.T) { + event := &events.CloudWatchEvent{ + DetailType: "immaterial-change-confirmed", + Detail: json.RawMessage(fmt.Sprintf(`{"uid":"M-1111-2222-3333","actorUID":"740e5834-3a29-46b4-9a6f-16142fde533a","actorType":"%s"}`, actorType)), + } + + err := handleChangeConfirmed(ctx, tc.setupDynamoClient(), event, testNowFn, false) + + assert.ErrorIs(t, err, expectedError) + }) + } +} + +func TestHandleImmaterialChangeConfirmedWhenUnexpectedActorType(t *testing.T) { + event := &events.CloudWatchEvent{ + DetailType: "immaterial-change-confirmed", + Detail: json.RawMessage(`{"uid":"M-1111-2222-3333","actorUID":"740e5834-3a29-46b4-9a6f-16142fde533a","actorType":"attorney"}`), + } + + err := handleChangeConfirmed(ctx, nil, event, testNowFn, false) + + assert.ErrorContains(t, err, "invalid actorType, got attorney, want donor or certificateProvider") +} + +func TestHandleMaterialChangeConfirmed(t *testing.T) { + testcases := map[string]struct { + setupDynamoClient func() *mockDynamodbClient + }{ + "donor": { + setupDynamoClient: func() *mockDynamodbClient { + updated := &donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + UpdatedAt: testNow, + MaterialChangeConfirmedAt: testNow, + } + updated.UpdateHash() + + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + One(ctx, dynamo.LpaKey("123"), dynamo.DonorKey("456"), mock.Anything). + Return(nil). + SetData(donordata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.LpaOwnerKey(dynamo.DonorKey("456")), + Tasks: donordata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(ctx, updated). + Return(nil) + + return c + }, + }, + "certificateProvider": { + setupDynamoClient: func() *mockDynamodbClient { + c := newMockDynamodbClient(t) + c.EXPECT(). + OneByUID(ctx, "M-1111-2222-3333", mock.Anything). + Return(nil). + SetData(dynamo.Keys{PK: dynamo.LpaKey("123"), SK: dynamo.DonorKey("456")}) + c.EXPECT(). + OneByPartialSK(ctx, dynamo.LpaKey("123"), dynamo.CertificateProviderKey(""), mock.Anything). + Return(nil). + SetData(&certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + }) + c.EXPECT(). + Put(ctx, &certificateproviderdata.Provided{ + PK: dynamo.LpaKey("123"), + SK: dynamo.CertificateProviderKey("789"), + Tasks: certificateproviderdata.Tasks{ConfirmYourIdentity: task.IdentityStateProblem}, + UpdatedAt: testNow, + MaterialChangeConfirmedAt: testNow, + }). + Return(nil) + + return c + }, + }, + } + + for actorType, tc := range testcases { + t.Run(actorType, func(t *testing.T) { + event := &events.CloudWatchEvent{ + DetailType: "material-change-confirmed", + Detail: json.RawMessage(fmt.Sprintf(`{"uid":"M-1111-2222-3333","actorUID":"740e5834-3a29-46b4-9a6f-16142fde533a","actorType":"%s"}`, actorType)), + } + + factory := newMockFactory(t) + factory.EXPECT(). + DynamoClient(). + Return(tc.setupDynamoClient()) + factory.EXPECT(). + Now(). + Return(testNowFn) + + handler := &siriusEventHandler{} + err := handler.Handle(ctx, factory, event) + + assert.Nil(t, err) + }) + } +} diff --git a/internal/certificateprovider/certificateproviderdata/provided.go b/internal/certificateprovider/certificateproviderdata/provided.go index 0566002a5f..d9c677695f 100644 --- a/internal/certificateprovider/certificateproviderdata/provided.go +++ b/internal/certificateprovider/certificateproviderdata/provided.go @@ -38,6 +38,10 @@ type Provided struct { ContactLanguagePreference localize.Lang // Email is the email address returned from OneLogin when the certificate provider logged in Email string + + // MaterialChangeConfirmedAt records when a material change to LPA data was + // confirmed by a caseworker + MaterialChangeConfirmedAt time.Time `checkhash:"-"` } func (c *Provided) CertificateProviderIdentityConfirmed(firstNames, lastName string) bool { diff --git a/internal/donor/donordata/provided.go b/internal/donor/donordata/provided.go index a231f63355..383300d43c 100644 --- a/internal/donor/donordata/provided.go +++ b/internal/donor/donordata/provided.go @@ -200,12 +200,20 @@ type Provided struct { // donor informing them of a problem. PriorityCorrespondenceSentAt time.Time `checkhash:"-"` + // MaterialChangeConfirmedAt records when a material change to LPA data was + // confirmed by a caseworker + MaterialChangeConfirmedAt time.Time `checkhash:"-"` + // HasSeenSuccessfulVouchBanner records if the donor has seen the progress tracker successful vouch banner HasSeenSuccessfulVouchBanner bool `checkhash:"-"` // HasSeenReducedFeeApprovalNotification records if the donor has seen the progress tracker exemption/remission fee approved banner HasSeenReducedFeeApprovalNotification bool `checkhash:"-"` + // HasSeenIdentityMismatchResolvedNotification records if the donor has seen the progress tracker identity + // confirmed banner + HasSeenIdentityMismatchResolvedNotification bool `checkhash:"-"` + // ReducedFeeApprovedAt records when an exemption/remission was approved. ReducedFeeApprovedAt time.Time `checkhash:"-"` diff --git a/internal/donor/donordata/provided_test.go b/internal/donor/donordata/provided_test.go index af540bf350..c40d3151bb 100644 --- a/internal/donor/donordata/provided_test.go +++ b/internal/donor/donordata/provided_test.go @@ -217,14 +217,14 @@ func TestGenerateHash(t *testing.T) { } // DO change this value to match the updates - const modified uint64 = 0xd6e3407c0de9ebec + const modified uint64 = 0xd75cf269f55b2bb0 // DO NOT change these initial hash values. If a field has been added/removed // you will need to handle the version gracefully by modifying // (*Provided).HashInclude and adding another testcase for the new // version. testcases := map[uint8]uint64{ - 0: 0x424aa066b6c0f772, + 0: 0x5dc4d95782e4dcdc, } for version, initial := range testcases { diff --git a/internal/donor/donorpage/progress.go b/internal/donor/donorpage/progress.go index 93410cbb62..28234f77af 100644 --- a/internal/donor/donorpage/progress.go +++ b/internal/donor/donorpage/progress.go @@ -253,6 +253,14 @@ func Progress(tmpl template.Template, lpaStoreResolvingService LpaStoreResolving data.addInfo("confirmationOfIdentityPending", "youDoNotNeedToTakeAnyAction") } + if donor.Tasks.ConfirmYourIdentity.IsCompleted() && + donor.ContinueWithMismatchedIdentity && + !donor.HasSeenIdentityMismatchResolvedNotification { + data.addSuccess("yourIdentityHadBeenConfirmed", "youDoNotNeedToTakeAnyAction") + + donor.HasSeenIdentityMismatchResolvedNotification = true + } + if !lpa.Status.IsRegistered() && !donor.PriorityCorrespondenceSentAt.IsZero() { data.addInfo( @@ -264,6 +272,18 @@ func Progress(tmpl template.Template, lpaStoreResolvingService LpaStoreResolving ) } + if donor.Tasks.ConfirmYourIdentity.IsProblem() && + donor.ContinueWithMismatchedIdentity && + !donor.MaterialChangeConfirmedAt.IsZero() { + data.addInfo( + "yourLPACannotBeRegisteredByOPG", + appData.Localizer.Format( + "weContactedYouOnWithGuidanceAboutWhatToDoNext", + map[string]any{"ContactedDate": appData.Localizer.FormatDate(donor.MaterialChangeConfirmedAt)}, + ), + ) + } + if err := donorStore.Put(r.Context(), donor); err != nil { return fmt.Errorf("failed to update donor: %v", err) } diff --git a/internal/donor/donorpage/progress_test.go b/internal/donor/donorpage/progress_test.go index fe1c059f09..ff2a0a91da 100644 --- a/internal/donor/donorpage/progress_test.go +++ b/internal/donor/donorpage/progress_test.go @@ -666,6 +666,67 @@ func TestGetProgress(t *testing.T) { setupCertificateProviderStore: certificateProviderStoreNotFound, setupDonorStore: donorStoreNoUpdate, }, + "identity mismatch resolved": { + donor: &donordata.Provided{ + ContinueWithMismatchedIdentity: true, + IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, + Tasks: donordata.Tasks{ + ConfirmYourIdentity: task.IdentityStateCompleted, + }, + }, + lpa: &lpadata.Lpa{}, + setupCertificateProviderStore: certificateProviderStoreNotFound, + successNotifications: []progressNotification{ + {Heading: "yourIdentityHadBeenConfirmed", Body: "youDoNotNeedToTakeAnyAction"}, + }, + setupDonorStore: func(_ *testing.T, s *mockDonorStore) { + s.EXPECT(). + Put(mock.Anything, &donordata.Provided{ + ContinueWithMismatchedIdentity: true, + IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, + Tasks: donordata.Tasks{ + ConfirmYourIdentity: task.IdentityStateCompleted, + }, + HasSeenIdentityMismatchResolvedNotification: true, + }). + Return(nil) + }, + }, + "identity mismatch resolved when already seen": { + donor: &donordata.Provided{ + ContinueWithMismatchedIdentity: true, + IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, + Tasks: donordata.Tasks{ + ConfirmYourIdentity: task.IdentityStateCompleted, + }, + HasSeenIdentityMismatchResolvedNotification: true, + }, + lpa: &lpadata.Lpa{}, + setupCertificateProviderStore: certificateProviderStoreNotFound, + setupDonorStore: donorStoreNoUpdate, + }, + "identity mismatch material change confirmed": { + donor: &donordata.Provided{ + ContinueWithMismatchedIdentity: true, + IdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, + Tasks: donordata.Tasks{ + ConfirmYourIdentity: task.IdentityStateProblem, + }, + MaterialChangeConfirmedAt: testNow, + }, + lpa: &lpadata.Lpa{}, + setupCertificateProviderStore: certificateProviderStoreNotFound, + setupDonorStore: donorStoreNoUpdate, + infoNotifications: []progressNotification{ + {Heading: "yourLPACannotBeRegisteredByOPG", Body: "B"}, + }, + setupLocalizer: func(t *testing.T) *mockLocalizer { + l := newMockLocalizer(t) + l.EXPECT().Format("weContactedYouOnWithGuidanceAboutWhatToDoNext", map[string]any{"ContactedDate": "translated date"}).Return("B") + l.EXPECT().FormatDate(testNow).Return("translated date") + return l + }, + }, } for name, tc := range testCases { diff --git a/lang/cy.json b/lang/cy.json index ac6a68557c..ba1751a5a9 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -1582,5 +1582,6 @@ "and:en": "Welsh", "and:cy": "Welsh", "donorHasChosenToHaveTheirLpaRegisteredInContent": "
", - "donorHasChosenToHaveTheirLpaRegisteredInAttorneyContent": " " + "donorHasChosenToHaveTheirLpaRegisteredInAttorneyContent": " ", + "yourIdentityHadBeenConfirmed": "Welsh" } diff --git a/lang/en.json b/lang/en.json index aa1500f50a..5814984658 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1478,5 +1478,6 @@ "and:en": "and English", "and:cy": "and Welsh", "donorHasChosenToHaveTheirLpaRegisteredInContent": " ", - "donorHasChosenToHaveTheirLpaRegisteredInAttorneyContent": " " + "donorHasChosenToHaveTheirLpaRegisteredInAttorneyContent": " ", + "yourIdentityHadBeenConfirmed": "Your identity has been confirmed" }