From ed9a0bb795cec714bae9ab54414c2e6c996f8368 Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell <m@hawx.me> Date: Wed, 24 Jul 2024 08:31:14 +0100 Subject: [PATCH] MLPAB-2198 Allow attorney to opt-out (#1368) --- cypress/e2e/attorney/opt-out.cy.js | 21 + cypress/e2e/attorney/sign.cy.js | 22 + cypress/e2e/attorney/trust-corporation.cy.js | 258 ++++----- internal/app/app.go | 2 + internal/app/attorney_store.go | 18 + internal/app/attorney_store_test.go | 61 ++ internal/notify/email.go | 14 + .../confirm_dont_want_to_be_attorney.go | 63 +++ ...irm_dont_want_to_be_attorney_logged_out.go | 94 +++ ...ont_want_to_be_attorney_logged_out_test.go | 398 +++++++++++++ .../confirm_dont_want_to_be_attorney_test.go | 222 ++++++++ .../enter_reference_number_opt_out.go | 50 ++ .../enter_reference_number_opt_out_test.go | 179 ++++++ .../page/attorney/mock_AttorneyStore_test.go | 46 ++ internal/page/attorney/mock_Localizer_test.go | 534 ++++++++++++++++++ .../page/attorney/mock_LpaStoreClient_test.go | 53 +- .../page/attorney/mock_NotifyClient_test.go | 86 +++ .../page/attorney/mock_SessionStore_test.go | 106 ++++ internal/page/attorney/mock_test.go | 9 + internal/page/attorney/register.go | 23 + internal/page/attorney/register_test.go | 2 +- ...rm_dont_want_to_be_certificate_provider.go | 2 +- ...t_to_be_certificate_provider_logged_out.go | 14 +- ...be_certificate_provider_logged_out_test.go | 60 +- ...nt_want_to_be_certificate_provider_test.go | 2 +- .../enter_reference_number_opt_out.go | 2 +- internal/page/certificateprovider/register.go | 4 +- ...ecided_not_to_be_a_certificate_provider.go | 24 - ...d_not_to_be_a_certificate_provider_test.go | 47 -- internal/page/paths.go | 78 +-- internal/page/share_code.go | 8 +- internal/page/share_code_test.go | 7 + lang/cy.json | 12 +- lang/en.json | 12 +- .../confirm_dont_want_to_be_attorney.gohtml | 34 ++ .../attorney/enter_reference_number.gohtml | 4 +- .../enter_reference_number_opt_out.gohtml | 22 + web/template/attorney/sign.gohtml | 6 +- ...you_have_decided_not_to_be_attorney.gohtml | 15 + .../enter_reference_number.gohtml | 11 +- ...ed_not_to_be_a_certificate_provider.gohtml | 2 +- 41 files changed, 2340 insertions(+), 287 deletions(-) create mode 100644 cypress/e2e/attorney/opt-out.cy.js create mode 100644 internal/page/attorney/confirm_dont_want_to_be_attorney.go create mode 100644 internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out.go create mode 100644 internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out_test.go create mode 100644 internal/page/attorney/confirm_dont_want_to_be_attorney_test.go create mode 100644 internal/page/attorney/enter_reference_number_opt_out.go create mode 100644 internal/page/attorney/enter_reference_number_opt_out_test.go create mode 100644 internal/page/attorney/mock_Localizer_test.go create mode 100644 internal/page/attorney/mock_NotifyClient_test.go delete mode 100644 internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.go delete mode 100644 internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider_test.go create mode 100644 web/template/attorney/confirm_dont_want_to_be_attorney.gohtml create mode 100644 web/template/attorney/enter_reference_number_opt_out.gohtml create mode 100644 web/template/attorney/you_have_decided_not_to_be_attorney.gohtml diff --git a/cypress/e2e/attorney/opt-out.cy.js b/cypress/e2e/attorney/opt-out.cy.js new file mode 100644 index 0000000000..37709da717 --- /dev/null +++ b/cypress/e2e/attorney/opt-out.cy.js @@ -0,0 +1,21 @@ +const { TestEmail, randomShareCode } = require("../../support/e2e"); + +describe('Opting out', () => { + it('stops me being attorney', () => { + const shareCode = randomShareCode(); + cy.visit(`/fixtures/attorney?redirect=&withShareCode=${shareCode}&email=${TestEmail}`); + + cy.visit('/attorney-enter-reference-number-opt-out'); + cy.checkA11yApp(); + cy.get('#f-reference-number').type(shareCode); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/confirm-you-do-not-want-to-be-an-attorney'); + cy.checkA11yApp(); + cy.contains('M-FAKE-'); + + cy.contains('button', 'Confirm').click(); + + cy.url().should('contain', '/you-have-decided-not-to-be-an-attorney'); + }); +}); diff --git a/cypress/e2e/attorney/sign.cy.js b/cypress/e2e/attorney/sign.cy.js index 48f7d17e72..56f2fdea1a 100644 --- a/cypress/e2e/attorney/sign.cy.js +++ b/cypress/e2e/attorney/sign.cy.js @@ -18,6 +18,17 @@ describe('Sign', () => { cy.contains('h1', 'You’ve formally agreed to be an attorney'); }); + it('can be opted out of', () => { + cy.contains('a', 'I do not want to be an attorney').click(); + + cy.url().should('contain', '/confirm-you-do-not-want-to-be-an-attorney'); + cy.checkA11yApp(); + cy.contains('button', 'Confirm').click(); + + cy.url().should('contain', '/you-have-decided-not-to-be-an-attorney'); + cy.checkA11yApp(); + }); + it('shows an error when not selected', () => { cy.contains('button', 'Submit signature').click(); @@ -48,6 +59,17 @@ describe('Sign', () => { cy.contains('h1', 'You’ve formally agreed to be a replacement attorney'); }); + it('can be opted out of', () => { + cy.contains('a', 'I do not want to be an attorney').click(); + + cy.url().should('contain', '/confirm-you-do-not-want-to-be-an-attorney'); + cy.checkA11yApp(); + cy.contains('button', 'Confirm').click(); + + cy.url().should('contain', '/you-have-decided-not-to-be-an-attorney'); + cy.checkA11yApp(); + }); + it('shows an error when not selected', () => { cy.contains('button', 'Submit signature').click(); diff --git a/cypress/e2e/attorney/trust-corporation.cy.js b/cypress/e2e/attorney/trust-corporation.cy.js index 385ba2e06e..fd4a9c9c83 100644 --- a/cypress/e2e/attorney/trust-corporation.cy.js +++ b/cypress/e2e/attorney/trust-corporation.cy.js @@ -1,133 +1,133 @@ const { TestMobile, TestEmail, randomShareCode } = require("../../support/e2e"); describe('As a trust corporation', () => { - beforeEach(() => { - const shareCode = randomShareCode() - cy.visit(`/fixtures/attorney?redirect=/attorney-start&is-trust-corporation=1&progress=readTheLPA&&withShareCode=${shareCode}&email=${TestEmail}`); - - // start - cy.contains('a', 'Start').click(); - cy.get('form').submit(); - - // enter reference number - cy.get('#f-reference-number').type(shareCode); - cy.contains('button', 'Save and continue').click(); - - // acting as an attorney - cy.contains('We have identified the trust corporation’s attorney reference number'); - cy.contains('a', 'Continue').click(); - - // task list - cy.contains('a', 'Confirm your details').click(); - - // mobile number - cy.get('#f-mobile').type(TestMobile); - cy.contains('button', 'Save and continue').click(); - - // language preferences - cy.get('[name="language-preference"]').check('cy', { force: true }) - cy.contains('button', 'Save and continue').click() - - // confirm your company details - cy.contains('07700 900 000'); - cy.contains('Welsh'); - cy.contains('Confirm your company details'); - cy.contains('First Choice Trust Corporation Ltd.'); - cy.contains('555555555'); - cy.contains('simulate-delivered@notifications.service.gov.uk'); - cy.contains('2 RICHMOND PLACE'); - cy.contains('B14 7ED'); - cy.contains('button', 'Continue').click(); - - // task list - cy.contains('Read the LPA').click(); - cy.contains('button', 'Continue').click(); - - // legal rights and responsibilities - cy.contains('Sign the LPA').click(); - cy.contains('Before signing, you must read the trust corporation’s legal rights and responsibilities as an attorney.'); - cy.contains('a', 'Continue').click(); - - // what happens when you sign the lpa - cy.contains('What happens when you sign the LPA'); - cy.contains('a', 'Continue to signing page').click(); - }); - - it('allows a single signatory', () => { - // sign - cy.contains('Sign the LPA on behalf of the trust corporation'); - cy.get('#f-first-names').type('Sign'); - cy.get('#f-last-name').type('Signson'); - cy.get('#f-professional-title').type('Pro signer'); - cy.get('#f-confirm').check({ force: true }); - cy.contains('button', 'Submit signature').click(); - - // would like a 2nd signatory - cy.contains('label', 'No').click(); - cy.contains('button', 'Continue').click(); - - // what happens next - cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); - cy.contains('a', 'Go to your dashboard'); - }); - - it('allows a second signatory', () => { - // sign - cy.contains('Sign the LPA on behalf of the trust corporation'); - cy.get('#f-first-names').type('Sign'); - cy.get('#f-last-name').type('Signson'); - cy.get('#f-professional-title').type('Pro signer'); - cy.get('#f-confirm').check({ force: true }); - cy.contains('button', 'Submit signature').click(); - - // would like a 2nd signatory - cy.contains('label', 'Yes').click(); - cy.contains('button', 'Continue').click(); - - // task list - cy.contains('a', 'Return to task list').click(); - cy.contains('Sign the LPA (signatory 1)'); - cy.contains('Sign the LPA (signatory 2)').click(); - - // sign - cy.get('#f-first-names').type('Sign2'); - cy.get('#f-last-name').type('Signson2'); - cy.get('#f-professional-title').type('Pro signer2'); - cy.get('#f-confirm').check({ force: true }); - cy.contains('button', 'Submit signature').click(); - - // what happens next - cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); - cy.contains('a', 'Go to your dashboard'); - }); - - it('can remove second signatory', () => { - // sign - cy.contains('Sign the LPA on behalf of the trust corporation'); - cy.get('#f-first-names').type('Sign'); - cy.get('#f-last-name').type('Signson'); - cy.get('#f-professional-title').type('Pro signer'); - cy.get('#f-confirm').check({ force: true }); - cy.contains('button', 'Submit signature').click(); - - // would like a 2nd signatory - cy.contains('label', 'Yes').click(); - cy.contains('button', 'Continue').click(); - - // task list - cy.contains('a', 'Return to task list').click(); - cy.contains('Sign the LPA (signatory 1)'); - cy.contains('Sign the LPA (signatory 2)').click(); - - // sign - cy.contains('a', 'The trust corporation no longer requires a second signatory').click(); - - // would like a 2nd signatory - cy.contains('label', 'No').click(); - cy.contains('button', 'Continue').click(); - - // what happens next - cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); - cy.contains('a', 'Go to your dashboard'); - }); + beforeEach(() => { + const shareCode = randomShareCode() + cy.visit(`/fixtures/attorney?redirect=/attorney-start&is-trust-corporation=1&progress=readTheLPA&&withShareCode=${shareCode}&email=${TestEmail}`); + + // start + cy.contains('a', 'Start').click(); + cy.get('form').submit(); + + // enter reference number + cy.get('#f-reference-number').type(shareCode); + cy.contains('button', 'Save and continue').click(); + + // acting as an attorney + cy.contains('We have identified the trust corporation’s attorney reference number'); + cy.contains('a', 'Continue').click(); + + // task list + cy.contains('a', 'Confirm your details').click(); + + // mobile number + cy.get('#f-mobile').type(TestMobile); + cy.contains('button', 'Save and continue').click(); + + // language preferences + cy.get('[name="language-preference"]').check('cy', { force: true }) + cy.contains('button', 'Save and continue').click() + + // confirm your company details + cy.contains('07700 900 000'); + cy.contains('Welsh'); + cy.contains('Confirm your company details'); + cy.contains('First Choice Trust Corporation Ltd.'); + cy.contains('555555555'); + cy.contains('simulate-delivered@notifications.service.gov.uk'); + cy.contains('2 RICHMOND PLACE'); + cy.contains('B14 7ED'); + cy.contains('button', 'Continue').click(); + + // task list + cy.contains('Read the LPA').click(); + cy.contains('button', 'Continue').click(); + + // legal rights and responsibilities + cy.contains('Sign the LPA').click(); + cy.contains('Before signing, you must read the trust corporation’s legal rights and responsibilities as an attorney.'); + cy.contains('a', 'Continue').click(); + + // what happens when you sign the lpa + cy.contains('What happens when you sign the LPA'); + cy.contains('a', 'Continue to signing page').click(); + }); + + it('allows a single signatory', () => { + // sign + cy.contains('Sign the LPA on behalf of the trust corporation'); + cy.get('#f-first-names').type('Sign'); + cy.get('#f-last-name').type('Signson'); + cy.get('#f-professional-title').type('Pro signer'); + cy.get('#f-confirm').check({ force: true }); + cy.contains('button', 'Submit signature').click(); + + // would like a 2nd signatory + cy.contains('label', 'No').click(); + cy.contains('button', 'Continue').click(); + + // what happens next + cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); + cy.contains('a', 'Go to your dashboard'); + }); + + it('allows a second signatory', () => { + // sign + cy.contains('Sign the LPA on behalf of the trust corporation'); + cy.get('#f-first-names').type('Sign'); + cy.get('#f-last-name').type('Signson'); + cy.get('#f-professional-title').type('Pro signer'); + cy.get('#f-confirm').check({ force: true }); + cy.contains('button', 'Submit signature').click(); + + // would like a 2nd signatory + cy.contains('label', 'Yes').click(); + cy.contains('button', 'Continue').click(); + + // task list + cy.visitLpa('/task-list'); + cy.contains('Sign the LPA (signatory 1)'); + cy.contains('Sign the LPA (signatory 2)').click(); + + // sign + cy.get('#f-first-names').type('Sign2'); + cy.get('#f-last-name').type('Signson2'); + cy.get('#f-professional-title').type('Pro signer2'); + cy.get('#f-confirm').check({ force: true }); + cy.contains('button', 'Submit signature').click(); + + // what happens next + cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); + cy.contains('a', 'Go to your dashboard'); + }); + + it('can remove second signatory', () => { + // sign + cy.contains('Sign the LPA on behalf of the trust corporation'); + cy.get('#f-first-names').type('Sign'); + cy.get('#f-last-name').type('Signson'); + cy.get('#f-professional-title').type('Pro signer'); + cy.get('#f-confirm').check({ force: true }); + cy.contains('button', 'Submit signature').click(); + + // would like a 2nd signatory + cy.contains('label', 'Yes').click(); + cy.contains('button', 'Continue').click(); + + // task list + cy.visitLpa('/task-list'); + cy.contains('Sign the LPA (signatory 1)'); + cy.contains('Sign the LPA (signatory 2)').click(); + + // sign + cy.contains('a', 'The trust corporation no longer requires a second signatory').click(); + + // would like a 2nd signatory + cy.contains('label', 'No').click(); + cy.contains('button', 'Continue').click(); + + // what happens next + cy.contains('First Choice Trust Corporation Ltd. has formally agreed to be an attorney'); + cy.contains('a', 'Go to your dashboard'); + }); }); diff --git a/internal/app/app.go b/internal/app/app.go index 2a406d1f34..53987bf905 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -199,6 +199,8 @@ func App( dashboardStore, lpaStoreClient, lpaStoreResolvingService, + notifyClient, + appPublicURL, ) donor.Register( diff --git a/internal/app/attorney_store.go b/internal/app/attorney_store.go index 09950d1657..9708d8c0c4 100644 --- a/internal/app/attorney_store.go +++ b/internal/app/attorney_store.go @@ -3,6 +3,7 @@ package app import ( "context" "errors" + "fmt" "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -74,3 +75,20 @@ func (s *attorneyStore) Put(ctx context.Context, attorney *actor.AttorneyProvide attorney.UpdatedAt = s.now() return s.dynamoClient.Put(ctx, attorney) } + +func (s *attorneyStore) Delete(ctx context.Context) error { + data, err := page.SessionDataFromContext(ctx) + if err != nil { + return err + } + + if data.LpaID == "" || data.SessionID == "" { + return errors.New("attorneyStore.Delete requires LpaID and SessionID") + } + + if err := s.dynamoClient.DeleteOne(ctx, dynamo.LpaKey(data.LpaID), dynamo.AttorneyKey(data.SessionID)); err != nil { + return fmt.Errorf("error deleting attorney: %w", err) + } + + return nil +} diff --git a/internal/app/attorney_store_test.go b/internal/app/attorney_store_test.go index b66220b578..b0cb4e1151 100644 --- a/internal/app/attorney_store_test.go +++ b/internal/app/attorney_store_test.go @@ -3,6 +3,7 @@ package app import ( "context" "errors" + "fmt" "testing" "time" @@ -221,3 +222,63 @@ func TestAttorneyStorePutOnError(t *testing.T) { err := attorneyStore.Put(ctx, &actor.AttorneyProvidedDetails{PK: dynamo.LpaKey("123"), SK: dynamo.AttorneyKey("456"), LpaID: "123"}) assert.Equal(t, expectedError, err) } + +func TestAttorneyStoreDelete(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123", SessionID: "456"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + DeleteOne(ctx, dynamo.LpaKey("123"), dynamo.AttorneyKey("456")). + Return(nil) + + attorneyStore := &attorneyStore{dynamoClient: dynamoClient} + + err := attorneyStore.Delete(ctx) + assert.Nil(t, err) +} + +func TestAttorneyStoreDeleteWhenSessionDataErrors(t *testing.T) { + attorneyStore := &attorneyStore{} + + err := attorneyStore.Delete(ctx) + assert.Error(t, err) +} + +func TestAttorneyStoreDeleteWhenMissingSessionValues(t *testing.T) { + testcases := map[string]struct { + lpaID string + sessionID string + }{ + "missing LpaID": { + sessionID: "456", + }, + "missing SessionID": { + lpaID: "123", + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: tc.lpaID, SessionID: tc.sessionID}) + + attorneyStore := &attorneyStore{} + + err := attorneyStore.Delete(ctx) + assert.Error(t, err) + }) + } +} + +func TestAttorneyStoreDeleteWhenDynamoClientError(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123", SessionID: "456"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + DeleteOne(mock.Anything, mock.Anything, mock.Anything). + Return(expectedError) + + attorneyStore := &attorneyStore{dynamoClient: dynamoClient} + + err := attorneyStore.Delete(ctx) + assert.Equal(t, fmt.Errorf("error deleting attorney: %w", expectedError), err) +} diff --git a/internal/notify/email.go b/internal/notify/email.go index 2d1886c13c..13adbe9d1f 100644 --- a/internal/notify/email.go +++ b/internal/notify/email.go @@ -12,6 +12,7 @@ type InitialOriginalAttorneyEmail struct { AttorneyStartPageURL string ShareCode string DonorFirstNamesPossessive string + AttorneyOptOutURL string } func (e InitialOriginalAttorneyEmail) emailID(isProduction bool) string { @@ -30,6 +31,7 @@ type InitialReplacementAttorneyEmail struct { AttorneyStartPageURL string ShareCode string DonorFirstNamesPossessive string + AttorneyOptOutURL string } func (e InitialReplacementAttorneyEmail) emailID(isProduction bool) string { @@ -191,3 +193,15 @@ func (e PaymentConfirmationEmail) emailID(isProduction bool) string { return "ff757818-f066-4605-8751-af481afe8a2b" } + +type AttorneyOptedOutEmail struct { + AttorneyFullName string + DonorFullName string + LpaType string + LpaUID string + DonorStartPageURL string +} + +func (e AttorneyOptedOutEmail) emailID(isProduction bool) string { + return "TODO" +} diff --git a/internal/page/attorney/confirm_dont_want_to_be_attorney.go b/internal/page/attorney/confirm_dont_want_to_be_attorney.go new file mode 100644 index 0000000000..3a647bc196 --- /dev/null +++ b/internal/page/attorney/confirm_dont_want_to_be_attorney.go @@ -0,0 +1,63 @@ +package attorney + +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/lpastore" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +type confirmDontWantToBeAttorneyData struct { + App page.AppData + Errors validation.List + Lpa *lpastore.Lpa +} + +func ConfirmDontWantToBeAttorney(tmpl template.Template, lpaStoreResolvingService LpaStoreResolvingService, attorneyStore AttorneyStore, notifyClient NotifyClient, appPublicURL string) Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request, attorneyProvidedDetails *actor.AttorneyProvidedDetails) error { + lpa, err := lpaStoreResolvingService.Get(r.Context()) + if err != nil { + return err + } + + data := &confirmDontWantToBeAttorneyData{ + App: appData, + Lpa: lpa, + } + + if r.Method == http.MethodPost { + attorneyFullName, err := findAttorneyFullName(lpa, attorneyProvidedDetails.UID, attorneyProvidedDetails.IsTrustCorporation, attorneyProvidedDetails.IsReplacement) + if err != nil { + return err + } + + email := notify.AttorneyOptedOutEmail{ + AttorneyFullName: attorneyFullName, + DonorFullName: lpa.Donor.FullName(), + LpaType: appData.Localizer.T(lpa.Type.String()), + LpaUID: lpa.LpaUID, + DonorStartPageURL: appPublicURL + page.Paths.Start.Format(), + } + + if err := attorneyStore.Delete(r.Context()); err != nil { + return err + } + + if err := notifyClient.SendActorEmail(r.Context(), lpa.Donor.Email, lpa.LpaUID, email); err != nil { + return err + } + + return page.Paths.Attorney.YouHaveDecidedNotToBeAttorney.RedirectQuery(w, r, appData, url.Values{ + "donorFullName": {lpa.Donor.FullName()}, + "donorFirstNames": {lpa.Donor.FirstNames}, + }) + } + + return tmpl(w, data) + } +} diff --git a/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out.go b/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out.go new file mode 100644 index 0000000000..08306d70e5 --- /dev/null +++ b/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out.go @@ -0,0 +1,94 @@ +package attorney + +import ( + "errors" + "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/actor/actoruid" + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +type confirmDontWantToBeAttorneyDataLoggedOut struct { + App page.AppData + Errors validation.List + Lpa *lpastore.Lpa +} + +func ConfirmDontWantToBeAttorneyLoggedOut(tmpl template.Template, shareCodeStore ShareCodeStore, lpaStoreResolvingService LpaStoreResolvingService, sessionStore SessionStore, notifyClient NotifyClient, appPublicURL string) page.Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { + session, err := sessionStore.LpaData(r) + if err != nil { + return err + } + + ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{LpaID: session.LpaID}) + + lpa, err := lpaStoreResolvingService.Get(ctx) + if err != nil { + return err + } + + data := &confirmDontWantToBeAttorneyDataLoggedOut{ + App: appData, + Lpa: lpa, + } + + if r.Method == http.MethodPost { + shareCode, err := shareCodeStore.Get(r.Context(), actor.TypeAttorney, r.URL.Query().Get("referenceNumber")) + if err != nil { + return err + } + + attorneyFullName, err := findAttorneyFullName(lpa, shareCode.ActorUID, shareCode.IsTrustCorporation, shareCode.IsReplacementAttorney) + if err != nil { + return err + } + + email := notify.AttorneyOptedOutEmail{ + AttorneyFullName: attorneyFullName, + DonorFullName: lpa.Donor.FullName(), + LpaType: appData.Localizer.T(lpa.Type.String()), + LpaUID: lpa.LpaUID, + DonorStartPageURL: appPublicURL + page.Paths.Start.Format(), + } + + if err := notifyClient.SendActorEmail(ctx, lpa.Donor.Email, lpa.LpaUID, email); err != nil { + return err + } + + if err := shareCodeStore.Delete(r.Context(), shareCode); err != nil { + return err + } + + return page.Paths.Attorney.YouHaveDecidedNotToBeAttorney.RedirectQuery(w, r, appData, url.Values{"donorFullName": {lpa.Donor.FullName()}}) + } + + return tmpl(w, data) + } +} + +func findAttorneyFullName(lpa *lpastore.Lpa, uid actoruid.UID, isTrustCorporation, isReplacement bool) (string, error) { + if t := lpa.ReplacementAttorneys.TrustCorporation; t.UID == uid { + return t.Name, nil + } + + if t := lpa.Attorneys.TrustCorporation; t.UID == uid { + return t.Name, nil + } + + if a, ok := lpa.ReplacementAttorneys.Get(uid); ok { + return a.FullName(), nil + } + + if a, ok := lpa.Attorneys.Get(uid); ok { + return a.FullName(), nil + } + + return "", errors.New("attorney not found") +} diff --git a/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out_test.go b/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out_test.go new file mode 100644 index 0000000000..bb856c3b81 --- /dev/null +++ b/internal/page/attorney/confirm_dont_want_to_be_attorney_logged_out_test.go @@ -0,0 +1,398 @@ +package attorney + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor/actoruid" + "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetConfirmDontWantToBeAttorneyLoggedOut(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpa := lpastore.Lpa{LpaUID: "lpa-uid"} + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(page.ContextWithSessionData(r.Context(), &page.SessionData{LpaID: "lpa-id"})). + Return(&lpa, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &confirmDontWantToBeAttorneyDataLoggedOut{ + App: testAppData, + Lpa: &lpa, + }). + Return(nil) + + err := ConfirmDontWantToBeAttorneyLoggedOut(template.Execute, nil, lpaStoreResolvingService, sessionStore, nil, "example.com")(testAppData, w, r) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetConfirmDontWantToBeAttorneyLoggedOutWhenSessionStoreErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{}, expectedError) + + err := ConfirmDontWantToBeAttorneyLoggedOut(nil, nil, nil, sessionStore, nil, "example.com")(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetConfirmDontWantToBeAttorneyLoggedOutWhenLpaStoreResolvingServiceErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(mock.Anything). + Return(&lpastore.Lpa{}, expectedError) + + err := ConfirmDontWantToBeAttorneyLoggedOut(nil, nil, lpaStoreResolvingService, sessionStore, nil, "example.com")(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetConfirmDontWantToBeAttorneyLoggedOutWhenTemplateErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(mock.Anything). + Return(&lpastore.Lpa{}, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(mock.Anything, mock.Anything). + Return(expectedError) + + err := ConfirmDontWantToBeAttorneyLoggedOut(template.Execute, nil, lpaStoreResolvingService, sessionStore, nil, "example.com")(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostConfirmDontWantToBeAttorneyLoggedOut(t *testing.T) { + attorneyUID := actoruid.New() + replacementAttorneyUID := actoruid.New() + trustCorporationUID := actoruid.New() + replacementTrustCorporationUID := actoruid.New() + + testcases := map[string]struct { + uid actoruid.UID + attorneyFullName string + }{ + "attorney": { + uid: attorneyUID, + attorneyFullName: "d e f", + }, + "replacement attorney": { + uid: replacementAttorneyUID, + attorneyFullName: "x y z", + }, + "trust corporation": { + uid: trustCorporationUID, + attorneyFullName: "trusty", + }, + "replacement trust corporation": { + uid: replacementTrustCorporationUID, + attorneyFullName: "untrusty", + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "/?referenceNumber=123", nil) + w := httptest.NewRecorder() + ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{LpaID: "lpa-id"}) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + shareCodeData := actor.ShareCodeData{ + LpaKey: dynamo.LpaKey("lpa-id"), + LpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("donor")), + ActorUID: tc.uid, + } + + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(r.Context(), actor.TypeAttorney, "123"). + Return(shareCodeData, nil) + shareCodeStore.EXPECT(). + Delete(r.Context(), shareCodeData). + Return(nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(ctx). + Return(&lpastore.Lpa{ + LpaUID: "lpa-uid", + SignedAt: time.Now(), + Donor: lpastore.Donor{ + FirstNames: "a b", LastName: "c", Email: "a@example.com", + }, + Attorneys: lpastore.Attorneys{ + TrustCorporation: lpastore.TrustCorporation{UID: trustCorporationUID, Name: "trusty"}, + Attorneys: []lpastore.Attorney{ + {UID: attorneyUID, FirstNames: "d e", LastName: "f"}, + }, + }, + ReplacementAttorneys: lpastore.Attorneys{ + TrustCorporation: lpastore.TrustCorporation{UID: replacementTrustCorporationUID, Name: "untrusty"}, + Attorneys: []lpastore.Attorney{ + {UID: replacementAttorneyUID, FirstNames: "x y", LastName: "z"}, + }, + }, + Type: actor.LpaTypePersonalWelfare, + }, nil) + + notifyClient := newMockNotifyClient(t) + notifyClient.EXPECT(). + SendActorEmail(ctx, "a@example.com", "lpa-uid", notify.AttorneyOptedOutEmail{ + AttorneyFullName: tc.attorneyFullName, + DonorFullName: "a b c", + LpaType: "Personal welfare", + LpaUID: "lpa-uid", + DonorStartPageURL: "example.com" + page.Paths.Start.Format(), + }). + Return(nil) + + localizer := newMockLocalizer(t) + localizer.EXPECT(). + T("personal-welfare"). + Return("Personal welfare") + + testAppData.Localizer = localizer + + err := ConfirmDontWantToBeAttorneyLoggedOut(nil, shareCodeStore, lpaStoreResolvingService, sessionStore, notifyClient, "example.com")(testAppData, w, r) + + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, page.Paths.Attorney.YouHaveDecidedNotToBeAttorney.Format()+"?donorFullName=a+b+c", resp.Header.Get("Location")) + assert.Equal(t, http.StatusFound, resp.StatusCode) + }) + } +} + +func TestPostConfirmDontWantToBeAttorneyLoggedOutWhenAttorneyNotFound(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "/?referenceNumber=123", nil) + w := httptest.NewRecorder() + ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{LpaID: "lpa-id"}) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + shareCodeData := actor.ShareCodeData{ + LpaKey: dynamo.LpaKey("lpa-id"), + LpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("donor")), + ActorUID: actoruid.New(), + } + + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(r.Context(), actor.TypeAttorney, "123"). + Return(shareCodeData, nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(ctx). + Return(&lpastore.Lpa{ + LpaUID: "lpa-uid", + SignedAt: time.Now(), + Donor: lpastore.Donor{ + FirstNames: "a b", LastName: "c", Email: "a@example.com", + }, + Type: actor.LpaTypePersonalWelfare, + }, nil) + + err := ConfirmDontWantToBeAttorneyLoggedOut(nil, shareCodeStore, lpaStoreResolvingService, sessionStore, nil, "example.com")(testAppData, w, r) + assert.EqualError(t, err, "attorney not found") +} + +func TestPostConfirmDontWantToBeAttorneyLoggedOutErrors(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "/?referenceNumber=123", nil) + ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{LpaID: "lpa-id"}) + + shareCodeData := actor.ShareCodeData{ + LpaKey: dynamo.LpaKey("lpa-id"), + } + + signedLPA := lpastore.Lpa{LpaUID: "lpa-uid", SignedAt: time.Now()} + + localizer := func(t *testing.T) *mockLocalizer { + l := newMockLocalizer(t) + l.EXPECT(). + T(mock.Anything). + Return("a") + + return l + } + + testcases := map[string]struct { + sessionStore func(*testing.T) *mockSessionStore + lpaStoreResolvingService func(*testing.T) *mockLpaStoreResolvingService + localizer func(*testing.T) *mockLocalizer + shareCodeStore func(*testing.T) *mockShareCodeStore + notifyClient func(*testing.T) *mockNotifyClient + }{ + "when shareCodeStore.Get() error": { + sessionStore: func(t *testing.T) *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + return sessionStore + }, + lpaStoreResolvingService: func(t *testing.T) *mockLpaStoreResolvingService { + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(ctx). + Return(&signedLPA, nil) + + return lpaStoreResolvingService + }, + shareCodeStore: func(t *testing.T) *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, expectedError) + + return shareCodeStore + }, + }, + "when shareCodeStore.Delete() error": { + sessionStore: func(t *testing.T) *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + return sessionStore + }, + lpaStoreResolvingService: func(t *testing.T) *mockLpaStoreResolvingService { + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(ctx). + Return(&signedLPA, nil) + + return lpaStoreResolvingService + }, + localizer: localizer, + shareCodeStore: func(t *testing.T) *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, nil) + shareCodeStore.EXPECT(). + Delete(mock.Anything, mock.Anything). + Return(expectedError) + + return shareCodeStore + }, + notifyClient: func(t *testing.T) *mockNotifyClient { + client := newMockNotifyClient(t) + client.EXPECT(). + SendActorEmail(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + + return client + }, + }, + "when notifyClient.SendActorEmail() error": { + sessionStore: func(t *testing.T) *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + LpaData(r). + Return(&sesh.LpaDataSession{LpaID: "lpa-id"}, nil) + + return sessionStore + }, + lpaStoreResolvingService: func(t *testing.T) *mockLpaStoreResolvingService { + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(ctx). + Return(&signedLPA, nil) + + return lpaStoreResolvingService + }, + localizer: localizer, + shareCodeStore: func(t *testing.T) *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, nil) + + return shareCodeStore + }, + notifyClient: func(t *testing.T) *mockNotifyClient { + client := newMockNotifyClient(t) + client.EXPECT(). + SendActorEmail(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(expectedError) + + return client + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + + testAppData.Localizer = evalT(tc.localizer, t) + + err := ConfirmDontWantToBeAttorneyLoggedOut(nil, evalT(tc.shareCodeStore, t), evalT(tc.lpaStoreResolvingService, t), evalT(tc.sessionStore, t), evalT(tc.notifyClient, t), "example.com")(testAppData, w, r) + + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} diff --git a/internal/page/attorney/confirm_dont_want_to_be_attorney_test.go b/internal/page/attorney/confirm_dont_want_to_be_attorney_test.go new file mode 100644 index 0000000000..e2b2668970 --- /dev/null +++ b/internal/page/attorney/confirm_dont_want_to_be_attorney_test.go @@ -0,0 +1,222 @@ +package attorney + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor/actoruid" + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetConfirmDontWantToBeAttorney(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpa := lpastore.Lpa{LpaUID: "lpa-uid"} + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpa, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &confirmDontWantToBeAttorneyData{ + App: testAppData, + Lpa: &lpa, + }). + Return(nil) + + err := ConfirmDontWantToBeAttorney(template.Execute, lpaStoreResolvingService, nil, nil, "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetConfirmDontWantToBeAttorneyWhenLpaStoreResolvingServiceErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(mock.Anything). + Return(&lpastore.Lpa{}, expectedError) + + err := ConfirmDontWantToBeAttorney(nil, lpaStoreResolvingService, nil, nil, "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{}) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetConfirmDontWantToBeAttorneyWhenTemplateErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(mock.Anything). + Return(&lpastore.Lpa{}, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(mock.Anything, mock.Anything). + Return(expectedError) + + err := ConfirmDontWantToBeAttorney(template.Execute, lpaStoreResolvingService, nil, nil, "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{}) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostConfirmDontWantToBeAttorney(t *testing.T) { + r, _ := http.NewRequestWithContext(page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123", SessionID: "456"}), http.MethodPost, "/?referenceNumber=123", nil) + w := httptest.NewRecorder() + uid := actoruid.New() + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpastore.Lpa{ + LpaUID: "lpa-uid", + SignedAt: time.Now(), + Donor: lpastore.Donor{ + FirstNames: "a b", LastName: "c", Email: "a@example.com", + }, + Attorneys: lpastore.Attorneys{ + Attorneys: []lpastore.Attorney{ + {FirstNames: "d e", LastName: "f", UID: uid}, + }, + }, + Type: actor.LpaTypePersonalWelfare, + }, nil) + + certificateProviderStore := newMockAttorneyStore(t) + certificateProviderStore.EXPECT(). + Delete(r.Context()). + Return(nil) + + localizer := newMockLocalizer(t) + localizer.EXPECT(). + T("personal-welfare"). + Return("Personal welfare") + + testAppData.Localizer = localizer + + notifyClient := newMockNotifyClient(t) + notifyClient.EXPECT(). + SendActorEmail(r.Context(), "a@example.com", "lpa-uid", notify.AttorneyOptedOutEmail{ + AttorneyFullName: "d e f", + DonorFullName: "a b c", + LpaType: "Personal welfare", + LpaUID: "lpa-uid", + DonorStartPageURL: "example.com" + page.Paths.Start.Format(), + }). + Return(nil) + + err := ConfirmDontWantToBeAttorney(nil, lpaStoreResolvingService, certificateProviderStore, notifyClient, "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{ + UID: uid, + }) + + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, page.Paths.Attorney.YouHaveDecidedNotToBeAttorney.Format()+"?donorFirstNames=a+b&donorFullName=a+b+c", resp.Header.Get("Location")) + assert.Equal(t, http.StatusFound, resp.StatusCode) +} + +func TestPostConfirmDontWantToBeAttorneyWhenAttorneyNotFound(t *testing.T) { + r, _ := http.NewRequestWithContext(page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123", SessionID: "456"}), http.MethodPost, "/?referenceNumber=123", nil) + w := httptest.NewRecorder() + uid := actoruid.New() + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpastore.Lpa{ + LpaUID: "lpa-uid", + SignedAt: time.Now(), + Donor: lpastore.Donor{ + FirstNames: "a b", LastName: "c", Email: "a@example.com", + }, + Type: actor.LpaTypePersonalWelfare, + }, nil) + + err := ConfirmDontWantToBeAttorney(nil, lpaStoreResolvingService, nil, nil, "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{ + UID: uid, + }) + assert.EqualError(t, err, "attorney not found") +} + +func TestPostConfirmDontWantToBeAttorneyErrors(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "/?referenceNumber=123", nil) + + testcases := map[string]struct { + certificateProviderStore func(*testing.T) *mockAttorneyStore + notifyClient func(*testing.T) *mockNotifyClient + }{ + "when attorneyStore.Delete() error": { + certificateProviderStore: func(t *testing.T) *mockAttorneyStore { + certificateProviderStore := newMockAttorneyStore(t) + certificateProviderStore.EXPECT(). + Delete(mock.Anything). + Return(expectedError) + + return certificateProviderStore + }, + }, + "when notifyClient.SendActorEmail() error": { + certificateProviderStore: func(t *testing.T) *mockAttorneyStore { + certificateProviderStore := newMockAttorneyStore(t) + certificateProviderStore.EXPECT(). + Delete(mock.Anything). + Return(nil) + + return certificateProviderStore + }, + notifyClient: func(t *testing.T) *mockNotifyClient { + client := newMockNotifyClient(t) + client.EXPECT(). + SendActorEmail(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(expectedError) + + return client + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpastore.Lpa{LpaUID: "lpa-uid", SignedAt: time.Now()}, nil) + + localizer := newMockLocalizer(t) + localizer.EXPECT(). + T(mock.Anything). + Return("a") + + testAppData.Localizer = localizer + + err := ConfirmDontWantToBeAttorney(nil, lpaStoreResolvingService, evalT(tc.certificateProviderStore, t), evalT(tc.notifyClient, t), "example.com")(testAppData, w, r, &actor.AttorneyProvidedDetails{}) + + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} diff --git a/internal/page/attorney/enter_reference_number_opt_out.go b/internal/page/attorney/enter_reference_number_opt_out.go new file mode 100644 index 0000000000..dfdbe6e145 --- /dev/null +++ b/internal/page/attorney/enter_reference_number_opt_out.go @@ -0,0 +1,50 @@ +package attorney + +import ( + "errors" + "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/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +func EnterReferenceNumberOptOut(tmpl template.Template, shareCodeStore ShareCodeStore, sessionStore SessionStore) page.Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { + data := enterReferenceNumberData{ + App: appData, + Form: &enterReferenceNumberForm{}, + } + + if r.Method == http.MethodPost { + data.Form = readEnterReferenceNumberForm(r) + data.Errors = data.Form.Validate() + + if data.Errors.None() { + referenceNumber := data.Form.ReferenceNumber + + shareCode, err := shareCodeStore.Get(r.Context(), actor.TypeAttorney, referenceNumber) + if err != nil { + if errors.Is(err, dynamo.NotFoundError{}) { + data.Errors.Add("reference-number", validation.CustomError{Label: "incorrectReferenceNumber"}) + return tmpl(w, data) + } else { + return err + } + } + + if err := sessionStore.SetLpaData(r, w, &sesh.LpaDataSession{LpaID: shareCode.LpaKey.ID()}); err != nil { + return err + } + + return page.Paths.Attorney.ConfirmDontWantToBeAttorneyLoggedOut.RedirectQuery(w, r, appData, url.Values{"referenceNumber": {referenceNumber}}) + } + } + + return tmpl(w, data) + } +} diff --git a/internal/page/attorney/enter_reference_number_opt_out_test.go b/internal/page/attorney/enter_reference_number_opt_out_test.go new file mode 100644 index 0000000000..485c21fda5 --- /dev/null +++ b/internal/page/attorney/enter_reference_number_opt_out_test.go @@ -0,0 +1,179 @@ +package attorney + +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/actor/actoruid" + "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEnterReferenceNumberOptOut(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + data := enterReferenceNumberData{ + App: testAppData, + Form: &enterReferenceNumberForm{}, + } + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, data). + Return(nil) + + err := EnterReferenceNumberOptOut(template.Execute, newMockShareCodeStore(t), nil)(testAppData, w, r) + + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetEnterReferenceNumberOptOutOnTemplateError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + data := enterReferenceNumberData{ + App: testAppData, + Form: &enterReferenceNumberForm{}, + } + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, data). + Return(expectedError) + + err := EnterReferenceNumberOptOut(template.Execute, newMockShareCodeStore(t), nil)(testAppData, w, r) + + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostEnterReferenceNumberOptOut(t *testing.T) { + form := url.Values{ + "reference-number": {"abcdef 123-456"}, + } + + uid := actoruid.New() + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(r.Context(), actor.TypeAttorney, "abcdef123456"). + Return(actor.ShareCodeData{LpaKey: dynamo.LpaKey("lpa-id"), LpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("session-id")), ActorUID: uid}, nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + SetLpaData(r, w, &sesh.LpaDataSession{LpaID: "lpa-id"}). + Return(nil) + + err := EnterReferenceNumberOptOut(nil, shareCodeStore, sessionStore)(testAppData, w, r) + + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.Attorney.ConfirmDontWantToBeAttorneyLoggedOut.Format()+"?referenceNumber=abcdef123456", resp.Header.Get("Location")) +} + +func TestPostEnterReferenceNumberOptOutErrors(t *testing.T) { + form := url.Values{ + "reference-number": {"abcdef 123-456"}, + } + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + testcases := map[string]struct { + shareCodeStore func() *mockShareCodeStore + sessionStore func() *mockSessionStore + }{ + "when shareCodeStore error": { + shareCodeStore: func() *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(actor.ShareCodeData{}, expectedError) + + return shareCodeStore + }, + sessionStore: func() *mockSessionStore { return nil }, + }, + "when sessionStore error": { + shareCodeStore: func() *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(actor.ShareCodeData{LpaKey: dynamo.LpaKey("lpa-id")}, nil) + + return shareCodeStore + }, + sessionStore: func() *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + SetLpaData(mock.Anything, mock.Anything, mock.Anything). + Return(expectedError) + + return sessionStore + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + err := EnterReferenceNumberOptOut(nil, tc.shareCodeStore(), tc.sessionStore())(testAppData, w, r) + + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} + +func TestPostEnterReferenceNumberOptOutOnShareCodeStoreNotFoundError(t *testing.T) { + form := url.Values{ + "reference-number": {"abcdef 123456"}, + } + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + data := enterReferenceNumberData{ + App: testAppData, + Form: &enterReferenceNumberForm{ReferenceNumber: "abcdef123456", ReferenceNumberRaw: "abcdef 123456"}, + Errors: validation.With("reference-number", validation.CustomError{Label: "incorrectReferenceNumber"}), + } + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, data). + Return(nil) + + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(r.Context(), actor.TypeAttorney, "abcdef123456"). + Return(actor.ShareCodeData{LpaKey: dynamo.LpaKey("lpa-id"), LpaOwnerKey: dynamo.LpaOwnerKey(dynamo.DonorKey("session-id"))}, dynamo.NotFoundError{}) + + err := EnterReferenceNumberOptOut(template.Execute, shareCodeStore, nil)(testAppData, w, r) + + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/internal/page/attorney/mock_AttorneyStore_test.go b/internal/page/attorney/mock_AttorneyStore_test.go index da813a3987..f47c5261aa 100644 --- a/internal/page/attorney/mock_AttorneyStore_test.go +++ b/internal/page/attorney/mock_AttorneyStore_test.go @@ -83,6 +83,52 @@ func (_c *mockAttorneyStore_Create_Call) RunAndReturn(run func(context.Context, return _c } +// Delete provides a mock function with given fields: ctx +func (_m *mockAttorneyStore) Delete(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockAttorneyStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type mockAttorneyStore_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockAttorneyStore_Expecter) Delete(ctx interface{}) *mockAttorneyStore_Delete_Call { + return &mockAttorneyStore_Delete_Call{Call: _e.mock.On("Delete", ctx)} +} + +func (_c *mockAttorneyStore_Delete_Call) Run(run func(ctx context.Context)) *mockAttorneyStore_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockAttorneyStore_Delete_Call) Return(_a0 error) *mockAttorneyStore_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockAttorneyStore_Delete_Call) RunAndReturn(run func(context.Context) error) *mockAttorneyStore_Delete_Call { + _c.Call.Return(run) + return _c +} + // Get provides a mock function with given fields: ctx func (_m *mockAttorneyStore) Get(ctx context.Context) (*actor.AttorneyProvidedDetails, error) { ret := _m.Called(ctx) diff --git a/internal/page/attorney/mock_Localizer_test.go b/internal/page/attorney/mock_Localizer_test.go new file mode 100644 index 0000000000..e3f9c8eb70 --- /dev/null +++ b/internal/page/attorney/mock_Localizer_test.go @@ -0,0 +1,534 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package attorney + +import ( + date "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// mockLocalizer is an autogenerated mock type for the Localizer type +type mockLocalizer struct { + mock.Mock +} + +type mockLocalizer_Expecter struct { + mock *mock.Mock +} + +func (_m *mockLocalizer) EXPECT() *mockLocalizer_Expecter { + return &mockLocalizer_Expecter{mock: &_m.Mock} +} + +// Concat provides a mock function with given fields: _a0, _a1 +func (_m *mockLocalizer) Concat(_a0 []string, _a1 string) string { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Concat") + } + + var r0 string + if rf, ok := ret.Get(0).(func([]string, string) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_Concat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Concat' +type mockLocalizer_Concat_Call struct { + *mock.Call +} + +// Concat is a helper method to define mock.On call +// - _a0 []string +// - _a1 string +func (_e *mockLocalizer_Expecter) Concat(_a0 interface{}, _a1 interface{}) *mockLocalizer_Concat_Call { + return &mockLocalizer_Concat_Call{Call: _e.mock.On("Concat", _a0, _a1)} +} + +func (_c *mockLocalizer_Concat_Call) Run(run func(_a0 []string, _a1 string)) *mockLocalizer_Concat_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].([]string), args[1].(string)) + }) + return _c +} + +func (_c *mockLocalizer_Concat_Call) Return(_a0 string) *mockLocalizer_Concat_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_Concat_Call) RunAndReturn(run func([]string, string) string) *mockLocalizer_Concat_Call { + _c.Call.Return(run) + return _c +} + +// Count provides a mock function with given fields: _a0, _a1 +func (_m *mockLocalizer) Count(_a0 string, _a1 int) string { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Count") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, int) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count' +type mockLocalizer_Count_Call struct { + *mock.Call +} + +// Count is a helper method to define mock.On call +// - _a0 string +// - _a1 int +func (_e *mockLocalizer_Expecter) Count(_a0 interface{}, _a1 interface{}) *mockLocalizer_Count_Call { + return &mockLocalizer_Count_Call{Call: _e.mock.On("Count", _a0, _a1)} +} + +func (_c *mockLocalizer_Count_Call) Run(run func(_a0 string, _a1 int)) *mockLocalizer_Count_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(int)) + }) + return _c +} + +func (_c *mockLocalizer_Count_Call) Return(_a0 string) *mockLocalizer_Count_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_Count_Call) RunAndReturn(run func(string, int) string) *mockLocalizer_Count_Call { + _c.Call.Return(run) + return _c +} + +// Format provides a mock function with given fields: _a0, _a1 +func (_m *mockLocalizer) Format(_a0 string, _a1 map[string]interface{}) string { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Format") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, map[string]interface{}) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_Format_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Format' +type mockLocalizer_Format_Call struct { + *mock.Call +} + +// Format is a helper method to define mock.On call +// - _a0 string +// - _a1 map[string]interface{} +func (_e *mockLocalizer_Expecter) Format(_a0 interface{}, _a1 interface{}) *mockLocalizer_Format_Call { + return &mockLocalizer_Format_Call{Call: _e.mock.On("Format", _a0, _a1)} +} + +func (_c *mockLocalizer_Format_Call) Run(run func(_a0 string, _a1 map[string]interface{})) *mockLocalizer_Format_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(map[string]interface{})) + }) + return _c +} + +func (_c *mockLocalizer_Format_Call) Return(_a0 string) *mockLocalizer_Format_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_Format_Call) RunAndReturn(run func(string, map[string]interface{}) string) *mockLocalizer_Format_Call { + _c.Call.Return(run) + return _c +} + +// FormatCount provides a mock function with given fields: _a0, _a1, _a2 +func (_m *mockLocalizer) FormatCount(_a0 string, _a1 int, _a2 map[string]interface{}) string { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for FormatCount") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, int, map[string]interface{}) string); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_FormatCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatCount' +type mockLocalizer_FormatCount_Call struct { + *mock.Call +} + +// FormatCount is a helper method to define mock.On call +// - _a0 string +// - _a1 int +// - _a2 map[string]interface{} +func (_e *mockLocalizer_Expecter) FormatCount(_a0 interface{}, _a1 interface{}, _a2 interface{}) *mockLocalizer_FormatCount_Call { + return &mockLocalizer_FormatCount_Call{Call: _e.mock.On("FormatCount", _a0, _a1, _a2)} +} + +func (_c *mockLocalizer_FormatCount_Call) Run(run func(_a0 string, _a1 int, _a2 map[string]interface{})) *mockLocalizer_FormatCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(int), args[2].(map[string]interface{})) + }) + return _c +} + +func (_c *mockLocalizer_FormatCount_Call) Return(_a0 string) *mockLocalizer_FormatCount_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_FormatCount_Call) RunAndReturn(run func(string, int, map[string]interface{}) string) *mockLocalizer_FormatCount_Call { + _c.Call.Return(run) + return _c +} + +// FormatDate provides a mock function with given fields: _a0 +func (_m *mockLocalizer) FormatDate(_a0 date.TimeOrDate) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for FormatDate") + } + + var r0 string + if rf, ok := ret.Get(0).(func(date.TimeOrDate) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_FormatDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatDate' +type mockLocalizer_FormatDate_Call struct { + *mock.Call +} + +// FormatDate is a helper method to define mock.On call +// - _a0 date.TimeOrDate +func (_e *mockLocalizer_Expecter) FormatDate(_a0 interface{}) *mockLocalizer_FormatDate_Call { + return &mockLocalizer_FormatDate_Call{Call: _e.mock.On("FormatDate", _a0)} +} + +func (_c *mockLocalizer_FormatDate_Call) Run(run func(_a0 date.TimeOrDate)) *mockLocalizer_FormatDate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(date.TimeOrDate)) + }) + return _c +} + +func (_c *mockLocalizer_FormatDate_Call) Return(_a0 string) *mockLocalizer_FormatDate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_FormatDate_Call) RunAndReturn(run func(date.TimeOrDate) string) *mockLocalizer_FormatDate_Call { + _c.Call.Return(run) + return _c +} + +// FormatDateTime provides a mock function with given fields: _a0 +func (_m *mockLocalizer) FormatDateTime(_a0 time.Time) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for FormatDateTime") + } + + var r0 string + if rf, ok := ret.Get(0).(func(time.Time) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_FormatDateTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatDateTime' +type mockLocalizer_FormatDateTime_Call struct { + *mock.Call +} + +// FormatDateTime is a helper method to define mock.On call +// - _a0 time.Time +func (_e *mockLocalizer_Expecter) FormatDateTime(_a0 interface{}) *mockLocalizer_FormatDateTime_Call { + return &mockLocalizer_FormatDateTime_Call{Call: _e.mock.On("FormatDateTime", _a0)} +} + +func (_c *mockLocalizer_FormatDateTime_Call) Run(run func(_a0 time.Time)) *mockLocalizer_FormatDateTime_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockLocalizer_FormatDateTime_Call) Return(_a0 string) *mockLocalizer_FormatDateTime_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_FormatDateTime_Call) RunAndReturn(run func(time.Time) string) *mockLocalizer_FormatDateTime_Call { + _c.Call.Return(run) + return _c +} + +// FormatTime provides a mock function with given fields: _a0 +func (_m *mockLocalizer) FormatTime(_a0 time.Time) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for FormatTime") + } + + var r0 string + if rf, ok := ret.Get(0).(func(time.Time) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_FormatTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatTime' +type mockLocalizer_FormatTime_Call struct { + *mock.Call +} + +// FormatTime is a helper method to define mock.On call +// - _a0 time.Time +func (_e *mockLocalizer_Expecter) FormatTime(_a0 interface{}) *mockLocalizer_FormatTime_Call { + return &mockLocalizer_FormatTime_Call{Call: _e.mock.On("FormatTime", _a0)} +} + +func (_c *mockLocalizer_FormatTime_Call) Run(run func(_a0 time.Time)) *mockLocalizer_FormatTime_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Time)) + }) + return _c +} + +func (_c *mockLocalizer_FormatTime_Call) Return(_a0 string) *mockLocalizer_FormatTime_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_FormatTime_Call) RunAndReturn(run func(time.Time) string) *mockLocalizer_FormatTime_Call { + _c.Call.Return(run) + return _c +} + +// Possessive provides a mock function with given fields: _a0 +func (_m *mockLocalizer) Possessive(_a0 string) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Possessive") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_Possessive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Possessive' +type mockLocalizer_Possessive_Call struct { + *mock.Call +} + +// Possessive is a helper method to define mock.On call +// - _a0 string +func (_e *mockLocalizer_Expecter) Possessive(_a0 interface{}) *mockLocalizer_Possessive_Call { + return &mockLocalizer_Possessive_Call{Call: _e.mock.On("Possessive", _a0)} +} + +func (_c *mockLocalizer_Possessive_Call) Run(run func(_a0 string)) *mockLocalizer_Possessive_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *mockLocalizer_Possessive_Call) Return(_a0 string) *mockLocalizer_Possessive_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_Possessive_Call) RunAndReturn(run func(string) string) *mockLocalizer_Possessive_Call { + _c.Call.Return(run) + return _c +} + +// SetShowTranslationKeys provides a mock function with given fields: _a0 +func (_m *mockLocalizer) SetShowTranslationKeys(_a0 bool) { + _m.Called(_a0) +} + +// mockLocalizer_SetShowTranslationKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetShowTranslationKeys' +type mockLocalizer_SetShowTranslationKeys_Call struct { + *mock.Call +} + +// SetShowTranslationKeys is a helper method to define mock.On call +// - _a0 bool +func (_e *mockLocalizer_Expecter) SetShowTranslationKeys(_a0 interface{}) *mockLocalizer_SetShowTranslationKeys_Call { + return &mockLocalizer_SetShowTranslationKeys_Call{Call: _e.mock.On("SetShowTranslationKeys", _a0)} +} + +func (_c *mockLocalizer_SetShowTranslationKeys_Call) Run(run func(_a0 bool)) *mockLocalizer_SetShowTranslationKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *mockLocalizer_SetShowTranslationKeys_Call) Return() *mockLocalizer_SetShowTranslationKeys_Call { + _c.Call.Return() + return _c +} + +func (_c *mockLocalizer_SetShowTranslationKeys_Call) RunAndReturn(run func(bool)) *mockLocalizer_SetShowTranslationKeys_Call { + _c.Call.Return(run) + return _c +} + +// ShowTranslationKeys provides a mock function with given fields: +func (_m *mockLocalizer) ShowTranslationKeys() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ShowTranslationKeys") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// mockLocalizer_ShowTranslationKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ShowTranslationKeys' +type mockLocalizer_ShowTranslationKeys_Call struct { + *mock.Call +} + +// ShowTranslationKeys is a helper method to define mock.On call +func (_e *mockLocalizer_Expecter) ShowTranslationKeys() *mockLocalizer_ShowTranslationKeys_Call { + return &mockLocalizer_ShowTranslationKeys_Call{Call: _e.mock.On("ShowTranslationKeys")} +} + +func (_c *mockLocalizer_ShowTranslationKeys_Call) Run(run func()) *mockLocalizer_ShowTranslationKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockLocalizer_ShowTranslationKeys_Call) Return(_a0 bool) *mockLocalizer_ShowTranslationKeys_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_ShowTranslationKeys_Call) RunAndReturn(run func() bool) *mockLocalizer_ShowTranslationKeys_Call { + _c.Call.Return(run) + return _c +} + +// T provides a mock function with given fields: _a0 +func (_m *mockLocalizer) T(_a0 string) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for T") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockLocalizer_T_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'T' +type mockLocalizer_T_Call struct { + *mock.Call +} + +// T is a helper method to define mock.On call +// - _a0 string +func (_e *mockLocalizer_Expecter) T(_a0 interface{}) *mockLocalizer_T_Call { + return &mockLocalizer_T_Call{Call: _e.mock.On("T", _a0)} +} + +func (_c *mockLocalizer_T_Call) Run(run func(_a0 string)) *mockLocalizer_T_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *mockLocalizer_T_Call) Return(_a0 string) *mockLocalizer_T_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLocalizer_T_Call) RunAndReturn(run func(string) string) *mockLocalizer_T_Call { + _c.Call.Return(run) + return _c +} + +// newMockLocalizer creates a new instance of mockLocalizer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockLocalizer(t interface { + mock.TestingT + Cleanup(func()) +}) *mockLocalizer { + mock := &mockLocalizer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/page/attorney/mock_LpaStoreClient_test.go b/internal/page/attorney/mock_LpaStoreClient_test.go index 176d811836..ae824dac22 100644 --- a/internal/page/attorney/mock_LpaStoreClient_test.go +++ b/internal/page/attorney/mock_LpaStoreClient_test.go @@ -3,9 +3,10 @@ package attorney import ( - context "context" - actor "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + actoruid "github.com/ministryofjustice/opg-modernising-lpa/internal/actor/actoruid" + + context "context" lpastore "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" @@ -73,6 +74,54 @@ func (_c *mockLpaStoreClient_SendAttorney_Call) RunAndReturn(run func(context.Co return _c } +// SendAttorneyOptOut provides a mock function with given fields: ctx, lpaUID, actorUID +func (_m *mockLpaStoreClient) SendAttorneyOptOut(ctx context.Context, lpaUID string, actorUID actoruid.UID) error { + ret := _m.Called(ctx, lpaUID, actorUID) + + if len(ret) == 0 { + panic("no return value specified for SendAttorneyOptOut") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, actoruid.UID) error); ok { + r0 = rf(ctx, lpaUID, actorUID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockLpaStoreClient_SendAttorneyOptOut_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendAttorneyOptOut' +type mockLpaStoreClient_SendAttorneyOptOut_Call struct { + *mock.Call +} + +// SendAttorneyOptOut is a helper method to define mock.On call +// - ctx context.Context +// - lpaUID string +// - actorUID actoruid.UID +func (_e *mockLpaStoreClient_Expecter) SendAttorneyOptOut(ctx interface{}, lpaUID interface{}, actorUID interface{}) *mockLpaStoreClient_SendAttorneyOptOut_Call { + return &mockLpaStoreClient_SendAttorneyOptOut_Call{Call: _e.mock.On("SendAttorneyOptOut", ctx, lpaUID, actorUID)} +} + +func (_c *mockLpaStoreClient_SendAttorneyOptOut_Call) Run(run func(ctx context.Context, lpaUID string, actorUID actoruid.UID)) *mockLpaStoreClient_SendAttorneyOptOut_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(actoruid.UID)) + }) + return _c +} + +func (_c *mockLpaStoreClient_SendAttorneyOptOut_Call) Return(_a0 error) *mockLpaStoreClient_SendAttorneyOptOut_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockLpaStoreClient_SendAttorneyOptOut_Call) RunAndReturn(run func(context.Context, string, actoruid.UID) error) *mockLpaStoreClient_SendAttorneyOptOut_Call { + _c.Call.Return(run) + return _c +} + // newMockLpaStoreClient creates a new instance of mockLpaStoreClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockLpaStoreClient(t interface { diff --git a/internal/page/attorney/mock_NotifyClient_test.go b/internal/page/attorney/mock_NotifyClient_test.go new file mode 100644 index 0000000000..88f88489d9 --- /dev/null +++ b/internal/page/attorney/mock_NotifyClient_test.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package attorney + +import ( + context "context" + + notify "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + mock "github.com/stretchr/testify/mock" +) + +// mockNotifyClient is an autogenerated mock type for the NotifyClient type +type mockNotifyClient struct { + mock.Mock +} + +type mockNotifyClient_Expecter struct { + mock *mock.Mock +} + +func (_m *mockNotifyClient) EXPECT() *mockNotifyClient_Expecter { + return &mockNotifyClient_Expecter{mock: &_m.Mock} +} + +// SendActorEmail provides a mock function with given fields: ctx, to, lpaUID, email +func (_m *mockNotifyClient) SendActorEmail(ctx context.Context, to string, lpaUID string, email notify.Email) error { + ret := _m.Called(ctx, to, lpaUID, email) + + if len(ret) == 0 { + panic("no return value specified for SendActorEmail") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, notify.Email) error); ok { + r0 = rf(ctx, to, lpaUID, email) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockNotifyClient_SendActorEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendActorEmail' +type mockNotifyClient_SendActorEmail_Call struct { + *mock.Call +} + +// SendActorEmail is a helper method to define mock.On call +// - ctx context.Context +// - to string +// - lpaUID string +// - email notify.Email +func (_e *mockNotifyClient_Expecter) SendActorEmail(ctx interface{}, to interface{}, lpaUID interface{}, email interface{}) *mockNotifyClient_SendActorEmail_Call { + return &mockNotifyClient_SendActorEmail_Call{Call: _e.mock.On("SendActorEmail", ctx, to, lpaUID, email)} +} + +func (_c *mockNotifyClient_SendActorEmail_Call) Run(run func(ctx context.Context, to string, lpaUID string, email notify.Email)) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(notify.Email)) + }) + return _c +} + +func (_c *mockNotifyClient_SendActorEmail_Call) Return(_a0 error) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockNotifyClient_SendActorEmail_Call) RunAndReturn(run func(context.Context, string, string, notify.Email) error) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Return(run) + return _c +} + +// newMockNotifyClient creates a new instance of mockNotifyClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockNotifyClient(t interface { + mock.TestingT + Cleanup(func()) +}) *mockNotifyClient { + mock := &mockNotifyClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/page/attorney/mock_SessionStore_test.go b/internal/page/attorney/mock_SessionStore_test.go index 0721f6a6a5..d1506eae91 100644 --- a/internal/page/attorney/mock_SessionStore_test.go +++ b/internal/page/attorney/mock_SessionStore_test.go @@ -80,6 +80,64 @@ func (_c *mockSessionStore_Login_Call) RunAndReturn(run func(*http.Request) (*se return _c } +// LpaData provides a mock function with given fields: r +func (_m *mockSessionStore) LpaData(r *http.Request) (*sesh.LpaDataSession, error) { + ret := _m.Called(r) + + if len(ret) == 0 { + panic("no return value specified for LpaData") + } + + var r0 *sesh.LpaDataSession + var r1 error + if rf, ok := ret.Get(0).(func(*http.Request) (*sesh.LpaDataSession, error)); ok { + return rf(r) + } + if rf, ok := ret.Get(0).(func(*http.Request) *sesh.LpaDataSession); ok { + r0 = rf(r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sesh.LpaDataSession) + } + } + + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockSessionStore_LpaData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LpaData' +type mockSessionStore_LpaData_Call struct { + *mock.Call +} + +// LpaData is a helper method to define mock.On call +// - r *http.Request +func (_e *mockSessionStore_Expecter) LpaData(r interface{}) *mockSessionStore_LpaData_Call { + return &mockSessionStore_LpaData_Call{Call: _e.mock.On("LpaData", r)} +} + +func (_c *mockSessionStore_LpaData_Call) Run(run func(r *http.Request)) *mockSessionStore_LpaData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*http.Request)) + }) + return _c +} + +func (_c *mockSessionStore_LpaData_Call) Return(_a0 *sesh.LpaDataSession, _a1 error) *mockSessionStore_LpaData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockSessionStore_LpaData_Call) RunAndReturn(run func(*http.Request) (*sesh.LpaDataSession, error)) *mockSessionStore_LpaData_Call { + _c.Call.Return(run) + return _c +} + // OneLogin provides a mock function with given fields: r func (_m *mockSessionStore) OneLogin(r *http.Request) (*sesh.OneLoginSession, error) { ret := _m.Called(r) @@ -186,6 +244,54 @@ func (_c *mockSessionStore_SetLogin_Call) RunAndReturn(run func(*http.Request, h return _c } +// SetLpaData provides a mock function with given fields: r, w, lpaDataSession +func (_m *mockSessionStore) SetLpaData(r *http.Request, w http.ResponseWriter, lpaDataSession *sesh.LpaDataSession) error { + ret := _m.Called(r, w, lpaDataSession) + + if len(ret) == 0 { + panic("no return value specified for SetLpaData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*http.Request, http.ResponseWriter, *sesh.LpaDataSession) error); ok { + r0 = rf(r, w, lpaDataSession) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockSessionStore_SetLpaData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLpaData' +type mockSessionStore_SetLpaData_Call struct { + *mock.Call +} + +// SetLpaData is a helper method to define mock.On call +// - r *http.Request +// - w http.ResponseWriter +// - lpaDataSession *sesh.LpaDataSession +func (_e *mockSessionStore_Expecter) SetLpaData(r interface{}, w interface{}, lpaDataSession interface{}) *mockSessionStore_SetLpaData_Call { + return &mockSessionStore_SetLpaData_Call{Call: _e.mock.On("SetLpaData", r, w, lpaDataSession)} +} + +func (_c *mockSessionStore_SetLpaData_Call) Run(run func(r *http.Request, w http.ResponseWriter, lpaDataSession *sesh.LpaDataSession)) *mockSessionStore_SetLpaData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*http.Request), args[1].(http.ResponseWriter), args[2].(*sesh.LpaDataSession)) + }) + return _c +} + +func (_c *mockSessionStore_SetLpaData_Call) Return(_a0 error) *mockSessionStore_SetLpaData_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockSessionStore_SetLpaData_Call) RunAndReturn(run func(*http.Request, http.ResponseWriter, *sesh.LpaDataSession) error) *mockSessionStore_SetLpaData_Call { + _c.Call.Return(run) + return _c +} + // SetOneLogin provides a mock function with given fields: r, w, session func (_m *mockSessionStore) SetOneLogin(r *http.Request, w http.ResponseWriter, session *sesh.OneLoginSession) error { ret := _m.Called(r, w, session) diff --git a/internal/page/attorney/mock_test.go b/internal/page/attorney/mock_test.go index e46b515152..3ce93a7755 100644 --- a/internal/page/attorney/mock_test.go +++ b/internal/page/attorney/mock_test.go @@ -2,6 +2,7 @@ package attorney import ( "errors" + "testing" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor/actoruid" @@ -41,3 +42,11 @@ var ( ActorType: actor.TypeReplacementTrustCorporation, } ) + +func evalT[T any](fn func(*testing.T) T, t *testing.T) T { + if fn == nil { + return *new(T) + } + + return fn(t) +} diff --git a/internal/page/attorney/register.go b/internal/page/attorney/register.go index b3e8358227..eca50e8c7f 100644 --- a/internal/page/attorney/register.go +++ b/internal/page/attorney/register.go @@ -9,6 +9,7 @@ import ( "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/place" @@ -16,6 +17,10 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" ) +type Localizer interface { + page.Localizer +} + type LpaStoreResolvingService interface { Get(ctx context.Context) (*lpastore.Lpa, error) } @@ -32,8 +37,10 @@ type Logger interface { type SessionStore interface { Login(r *http.Request) (*sesh.LoginSession, error) + LpaData(r *http.Request) (*sesh.LpaDataSession, error) OneLogin(r *http.Request) (*sesh.OneLoginSession, error) SetLogin(r *http.Request, w http.ResponseWriter, session *sesh.LoginSession) error + SetLpaData(r *http.Request, w http.ResponseWriter, lpaDataSession *sesh.LpaDataSession) error SetOneLogin(r *http.Request, w http.ResponseWriter, session *sesh.OneLoginSession) error } @@ -53,6 +60,7 @@ type AttorneyStore interface { Create(ctx context.Context, shareCode actor.ShareCodeData, email string) (*actor.AttorneyProvidedDetails, error) Get(ctx context.Context) (*actor.AttorneyProvidedDetails, error) Put(ctx context.Context, attorney *actor.AttorneyProvidedDetails) error + Delete(ctx context.Context) error } type AddressClient interface { @@ -68,6 +76,10 @@ type LpaStoreClient interface { SendAttorney(context.Context, *lpastore.Lpa, *actor.AttorneyProvidedDetails) error } +type NotifyClient interface { + SendActorEmail(ctx context.Context, to, lpaUID string, email notify.Email) error +} + type ErrorHandler func(http.ResponseWriter, *http.Request, error) func Register( @@ -82,6 +94,8 @@ func Register( dashboardStore DashboardStore, lpaStoreClient LpaStoreClient, lpaStoreResolvingService LpaStoreResolvingService, + notifyClient NotifyClient, + appPublicURL string, ) { handleRoot := makeHandle(rootMux, sessionStore, errorHandler) @@ -91,6 +105,12 @@ func Register( page.LoginCallback(logger, oneLoginClient, sessionStore, page.Paths.Attorney.EnterReferenceNumber, dashboardStore, actor.TypeAttorney)) handleRoot(page.Paths.Attorney.EnterReferenceNumber, RequireSession, EnterReferenceNumber(tmpls.Get("enter_reference_number.gohtml"), shareCodeStore, sessionStore, attorneyStore)) + handleRoot(page.Paths.Attorney.EnterReferenceNumberOptOut, None, + EnterReferenceNumberOptOut(tmpls.Get("enter_reference_number_opt_out.gohtml"), shareCodeStore, sessionStore)) + handleRoot(page.Paths.Attorney.ConfirmDontWantToBeAttorneyLoggedOut, None, + ConfirmDontWantToBeAttorneyLoggedOut(tmpls.Get("confirm_dont_want_to_be_attorney.gohtml"), shareCodeStore, lpaStoreResolvingService, sessionStore, notifyClient, appPublicURL)) + handleRoot(page.Paths.Attorney.YouHaveDecidedNotToBeAttorney, None, + page.Guidance(tmpls.Get("you_have_decided_not_to_be_attorney.gohtml"))) handleAttorney := makeAttorneyHandle(rootMux, sessionStore, errorHandler, attorneyStore) @@ -118,6 +138,9 @@ func Register( Guidance(tmpls.Get("what_happens_next.gohtml"), lpaStoreResolvingService)) handleAttorney(page.Paths.Attorney.Progress, None, Progress(tmpls.Get("progress.gohtml"), lpaStoreResolvingService)) + + handleAttorney(page.Paths.Attorney.ConfirmDontWantToBeAttorney, CanGoBack, + ConfirmDontWantToBeAttorney(tmpls.Get("confirm_dont_want_to_be_attorney.gohtml"), lpaStoreResolvingService, attorneyStore, notifyClient, appPublicURL)) } type handleOpt byte diff --git a/internal/page/attorney/register_test.go b/internal/page/attorney/register_test.go index 29bb8c9abe..86a05c2ee0 100644 --- a/internal/page/attorney/register_test.go +++ b/internal/page/attorney/register_test.go @@ -18,7 +18,7 @@ import ( func TestRegister(t *testing.T) { mux := http.NewServeMux() - Register(mux, &mockLogger{}, template.Templates{}, template.Templates{}, nil, nil, nil, nil, nil, &mockDashboardStore{}, &lpastore.Client{}, &lpastore.ResolvingService{}) + Register(mux, &mockLogger{}, template.Templates{}, template.Templates{}, nil, nil, nil, nil, nil, &mockDashboardStore{}, &lpastore.Client{}, &lpastore.ResolvingService{}, &mockNotifyClient{}, "http://app") assert.Implements(t, (*http.Handler)(nil), mux) } diff --git a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider.go b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider.go index cc37b6b1cb..544b8f8e1f 100644 --- a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider.go +++ b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider.go @@ -79,7 +79,7 @@ func ConfirmDontWantToBeCertificateProvider(tmpl template.Template, lpaStoreReso return err } - return page.Paths.CertificateProvider.YouHaveDecidedNotToBeACertificateProvider.RedirectQuery(w, r, appData, url.Values{"donorFullName": {lpa.Donor.FullName()}}) + return page.Paths.CertificateProvider.YouHaveDecidedNotToBeCertificateProvider.RedirectQuery(w, r, appData, url.Values{"donorFullName": {lpa.Donor.FullName()}}) } return tmpl(w, data) diff --git a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out.go b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out.go index f9238a117e..75027317b7 100644 --- a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out.go +++ b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out.go @@ -39,6 +39,11 @@ func ConfirmDontWantToBeCertificateProviderLoggedOut(tmpl template.Template, sha } if r.Method == http.MethodPost { + shareCode, err := shareCodeStore.Get(r.Context(), actor.TypeCertificateProvider, r.URL.Query().Get("referenceNumber")) + if err != nil { + return err + } + var email notify.Email if !lpa.SignedAt.IsZero() { @@ -79,8 +84,7 @@ func ConfirmDontWantToBeCertificateProviderLoggedOut(tmpl template.Template, sha } } - shareCode, err := shareCodeStore.Get(r.Context(), actor.TypeCertificateProvider, r.URL.Query().Get("referenceNumber")) - if err != nil { + if err := notifyClient.SendActorEmail(ctx, lpa.Donor.Email, lpa.LpaUID, email); err != nil { return err } @@ -88,11 +92,7 @@ func ConfirmDontWantToBeCertificateProviderLoggedOut(tmpl template.Template, sha return err } - if err := notifyClient.SendActorEmail(ctx, lpa.Donor.Email, lpa.LpaUID, email); err != nil { - return err - } - - return page.Paths.CertificateProvider.YouHaveDecidedNotToBeACertificateProvider.RedirectQuery(w, r, appData, url.Values{"donorFullName": {lpa.Donor.FullName()}}) + return page.Paths.CertificateProvider.YouHaveDecidedNotToBeCertificateProvider.RedirectQuery(w, r, appData, url.Values{"donorFullName": {lpa.Donor.FullName()}}) } return tmpl(w, data) diff --git a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out_test.go b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out_test.go index abed989adc..850288c90d 100644 --- a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out_test.go +++ b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_logged_out_test.go @@ -286,7 +286,7 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOut(t *testing.T) { resp := w.Result() assert.Nil(t, err) - assert.Equal(t, page.Paths.CertificateProvider.YouHaveDecidedNotToBeACertificateProvider.Format()+"?donorFullName=a+b+c", resp.Header.Get("Location")) + assert.Equal(t, page.Paths.CertificateProvider.YouHaveDecidedNotToBeCertificateProvider.Format()+"?donorFullName=a+b+c", resp.Header.Get("Location")) assert.Equal(t, http.StatusFound, resp.StatusCode) }) } @@ -341,10 +341,17 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) return lpaStoreClient }, - shareCodeStore: func() *mockShareCodeStore { return nil }, - donorStore: func() *mockDonorStore { return nil }, - localizer: func() *mockLocalizer { return localizer }, - notifyClient: func() *mockNotifyClient { return nil }, + shareCodeStore: func() *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, nil) + + return shareCodeStore + }, + donorStore: func() *mockDonorStore { return nil }, + localizer: func() *mockLocalizer { return localizer }, + notifyClient: func() *mockNotifyClient { return nil }, }, "when donorStore.GetAny() error": { sessionStore: func() *mockSessionStore { @@ -364,7 +371,14 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) return lpaStoreResolvingService }, lpaStoreClient: func() *mockLpaStoreClient { return nil }, - shareCodeStore: func() *mockShareCodeStore { return nil }, + shareCodeStore: func() *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, nil) + + return shareCodeStore + }, donorStore: func() *mockDonorStore { donorStore := newMockDonorStore(t) donorStore.EXPECT(). @@ -394,7 +408,14 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) return lpaStoreResolvingService }, lpaStoreClient: func() *mockLpaStoreClient { return nil }, - shareCodeStore: func() *mockShareCodeStore { return nil }, + shareCodeStore: func() *mockShareCodeStore { + shareCodeStore := newMockShareCodeStore(t) + shareCodeStore.EXPECT(). + Get(mock.Anything, mock.Anything, mock.Anything). + Return(shareCodeData, nil) + + return shareCodeStore + }, donorStore: func() *mockDonorStore { donorStore := newMockDonorStore(t) donorStore.EXPECT(). @@ -426,14 +447,7 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) return lpaStoreResolvingService }, - lpaStoreClient: func() *mockLpaStoreClient { - lpaStoreClient := newMockLpaStoreClient(t) - lpaStoreClient.EXPECT(). - SendCertificateProviderOptOut(mock.Anything, mock.Anything, mock.Anything). - Return(nil) - - return lpaStoreClient - }, + lpaStoreClient: func() *mockLpaStoreClient { return nil }, shareCodeStore: func() *mockShareCodeStore { shareCodeStore := newMockShareCodeStore(t) shareCodeStore.EXPECT(). @@ -482,9 +496,16 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) return shareCodeStore }, - donorStore: func() *mockDonorStore { return nil }, - localizer: func() *mockLocalizer { return localizer }, - notifyClient: func() *mockNotifyClient { return nil }, + donorStore: func() *mockDonorStore { return nil }, + localizer: func() *mockLocalizer { return localizer }, + notifyClient: func() *mockNotifyClient { + client := newMockNotifyClient(t) + client.EXPECT(). + SendActorEmail(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + + return client + }, }, "when notifyClient.SendActorEmail() error": { sessionStore: func() *mockSessionStore { @@ -516,9 +537,6 @@ func TestPostConfirmDontWantToBeCertificateProviderLoggedOutErrors(t *testing.T) shareCodeStore.EXPECT(). Get(mock.Anything, mock.Anything, mock.Anything). Return(shareCodeData, nil) - shareCodeStore.EXPECT(). - Delete(mock.Anything, mock.Anything). - Return(nil) return shareCodeStore }, diff --git a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_test.go b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_test.go index 71793f8f2a..5497b012a5 100644 --- a/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_test.go +++ b/internal/page/certificateprovider/confirm_dont_want_to_be_certificate_provider_test.go @@ -239,7 +239,7 @@ func TestPostConfirmDontWantToBeCertificateProvider(t *testing.T) { resp := w.Result() assert.Nil(t, err) - assert.Equal(t, page.Paths.CertificateProvider.YouHaveDecidedNotToBeACertificateProvider.Format()+"?donorFullName=a+b+c", resp.Header.Get("Location")) + assert.Equal(t, page.Paths.CertificateProvider.YouHaveDecidedNotToBeCertificateProvider.Format()+"?donorFullName=a+b+c", resp.Header.Get("Location")) assert.Equal(t, http.StatusFound, resp.StatusCode) }) } diff --git a/internal/page/certificateprovider/enter_reference_number_opt_out.go b/internal/page/certificateprovider/enter_reference_number_opt_out.go index dcb2af9a28..3de5276df7 100644 --- a/internal/page/certificateprovider/enter_reference_number_opt_out.go +++ b/internal/page/certificateprovider/enter_reference_number_opt_out.go @@ -24,7 +24,7 @@ func EnterReferenceNumberOptOut(tmpl template.Template, shareCodeStore ShareCode data.Form = readEnterReferenceNumberForm(r) data.Errors = data.Form.Validate() - if len(data.Errors) == 0 { + if data.Errors.None() { referenceNumber := data.Form.ReferenceNumber shareCode, err := shareCodeStore.Get(r.Context(), actor.TypeCertificateProvider, referenceNumber) diff --git a/internal/page/certificateprovider/register.go b/internal/page/certificateprovider/register.go index a65cc38d88..b9343ed07c 100644 --- a/internal/page/certificateprovider/register.go +++ b/internal/page/certificateprovider/register.go @@ -125,8 +125,8 @@ func Register( EnterReferenceNumberOptOut(tmpls.Get("enter_reference_number_opt_out.gohtml"), shareCodeStore, sessionStore)) handleRoot(page.Paths.CertificateProvider.ConfirmDontWantToBeCertificateProviderLoggedOut, ConfirmDontWantToBeCertificateProviderLoggedOut(tmpls.Get("confirm_dont_want_to_be_certificate_provider.gohtml"), shareCodeStore, lpaStoreResolvingService, lpaStoreClient, donorStore, sessionStore, notifyClient, appPublicURL)) - handleRoot(page.Paths.CertificateProvider.YouHaveDecidedNotToBeACertificateProvider, - YouHaveDecidedNotToBeACertificateProvider(tmpls.Get("you_have_decided_not_to_be_a_certificate_provider.gohtml"))) + handleRoot(page.Paths.CertificateProvider.YouHaveDecidedNotToBeCertificateProvider, + Guidance(tmpls.Get("you_have_decided_not_to_be_a_certificate_provider.gohtml"), nil, nil)) handleCertificateProvider := makeCertificateProviderHandle(rootMux, sessionStore, errorHandler, certificateProviderStore) diff --git a/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.go b/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.go deleted file mode 100644 index a1dfcf36f3..0000000000 --- a/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.go +++ /dev/null @@ -1,24 +0,0 @@ -package certificateprovider - -import ( - "net/http" - - "github.com/ministryofjustice/opg-go-common/template" - "github.com/ministryofjustice/opg-modernising-lpa/internal/page" - "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" -) - -type youHaveDecidedNotToBeACertificateProviderData struct { - App page.AppData - Errors validation.List - DonorFullName string -} - -func YouHaveDecidedNotToBeACertificateProvider(tmpl template.Template) page.Handler { - return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { - return tmpl(w, youHaveDecidedNotToBeACertificateProviderData{ - App: appData, - DonorFullName: r.URL.Query().Get("donorFullName"), - }) - } -} diff --git a/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider_test.go b/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider_test.go deleted file mode 100644 index d2bb313c23..0000000000 --- a/internal/page/certificateprovider/you_have_decided_not_to_be_a_certificate_provider_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package certificateprovider - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestYouHaveDecidedNotToBeACertificateProvider(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/?donorFullName=a+b+c", nil) - - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, youHaveDecidedNotToBeACertificateProviderData{ - App: testAppData, - DonorFullName: "a b c", - }). - Return(nil) - - err := YouHaveDecidedNotToBeACertificateProvider(template.Execute)(testAppData, w, r) - - resp := w.Result() - - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestYouHaveDecidedNotToBeACertificateProviderWhenTemplateError(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/?donorFullName=a+b+c", nil) - - template := newMockTemplate(t) - template.EXPECT(). - Execute(mock.Anything, mock.Anything). - Return(expectedError) - - err := YouHaveDecidedNotToBeACertificateProvider(template.Execute)(testAppData, w, r) - - resp := w.Result() - - assert.Equal(t, expectedError, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} diff --git a/internal/page/paths.go b/internal/page/paths.go index 636e7473cc..4eeab84a75 100644 --- a/internal/page/paths.go +++ b/internal/page/paths.go @@ -237,23 +237,27 @@ func (p SupporterLpaPath) IsManageOrganisation() bool { } type AttorneyPaths struct { - EnterReferenceNumber Path - Login Path - LoginCallback Path - Start Path - - CodeOfConduct AttorneyPath - ConfirmYourDetails AttorneyPath - MobileNumber AttorneyPath - Progress AttorneyPath - ReadTheLpa AttorneyPath - RightsAndResponsibilities AttorneyPath - Sign AttorneyPath - TaskList AttorneyPath - WhatHappensNext AttorneyPath - WhatHappensWhenYouSign AttorneyPath - WouldLikeSecondSignatory AttorneyPath - YourPreferredLanguage AttorneyPath + ConfirmDontWantToBeAttorneyLoggedOut Path + EnterReferenceNumber Path + EnterReferenceNumberOptOut Path + Login Path + LoginCallback Path + Start Path + YouHaveDecidedNotToBeAttorney Path + + CodeOfConduct AttorneyPath + ConfirmDontWantToBeAttorney AttorneyPath + ConfirmYourDetails AttorneyPath + MobileNumber AttorneyPath + Progress AttorneyPath + ReadTheLpa AttorneyPath + RightsAndResponsibilities AttorneyPath + Sign AttorneyPath + TaskList AttorneyPath + WhatHappensNext AttorneyPath + WhatHappensWhenYouSign AttorneyPath + WouldLikeSecondSignatory AttorneyPath + YourPreferredLanguage AttorneyPath } type CertificateProviderPaths struct { @@ -262,7 +266,7 @@ type CertificateProviderPaths struct { EnterReferenceNumber Path EnterReferenceNumberOptOut Path ConfirmDontWantToBeCertificateProviderLoggedOut Path - YouHaveDecidedNotToBeACertificateProvider Path + YouHaveDecidedNotToBeCertificateProvider Path CertificateProvided CertificateProviderPath ConfirmDontWantToBeCertificateProvider CertificateProviderPath @@ -475,28 +479,32 @@ var Paths = AppPaths{ WhatHappensNext: "/what-happens-next", WhatIsYourHomeAddress: "/what-is-your-home-address", WhoIsEligible: "/certificate-provider-who-is-eligible", - YouHaveDecidedNotToBeACertificateProvider: "/you-have-decided-not-to-be-a-certificate-provider", + YouHaveDecidedNotToBeCertificateProvider: "/you-have-decided-not-to-be-a-certificate-provider", YourPreferredLanguage: "/your-preferred-language", YourRole: "/your-role", }, Attorney: AttorneyPaths{ - CodeOfConduct: "/code-of-conduct", - ConfirmYourDetails: "/confirm-your-details", - EnterReferenceNumber: "/attorney-enter-reference-number", - Login: "/attorney-login", - LoginCallback: "/attorney-login-callback", - MobileNumber: "/mobile-number", - Progress: "/progress", - ReadTheLpa: "/read-the-lpa", - RightsAndResponsibilities: "/legal-rights-and-responsibilities", - Sign: "/sign", - Start: "/attorney-start", - TaskList: "/task-list", - WhatHappensNext: "/what-happens-next", - WhatHappensWhenYouSign: "/what-happens-when-you-sign-the-lpa", - WouldLikeSecondSignatory: "/would-like-second-signatory", - YourPreferredLanguage: "/your-preferred-language", + CodeOfConduct: "/code-of-conduct", + ConfirmDontWantToBeAttorney: "/confirm-you-do-not-want-to-be-an-attorney", + ConfirmDontWantToBeAttorneyLoggedOut: "/confirm-you-do-not-want-to-be-an-attorney", + ConfirmYourDetails: "/confirm-your-details", + EnterReferenceNumber: "/attorney-enter-reference-number", + EnterReferenceNumberOptOut: "/attorney-enter-reference-number-opt-out", + Login: "/attorney-login", + LoginCallback: "/attorney-login-callback", + MobileNumber: "/mobile-number", + Progress: "/progress", + ReadTheLpa: "/read-the-lpa", + RightsAndResponsibilities: "/legal-rights-and-responsibilities", + Sign: "/sign", + Start: "/attorney-start", + TaskList: "/task-list", + WhatHappensNext: "/what-happens-next", + WhatHappensWhenYouSign: "/what-happens-when-you-sign-the-lpa", + WouldLikeSecondSignatory: "/would-like-second-signatory", + YouHaveDecidedNotToBeAttorney: "/you-have-decided-not-to-be-an-attorney", + YourPreferredLanguage: "/your-preferred-language", }, Supporter: SupporterPaths{ diff --git a/internal/page/share_code.go b/internal/page/share_code.go index d88b5dbd19..70640b979c 100644 --- a/internal/page/share_code.go +++ b/internal/page/share_code.go @@ -126,8 +126,9 @@ func (s *ShareCodeSender) sendOriginalAttorney(ctx context.Context, appData AppD DonorFirstNamesPossessive: appData.Localizer.Possessive(lpa.Donor.FirstNames), DonorFullName: lpa.Donor.FullName(), LpaType: localize.LowerFirst(appData.Localizer.T(lpa.Type.String())), - AttorneyStartPageURL: fmt.Sprintf("%s%s", s.appPublicURL, Paths.Attorney.Start), + AttorneyStartPageURL: s.appPublicURL + Paths.Attorney.Start.Format(), ShareCode: shareCode, + AttorneyOptOutURL: s.appPublicURL + Paths.Attorney.EnterReferenceNumberOptOut.Format(), }) } @@ -148,8 +149,9 @@ func (s *ShareCodeSender) sendReplacementAttorney(ctx context.Context, appData A DonorFirstNamesPossessive: appData.Localizer.Possessive(lpa.Donor.FirstNames), DonorFullName: lpa.Donor.FullName(), LpaType: localize.LowerFirst(appData.Localizer.T(lpa.Type.String())), - AttorneyStartPageURL: fmt.Sprintf("%s%s", s.appPublicURL, Paths.Attorney.Start), + AttorneyStartPageURL: s.appPublicURL + Paths.Attorney.Start.Format(), ShareCode: shareCode, + AttorneyOptOutURL: s.appPublicURL + Paths.Attorney.EnterReferenceNumberOptOut.Format(), }) } @@ -176,6 +178,7 @@ func (s *ShareCodeSender) sendTrustCorporation(ctx context.Context, appData AppD LpaType: localize.LowerFirst(appData.Localizer.T(lpa.Type.String())), AttorneyStartPageURL: fmt.Sprintf("%s%s", s.appPublicURL, Paths.Attorney.Start), ShareCode: shareCode, + AttorneyOptOutURL: s.appPublicURL + Paths.Attorney.EnterReferenceNumberOptOut.Format(), }) } @@ -202,6 +205,7 @@ func (s *ShareCodeSender) sendReplacementTrustCorporation(ctx context.Context, a LpaType: localize.LowerFirst(appData.Localizer.T(lpa.Type.String())), AttorneyStartPageURL: fmt.Sprintf("%s%s", s.appPublicURL, Paths.Attorney.Start), ShareCode: shareCode, + AttorneyOptOutURL: s.appPublicURL + Paths.Attorney.EnterReferenceNumberOptOut.Format(), }) } diff --git a/internal/page/share_code_test.go b/internal/page/share_code_test.go index d7d010a51b..1400c58357 100644 --- a/internal/page/share_code_test.go +++ b/internal/page/share_code_test.go @@ -682,6 +682,7 @@ func TestShareCodeSenderSendAttorneys(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) notifyClient.EXPECT(). @@ -693,6 +694,7 @@ func TestShareCodeSenderSendAttorneys(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) notifyClient.EXPECT(). @@ -704,6 +706,7 @@ func TestShareCodeSenderSendAttorneys(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) notifyClient.EXPECT(). @@ -715,6 +718,7 @@ func TestShareCodeSenderSendAttorneys(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) notifyClient.EXPECT(). @@ -726,6 +730,7 @@ func TestShareCodeSenderSendAttorneys(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) @@ -889,6 +894,7 @@ func TestShareCodeSenderSendAttorneysWithTestCode(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) notifyClient.EXPECT(). @@ -900,6 +906,7 @@ func TestShareCodeSenderSendAttorneysWithTestCode(t *testing.T) { DonorFirstNamesPossessive: "Jan's", LpaType: "property and affairs", AttorneyStartPageURL: fmt.Sprintf("http://app%s", Paths.Attorney.Start), + AttorneyOptOutURL: fmt.Sprintf("http://app%s", Paths.Attorney.EnterReferenceNumberOptOut), }). Return(nil) diff --git a/lang/cy.json b/lang/cy.json index 09057e907f..4d13a29ae8 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -554,7 +554,8 @@ "certificateProvidedConcerns": "<p class=\"govuk-body\">Os bydd unrhyw bryderon o gwbl gan Swyddfa’r Gwarcheidwad Cyhoeddus ynghylch LPA {{ .DonorFirstNamesPossessive }} yn y dyfodol, gallem gysylltu â chi.</p> <p class=\"govuk-body\">Os bydd angen i chi adrodd am bryder o gwbl, gallwch wneud hyn ar unrhyw adeg gan ddefnyddio’r <a href=\"/\" class=\"govuk-link\">gwasanaeth adrodd am bryder ynghylch LPA</a>.</p>", "goToYourDashboard": "Mynd i’ch dangosfwrdd", "beAnAttorney": "Bod yn atwrnai ar atwrneiaeth arhosol (LPA)", - "enterYourAttorneyReferenceNumber": "Rhowch eich cyfeirnod", + "enterYourReferenceNumber": "Rhowch eich cyfeirnod", + "enterYourAttorneyReferenceNumber": "Welsh", "attorneyReferenceNumberContent": "<p class=\"govuk-body\">Gallwch weld y cyfeirnod yn yr e-bost sy’n eich gwahodd i fod yn atwrnai neu’n atwrnai wrth gefn.</p>", "attorneyReferenceNumber": "Cyfeirnod", "attorney18OrOverWarning": "Rhaid i chi fod yn 18 oed neu hŷn i fod yn atwrnai.", @@ -1301,5 +1302,12 @@ "thisIsNotTheRegisteredLpaWarning": "Welsh", "howLpaCanBeUsed": "Welsh", "trustCorporationAttorney": "Welsh", - "replacementTrustCorporationAttorney": "Welsh" + "replacementTrustCorporationAttorney": "Welsh", + "iDoNotWantToBeAttorney": "Welsh", + "confirmYouDoNotWantToBeAttorney": "Welsh", + "youHaveToldUsYouDoNotWantToBeAttorneyOn": "Welsh {{.DonorFullNamePossessive}}", + "whenYouConfirmWeWillContactToExplain": "Welsh {{.DonorFullName}}", + "youHaveConfirmedYouDoNotWantToBeAttorney": "Welsh", + "youHaveConfirmedYouDoNotWantToBeDonorsAttorney": "Welsh {{.DonorFullNamePossessive}}", + "youHaveConfirmedYouDoNotWantToBeAttorneyContent": "<p class=\"govuk-body\">Welsh {{.DonorFirstNames}}</p>" } diff --git a/lang/en.json b/lang/en.json index bef7b3e9e4..e4bddca28c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -504,7 +504,8 @@ "certificateProvidedConcerns": "<p class=\"govuk-body\">If the Office of the Public Guardian ever has any concerns about {{ .DonorFirstNamesPossessive }} LPA in future, we may contact you.</p> <p class=\"govuk-body\">If you ever need to report a concern, you can do this at any time using the <a href=\"/\" class=\"govuk-link\">report a concern about an LPA service</a>.</p>", "goToYourDashboard": "Go to your dashboard", "beAnAttorney": "Be an attorney on a lasting power of attorney (LPA)", - "enterYourAttorneyReferenceNumber": "Enter your reference number", + "enterYourReferenceNumber": "Enter your reference number", + "enterYourAttorneyReferenceNumber": "Enter your attorney reference number", "attorneyReferenceNumberContent": "<p class=\"govuk-body\">You can find this in the email inviting you to be an attorney or a replacement attorney.</p>", "attorneyReferenceNumber": "Reference number", "attorney18OrOverWarning": "You must be aged 18 or over to be an attorney.", @@ -1230,5 +1231,12 @@ "thisIsNotTheRegisteredLpaWarning": "This is not the registered LPA. You cannot use it or give organisations access to it.", "howLpaCanBeUsed": "How the LPA can be used", "trustCorporationAttorney": "Trust corporation attorney", - "replacementTrustCorporationAttorney": "Replacement trust corporation attorney" + "replacementTrustCorporationAttorney": "Replacement trust corporation attorney", + "iDoNotWantToBeAttorney": "I do not want to be an attorney", + "confirmYouDoNotWantToBeAttorney": "Confirm you do not want to be an attorney", + "youHaveToldUsYouDoNotWantToBeAttorneyOn": "You have told us that you do not want to be an attorney on {{.DonorFullNamePossessive}} LPA:", + "whenYouConfirmWeWillContactToExplain": "When you confirm, we will contact {{.DonorFullName}} to explain what they can do next.", + "youHaveConfirmedYouDoNotWantToBeAttorney": "You have confirmed that you do not want to be an attorney", + "youHaveConfirmedYouDoNotWantToBeDonorsAttorney": "You have confirmed that you do not want to be {{.DonorFullNamePossessive}} attorney.", + "youHaveConfirmedYouDoNotWantToBeAttorneyContent": "<p class=\"govuk-body\">We have let {{.DonorFirstNames}} know about your decision.</p><p class=\"govuk-body\">You do not need to do anything else.</p>" } diff --git a/web/template/attorney/confirm_dont_want_to_be_attorney.gohtml b/web/template/attorney/confirm_dont_want_to_be_attorney.gohtml new file mode 100644 index 0000000000..009fa92b67 --- /dev/null +++ b/web/template/attorney/confirm_dont_want_to_be_attorney.gohtml @@ -0,0 +1,34 @@ +{{ template "page" . }} + +{{ define "pageTitle" }}{{ tr .App "confirmYouDoNotWantToBeAttorney" }}{{ end }} + +{{ define "main" }} + <div class="govuk-grid-row"> + <div class="govuk-grid-column-two-thirds"> + <h1 class="govuk-heading-xl">{{ tr .App "confirmYouDoNotWantToBeAttorney" }}</h1> + + <p class="govuk-body">{{ trFormat .App "youHaveToldUsYouDoNotWantToBeAttorneyOn" "DonorFullNamePossessive" (possessive .App .Lpa.Donor.FullName) }} + + <div class="govuk-inset-text"> + <dl class="govuk-summary-list govuk-summary-list--no-border app-summary-list--no-vertical-padding"> + <div class="govuk-summary-list__row"> + <dt class="govuk-summary-list__key">{{ tr .App "lpaType" }}:</dt> + <dd class="govuk-summary-list__value">{{ tr .App .Lpa.Type.String }}</dd> + </div> + <div class="govuk-summary-list__row"> + <dt class="govuk-summary-list__key">{{ tr .App "lpaNumber" }}:</dt> + <dd class="govuk-summary-list__value">{{ .Lpa.LpaUID }}</dd> + </div> + </dl> + </div> + + <p class="govuk-body">{{ trFormat .App "whenYouConfirmWeWillContactToExplain" "DonorFullName" .Lpa.Donor.FullName }} + + <form novalidate method="post"> + <button type="submit" class="govuk-button" data-module="govuk-button">{{ tr .App "confirm" }}</button> + + {{ template "csrf-field" . }} + </form> + </div> + </div> +{{ end }} diff --git a/web/template/attorney/enter_reference_number.gohtml b/web/template/attorney/enter_reference_number.gohtml index 9a00c5dd49..8d21782985 100644 --- a/web/template/attorney/enter_reference_number.gohtml +++ b/web/template/attorney/enter_reference_number.gohtml @@ -1,11 +1,11 @@ {{ template "page" . }} -{{ define "pageTitle" }}{{ tr .App "enterYourAttorneyReferenceNumber" }}{{ end }} +{{ define "pageTitle" }}{{ tr .App "enterYourReferenceNumber" }}{{ end }} {{ define "main" }} <div class="govuk-grid-row"> <div class="govuk-grid-column-two-thirds"> - <h1 class="govuk-heading-xl">{{ tr .App "enterYourAttorneyReferenceNumber" }}</h1> + <h1 class="govuk-heading-xl">{{ tr .App "enterYourReferenceNumber" }}</h1> {{ trHtml .App "attorneyReferenceNumberContent" }} diff --git a/web/template/attorney/enter_reference_number_opt_out.gohtml b/web/template/attorney/enter_reference_number_opt_out.gohtml new file mode 100644 index 0000000000..401059b103 --- /dev/null +++ b/web/template/attorney/enter_reference_number_opt_out.gohtml @@ -0,0 +1,22 @@ +{{ template "page" . }} + +{{ define "pageTitle" }}{{ tr .App "enterYourAttorneyReferenceNumber" }}{{ end }} + +{{ define "main" }} + <div class="govuk-grid-row"> + <div class="govuk-grid-column-two-thirds"> + <h1 class="govuk-heading-xl">{{ tr .App "enterYourAttorneyReferenceNumber" }}</h1> + + {{ trHtml .App "attorneyReferenceNumberContent" }} + + <form novalidate method="post"> + {{ template "input" (input . "reference-number" "attorneyReferenceNumber" .Form.ReferenceNumberRaw + "classes" "govuk-input--width-20 govuk-!-margin-bottom-3 govuk-input--extra-letter-spacing" + "hint" "referenceNumberHint")}} + + <button type="submit" class="govuk-button" data-module="govuk-button">{{ tr .App "continue" }}</button> + {{ template "csrf-field" . }} + </form> + </div> + </div> +{{ end }} diff --git a/web/template/attorney/sign.gohtml b/web/template/attorney/sign.gohtml index 6951c96d03..333214abcb 100644 --- a/web/template/attorney/sign.gohtml +++ b/web/template/attorney/sign.gohtml @@ -99,7 +99,11 @@ </div> </div> - {{ template "buttons" (button .App "submitSignature") }} + <div class="govuk-button-group"> + <button type="submit" class="govuk-button" data-module="govuk-button">{{ tr .App "submitSignature" }}</button> + <a href="{{ link .App (global.Paths.Attorney.ConfirmDontWantToBeAttorney.Format .LpaID) }}" class="govuk-button govuk-button--warning" data-module="govuk-button">{{ tr .App "iDoNotWantToBeAttorney" }}</a> + </div> + {{ template "csrf-field" . }} </form> </div> diff --git a/web/template/attorney/you_have_decided_not_to_be_attorney.gohtml b/web/template/attorney/you_have_decided_not_to_be_attorney.gohtml new file mode 100644 index 0000000000..5325d477f8 --- /dev/null +++ b/web/template/attorney/you_have_decided_not_to_be_attorney.gohtml @@ -0,0 +1,15 @@ +{{ template "page" . }} + +{{ define "pageTitle" }}{{ tr .App "youHaveConfirmedYouDoNotWantToBeAttorney" }}{{ end }} + +{{ define "main" }} + <div class="govuk-grid-row"> + <div class="govuk-grid-column-two-thirds"> + <div class="govuk-panel govuk-panel--confirmation"> + <h1 class="govuk-panel__title">{{ trFormat .App "youHaveConfirmedYouDoNotWantToBeDonorsAttorney" "DonorFullNamePossessive" (possessive .App (.App.Query.Get "donorFullName")) }}</h1> + </div> + + {{ trFormatHtml .App "youHaveConfirmedYouDoNotWantToBeAttorneyContent" "DonorFirstNames" (.App.Query.Get "donorFirstNames") }} + </div> + </div> +{{ end }} diff --git a/web/template/certificateprovider/enter_reference_number.gohtml b/web/template/certificateprovider/enter_reference_number.gohtml index b614b98962..fe40786d3f 100644 --- a/web/template/certificateprovider/enter_reference_number.gohtml +++ b/web/template/certificateprovider/enter_reference_number.gohtml @@ -10,12 +10,11 @@ {{ trHtml .App "certificateProviderReferenceNumberContent" }} <form novalidate method="post"> - <div class="govuk-form-group"> - <fieldset class="govuk-fieldset"> - {{ template "input" (input . "reference-number" "certificateProviderReferenceNumber" .Form.ReferenceNumberRaw "classes" "govuk-input--width-20 govuk-!-margin-bottom-3 govuk-input--extra-letter-spacing" "hint" "referenceNumberHint")}} - <button type="submit" class="govuk-button" data-module="govuk-button">{{ tr .App "saveAndContinue" }}</button> - </fieldset> - </div> + {{ template "input" (input . "reference-number" "certificateProviderReferenceNumber" .Form.ReferenceNumberRaw + "classes" "govuk-input--width-20 govuk-!-margin-bottom-3 govuk-input--extra-letter-spacing" + "hint" "referenceNumberHint")}} + + <button type="submit" class="govuk-button" data-module="govuk-button">{{ tr .App "saveAndContinue" }}</button> {{ template "csrf-field" . }} </form> </div> diff --git a/web/template/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.gohtml b/web/template/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.gohtml index d59a13a030..6d1a7137e5 100644 --- a/web/template/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.gohtml +++ b/web/template/certificateprovider/you_have_decided_not_to_be_a_certificate_provider.gohtml @@ -8,7 +8,7 @@ <div class="govuk-panel govuk-panel--confirmation"> <h1 class="govuk-panel__title">{{ tr .App "thankYou" }}</h1> <div class="govuk-panel__body"> - {{ trFormat .App "weHaveContactedDonorToLetThemKnowYourDecision" "DonorFullName" .DonorFullName }} + {{ trFormat .App "weHaveContactedDonorToLetThemKnowYourDecision" "DonorFullName" (.App.Query.Get "donorFullName") }} </div> </div>