From 8c42ac5f95e8e29700655efa880c3994ae3ad7e6 Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell <m@hawx.me> Date: Thu, 16 Nov 2023 13:30:27 +0000 Subject: [PATCH] MLPAB-1365 Prevent re-checking LPA when no changes have been made (#850) --- cypress/e2e/donor/check-your-lpa.cy.js | 15 ++ go.mod | 1 + go.sum | 2 + internal/app/donor_store.go | 12 ++ internal/app/donor_store_test.go | 113 +++++++----- internal/page/data.go | 23 ++- internal/page/data_test.go | 12 ++ internal/page/donor/check_your_lpa.go | 166 +++++++++++------- internal/page/donor/check_your_lpa_test.go | 136 ++++++++++---- internal/page/donor/mock_test.go | 3 + internal/page/donor/register.go | 2 +- internal/page/donor/upload_evidence_test.go | 6 - .../donor/witnessing_your_signature_test.go | 3 - internal/page/fixtures/donor.go | 2 +- web/template/check_your_lpa.gohtml | 70 ++++---- 15 files changed, 363 insertions(+), 203 deletions(-) diff --git a/cypress/e2e/donor/check-your-lpa.cy.js b/cypress/e2e/donor/check-your-lpa.cy.js index baaef95556..e452db9d83 100644 --- a/cypress/e2e/donor/check-your-lpa.cy.js +++ b/cypress/e2e/donor/check-your-lpa.cy.js @@ -34,6 +34,21 @@ describe('Check the LPA', () => { cy.url().should('contain', '/lpa-details-saved'); }); + it('does not allow checking when no changes', () => { + cy.get('#f-checked-and-happy').check() + cy.contains('button', 'Confirm').click(); + + cy.visitLpa('/check-your-lpa'); + cy.contains('button', 'Confirm').should('not.exist'); + + cy.visitLpa('/restrictions'); + cy.get('#f-restrictions').type('2'); + cy.contains('button', 'Save and continue').click(); + + cy.visitLpa('/check-your-lpa'); + cy.contains('button', 'Confirm'); + }); + describe('CP acting on paper', () => { describe('on first check', () => { it('content is tailored for paper CPs, a details component is shown and nav redirects to payment', () => { diff --git a/go.mod b/go.mod index 8521f602f0..34c5b735aa 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,7 @@ require ( github.com/hashicorp/go-version v1.5.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect diff --git a/go.sum b/go.sum index 957d212ff0..e34a6dbcff 100644 --- a/go.sum +++ b/go.sum @@ -169,6 +169,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/ministryofjustice/opg-go-common v0.0.0-20231106092059-b3dcf8bd1eeb h1:nZ2pEcU9r5sAewHyQ0ZrXnPNLSHgfvxa7xf7rpZpbno= github.com/ministryofjustice/opg-go-common v0.0.0-20231106092059-b3dcf8bd1eeb/go.mod h1:qktwZb46YkojkLVHU2QNnVK6yVktXkNpBuJ+TyobvuY= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= diff --git a/internal/app/donor_store.go b/internal/app/donor_store.go index af7e0afdac..11745126f4 100644 --- a/internal/app/donor_store.go +++ b/internal/app/donor_store.go @@ -62,9 +62,14 @@ func (s *donorStore) Create(ctx context.Context) (*page.Lpa, error) { Version: 1, } + if lpa.Hash, err = lpa.GenerateHash(); err != nil { + return nil, err + } + if err := s.dynamoClient.Create(ctx, lpa); err != nil { return nil, err } + if err := s.dynamoClient.Create(ctx, lpaLink{ PK: lpaKey(lpaID), SK: subKey(data.SessionID), @@ -130,6 +135,13 @@ func (s *donorStore) Latest(ctx context.Context) (*page.Lpa, error) { } func (s *donorStore) Put(ctx context.Context, lpa *page.Lpa) error { + newHash, err := lpa.GenerateHash() + if newHash == lpa.Hash || err != nil { + return err + } + + lpa.Hash = newHash + // By not setting UpdatedAt until a UID exists, queries for SK=#DONOR#xyz on // ActorUpdatedAtIndex will not return UID-less LPAs. if lpa.UID != "" { diff --git a/internal/app/donor_store_test.go b/internal/app/donor_store_test.go index ba83039adc..c1642882e9 100644 --- a/internal/app/donor_store_test.go +++ b/internal/app/donor_store_test.go @@ -14,11 +14,16 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" + "github.com/mitchellh/hashstructure/v2" "github.com/stretchr/testify/assert" mock "github.com/stretchr/testify/mock" ) -var expectedError = errors.New("err") +var ( + expectedError = errors.New("err") + testNow = time.Date(2023, time.April, 2, 3, 4, 5, 6, time.UTC) + testNowFn = func() time.Time { return testNow } +) func (m *mockDynamoClient) ExpectOne(ctx, pk, sk, data interface{}, err error) { m. @@ -180,29 +185,30 @@ func TestDonorStoreLatestWhenDataStoreError(t *testing.T) { func TestDonorStorePut(t *testing.T) { ctx := context.Background() - now := time.Now() testcases := map[string]struct { input, saved *page.Lpa }{ "no uid": { - input: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true}, + input: &page.Lpa{PK: "LPA#5", Hash: 5, SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true}, saved: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true}, }, "with uid": { - input: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M"}, - saved: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M", UpdatedAt: now}, + input: &page.Lpa{PK: "LPA#5", Hash: 5, SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M"}, + saved: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M", UpdatedAt: testNow}, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { + tc.saved.Hash, _ = tc.saved.GenerateHash() + dynamoClient := newMockDynamoClient(t) dynamoClient. On("Put", ctx, tc.saved). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, now: testNowFn} err := donorStore.Put(ctx, tc.input) assert.Nil(t, err) @@ -210,6 +216,17 @@ func TestDonorStorePut(t *testing.T) { } } +func TestDonorStorePutWhenNoChange(t *testing.T) { + ctx := context.Background() + donorStore := &donorStore{} + + lpa := &page.Lpa{ID: "an-id"} + lpa.Hash, _ = hashstructure.Hash(lpa, hashstructure.FormatV2, nil) + + err := donorStore.Put(ctx, lpa) + assert.Nil(t, err) +} + func TestDonorStorePutWhenError(t *testing.T) { ctx := context.Background() @@ -224,7 +241,6 @@ func TestDonorStorePutWhenError(t *testing.T) { func TestDonorStorePutWhenUIDNeeded(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) - now := time.Now() eventClient := newMockEventClient(t) eventClient. @@ -240,27 +256,30 @@ func TestDonorStorePutWhenUIDNeeded(t *testing.T) { }). Return(nil) + updatedLpa := &page.Lpa{ + PK: "LPA#5", + SK: "#DONOR#an-id", + ID: "5", + Donor: actor.Donor{ + FirstNames: "John", + LastName: "Smith", + DateOfBirth: date.New("2000", "01", "01"), + Address: place.Address{ + Line1: "line", + Postcode: "F1 1FF", + }, + }, + Type: page.LpaTypeHealthWelfare, + HasSentUidRequestedEvent: true, + } + updatedLpa.Hash, _ = updatedLpa.GenerateHash() + dynamoClient := newMockDynamoClient(t) dynamoClient. - On("Put", ctx, &page.Lpa{ - PK: "LPA#5", - SK: "#DONOR#an-id", - ID: "5", - Donor: actor.Donor{ - FirstNames: "John", - LastName: "Smith", - DateOfBirth: date.New("2000", "01", "01"), - Address: place.Address{ - Line1: "line", - Postcode: "F1 1FF", - }, - }, - Type: page.LpaTypeHealthWelfare, - HasSentUidRequestedEvent: true, - }). + On("Put", ctx, updatedLpa). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -335,14 +354,13 @@ func TestDonorStorePutWhenUIDFails(t *testing.T) { func TestDonorStorePutWhenApplicationUpdatedWhenError(t *testing.T) { ctx := context.Background() - now := time.Now() eventClient := newMockEventClient(t) eventClient. On("SendApplicationUpdated", ctx, mock.Anything). Return(expectedError) - donorStore := &donorStore{eventClient: eventClient, now: func() time.Time { return now }} + donorStore := &donorStore{eventClient: eventClient, now: testNowFn} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -365,7 +383,6 @@ func TestDonorStorePutWhenApplicationUpdatedWhenError(t *testing.T) { func TestDonorStorePutWhenPreviousApplicationLinked(t *testing.T) { ctx := context.Background() - now := time.Now() eventClient := newMockEventClient(t) eventClient. @@ -375,21 +392,24 @@ func TestDonorStorePutWhenPreviousApplicationLinked(t *testing.T) { }). Return(nil) + updatedLpa := &page.Lpa{ + PK: "LPA#5", + SK: "#DONOR#an-id", + ID: "5", + UID: "M-1111", + UpdatedAt: testNow, + PreviousApplicationNumber: "5555", + HasSentApplicationUpdatedEvent: true, + HasSentPreviousApplicationLinkedEvent: true, + } + updatedLpa.Hash, _ = updatedLpa.GenerateHash() + dynamoClient := newMockDynamoClient(t) dynamoClient. - On("Put", ctx, &page.Lpa{ - PK: "LPA#5", - SK: "#DONOR#an-id", - ID: "5", - UID: "M-1111", - UpdatedAt: now, - PreviousApplicationNumber: "5555", - HasSentApplicationUpdatedEvent: true, - HasSentPreviousApplicationLinkedEvent: true, - }). + On("Put", ctx, updatedLpa). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, now: testNowFn} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -405,14 +425,13 @@ func TestDonorStorePutWhenPreviousApplicationLinked(t *testing.T) { func TestDonorStorePutWhenPreviousApplicationLinkedWontResend(t *testing.T) { ctx := context.Background() - now := time.Now() dynamoClient := newMockDynamoClient(t) dynamoClient. On("Put", ctx, mock.Anything). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, now: testNowFn} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -429,14 +448,13 @@ func TestDonorStorePutWhenPreviousApplicationLinkedWontResend(t *testing.T) { func TestDonorStorePutWhenPreviousApplicationLinkedWhenError(t *testing.T) { ctx := context.Background() - now := time.Now() eventClient := newMockEventClient(t) eventClient. On("SendPreviousApplicationLinked", ctx, mock.Anything). Return(expectedError) - donorStore := &donorStore{eventClient: eventClient, now: func() time.Time { return now }} + donorStore := &donorStore{eventClient: eventClient, now: testNowFn} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -451,18 +469,18 @@ func TestDonorStorePutWhenPreviousApplicationLinkedWhenError(t *testing.T) { func TestDonorStoreCreate(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) - now := time.Now() - lpa := &page.Lpa{PK: "LPA#10100000", SK: "#DONOR#an-id", ID: "10100000", CreatedAt: now, Version: 1} + lpa := &page.Lpa{PK: "LPA#10100000", SK: "#DONOR#an-id", ID: "10100000", CreatedAt: testNow, Version: 1} + lpa.Hash, _ = lpa.GenerateHash() dynamoClient := newMockDynamoClient(t) dynamoClient. On("Create", ctx, lpa). Return(nil) dynamoClient. - On("Create", ctx, lpaLink{PK: "LPA#10100000", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor, UpdatedAt: now}). + On("Create", ctx, lpaLink{PK: "LPA#10100000", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor, UpdatedAt: testNow}). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }, now: testNowFn} result, err := donorStore.Create(ctx) assert.Nil(t, err) @@ -480,7 +498,6 @@ func TestDonorStoreCreateWithSessionMissing(t *testing.T) { func TestDonorStoreCreateWhenError(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) - now := time.Now() testcases := map[string]func(*testing.T) *mockDynamoClient{ "certificate provider record": func(t *testing.T) *mockDynamoClient { @@ -512,7 +529,7 @@ func TestDonorStoreCreateWhenError(t *testing.T) { donorStore := &donorStore{ dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }, - now: func() time.Time { return now }, + now: testNowFn, } _, err := donorStore.Create(ctx) diff --git a/internal/page/data.go b/internal/page/data.go index 7971315cc5..9fa4d62b40 100644 --- a/internal/page/data.go +++ b/internal/page/data.go @@ -21,6 +21,7 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/pay" "github.com/ministryofjustice/opg-modernising-lpa/internal/place" + "github.com/mitchellh/hashstructure/v2" "golang.org/x/exp/slices" ) @@ -70,6 +71,8 @@ const ( // Lpa contains all the data related to the LPA application type Lpa struct { PK, SK string + // Hash is used to determine whether the Lpa has been changed since last read + Hash uint64 `hash:"-"` // Identifies the LPA being drafted ID string // A unique identifier created after sending basic LPA details to the UID service @@ -77,7 +80,7 @@ type Lpa struct { // CreatedAt is when the LPA was created CreatedAt time.Time // UpdatedAt is when the LPA was last updated - UpdatedAt time.Time + UpdatedAt time.Time `hash:"-"` // The donor the LPA relates to Donor actor.Donor // Attorneys named in the LPA @@ -98,8 +101,6 @@ type Lpa struct { Restrictions string // Used to show the task list Tasks Tasks - // Whether the applicant has checked the LPA and is happy to share the LPA with the certificate provider - CheckedAndHappy bool // PaymentDetails are records of payments made for the LPA via GOV.UK Pay PaymentDetails []Payment // Information returned by the identity service related to the applicant @@ -124,6 +125,10 @@ type Lpa struct { WantToApplyForLpa bool // Confirmation that the applicant wants to sign the LPA WantToSignLpa bool + // CheckedAt is when the donor checked their LPA + CheckedAt time.Time + // CheckedHash is the Hash value of the LPA when last checked + CheckedHash uint64 `hash:"-"` // SignedAt is when the donor submitted their signature SignedAt time.Time // SubmittedAt is when the Lpa was sent to the OPG @@ -133,7 +138,7 @@ type Lpa struct { // WithdrawnAt is when the Lpa was withdrawn by the donor WithdrawnAt time.Time // Version is the number of times the LPA has been updated (auto-incremented on PUT) - Version int + Version int `hash:"-"` // Codes used for the certificate provider to witness signing CertificateProviderCodes WitnessCodes @@ -155,9 +160,9 @@ type Lpa struct { // PreviousFee is the fee previously paid for an LPA PreviousFee pay.PreviousFee - HasSentUidRequestedEvent bool - HasSentApplicationUpdatedEvent bool - HasSentPreviousApplicationLinkedEvent bool + HasSentUidRequestedEvent bool `hash:"-"` + HasSentApplicationUpdatedEvent bool `hash:"-"` + HasSentPreviousApplicationLinkedEvent bool `hash:"-"` } type Payment struct { @@ -218,6 +223,10 @@ func ContextWithSessionData(ctx context.Context, data *SessionData) context.Cont return context.WithValue(ctx, (*SessionData)(nil), data) } +func (l *Lpa) GenerateHash() (uint64, error) { + return hashstructure.Hash(l, hashstructure.FormatV2, nil) +} + func (l *Lpa) DonorIdentityConfirmed() bool { return l.DonorIdentityUserData.OK && l.DonorIdentityUserData.MatchName(l.Donor.FirstNames, l.Donor.LastName) && diff --git a/internal/page/data_test.go b/internal/page/data_test.go index d462ccb9fe..095f454fc0 100644 --- a/internal/page/data_test.go +++ b/internal/page/data_test.go @@ -147,6 +147,18 @@ func TestReplacementAttorneysStepIn(t *testing.T) { }) } +func TestGenerateHash(t *testing.T) { + lpa := &Lpa{} + hash, err := lpa.GenerateHash() + assert.Nil(t, err) + assert.Equal(t, uint64(0x53e4ea75485b238f), hash) + + lpa.ID = "1" + hash, err = lpa.GenerateHash() + assert.Nil(t, err) + assert.Equal(t, uint64(0x8dbcd47c6136ec6f), hash) +} + func TestIdentityConfirmed(t *testing.T) { testCases := map[string]struct { lpa *Lpa diff --git a/internal/page/donor/check_your_lpa.go b/internal/page/donor/check_your_lpa.go index cbc5905b22..06ac36a2cf 100644 --- a/internal/page/donor/check_your_lpa.go +++ b/internal/page/donor/check_your_lpa.go @@ -1,8 +1,10 @@ package donor import ( + "context" "errors" "net/http" + "time" "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -13,25 +15,105 @@ import ( ) type checkYourLpaData struct { - App page.AppData - Errors validation.List - Lpa *page.Lpa - Form *checkYourLpaForm - Completed bool + App page.AppData + Errors validation.List + Lpa *page.Lpa + Form *checkYourLpaForm + Completed bool + CanContinue bool } -func CheckYourLpa(tmpl template.Template, donorStore DonorStore, shareCodeSender ShareCodeSender, notifyClient NotifyClient, certificateProviderStore CertificateProviderStore) Handler { +type checkYourLpaNotifier struct { + notifyClient NotifyClient + shareCodeSender ShareCodeSender + certificateProviderStore CertificateProviderStore +} + +func (n *checkYourLpaNotifier) Notify(ctx context.Context, appData page.AppData, lpa *page.Lpa, wasCompleted bool) error { + if lpa.CertificateProvider.CarryOutBy.IsPaper() { + err := n.sendPaperNotification(ctx, appData, lpa, wasCompleted) + return err + } + + err := n.sendOnlineNotification(ctx, appData, lpa, wasCompleted) + return err +} + +func (n *checkYourLpaNotifier) sendPaperNotification(ctx context.Context, appData page.AppData, lpa *page.Lpa, wasCompleted bool) error { + sms := notify.Sms{ + PhoneNumber: lpa.CertificateProvider.Mobile, + Personalisation: map[string]string{ + "donorFullName": lpa.Donor.FullName(), + "donorFirstNames": lpa.Donor.FirstNames, + }, + } + + if wasCompleted { + sms.TemplateID = n.notifyClient.TemplateID(notify.CertificateProviderPaperLpaDetailsChangedSMS) + sms.Personalisation["lpaId"] = lpa.ID + } else { + sms.TemplateID = n.notifyClient.TemplateID(notify.CertificateProviderPaperMeetingPromptSMS) + sms.Personalisation["lpaType"] = appData.Localizer.T(lpa.Type.LegalTermTransKey()) + } + + _, err := n.notifyClient.Sms(ctx, sms) + return err +} + +func (n *checkYourLpaNotifier) sendOnlineNotification(ctx context.Context, appData page.AppData, lpa *page.Lpa, wasCompleted bool) error { + if !wasCompleted { + err := n.shareCodeSender.SendCertificateProvider(ctx, notify.CertificateProviderInviteEmail, appData, true, lpa) + return err + } + + certificateProvider, err := n.certificateProviderStore.GetAny(ctx) + if err != nil && !errors.Is(err, dynamo.NotFoundError{}) { + return err + } + + sms := notify.Sms{ + PhoneNumber: lpa.CertificateProvider.Mobile, + } + + if certificateProvider.Tasks.ConfirmYourDetails.NotStarted() { + sms.TemplateID = n.notifyClient.TemplateID(notify.CertificateProviderDigitalLpaDetailsChangedNotSeenLpaSMS) + sms.Personalisation = map[string]string{ + "donorFullName": lpa.Donor.FullName(), + "lpaType": appData.Localizer.T(lpa.Type.LegalTermTransKey()), + } + } else { + sms.TemplateID = n.notifyClient.TemplateID(notify.CertificateProviderDigitalLpaDetailsChangedSeenLpaSMS) + sms.Personalisation = map[string]string{ + "donorFullNamePossessive": appData.Localizer.Possessive(lpa.Donor.FullName()), + "lpaType": appData.Localizer.T(lpa.Type.LegalTermTransKey()), + "lpaId": lpa.ID, + "donorFirstNames": lpa.Donor.FirstNames, + } + } + + _, err = n.notifyClient.Sms(ctx, sms) + return err +} + +func CheckYourLpa(tmpl template.Template, donorStore DonorStore, shareCodeSender ShareCodeSender, notifyClient NotifyClient, certificateProviderStore CertificateProviderStore, now func() time.Time) Handler { + notifier := &checkYourLpaNotifier{ + notifyClient: notifyClient, + shareCodeSender: shareCodeSender, + certificateProviderStore: certificateProviderStore, + } + return func(appData page.AppData, w http.ResponseWriter, r *http.Request, lpa *page.Lpa) error { data := &checkYourLpaData{ App: appData, Lpa: lpa, Form: &checkYourLpaForm{ - CheckedAndHappy: lpa.CheckedAndHappy, + CheckedAndHappy: !lpa.CheckedAt.IsZero(), }, - Completed: lpa.Tasks.CheckYourLpa.Completed(), + Completed: lpa.Tasks.CheckYourLpa.Completed(), + CanContinue: lpa.CheckedHash != lpa.Hash, } - if r.Method == http.MethodPost { + if r.Method == http.MethodPost && data.CanContinue { data.Form = readCheckYourLpaForm(r) data.Errors = data.Form.Validate() @@ -42,69 +124,21 @@ func CheckYourLpa(tmpl template.Template, donorStore DonorStore, shareCodeSender redirect = redirect + "?firstCheck=1" } - lpa.CheckedAndHappy = data.Form.CheckedAndHappy lpa.Tasks.CheckYourLpa = actor.TaskCompleted + lpa.CheckedAt = now() + + newHash, err := lpa.GenerateHash() + if err != nil { + return err + } + lpa.CheckedHash = newHash if err := donorStore.Put(r.Context(), lpa); err != nil { return err } - if lpa.CertificateProvider.CarryOutBy.IsPaper() { - sms := notify.Sms{ - PhoneNumber: lpa.CertificateProvider.Mobile, - Personalisation: map[string]string{ - "donorFullName": lpa.Donor.FullName(), - "donorFirstNames": lpa.Donor.FirstNames, - }, - } - - if data.Completed { - sms.TemplateID = notifyClient.TemplateID(notify.CertificateProviderPaperLpaDetailsChangedSMS) - sms.Personalisation["lpaId"] = lpa.ID - } else { - sms.TemplateID = notifyClient.TemplateID(notify.CertificateProviderPaperMeetingPromptSMS) - sms.Personalisation["lpaType"] = appData.Localizer.T(lpa.Type.LegalTermTransKey()) - } - - if _, err := notifyClient.Sms(r.Context(), sms); err != nil { - return err - } - } else { - if data.Completed { - certificateProvider, err := certificateProviderStore.GetAny(r.Context()) - - if err != nil && !errors.Is(err, dynamo.NotFoundError{}) { - return err - } - - sms := notify.Sms{ - PhoneNumber: lpa.CertificateProvider.Mobile, - } - - if certificateProvider.Tasks.ConfirmYourDetails.NotStarted() { - sms.TemplateID = notifyClient.TemplateID(notify.CertificateProviderDigitalLpaDetailsChangedNotSeenLpaSMS) - sms.Personalisation = map[string]string{ - "donorFullName": lpa.Donor.FullName(), - "lpaType": appData.Localizer.T(lpa.Type.LegalTermTransKey()), - } - } else { - sms.TemplateID = notifyClient.TemplateID(notify.CertificateProviderDigitalLpaDetailsChangedSeenLpaSMS) - sms.Personalisation = map[string]string{ - "donorFullNamePossessive": appData.Localizer.Possessive(lpa.Donor.FullName()), - "lpaType": appData.Localizer.T(lpa.Type.LegalTermTransKey()), - "lpaId": lpa.ID, - "donorFirstNames": lpa.Donor.FirstNames, - } - } - - if _, err := notifyClient.Sms(r.Context(), sms); err != nil { - return err - } - } else { - if err := shareCodeSender.SendCertificateProvider(r.Context(), notify.CertificateProviderInviteEmail, appData, true, lpa); err != nil { - return err - } - } + if err := notifier.Notify(r.Context(), appData, lpa, data.Completed); err != nil { + return err } return appData.Redirect(w, r, lpa, redirect) diff --git a/internal/page/donor/check_your_lpa_test.go b/internal/page/donor/check_your_lpa_test.go index 99038d17e1..e13348a1eb 100644 --- a/internal/page/donor/check_your_lpa_test.go +++ b/internal/page/donor/check_your_lpa_test.go @@ -28,7 +28,7 @@ func TestGetCheckYourLpa(t *testing.T) { }). Return(nil) - err := CheckYourLpa(template.Execute, nil, nil, nil, nil)(testAppData, w, r, &page.Lpa{}) + err := CheckYourLpa(template.Execute, nil, nil, nil, nil, testNowFn)(testAppData, w, r, &page.Lpa{}) resp := w.Result() assert.Nil(t, err) @@ -40,7 +40,7 @@ func TestGetCheckYourLpaFromStore(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, "/", nil) lpa := &page.Lpa{ - CheckedAndHappy: true, + CheckedAt: testNow, } template := newMockTemplate(t) @@ -54,7 +54,44 @@ func TestGetCheckYourLpaFromStore(t *testing.T) { }). Return(nil) - err := CheckYourLpa(template.Execute, nil, nil, nil, nil)(testAppData, w, r, lpa) + err := CheckYourLpa(template.Execute, nil, nil, nil, nil, testNowFn)(testAppData, w, r, lpa) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostCheckYourLpaWhenNotChanged(t *testing.T) { + form := url.Values{ + "checked-and-happy": {"1"}, + } + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + lpa := &page.Lpa{ + ID: "lpa-id", + Hash: 5, + CheckedAt: testNow, + CheckedHash: 5, + Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, + CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Online}, + } + + template := newMockTemplate(t) + template. + On("Execute", w, &checkYourLpaData{ + App: testAppData, + Lpa: lpa, + Form: &checkYourLpaForm{ + CheckedAndHappy: true, + }, + Completed: true, + }). + Return(nil) + + err := CheckYourLpa(template.Execute, nil, nil, nil, nil, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Nil(t, err) @@ -79,17 +116,19 @@ func TestPostCheckYourLpaDigitalCertificateProviderOnFirstCheck(t *testing.T) { lpa := &page.Lpa{ ID: "lpa-id", - CheckedAndHappy: false, + Hash: 5, Tasks: page.Tasks{CheckYourLpa: existingTaskState}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Online}, } updatedLpa := &page.Lpa{ ID: "lpa-id", - CheckedAndHappy: true, + Hash: 5, + CheckedAt: testNow, Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Online}, } + updatedLpa.CheckedHash, _ = updatedLpa.GenerateHash() shareCodeSender := newMockShareCodeSender(t) shareCodeSender. @@ -101,7 +140,7 @@ func TestPostCheckYourLpaDigitalCertificateProviderOnFirstCheck(t *testing.T) { On("Put", r.Context(), updatedLpa). Return(nil) - err := CheckYourLpa(nil, donorStore, shareCodeSender, nil, nil)(testAppData, w, r, lpa) + err := CheckYourLpa(nil, donorStore, shareCodeSender, nil, nil, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Nil(t, err) @@ -182,9 +221,10 @@ func TestPostCheckYourLpaDigitalCertificateProviderOnSubsequentChecks(t *testing lpa := &page.Lpa{ ID: "lpa-id", + Hash: 5, Type: page.LpaTypePropertyFinance, Donor: actor.Donor{FirstNames: "Teneil", LastName: "Throssell"}, - CheckedAndHappy: true, + CheckedAt: testNow, Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Online, Mobile: "07700900000"}, } @@ -209,7 +249,7 @@ func TestPostCheckYourLpaDigitalCertificateProviderOnSubsequentChecks(t *testing Tasks: actor.CertificateProviderTasks{ConfirmYourDetails: tc.certificateProviderDetailsTaskState}, }, nil) - err := CheckYourLpa(nil, donorStore, nil, notifyClient, certificateProviderStore)(testAppData, w, r, lpa) + err := CheckYourLpa(nil, donorStore, nil, notifyClient, certificateProviderStore, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Nil(t, err) @@ -219,13 +259,39 @@ func TestPostCheckYourLpaDigitalCertificateProviderOnSubsequentChecks(t *testing } } -func TestPostCheckYourLpaPaperCertificateProviderOnFirstCheck(t *testing.T) { - testCases := map[actor.TaskState]string{ - actor.TaskNotStarted: page.Paths.LpaDetailsSaved.Format("lpa-id") + "?firstCheck=1", - actor.TaskInProgress: page.Paths.LpaDetailsSaved.Format("lpa-id") + "?firstCheck=1", +func TestPostCheckYourLpaDigitalCertificateProviderOnSubsequentChecksCertificateProviderStoreErrors(t *testing.T) { + form := url.Values{ + "checked-and-happy": {"1"}, } - for existingTaskState, expectedURL := range testCases { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + donorStore := newMockDonorStore(t) + donorStore. + On("Put", r.Context(), mock.Anything). + Return(nil) + + certificateProviderStore := newMockCertificateProviderStore(t) + certificateProviderStore. + On("GetAny", r.Context()). + Return(nil, expectedError) + + err := CheckYourLpa(nil, donorStore, nil, nil, certificateProviderStore, testNowFn)(testAppData, w, r, &page.Lpa{ + ID: "lpa-id", + Hash: 5, + Type: page.LpaTypePropertyFinance, + Donor: actor.Donor{FirstNames: "Teneil", LastName: "Throssell"}, + CheckedAt: testNow, + Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, + CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Online, Mobile: "07700900000"}, + }) + assert.Equal(t, expectedError, err) +} + +func TestPostCheckYourLpaPaperCertificateProviderOnFirstCheck(t *testing.T) { + for _, existingTaskState := range []actor.TaskState{actor.TaskNotStarted, actor.TaskInProgress} { t.Run(existingTaskState.String(), func(t *testing.T) { form := url.Values{ "checked-and-happy": {"1"}, @@ -244,8 +310,8 @@ func TestPostCheckYourLpaPaperCertificateProviderOnFirstCheck(t *testing.T) { lpa := &page.Lpa{ ID: "lpa-id", + Hash: 5, Donor: actor.Donor{FirstNames: "Teneil", LastName: "Throssell"}, - CheckedAndHappy: false, Tasks: page.Tasks{CheckYourLpa: existingTaskState}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Paper, Mobile: "07700900000"}, Type: page.LpaTypePropertyFinance, @@ -253,12 +319,14 @@ func TestPostCheckYourLpaPaperCertificateProviderOnFirstCheck(t *testing.T) { updatedLpa := &page.Lpa{ ID: "lpa-id", + Hash: 5, Donor: actor.Donor{FirstNames: "Teneil", LastName: "Throssell"}, - CheckedAndHappy: true, + CheckedAt: testNow, Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Paper, Mobile: "07700900000"}, Type: page.LpaTypePropertyFinance, } + updatedLpa.CheckedHash, _ = updatedLpa.GenerateHash() donorStore := newMockDonorStore(t) donorStore. @@ -281,12 +349,12 @@ func TestPostCheckYourLpaPaperCertificateProviderOnFirstCheck(t *testing.T) { }). Return("", nil) - err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil)(testAppData, w, r, lpa) + err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Nil(t, err) assert.Equal(t, http.StatusFound, resp.StatusCode) - assert.Equal(t, expectedURL, resp.Header.Get("Location")) + assert.Equal(t, page.Paths.LpaDetailsSaved.Format("lpa-id")+"?firstCheck=1", resp.Header.Get("Location")) }) } } @@ -302,8 +370,9 @@ func TestPostCheckYourLpaPaperCertificateProviderOnSubsequentCheck(t *testing.T) lpa := &page.Lpa{ ID: "lpa-id", + Hash: 5, Donor: actor.Donor{FirstNames: "Teneil", LastName: "Throssell"}, - CheckedAndHappy: true, + CheckedAt: testNow, Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Paper, Mobile: "07700900000"}, Type: page.LpaTypePropertyFinance, @@ -330,7 +399,7 @@ func TestPostCheckYourLpaPaperCertificateProviderOnSubsequentCheck(t *testing.T) }). Return("", nil) - err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil)(testAppData, w, r, lpa) + err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Nil(t, err) @@ -349,13 +418,10 @@ func TestPostCheckYourLpaWhenStoreErrors(t *testing.T) { donorStore := newMockDonorStore(t) donorStore. - On("Put", r.Context(), &page.Lpa{ - CheckedAndHappy: true, - Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, - }). + On("Put", r.Context(), mock.Anything). Return(expectedError) - err := CheckYourLpa(nil, donorStore, nil, nil, nil)(testAppData, w, r, &page.Lpa{}) + err := CheckYourLpa(nil, donorStore, nil, nil, nil, testNowFn)(testAppData, w, r, &page.Lpa{Hash: 5}) resp := w.Result() assert.Equal(t, expectedError, err) @@ -372,28 +438,22 @@ func TestPostCheckYourLpaWhenShareCodeSenderErrors(t *testing.T) { r.Header.Add("Content-Type", page.FormUrlEncoded) lpa := &page.Lpa{ - ID: "lpa-id", - CheckedAndHappy: false, - Tasks: page.Tasks{CheckYourLpa: actor.TaskInProgress}, - } - - updatedLpa := &page.Lpa{ - ID: "lpa-id", - CheckedAndHappy: true, - Tasks: page.Tasks{CheckYourLpa: actor.TaskCompleted}, + ID: "lpa-id", + Hash: 5, + Tasks: page.Tasks{CheckYourLpa: actor.TaskInProgress}, } donorStore := newMockDonorStore(t) donorStore. - On("Put", r.Context(), updatedLpa). + On("Put", r.Context(), mock.Anything). Return(nil) shareCodeSender := newMockShareCodeSender(t) shareCodeSender. - On("SendCertificateProvider", r.Context(), notify.CertificateProviderInviteEmail, testAppData, true, updatedLpa). + On("SendCertificateProvider", r.Context(), notify.CertificateProviderInviteEmail, testAppData, true, mock.Anything). Return(expectedError) - err := CheckYourLpa(nil, donorStore, shareCodeSender, nil, nil)(testAppData, w, r, lpa) + err := CheckYourLpa(nil, donorStore, shareCodeSender, nil, nil, testNowFn)(testAppData, w, r, lpa) resp := w.Result() assert.Equal(t, expectedError, err) @@ -429,7 +489,7 @@ func TestPostCheckYourLpaWhenNotifyClientErrors(t *testing.T) { On("Sms", mock.Anything, mock.Anything). Return("", expectedError) - err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil)(testAppData, w, r, &page.Lpa{CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Paper}}) + err := CheckYourLpa(nil, donorStore, nil, notifyClient, nil, testNowFn)(testAppData, w, r, &page.Lpa{Hash: 5, CertificateProvider: actor.CertificateProvider{CarryOutBy: actor.Paper}}) resp := w.Result() assert.Equal(t, expectedError, err) @@ -452,7 +512,7 @@ func TestPostCheckYourLpaWhenValidationErrors(t *testing.T) { })). Return(nil) - err := CheckYourLpa(template.Execute, nil, nil, nil, nil)(testAppData, w, r, &page.Lpa{}) + err := CheckYourLpa(template.Execute, nil, nil, nil, nil, nil)(testAppData, w, r, &page.Lpa{Hash: 5}) resp := w.Result() assert.Nil(t, err) diff --git a/internal/page/donor/mock_test.go b/internal/page/donor/mock_test.go index dced2c9cfb..23db1c7d7b 100644 --- a/internal/page/donor/mock_test.go +++ b/internal/page/donor/mock_test.go @@ -4,6 +4,7 @@ import ( "errors" "net/http" "net/http/httptest" + "time" "github.com/gorilla/sessions" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -31,6 +32,8 @@ var ( Lang: localize.En, Paths: page.Paths, } + testNow = time.Date(2023, time.July, 3, 4, 5, 6, 1, time.UTC) + testNowFn = func() time.Time { return testNow } ) func (m *mockDonorStore) willReturnEmptyLpa(r *http.Request) *mockDonorStore { diff --git a/internal/page/donor/register.go b/internal/page/donor/register.go index abfccaeb9b..e25b3eb692 100644 --- a/internal/page/donor/register.go +++ b/internal/page/donor/register.go @@ -308,7 +308,7 @@ func Register( handleWithLpa(page.Paths.ConfirmYourCertificateProviderIsNotRelated, CanGoBack, ConfirmYourCertificateProviderIsNotRelated(tmpls.Get("confirm_your_certificate_provider_is_not_related.gohtml"), donorStore)) handleWithLpa(page.Paths.CheckYourLpa, CanGoBack, - CheckYourLpa(tmpls.Get("check_your_lpa.gohtml"), donorStore, shareCodeSender, notifyClient, certificateProviderStore)) + CheckYourLpa(tmpls.Get("check_your_lpa.gohtml"), donorStore, shareCodeSender, notifyClient, certificateProviderStore, time.Now)) handleWithLpa(page.Paths.LpaDetailsSaved, CanGoBack, LpaDetailsSaved(tmpls.Get("lpa_details_saved.gohtml"))) diff --git a/internal/page/donor/upload_evidence_test.go b/internal/page/donor/upload_evidence_test.go index bda8a59ef8..cecd3f3eea 100644 --- a/internal/page/donor/upload_evidence_test.go +++ b/internal/page/donor/upload_evidence_test.go @@ -10,7 +10,6 @@ import ( "os" "strings" "testing" - "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" @@ -20,11 +19,6 @@ import ( "github.com/stretchr/testify/mock" ) -var ( - testNow = time.Now() - testNowFn = func() time.Time { return testNow } -) - func TestGetUploadEvidence(t *testing.T) { w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodGet, "/", nil) diff --git a/internal/page/donor/witnessing_your_signature_test.go b/internal/page/donor/witnessing_your_signature_test.go index 1c9bbb0821..08c6708d78 100644 --- a/internal/page/donor/witnessing_your_signature_test.go +++ b/internal/page/donor/witnessing_your_signature_test.go @@ -79,7 +79,6 @@ func TestPostWitnessingYourSignatureCannotSign(t *testing.T) { r, _ := http.NewRequest(http.MethodPost, "/", nil) lpa := &page.Lpa{ - Version: 1, ID: "lpa-id", Donor: actor.Donor{CanSign: form.No}, DonorIdentityUserData: identity.UserData{OK: true}, @@ -92,7 +91,6 @@ func TestPostWitnessingYourSignatureCannotSign(t *testing.T) { Return(nil) witnessCodeSender. On("SendToIndependentWitness", r.Context(), &page.Lpa{ - Version: 2, ID: "lpa-id", Donor: actor.Donor{CanSign: form.No}, DonorIdentityUserData: identity.UserData{OK: true}, @@ -104,7 +102,6 @@ func TestPostWitnessingYourSignatureCannotSign(t *testing.T) { donorStore. On("Get", r.Context()). Return(&page.Lpa{ - Version: 2, ID: "lpa-id", Donor: actor.Donor{CanSign: form.No}, DonorIdentityUserData: identity.UserData{OK: true}, diff --git a/internal/page/fixtures/donor.go b/internal/page/fixtures/donor.go index a1c1c2733e..d3e4d01f91 100644 --- a/internal/page/fixtures/donor.go +++ b/internal/page/fixtures/donor.go @@ -196,7 +196,7 @@ func Donor( } if progress >= slices.Index(progressValues, "checkAndSendToYourCertificateProvider") { - lpa.CheckedAndHappy = true + lpa.CheckedAt = time.Now() lpa.Tasks.CheckYourLpa = actor.TaskCompleted } diff --git a/web/template/check_your_lpa.gohtml b/web/template/check_your_lpa.gohtml index 608f0e35c1..e5bfdcb6dc 100644 --- a/web/template/check_your_lpa.gohtml +++ b/web/template/check_your_lpa.gohtml @@ -23,45 +23,49 @@ {{ template "people-named-on-lpa" . }} - <form novalidate method="post"> - <div class="govuk-form-group {{ if .Errors.Has "checked-and-happy" }}govuk-form-group--error{{ end }}"> - {{ template "error-message" (errorMessage . "checked-and-happy") }} - <div class="govuk-checkboxes" data-module="govuk-checkboxes"> - <div class="govuk-checkboxes__item"> - <input class="govuk-checkboxes__input" id="f-checked-and-happy" name="checked-and-happy" type="checkbox" value="1"> - <label class="govuk-label govuk-checkboxes__label" for="f-checked-and-happy"> - {{ if .Lpa.CertificateProvider.CarryOutBy.IsPaper }} - {{ trFormat .App "iveCheckedThisLpaAndImHappyToShowToCertificateProvider" "CertificateProviderFullName" .Lpa.CertificateProvider.FullName }} - {{ else }} - {{ trFormat .App "iveCheckedThisLpaAndImHappyToShareWithCertificateProvider" "CertificateProviderFullName" .Lpa.CertificateProvider.FullName }} - {{ end }} - </label> + {{ if .CanContinue }} + <form novalidate method="post"> + <div class="govuk-form-group {{ if .Errors.Has "checked-and-happy" }}govuk-form-group--error{{ end }}"> + {{ template "error-message" (errorMessage . "checked-and-happy") }} + <div class="govuk-checkboxes" data-module="govuk-checkboxes"> + <div class="govuk-checkboxes__item"> + <input class="govuk-checkboxes__input" id="f-checked-and-happy" name="checked-and-happy" type="checkbox" value="1"> + <label class="govuk-label govuk-checkboxes__label" for="f-checked-and-happy"> + {{ if .Lpa.CertificateProvider.CarryOutBy.IsPaper }} + {{ trFormat .App "iveCheckedThisLpaAndImHappyToShowToCertificateProvider" "CertificateProviderFullName" .Lpa.CertificateProvider.FullName }} + {{ else }} + {{ trFormat .App "iveCheckedThisLpaAndImHappyToShareWithCertificateProvider" "CertificateProviderFullName" .Lpa.CertificateProvider.FullName }} + {{ end }} + </label> + </div> </div> </div> - </div> - {{ if .Completed }} - <div class="govuk-warning-text"> - <span class="govuk-warning-text__icon" aria-hidden="true">!</span> - <strong class="govuk-warning-text__text"> - <span class="govuk-warning-text__assistive"></span> - {{ tr .App "onceYouClickCertificateProviderWillBeSentText" }} - </strong> - </div> - {{ else }} - {{ template "details" (details . "whatHappensIfIChange" "whatHappensIfIChangeDetails" false) }} - {{ end }} + {{ if .Completed }} + <div class="govuk-warning-text"> + <span class="govuk-warning-text__icon" aria-hidden="true">!</span> + <strong class="govuk-warning-text__text"> + <span class="govuk-warning-text__assistive"></span> + {{ tr .App "onceYouClickCertificateProviderWillBeSentText" }} + </strong> + </div> + {{ else }} + {{ template "details" (details . "whatHappensIfIChange" "whatHappensIfIChangeDetails" false) }} + {{ end }} - <p class="govuk-body govuk-!-font-weight-bold">{{ tr .App "onceConfirmedNotAbleToMakeChanges" }}</p> + <p class="govuk-body govuk-!-font-weight-bold">{{ tr .App "onceConfirmedNotAbleToMakeChanges" }}</p> - {{ template "data-loss-warning-dialog" . }} + {{ template "data-loss-warning-dialog" . }} - <div class="govuk-button-group"> - <button type="submit" id="save-and-continue-btn" class="govuk-button" data-module="govuk-button">{{ tr .App "confirm" }}</button> - <a href="{{ link .App (.App.Paths.TaskList.Format .App.LpaID) }}" id="return-to-tasklist-btn" class="govuk-button govuk-button--secondary">{{ tr .App "returnToTaskList" }}</a> - </div> - {{ template "csrf-field" . }} - </form> + <div class="govuk-button-group"> + <button type="submit" id="save-and-continue-btn" class="govuk-button" data-module="govuk-button">{{ tr .App "confirm" }}</button> + <a href="{{ link .App (.App.Paths.TaskList.Format .App.LpaID) }}" id="return-to-tasklist-btn" class="govuk-button govuk-button--secondary">{{ tr .App "returnToTaskList" }}</a> + </div> + {{ template "csrf-field" . }} + </form> + {{ else }} + <a href="{{ link .App (.App.Paths.TaskList.Format .App.LpaID) }}" id="return-to-tasklist-btn" class="govuk-button">{{ tr .App "returnToTaskList" }}</a> + {{ end }} </div> </div> {{ end }}