diff --git a/Makefile b/Makefile index 963bc01035..1ca641cf95 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ up: ##@build Builds and brings the app up up-dev: ##@build Builds the app and brings up via Air hot reload with Delve debugging enabled using amd binaries COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 DOCKER_DEFAULT_PLATFORM=linux/$(shell go env GOARCH) docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml up -d --build --force-recreate --remove-orphans app +pull-latest-mock-onelogin: ## @build logs in to management AWS account and pulls the latest mock-onelogin image (assumes ~/.aws/config contains a profile called management) + aws-vault exec management -- aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 311462405659.dkr.ecr.eu-west-1.amazonaws.com + docker compose -f docker/docker-compose.yml pull mock-onelogin + run-cypress: ##@testing Runs cypress e2e tests. To run a specific spec file pass in spec e.g. make run-cypress spec=start ifdef spec yarn run cypress:run --spec "cypress/e2e/$(spec).cy.js" diff --git a/cypress/e2e/donor/confirm-your-identity-and-sign.cy.js b/cypress/e2e/donor/confirm-your-identity-and-sign.cy.js index 9efc583e2a..e26b8fb61d 100644 --- a/cypress/e2e/donor/confirm-your-identity-and-sign.cy.js +++ b/cypress/e2e/donor/confirm-your-identity-and-sign.cy.js @@ -23,7 +23,7 @@ describe('Confirm your identity and sign', () => { cy.contains('label', 'Sam Smith (donor)').click(); cy.contains('button', 'Continue').click(); - cy.url().should('contain', '/one-login/callback'); + cy.url().should('contain', '/onelogin-identity-details'); cy.checkA11yApp(); cy.contains('Sam'); @@ -283,4 +283,111 @@ describe('Confirm your identity and sign', () => { cy.contains('Withdrawn'); }) }) + + describe('when identity details do not match LPA', () => { + it('can update LPA details', () => { + cy.visit('/fixtures?redirect=/task-list&progress=payForTheLpa'); + cy.contains('li', "Confirm your identity and sign") + .should('contain', 'Not started') + .find('a') + .click(); + + cy.url().should('contain', '/how-to-confirm-your-identity-and-sign'); + cy.checkA11yApp(); + + cy.contains('h1', 'How to confirm your identity and sign the LPA'); + cy.contains('a', 'Continue').click(); + + cy.url().should('contain', '/prove-your-identity'); + cy.checkA11yApp(); + cy.contains('a', 'Continue').click(); + + cy.contains('label', 'Charlie Cooper (certificate provider)').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/onelogin-identity-details'); + cy.checkA11yApp(); + + cy.contains('dd', 'Sam').parent().contains('span', 'Does not match'); + cy.contains('dd', 'Smith').parent().contains('span', 'Does not match'); + cy.contains('dd', '2 January 2000').parent().contains('span', 'Does not match'); + + cy.contains('label', 'Yes').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/onelogin-identity-details'); + cy.checkA11yApp(); + + cy.contains('Your LPA details have been updated to match your confirmed identity') + cy.get('main').should('not.contain', 'Sam'); + cy.get('main').should('not.contain', 'Smith'); + cy.get('main').should('not.contain', '2 January 2000'); + }) + + it('can withdraw LPA', () => { + cy.visit('/fixtures?redirect=/task-list&progress=payForTheLpa'); + cy.contains('li', "Confirm your identity and sign") + .should('contain', 'Not started') + .find('a') + .click(); + + cy.url().should('contain', '/how-to-confirm-your-identity-and-sign'); + cy.checkA11yApp(); + + cy.contains('h1', 'How to confirm your identity and sign the LPA'); + cy.contains('a', 'Continue').click(); + + cy.url().should('contain', '/prove-your-identity'); + cy.checkA11yApp(); + cy.contains('a', 'Continue').click(); + + cy.contains('label', 'Charlie Cooper (certificate provider)').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/onelogin-identity-details'); + cy.checkA11yApp(); + + cy.contains('dd', 'Sam').parent().contains('span', 'Does not match'); + cy.contains('dd', 'Smith').parent().contains('span', 'Does not match'); + cy.contains('dd', '2 January 2000').parent().contains('span', 'Does not match'); + + cy.contains('label', 'No').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/withdraw-this-lpa'); + cy.checkA11yApp(); + }) + + it('errors when option not selected', () => { + cy.visit('/fixtures?redirect=/task-list&progress=payForTheLpa'); + cy.contains('li', "Confirm your identity and sign") + .should('contain', 'Not started') + .find('a') + .click(); + + cy.url().should('contain', '/how-to-confirm-your-identity-and-sign'); + cy.checkA11yApp(); + + cy.contains('h1', 'How to confirm your identity and sign the LPA'); + cy.contains('a', 'Continue').click(); + + cy.url().should('contain', '/prove-your-identity'); + cy.checkA11yApp(); + cy.contains('a', 'Continue').click(); + + cy.contains('label', 'Charlie Cooper (certificate provider)').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/onelogin-identity-details'); + cy.checkA11yApp(); + + cy.contains('button', 'Continue').click(); + + cy.get('.govuk-error-summary').within(() => { + cy.contains('Select yes if you would like to update your details'); + }); + + cy.contains('.govuk-error-message', 'Select yes if you would like to update your details'); + }); + }); }); diff --git a/internal/actor/donor_provided_test.go b/internal/actor/donor_provided_test.go index 3abeff700f..2ac87a4e21 100644 --- a/internal/actor/donor_provided_test.go +++ b/internal/actor/donor_provided_test.go @@ -34,14 +34,14 @@ func TestGenerateHash(t *testing.T) { } // DO change this value to match the updates - const modified uint64 = 0xb4795f8254204619 + const modified uint64 = 0xe8ee03e19c4313a1 // DO NOT change these initial hash values. If a field has been added/removed // you will need to handle the version gracefully by modifying // (*DonorProvidedDetails).HashInclude and adding another testcase for the new // version. testcases := map[uint8]uint64{ - 0: 0xc907539bbd47eeda, + 0: 0x240e672086c6a594, } for version, initial := range testcases { diff --git a/internal/identity/user_data.go b/internal/identity/user_data.go index 05d874e1d5..e736c9db8d 100644 --- a/internal/identity/user_data.go +++ b/internal/identity/user_data.go @@ -6,6 +6,7 @@ import ( "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" ) // https://www.icao.int/publications/Documents/9303_p3_cons_en.pdf @@ -111,11 +112,12 @@ var charmap = map[rune][]rune{ } type UserData struct { - Status Status - FirstNames string - LastName string - DateOfBirth date.Date - RetrievedAt time.Time + Status Status + FirstNames string + LastName string + DateOfBirth date.Date + RetrievedAt time.Time + CurrentAddress place.Address } func (u UserData) MatchName(firstNames, lastName string) bool { diff --git a/internal/onelogin/client.go b/internal/onelogin/client.go index 367182ad39..972559a6ef 100644 --- a/internal/onelogin/client.go +++ b/internal/onelogin/client.go @@ -66,7 +66,7 @@ func (c *Client) AuthCodeURL(state, nonce, locale string, identity bool) (string if identity { q.Add("vtr", `["Cl.Cm.P2"]`) - q.Add("claims", `{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT": null,"https://vocab.account.gov.uk/v1/returnCode": null}}`) + q.Add("claims", `{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT": null,"https://vocab.account.gov.uk/v1/returnCode": null,"https://vocab.account.gov.uk/v1/address": null}}`) } endpoint, err := c.openidConfiguration.AuthorizationEndpoint() diff --git a/internal/onelogin/client_test.go b/internal/onelogin/client_test.go index 894e227df6..d5d292a51e 100644 --- a/internal/onelogin/client_test.go +++ b/internal/onelogin/client_test.go @@ -28,7 +28,7 @@ func TestAuthCodeURL(t *testing.T) { } func TestAuthCodeURLForIdentity(t *testing.T) { - expected := "http://auth?claims=%7B%22userinfo%22%3A%7B%22https%3A%2F%2Fvocab.account.gov.uk%2Fv1%2FcoreIdentityJWT%22%3A+null%2C%22https%3A%2F%2Fvocab.account.gov.uk%2Fv1%2FreturnCode%22%3A+null%7D%7D&client_id=123&nonce=nonce&redirect_uri=http%3A%2F%2Fredirect&response_type=code&scope=openid+email&state=state&ui_locales=cy&vtr=%5B%22Cl.Cm.P2%22%5D" + expected := "http://auth?claims=%7B%22userinfo%22%3A%7B%22https%3A%2F%2Fvocab.account.gov.uk%2Fv1%2FcoreIdentityJWT%22%3A+null%2C%22https%3A%2F%2Fvocab.account.gov.uk%2Fv1%2FreturnCode%22%3A+null%2C%22https%3A%2F%2Fvocab.account.gov.uk%2Fv1%2Faddress%22%3A+null%7D%7D&client_id=123&nonce=nonce&redirect_uri=http%3A%2F%2Fredirect&response_type=code&scope=openid+email&state=state&ui_locales=cy&vtr=%5B%22Cl.Cm.P2%22%5D" c := &Client{ redirectURL: "http://redirect", diff --git a/internal/onelogin/user_info.go b/internal/onelogin/user_info.go index cb8e5c522c..a99e46cc94 100644 --- a/internal/onelogin/user_info.go +++ b/internal/onelogin/user_info.go @@ -12,19 +12,21 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/ministryofjustice/opg-modernising-lpa/internal/date" "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" ) var ErrMissingCoreIdentityJWT = errors.New("UserInfo missing CoreIdentityJWT property") type UserInfo struct { - Sub string `json:"sub"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` - Phone string `json:"phone"` - PhoneVerified bool `json:"phone_verified"` - UpdatedAt int `json:"updated_at"` - CoreIdentityJWT string `json:"https://vocab.account.gov.uk/v1/coreIdentityJWT"` - ReturnCodes []ReturnCodeInfo `json:"https://vocab.account.gov.uk/v1/returnCode,omitempty"` + Sub string `json:"sub"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Phone string `json:"phone"` + PhoneVerified bool `json:"phone_verified"` + UpdatedAt int `json:"updated_at"` + CoreIdentityJWT string `json:"https://vocab.account.gov.uk/v1/coreIdentityJWT"` + ReturnCodes []ReturnCodeInfo `json:"https://vocab.account.gov.uk/v1/returnCode,omitempty"` + Addresses []credentialAddress `json:"https://vocab.account.gov.uk/v1/address,omitempty"` } type ReturnCodeInfo struct { @@ -82,6 +84,36 @@ type CredentialBirthDate struct { Value date.Date `json:"value"` } +type credentialAddress struct { + UPRN string `json:"uprn"` + SubBuildingName string `json:"subBuildingName"` + BuildingName string `json:"buildingName"` + BuildingNumber string `json:"buildingNumber"` + DependentStreetName string `json:"dependentStreetName"` + StreetName string `json:"streetName"` + DoubleDependentAddressLocality string `json:"doubleDependentAddressLocality"` + DependentAddressLocality string `json:"dependentAddressLocality"` + AddressLocality string `json:"addressLocality"` + PostalCode string `json:"postalCode"` + AddressCountry string `json:"addressCountry"` + ValidFrom string `json:"validFrom"` + ValidUntil string `json:"validUntil"` +} + +func (a credentialAddress) transformToAddress() place.Address { + ad := place.AddressDetails{ + SubBuildingName: a.SubBuildingName, + BuildingName: a.BuildingName, + BuildingNumber: a.BuildingNumber, + ThoroughFareName: a.StreetName, + DependentLocality: a.DependentAddressLocality, + Town: a.AddressLocality, + Postcode: a.PostalCode, + } + + return ad.TransformToAddress() +} + type NamePart struct { Value string `json:"value"` @@ -177,11 +209,20 @@ func (c *Client) ParseIdentityClaim(ctx context.Context, u UserInfo) (identity.U return identity.UserData{Status: identity.StatusFailed}, nil } + var currentAddress credentialAddress + for _, a := range u.Addresses { + if a.ValidUntil == "" { + currentAddress = a + break + } + } + return identity.UserData{ - Status: identity.StatusConfirmed, - FirstNames: strings.Join(givenName, " "), - LastName: strings.Join(familyName, " "), - DateOfBirth: birthDates[0].Value, - RetrievedAt: claims.IssuedAt.Time, + Status: identity.StatusConfirmed, + FirstNames: strings.Join(givenName, " "), + LastName: strings.Join(familyName, " "), + DateOfBirth: birthDates[0].Value, + RetrievedAt: claims.IssuedAt.Time, + CurrentAddress: currentAddress.transformToAddress(), }, nil } diff --git a/internal/onelogin/user_info_test.go b/internal/onelogin/user_info_test.go index 694b6115ac..f4c878377b 100644 --- a/internal/onelogin/user_info_test.go +++ b/internal/onelogin/user_info_test.go @@ -15,6 +15,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/ministryofjustice/opg-modernising-lpa/internal/date" "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -159,6 +160,11 @@ func TestParseIdentityClaim(t *testing.T) { LastName: "Doe", DateOfBirth: date.New("1970", "01", "02"), RetrievedAt: issuedAt, + CurrentAddress: place.Address{ + Line1: "1 Fake Road", + Postcode: "B14 7ED", + Country: "GB", + }, }, }, "missing": { @@ -227,6 +233,14 @@ func TestParseIdentityClaim(t *testing.T) { t.Run(name, func(t *testing.T) { userInfo := UserInfo{ CoreIdentityJWT: tc.token, + Addresses: []credentialAddress{{ + BuildingNumber: "1", + StreetName: "Fake Road", + PostalCode: "B14 7ED", + AddressCountry: "GB", + ValidFrom: "2020-01-01", + ValidUntil: "", + }}, } userData, err := c.ParseIdentityClaim(context.Background(), userInfo) @@ -270,3 +284,93 @@ func TestParseIdentityClaimWhenIdentityPublicKeyFuncError(t *testing.T) { _, err := c.ParseIdentityClaim(context.Background(), UserInfo{}) assert.Equal(t, expectedError, err) } + +func TestCredentialAddressTransformToAddress(t *testing.T) { + testCases := map[string]struct { + ca credentialAddress + want place.Address + }{ + "building number no building name": { + ca: credentialAddress{ + BuildingName: "", + BuildingNumber: "1", + StreetName: "MELTON ROAD", + DependentAddressLocality: "", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "1 MELTON ROAD", Line2: "", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "building name no building number": { + ca: credentialAddress{ + BuildingName: "1A", + BuildingNumber: "", + StreetName: "MELTON ROAD", + DependentAddressLocality: "", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "1A", Line2: "MELTON ROAD", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "building name and building number": { + ca: credentialAddress{ + BuildingName: "MELTON HOUSE", + BuildingNumber: "2", + StreetName: "MELTON ROAD", + DependentAddressLocality: "", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "MELTON HOUSE", Line2: "2 MELTON ROAD", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "dependent locality building number": { + ca: credentialAddress{ + BuildingName: "", + BuildingNumber: "3", + StreetName: "MELTON ROAD", + DependentAddressLocality: "KINGS HEATH", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "3 MELTON ROAD", Line2: "KINGS HEATH", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "dependent locality building name": { + ca: credentialAddress{ + BuildingName: "MELTON HOUSE", + BuildingNumber: "", + StreetName: "MELTON ROAD", + DependentAddressLocality: "KINGS HEATH", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "MELTON HOUSE", Line2: "MELTON ROAD", Line3: "KINGS HEATH", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "dependent locality building name and building number": { + ca: credentialAddress{ + BuildingName: "MELTON HOUSE", + BuildingNumber: "5", + StreetName: "MELTON ROAD", + DependentAddressLocality: "KINGS HEATH", + AddressLocality: "BIRMINGHAM", + PostalCode: "B14 7ET", + }, + want: place.Address{Line1: "MELTON HOUSE", Line2: "5 MELTON ROAD", Line3: "KINGS HEATH", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, + }, + "building name and sub building name": { + ca: credentialAddress{ + SubBuildingName: "APARTMENT 34", + BuildingName: "CHARLES HOUSE", + StreetName: "PARK ROW", + AddressLocality: "NOTTINGHAM", + PostalCode: "NG1 6GR", + }, + want: place.Address{Line1: "APARTMENT 34, CHARLES HOUSE", Line2: "PARK ROW", TownOrCity: "NOTTINGHAM", Postcode: "NG1 6GR", Country: "GB"}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.ca.transformToAddress()) + }) + } +} diff --git a/internal/page/donor/identity_with_one_login_callback.go b/internal/page/donor/identity_with_one_login_callback.go index b9639b81a8..1dac5e7a04 100644 --- a/internal/page/donor/identity_with_one_login_callback.go +++ b/internal/page/donor/identity_with_one_login_callback.go @@ -1,49 +1,22 @@ package donor import ( + "errors" "net/http" - "time" - "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" - "github.com/ministryofjustice/opg-modernising-lpa/internal/date" "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" - "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" ) -type identityWithOneLoginCallbackData struct { - App page.AppData - Errors validation.List - FirstNames string - LastName string - DateOfBirth date.Date - ConfirmedAt time.Time -} - -func IdentityWithOneLoginCallback(tmpl template.Template, oneLoginClient OneLoginClient, sessionStore SessionStore, donorStore DonorStore) Handler { +func IdentityWithOneLoginCallback(oneLoginClient OneLoginClient, sessionStore SessionStore, donorStore DonorStore) Handler { return func(appData page.AppData, w http.ResponseWriter, r *http.Request, donor *actor.DonorProvidedDetails) error { - if r.Method == http.MethodPost { - if donor.DonorIdentityConfirmed() { - return page.Paths.ReadYourLpa.Redirect(w, r, appData, donor) - } else { - return page.Paths.ProveYourIdentity.Redirect(w, r, appData, donor) - } - } - - data := &identityWithOneLoginCallbackData{App: appData} - if donor.DonorIdentityConfirmed() { - data.FirstNames = donor.DonorIdentityUserData.FirstNames - data.LastName = donor.DonorIdentityUserData.LastName - data.DateOfBirth = donor.DonorIdentityUserData.DateOfBirth - data.ConfirmedAt = donor.DonorIdentityUserData.RetrievedAt - - return tmpl(w, data) + return page.Paths.OneloginIdentityDetails.Redirect(w, r, appData, donor) } if r.FormValue("error") == "access_denied" { - return tmpl(w, data) + return errors.New("access denied") } oneLoginSession, err := sessionStore.OneLogin(r) @@ -83,13 +56,8 @@ func IdentityWithOneLoginCallback(tmpl template.Template, oneLoginClient OneLogi return page.Paths.RegisterWithCourtOfProtection.Redirect(w, r, appData, donor) case identity.StatusInsufficientEvidence: return page.Paths.UnableToConfirmIdentity.Redirect(w, r, appData, donor) + default: + return page.Paths.OneloginIdentityDetails.Redirect(w, r, appData, donor) } - - data.FirstNames = userData.FirstNames - data.LastName = userData.LastName - data.DateOfBirth = userData.DateOfBirth - data.ConfirmedAt = userData.RetrievedAt - - return tmpl(w, data) } } diff --git a/internal/page/donor/identity_with_one_login_callback_test.go b/internal/page/donor/identity_with_one_login_callback_test.go index 7858cead5a..8926ae5b36 100644 --- a/internal/page/donor/identity_with_one_login_callback_test.go +++ b/internal/page/donor/identity_with_one_login_callback_test.go @@ -1,14 +1,13 @@ package donor import ( - "io" + "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" - "github.com/ministryofjustice/opg-modernising-lpa/internal/form" "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" @@ -25,6 +24,7 @@ func TestGetIdentityWithOneLoginCallback(t *testing.T) { userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} userData := identity.UserData{Status: identity.StatusConfirmed, FirstNames: "John", LastName: "Doe", RetrievedAt: now} updatedDonor := &actor.DonorProvidedDetails{ + LpaID: "lpa-id", Donor: actor.Donor{FirstNames: "John", LastName: "Doe"}, DonorIdentityUserData: userData, Tasks: actor.DonorTasks{ConfirmYourIdentityAndSign: actor.IdentityTaskInProgress}, @@ -51,42 +51,20 @@ func TestGetIdentityWithOneLoginCallback(t *testing.T) { ParseIdentityClaim(r.Context(), userInfo). Return(userData, nil) - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, &identityWithOneLoginCallbackData{ - App: testAppData, - FirstNames: "John", - LastName: "Doe", - ConfirmedAt: now, - }). - Return(nil) - - err := IdentityWithOneLoginCallback(template.Execute, oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ + LpaID: "lpa-id", Donor: actor.Donor{FirstNames: "John", LastName: "Doe"}, }) resp := w.Result() assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.OneloginIdentityDetails.Format("lpa-id"), resp.Header.Get("Location")) } func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} - templateCalled := func(t *testing.T, w io.Writer) *mockTemplate { - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, &identityWithOneLoginCallbackData{ - App: testAppData, - }). - Return(nil) - return template - } - - templateIgnored := func(t *testing.T, w io.Writer) *mockTemplate { - return nil - } - sessionRetrieved := func(t *testing.T) *mockSessionStore { sessionStore := newMockSessionStore(t) sessionStore.EXPECT(). @@ -106,7 +84,6 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { testCases := map[string]struct { oneLoginClient func(t *testing.T) *mockOneLoginClient sessionStore func(*testing.T) *mockSessionStore - template func(*testing.T, io.Writer) *mockTemplate donorStore func(*testing.T) *mockDonorStore url string error error @@ -127,7 +104,6 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { return oneLoginClient }, sessionStore: sessionRetrieved, - template: templateIgnored, donorStore: func(t *testing.T) *mockDonorStore { donorStore := newMockDonorStore(t) donorStore.EXPECT(). @@ -154,7 +130,6 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { return oneLoginClient }, sessionStore: sessionRetrieved, - template: templateIgnored, error: expectedError, donorStore: donorStoreIgnored, }, @@ -171,7 +146,6 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { return oneLoginClient }, sessionStore: sessionRetrieved, - template: templateIgnored, error: expectedError, donorStore: donorStoreIgnored, }, @@ -185,7 +159,6 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { return oneLoginClient }, sessionStore: sessionRetrieved, - template: templateIgnored, error: expectedError, donorStore: donorStoreIgnored, }, @@ -195,8 +168,8 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { return newMockOneLoginClient(t) }, sessionStore: sessionIgnored, - template: templateCalled, donorStore: donorStoreIgnored, + error: errors.New("access denied"), }, } @@ -207,9 +180,8 @@ func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { sessionStore := tc.sessionStore(t) oneLoginClient := tc.oneLoginClient(t) - template := tc.template(t, w) - err := IdentityWithOneLoginCallback(template.Execute, oneLoginClient, sessionStore, tc.donorStore(t))(testAppData, w, r, &actor.DonorProvidedDetails{}) + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, tc.donorStore(t))(testAppData, w, r, &actor.DonorProvidedDetails{}) resp := w.Result() assert.Equal(t, tc.error, err) @@ -249,7 +221,7 @@ func TestGetIdentityWithOneLoginCallbackWhenInsufficientEvidenceReturnCodeClaimP ParseIdentityClaim(mock.Anything, mock.Anything). Return(identity.UserData{Status: identity.StatusInsufficientEvidence}, nil) - err := IdentityWithOneLoginCallback(nil, oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ Donor: actor.Donor{FirstNames: "John", LastName: "Doe"}, LpaID: "lpa-id", }) @@ -291,7 +263,7 @@ func TestGetIdentityWithOneLoginCallbackWhenAnyOtherReturnCodeClaimPresent(t *te ParseIdentityClaim(mock.Anything, mock.Anything). Return(identity.UserData{Status: identity.StatusFailed}, nil) - err := IdentityWithOneLoginCallback(nil, oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ Donor: actor.Donor{FirstNames: "John", LastName: "Doe"}, LpaID: "lpa-id", }) @@ -328,7 +300,7 @@ func TestGetIdentityWithOneLoginCallbackWhenPutDonorStoreError(t *testing.T) { ParseIdentityClaim(mock.Anything, mock.Anything). Return(identity.UserData{Status: identity.StatusConfirmed}, nil) - err := IdentityWithOneLoginCallback(nil, oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{}) + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{}) assert.Equal(t, expectedError, err) } @@ -339,66 +311,14 @@ func TestGetIdentityWithOneLoginCallbackWhenReturning(t *testing.T) { now := time.Date(2012, time.January, 1, 2, 3, 4, 5, time.UTC) userData := identity.UserData{Status: identity.StatusConfirmed, FirstNames: "first-name", LastName: "last-name", RetrievedAt: now} - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, &identityWithOneLoginCallbackData{ - App: testAppData, - FirstNames: "first-name", - LastName: "last-name", - ConfirmedAt: now, - }). - Return(nil) - - err := IdentityWithOneLoginCallback(template.Execute, nil, nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{ + err := IdentityWithOneLoginCallback(nil, nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{ + LpaID: "lpa-id", Donor: actor.Donor{FirstNames: "first-name", LastName: "last-name"}, DonorIdentityUserData: userData, }) resp := w.Result() - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostIdentityWithOneLoginCallback(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodPost, "/", nil) - - err := IdentityWithOneLoginCallback(nil, nil, nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{ - LpaID: "lpa-id", - DonorIdentityUserData: identity.UserData{Status: identity.StatusConfirmed}, - }) - resp := w.Result() - - assert.Nil(t, err) - assert.Equal(t, http.StatusFound, resp.StatusCode) - assert.Equal(t, page.Paths.ReadYourLpa.Format("lpa-id"), resp.Header.Get("Location")) -} - -func TestPostIdentityWithOneLoginCallbackNotConfirmed(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodPost, "/", nil) - - err := IdentityWithOneLoginCallback(nil, nil, nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{ - LpaID: "lpa-id", - Donor: actor.Donor{ - CanSign: form.Yes, - }, - Type: actor.LpaTypePersonalWelfare, - Tasks: actor.DonorTasks{ - YourDetails: actor.TaskCompleted, - ChooseAttorneys: actor.TaskCompleted, - ChooseReplacementAttorneys: actor.TaskCompleted, - LifeSustainingTreatment: actor.TaskCompleted, - Restrictions: actor.TaskCompleted, - CertificateProvider: actor.TaskCompleted, - PeopleToNotify: actor.TaskCompleted, - CheckYourLpa: actor.TaskCompleted, - PayForLpa: actor.PaymentTaskCompleted, - }, - }) - resp := w.Result() - assert.Nil(t, err) assert.Equal(t, http.StatusFound, resp.StatusCode) - assert.Equal(t, page.Paths.ProveYourIdentity.Format("lpa-id"), resp.Header.Get("Location")) + assert.Equal(t, page.Paths.OneloginIdentityDetails.Format("lpa-id"), resp.Header.Get("Location")) } diff --git a/internal/page/donor/onelogin_identity_details.go b/internal/page/donor/onelogin_identity_details.go new file mode 100644 index 0000000000..c37c5fe72e --- /dev/null +++ b/internal/page/donor/onelogin_identity_details.go @@ -0,0 +1,64 @@ +package donor + +import ( + "net/http" + "net/url" + + "github.com/ministryofjustice/opg-go-common/template" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/form" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +type oneloginIdentityDetailsData struct { + App page.AppData + Errors validation.List + DonorProvided *actor.DonorProvidedDetails + DetailsMatch bool + DetailsUpdated bool + Form *form.YesNoForm +} + +func OneloginIdentityDetails(tmpl template.Template, donorStore DonorStore) Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request, donor *actor.DonorProvidedDetails) error { + data := &oneloginIdentityDetailsData{ + App: appData, + Form: form.NewYesNoForm(form.YesNoUnknown), + DonorProvided: donor, + DetailsUpdated: r.FormValue("detailsUpdated") == "1", + DetailsMatch: donor.Donor.FirstNames == donor.DonorIdentityUserData.FirstNames && + donor.Donor.LastName == donor.DonorIdentityUserData.LastName && + donor.Donor.DateOfBirth == donor.DonorIdentityUserData.DateOfBirth && + donor.Donor.Address.Postcode == donor.DonorIdentityUserData.CurrentAddress.Postcode, + } + + if r.Method == http.MethodPost { + if donor.DonorIdentityConfirmed() { + return page.Paths.ReadYourLpa.Redirect(w, r, appData, donor) + } + + f := form.ReadYesNoForm(r, "yesIfWouldLikeToUpdateDetails") + data.Errors = f.Validate() + + if data.Errors.None() { + if f.YesNo.IsYes() { + donor.Donor.FirstNames = donor.DonorIdentityUserData.FirstNames + donor.Donor.LastName = donor.DonorIdentityUserData.LastName + donor.Donor.DateOfBirth = donor.DonorIdentityUserData.DateOfBirth + donor.Donor.Address = donor.DonorIdentityUserData.CurrentAddress + + if err := donorStore.Put(r.Context(), donor); err != nil { + return err + } + + return page.Paths.OneloginIdentityDetails.RedirectQuery(w, r, appData, donor, url.Values{"detailsUpdated": {"1"}}) + } else { + return page.Paths.WithdrawThisLpa.Redirect(w, r, appData, donor) + } + } + } + + return tmpl(w, data) + } +} diff --git a/internal/page/donor/onelogin_identity_details_test.go b/internal/page/donor/onelogin_identity_details_test.go new file mode 100644 index 0000000000..261ac9358f --- /dev/null +++ b/internal/page/donor/onelogin_identity_details_test.go @@ -0,0 +1,178 @@ +package donor + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + "github.com/ministryofjustice/opg-modernising-lpa/internal/form" + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetOneloginIdentityDetails(t *testing.T) { + dob := date.New("1", "2", "3") + + testcases := map[string]struct { + donorProvided *actor.DonorProvidedDetails + expectedDetailsMatch bool + expectedDetailsUpdated bool + url string + }{ + "details match": { + donorProvided: &actor.DonorProvidedDetails{ + Donor: actor.Donor{FirstNames: "a", LastName: "b", DateOfBirth: dob, Address: testAddress}, + DonorIdentityUserData: identity.UserData{FirstNames: "a", LastName: "b", DateOfBirth: dob, CurrentAddress: testAddress}, + }, + expectedDetailsMatch: true, + url: "/", + }, + "details do not match": { + donorProvided: &actor.DonorProvidedDetails{ + Donor: actor.Donor{FirstNames: "a"}, + DonorIdentityUserData: identity.UserData{FirstNames: "b"}, + }, + url: "/", + }, + "details updated": { + donorProvided: &actor.DonorProvidedDetails{ + Donor: actor.Donor{FirstNames: "a"}, + DonorIdentityUserData: identity.UserData{FirstNames: "b"}, + }, + url: "/?detailsUpdated=1", + expectedDetailsUpdated: true, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, tc.url, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &oneloginIdentityDetailsData{ + App: testAppData, + Form: form.NewYesNoForm(form.YesNoUnknown), + DonorProvided: tc.donorProvided, + DetailsUpdated: tc.expectedDetailsUpdated, + DetailsMatch: tc.expectedDetailsMatch, + }). + Return(nil) + + err := OneloginIdentityDetails(template.Execute, nil)(testAppData, w, r, tc.donorProvided) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} + +func TestPostOneloginIdentityDetailsWhenYes(t *testing.T) { + f := url.Values{form.FieldNames.YesNo: {form.Yes.String()}} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + existingDob := date.New("1", "2", "3") + identityDob := date.New("4", "5", "6") + + donorStore := newMockDonorStore(t) + donorStore.EXPECT(). + Put(r.Context(), &actor.DonorProvidedDetails{ + LpaID: "lpa-id", + Donor: actor.Donor{FirstNames: "b", LastName: "b", DateOfBirth: identityDob, Address: place.Address{Line1: "a"}}, + DonorIdentityUserData: identity.UserData{FirstNames: "b", LastName: "b", DateOfBirth: identityDob, CurrentAddress: place.Address{Line1: "a"}}}). + Return(nil) + + err := OneloginIdentityDetails(nil, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{ + LpaID: "lpa-id", + Donor: actor.Donor{FirstNames: "a", LastName: "a", DateOfBirth: existingDob, Address: testAddress}, + DonorIdentityUserData: identity.UserData{FirstNames: "b", LastName: "b", DateOfBirth: identityDob, CurrentAddress: place.Address{Line1: "a"}}, + }) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.OneloginIdentityDetails.Format("lpa-id")+"?detailsUpdated=1", resp.Header.Get("Location")) +} + +func TestPostOneloginIdentityDetailsWhenNo(t *testing.T) { + f := url.Values{form.FieldNames.YesNo: {form.No.String()}} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + err := OneloginIdentityDetails(nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.WithdrawThisLpa.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestPostOneloginIdentityDetailsWhenIdentityAndLPADetailsAlreadyMatch(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + err := OneloginIdentityDetails(nil, nil)(testAppData, w, r, &actor.DonorProvidedDetails{LpaID: "lpa-id", DonorIdentityUserData: identity.UserData{Status: identity.StatusConfirmed}}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.ReadYourLpa.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestPostOneloginIdentityDetailsWhenDonorStoreError(t *testing.T) { + f := url.Values{form.FieldNames.YesNo: {form.Yes.String()}} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + donorStore := newMockDonorStore(t) + donorStore.EXPECT(). + Put(r.Context(), mock.Anything). + Return(expectedError) + + err := OneloginIdentityDetails(nil, donorStore)(testAppData, w, r, &actor.DonorProvidedDetails{}) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostOneloginIdentityDetailsWhenValidationError(t *testing.T) { + f := url.Values{form.FieldNames.YesNo: {""}} + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + validationError := validation.With(form.FieldNames.YesNo, validation.SelectError{Label: "yesIfWouldLikeToUpdateDetails"}) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, mock.MatchedBy(func(data *oneloginIdentityDetailsData) bool { + return assert.Equal(t, validationError, data.Errors) + })). + Return(nil) + + err := OneloginIdentityDetails(template.Execute, nil)(testAppData, w, r, &actor.DonorProvidedDetails{Donor: actor.Donor{FirstNames: "a"}}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/internal/page/donor/register.go b/internal/page/donor/register.go index 714ffd8b85..83970496ff 100644 --- a/internal/page/donor/register.go +++ b/internal/page/donor/register.go @@ -357,7 +357,9 @@ func Register( handleWithDonor(page.Paths.IdentityWithOneLogin, page.CanGoBack, IdentityWithOneLogin(oneLoginClient, sessionStore, random.String)) handleWithDonor(page.Paths.IdentityWithOneLoginCallback, page.CanGoBack, - IdentityWithOneLoginCallback(commonTmpls.Get("identity_with_one_login_callback.gohtml"), oneLoginClient, sessionStore, donorStore)) + IdentityWithOneLoginCallback(oneLoginClient, sessionStore, donorStore)) + handleWithDonor(page.Paths.OneloginIdentityDetails, page.CanGoBack, + OneloginIdentityDetails(tmpls.Get("onelogin_identity_details.gohtml"), donorStore)) handleWithDonor(page.Paths.RegisterWithCourtOfProtection, page.None, RegisterWithCourtOfProtection(tmpls.Get("register_with_court_of_protection.gohtml"), donorStore, time.Now)) diff --git a/internal/page/donor/task_list.go b/internal/page/donor/task_list.go index 2024974e39..17b11a06b6 100644 --- a/internal/page/donor/task_list.go +++ b/internal/page/donor/task_list.go @@ -168,6 +168,9 @@ func taskListSignSection(donor *actor.DonorProvidedDetails) taskListSection { switch donor.DonorIdentityUserData.Status { case identity.StatusConfirmed: signPath = page.Paths.ReadYourLpa + if !donor.DonorIdentityConfirmed() { + signPath = page.Paths.OneloginIdentityDetails + } case identity.StatusFailed: signPath = page.Paths.RegisterWithCourtOfProtection case identity.StatusInsufficientEvidence: diff --git a/internal/page/donor/task_list_test.go b/internal/page/donor/task_list_test.go index 0445901876..56ed313f3c 100644 --- a/internal/page/donor/task_list_test.go +++ b/internal/page/donor/task_list_test.go @@ -110,6 +110,21 @@ func TestGetTaskList(t *testing.T) { return sections }, }, + "confirmed identity does not match LPA": { + appData: testAppData, + donor: &actor.DonorProvidedDetails{ + LpaID: "lpa-id", + Donor: actor.Donor{LastName: "b", Address: place.Address{Line1: "x"}}, + DonorIdentityUserData: identity.UserData{Status: identity.StatusConfirmed, LastName: "a"}, + }, + expected: func(sections []taskListSection) []taskListSection { + sections[2].Items = []taskListItem{ + {Name: "confirmYourIdentityAndSign", Path: page.Paths.OneloginIdentityDetails.Format("lpa-id")}, + } + + return sections + }, + }, "failed identity": { appData: testAppData, donor: &actor.DonorProvidedDetails{ diff --git a/internal/page/paths.go b/internal/page/paths.go index dce3eb2aff..af38074fd2 100644 --- a/internal/page/paths.go +++ b/internal/page/paths.go @@ -106,6 +106,7 @@ func (p LpaPath) canVisit(donor *actor.DonorProvidedDetails) bool { case Paths.HowToConfirmYourIdentityAndSign, Paths.IdentityWithOneLogin, + Paths.OneloginIdentityDetails, Paths.LpaYourLegalRightsAndResponsibilities, Paths.SignTheLpaOnBehalf: return section1Completed && (donor.Tasks.PayForLpa.IsCompleted() || donor.Tasks.PayForLpa.IsPending()) @@ -396,6 +397,7 @@ type AppPaths struct { LpaYourLegalRightsAndResponsibilities LpaPath MakeANewLPA LpaPath NeedHelpSigningConfirmation LpaPath + OneloginIdentityDetails LpaPath PaymentConfirmation LpaPath PreviousApplicationNumber LpaPath PreviousFee LpaPath @@ -594,6 +596,7 @@ var Paths = AppPaths{ LpaYourLegalRightsAndResponsibilities: "/your-legal-rights-and-responsibilities", MakeANewLPA: "/make-a-new-lpa", NeedHelpSigningConfirmation: "/need-help-signing-confirmation", + OneloginIdentityDetails: "/onelogin-identity-details", PaymentConfirmation: "/payment-confirmation", PreviousApplicationNumber: "/previous-application-number", PreviousFee: "/how-much-did-you-previously-pay-for-your-lpa", diff --git a/internal/place/client.go b/internal/place/client.go index 09b0b6fb75..03933c8183 100644 --- a/internal/place/client.go +++ b/internal/place/client.go @@ -21,7 +21,7 @@ type Client struct { doer Doer } -type addressDetails struct { +type AddressDetails struct { Address string `json:"ADDRESS"` SubBuildingName string `json:"SUB_BUILDING_NAME"` BuildingName string `json:"BUILDING_NAME"` @@ -38,7 +38,7 @@ type postcodeLookupResponse struct { } type ResultSet struct { - AddressDetails addressDetails `json:"DPA"` + AddressDetails AddressDetails `json:"DPA"` } type BadRequestError struct { @@ -95,7 +95,7 @@ func (c *Client) LookupPostcode(ctx context.Context, postcode string) ([]Address var addresses []Address for _, resultSet := range postcodeLookupResponse.Results { - addresses = append(addresses, resultSet.AddressDetails.transformToAddress()) + addresses = append(addresses, resultSet.AddressDetails.TransformToAddress()) } return addresses, nil @@ -141,25 +141,25 @@ func (a Address) String() string { return strings.Join(a.Lines(), ", ") } -func (ad *addressDetails) transformToAddress() Address { +func (ad *AddressDetails) TransformToAddress() Address { a := Address{} if len(ad.BuildingName) > 0 { if len(ad.SubBuildingName) > 0 { - a.Line1 = fmt.Sprintf("%s, %s", ad.SubBuildingName, ad.BuildingName) + a.Line1 = strings.TrimSpace(fmt.Sprintf("%s, %s", ad.SubBuildingName, ad.BuildingName)) } else { a.Line1 = ad.BuildingName } if len(ad.BuildingNumber) > 0 { - a.Line2 = fmt.Sprintf("%s %s", ad.BuildingNumber, ad.ThoroughFareName) + a.Line2 = strings.TrimSpace(fmt.Sprintf("%s %s", ad.BuildingNumber, ad.ThoroughFareName)) } else { a.Line2 = ad.ThoroughFareName } a.Line3 = ad.DependentLocality } else { - a.Line1 = fmt.Sprintf("%s %s", ad.BuildingNumber, ad.ThoroughFareName) + a.Line1 = strings.TrimSpace(fmt.Sprintf("%s %s", ad.BuildingNumber, ad.ThoroughFareName)) a.Line2 = ad.DependentLocality } diff --git a/internal/place/client_test.go b/internal/place/client_test.go index b38230f894..4f5c55ec44 100644 --- a/internal/place/client_test.go +++ b/internal/place/client_test.go @@ -175,11 +175,11 @@ func TestAddress(t *testing.T) { func TestTransformAddressDetailsToAddress(t *testing.T) { testCases := map[string]struct { - ad addressDetails + ad AddressDetails want Address }{ "building number no building name": { - ad: addressDetails{ + ad: AddressDetails{ Address: "1, MELTON ROAD, BIRMINGHAM, B14 7ET", BuildingName: "", BuildingNumber: "1", @@ -191,7 +191,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "1 MELTON ROAD", Line2: "", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "building name no building number": { - ad: addressDetails{ + ad: AddressDetails{ Address: "1A, MELTON ROAD, BIRMINGHAM, B14 7ET", BuildingName: "1A", BuildingNumber: "", @@ -203,7 +203,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "1A", Line2: "MELTON ROAD", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "building name and building number": { - ad: addressDetails{ + ad: AddressDetails{ Address: "MELTON HOUSE, 2 MELTON ROAD, BIRMINGHAM, B14 7ET", BuildingName: "MELTON HOUSE", BuildingNumber: "2", @@ -215,7 +215,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "MELTON HOUSE", Line2: "2 MELTON ROAD", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "dependent locality building number": { - ad: addressDetails{ + ad: AddressDetails{ Address: "3, MELTON ROAD, BIRMINGHAM, B14 7ET", BuildingName: "", BuildingNumber: "3", @@ -227,7 +227,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "3 MELTON ROAD", Line2: "KINGS HEATH", Line3: "", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "dependent locality building name": { - ad: addressDetails{ + ad: AddressDetails{ Address: "MELTON HOUSE, MELTON ROAD, KINGS HEATH, BIRMINGHAM, B14 7ET", BuildingName: "MELTON HOUSE", BuildingNumber: "", @@ -239,7 +239,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "MELTON HOUSE", Line2: "MELTON ROAD", Line3: "KINGS HEATH", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "dependent locality building name and building number": { - ad: addressDetails{ + ad: AddressDetails{ Address: "MELTON HOUSE, 5 MELTON ROAD, KINGS HEATH BIRMINGHAM, B14 7ET", BuildingName: "MELTON HOUSE", BuildingNumber: "5", @@ -251,7 +251,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { want: Address{Line1: "MELTON HOUSE", Line2: "5 MELTON ROAD", Line3: "KINGS HEATH", TownOrCity: "BIRMINGHAM", Postcode: "B14 7ET", Country: "GB"}, }, "building name and sub building name": { - ad: addressDetails{ + ad: AddressDetails{ Address: "APARTMENT 34, CHARLES HOUSE, PARK ROW, NOTTINGHAM, NG1 6GR", SubBuildingName: "APARTMENT 34", BuildingName: "CHARLES HOUSE", @@ -265,7 +265,7 @@ func TestTransformAddressDetailsToAddress(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.want, tc.ad.transformToAddress()) + assert.Equal(t, tc.want, tc.ad.TransformToAddress()) }) } } diff --git a/lang/cy.json b/lang/cy.json index 5a0d7b2dfc..cdbbb15ce1 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -1261,5 +1261,15 @@ "many": "Welsh", "other": "Welsh" }, - "whatHappensNextRegisteringWithCOPContent": "

Welsh

" + "whatHappensNextRegisteringWithCOPContent": "

Welsh

", + "yourConfirmedIdentityDetails": "Welsh", + "theDetailsOnYourLPA": "Welsh", + "someDetailsDoNotMatchIdentityDetailsWarning": "Welsh", + "yourLPADetailsHaveBeenUpdatedToMatchIdentitySuccess": "Welsh", + "confirmedIdentityDetails": "Welsh", + "doesNotMatch": "Welsh", + "updateMyLPADetailsToMatchIdentityHint": "Welsh", + "iUnderstandThisWillWithdrawLPAHint": "Welsh", + "youCanOnlyContinueIfDetailsMatchWarning": "Welsh", + "yesIfWouldLikeToUpdateDetails": "Welsh" } diff --git a/lang/en.json b/lang/en.json index e0ca2430a1..ea50f539d2 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1190,5 +1190,15 @@ "one": "attorney", "other": "attorneys" }, - "whatHappensNextRegisteringWithCOPContent": "

You should now sign your LPA.

" + "whatHappensNextRegisteringWithCOPContent": "

You should now sign your LPA.

", + "yourConfirmedIdentityDetails": "Your confirmed identity details", + "theDetailsOnYourLPA": "The details on your LPA", + "someDetailsDoNotMatchIdentityDetailsWarning": "Some of the details on your LPA do not match your confirmed identity details", + "yourLPADetailsHaveBeenUpdatedToMatchIdentitySuccess": "Your LPA details have been updated to match your confirmed identity", + "confirmedIdentityDetails": "Confirmed identity details", + "doesNotMatch": "Does not match", + "updateMyLPADetailsToMatchIdentityHint": "Update my LPA details to match my confirmed identity details.", + "iUnderstandThisWillWithdrawLPAHint": "I understand that this will withdraw my LPA and I will no longer be able to access it.", + "youCanOnlyContinueIfDetailsMatchWarning": "You can only continue with this LPA if the details match your confirmed identity details", + "yesIfWouldLikeToUpdateDetails": "yes if you would like to update your details" } diff --git a/web/assets/scss/main.scss b/web/assets/scss/main.scss index b22462052f..77b299e8d4 100644 --- a/web/assets/scss/main.scss +++ b/web/assets/scss/main.scss @@ -145,3 +145,7 @@ body:not(.js-enabled) .govuk-back-link { body.js-enabled .js-only { display: revert; } + +.app-08rem-font-size { + font-size: .8rem; +} diff --git a/web/template/donor/onelogin_identity_details.gohtml b/web/template/donor/onelogin_identity_details.gohtml new file mode 100644 index 0000000000..fefdab3da0 --- /dev/null +++ b/web/template/donor/onelogin_identity_details.gohtml @@ -0,0 +1,101 @@ +{{ template "page" . }} + +{{ define "pageTitle" }} + {{ tr .App "yourConfirmedIdentityDetails" }} +{{ end }} + +{{ define "main" }} + {{ $donor := .DonorProvided.Donor }} + {{ $userData := .DonorProvided.DonorIdentityUserData }} + {{ $donorFullName := (printf "%s %s" $userData.FirstNames $userData.LastName) }} + {{ $firstNamesMatch := eq $donor.FirstNames $userData.FirstNames }} + {{ $lastNameMatch := eq $donor.LastName $userData.LastName }} + {{ $dobMatch := eq $donor.DateOfBirth $userData.DateOfBirth }} + {{ $addressMatch := eq $donor.Address.Postcode $userData.CurrentAddress.Postcode }} + +
+
+ {{ if not .DetailsMatch }} + {{ template "warning-banner" (content .App "someDetailsDoNotMatchIdentityDetailsWarning") }} + {{ else if .DetailsUpdated }} + {{ template "notification-banner" (notificationBanner .App "success" (trHtml .App "yourLPADetailsHaveBeenUpdatedToMatchIdentitySuccess") "success" "heading") }} + {{ end}} + +

{{ tr .App "theDetailsOnYourLPA" }}

+ +
+
+
{{ tr .App "firstNames" }}
+
{{ $donor.FirstNames }}
+ {{ if not $firstNamesMatch }} +
{{ tr .App "doesNotMatch" }}
+ {{ end }} +
+
+
{{ tr .App "lastName" }}
+
{{ $donor.LastName }}
+ {{ if not $lastNameMatch }} +
{{ tr .App "doesNotMatch" }} +
+ {{ end}} +
+
+
{{ tr .App "dateOfBirth" }}
+
{{ (formatDate .App $donor.DateOfBirth) }}
+ {{ if not $dobMatch }} +
{{ tr .App "doesNotMatch" }} +
+ {{ end}} +
+
+
{{ tr .App "address" }}
+
{{ template "address-lines" $donor.Address }}
+ {{ if not $addressMatch }} +
{{ tr .App "doesNotMatch" }}
+ {{ end }} +
+
+ +
+
+

{{ tr .App "confirmedIdentityDetails" }}

+
+
+
+ {{ template "summary-row" (summaryRow .App "firstNames" $userData.FirstNames "" $donorFullName false true ) }} + + {{ template "summary-row" (summaryRow .App "lastName" $userData.LastName "" $donorFullName false true ) }} + + {{ template "summary-row" (summaryRow .App "dateOfBirth" (formatDate .App $userData.DateOfBirth) "" $donorFullName false true ) }} + + {{ template "address-summary-row" (addressSummaryRow $.App "address" $userData.CurrentAddress "" $donorFullName false true ) }} +
+
+
+ +
+ {{ if not .DetailsMatch }} + {{ template "warning" (content .App "youCanOnlyContinueIfDetailsMatchWarning") }} + +
+
+ {{ template "error-message" (errorMessage . .Form.FieldName) }} + + {{ template "radios" (items . .Form.FieldName .Form.YesNo.String + (item .Form.Options.Yes.String "yes" "hint" "updateMyLPADetailsToMatchIdentityHint") + (item .Form.Options.No.String "no" "hint" "iUnderstandThisWillWithdrawLPAHint") + ) }} +
+
+ {{ end }} + + {{ if not .DetailsUpdated }} + {{ template "buttons" (button .App "continue") }} + {{ else }} + {{ template "continue-button" . }} + {{ end }} + {{ template "csrf-field" . }} +
+
+
+{{ end }} diff --git a/web/template/identity_with_one_login_callback.gohtml b/web/template/identity_with_one_login_callback.gohtml index 5ef4678963..13ed2ec991 100644 --- a/web/template/identity_with_one_login_callback.gohtml +++ b/web/template/identity_with_one_login_callback.gohtml @@ -5,33 +5,33 @@ {{ end }} {{ define "main" }} -
-
-

{{ tr .App "yourIdentityConfirmedWithOneLogin" }}

+
+
+

{{ tr .App "yourIdentityConfirmedWithOneLogin" }}

-
-
-
First Names
-
{{ .FirstNames }}
-
-
-
Last Name
-
{{ .LastName }}
-
-
-
Date of birth
-
{{ .DateOfBirth }}
-
-
-
Confirmed at
-
{{ formatDateTime .App .ConfirmedAt }}
-
-
+
+
+
First Names
+
{{ .FirstNames }}
+
+
+
Last Name
+
{{ .LastName }}
+
+
+
Date of birth
+
{{ .DateOfBirth }}
+
+
+
Confirmed at
+
{{ formatDateTime .App .ConfirmedAt }}
+
+
-
- {{ template "continue-button" . }} - {{ template "csrf-field" . }} -
+
+ {{ template "continue-button" . }} + {{ template "csrf-field" . }} +
+
-
{{ end }}