diff --git a/cypress/e2e/voucher/your-declaration.cy.js b/cypress/e2e/voucher/your-declaration.cy.js new file mode 100644 index 0000000000..5addfcad56 --- /dev/null +++ b/cypress/e2e/voucher/your-declaration.cy.js @@ -0,0 +1,14 @@ +describe('Confirm your identity', () => { + beforeEach(() => { + cy.visit('/fixtures/voucher?redirect=/sign-the-declaration&progress=confirmYourIdentity'); + }); + + it('can be signed', () => { + cy.checkA11yApp(); + cy.contains('label', 'To the best of my knowledge').click(); + cy.contains('button', 'Submit my signature').click(); + + // TODO: this will change when the next ticket is picked up + cy.url().should('contain', '/task-list'); + }); +}); diff --git a/internal/voucher/voucherdata/provided.go b/internal/voucher/voucherdata/provided.go index 05629cb451..d2cd6d8f75 100644 --- a/internal/voucher/voucherdata/provided.go +++ b/internal/voucher/voucherdata/provided.go @@ -31,6 +31,12 @@ type Provided struct { // IdentityUserData records the results of the identity check taken by the // voucher. IdentityUserData identity.UserData + // SignedAt is the time the declaration was signed. + SignedAt time.Time +} + +func (p Provided) FullName() string { + return p.FirstNames + " " + p.LastName } func (p Provided) IdentityConfirmed() bool { diff --git a/internal/voucher/voucherdata/provided_test.go b/internal/voucher/voucherdata/provided_test.go new file mode 100644 index 0000000000..4627844891 --- /dev/null +++ b/internal/voucher/voucherdata/provided_test.go @@ -0,0 +1,48 @@ +package voucherdata + +import ( + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/stretchr/testify/assert" +) + +func TestProvidedFullName(t *testing.T) { + assert.Equal(t, "John Smith", Provided{FirstNames: "John", LastName: "Smith"}.FullName()) +} + +func TestProvidedIdentityConfirmed(t *testing.T) { + assert.True(t, Provided{ + FirstNames: "X", + LastName: "Y", + IdentityUserData: identity.UserData{ + Status: identity.StatusConfirmed, + FirstNames: "X", + LastName: "Y", + }, + }.IdentityConfirmed()) +} + +func TestProvidedIdentityConfirmedWhenNameNotMatch(t *testing.T) { + assert.False(t, Provided{ + FirstNames: "A", + LastName: "Y", + IdentityUserData: identity.UserData{ + Status: identity.StatusConfirmed, + FirstNames: "X", + LastName: "Y", + }, + }.IdentityConfirmed()) +} + +func TestProvidedIdentityConfirmedWhenNotConfirmed(t *testing.T) { + assert.False(t, Provided{ + FirstNames: "X", + LastName: "Y", + IdentityUserData: identity.UserData{ + Status: identity.StatusFailed, + FirstNames: "X", + LastName: "Y", + }, + }.IdentityConfirmed()) +} diff --git a/internal/voucher/voucherpage/mock_test.go b/internal/voucher/voucherpage/mock_test.go index c4cf2bafc6..5aac3ba436 100644 --- a/internal/voucher/voucherpage/mock_test.go +++ b/internal/voucher/voucherpage/mock_test.go @@ -2,6 +2,7 @@ package voucherpage import ( "errors" + "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" ) @@ -9,4 +10,6 @@ import ( var ( expectedError = errors.New("err") testAppData = appcontext.Data{LpaID: "lpa-id"} + testNow = time.Now() + testNowFn = func() time.Time { return testNow } ) diff --git a/internal/voucher/voucherpage/register.go b/internal/voucher/voucherpage/register.go index 67504ae7d2..a463091d3a 100644 --- a/internal/voucher/voucherpage/register.go +++ b/internal/voucher/voucherpage/register.go @@ -5,6 +5,7 @@ import ( "context" "io" "net/http" + "time" "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -130,6 +131,9 @@ func Register( Guidance(tmpls.Get("one_login_identity_details.gohtml"), lpaStoreResolvingService)) handleVoucher(voucher.PathUnableToConfirmIdentity, None, Guidance(tmpls.Get("unable_to_confirm_identity.gohtml"), lpaStoreResolvingService)) + + handleVoucher(voucher.PathSignTheDeclaration, None, + YourDeclaration(tmpls.Get("your_declaration.gohtml"), lpaStoreResolvingService, voucherStore, time.Now)) } type handleOpt byte diff --git a/internal/voucher/voucherpage/your_declaration.go b/internal/voucher/voucherpage/your_declaration.go new file mode 100644 index 0000000000..b5b7f147bf --- /dev/null +++ b/internal/voucher/voucherpage/your_declaration.go @@ -0,0 +1,79 @@ +package voucherpage + +import ( + "net/http" + "time" + + "github.com/ministryofjustice/opg-go-common/template" + "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/task" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher/voucherdata" +) + +type yourDeclarationData struct { + App appcontext.Data + Errors validation.List + Form *yourDeclarationForm + Lpa *lpadata.Lpa + Voucher *voucherdata.Provided +} + +func YourDeclaration(tmpl template.Template, lpaStoreResolvingService LpaStoreResolvingService, voucherStore VoucherStore, now func() time.Time) Handler { + return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request, provided *voucherdata.Provided) error { + if !provided.SignedAt.IsZero() { + return voucher.PathTaskList.Redirect(w, r, appData, appData.LpaID) + } + + lpa, err := lpaStoreResolvingService.Get(r.Context()) + if err != nil { + return err + } + + data := &yourDeclarationData{ + App: appData, + Form: &yourDeclarationForm{}, + Lpa: lpa, + Voucher: provided, + } + + if r.Method == http.MethodPost { + data.Form = readYourDeclarationForm(r) + data.Errors = data.Form.Validate() + + if data.Errors.None() { + provided.SignedAt = now() + provided.Tasks.SignTheDeclaration = task.StateCompleted + if err := voucherStore.Put(r.Context(), provided); err != nil { + return err + } + + return voucher.PathTaskList.Redirect(w, r, appData, appData.LpaID) + } + } + + return tmpl(w, data) + } +} + +type yourDeclarationForm struct { + Confirm bool +} + +func readYourDeclarationForm(r *http.Request) *yourDeclarationForm { + return &yourDeclarationForm{ + Confirm: page.PostFormString(r, "confirm") == "1", + } +} + +func (f *yourDeclarationForm) Validate() validation.List { + var errors validation.List + + errors.Bool("confirm", "youMustSelectTheBoxToVouch", f.Confirm, + validation.Selected().CustomError()) + + return errors +} diff --git a/internal/voucher/voucherpage/your_declaration_test.go b/internal/voucher/voucherpage/your_declaration_test.go new file mode 100644 index 0000000000..4ff7223784 --- /dev/null +++ b/internal/voucher/voucherpage/your_declaration_test.go @@ -0,0 +1,215 @@ +package voucherpage + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/task" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher/voucherdata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetYourDeclaration(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpa := &lpadata.Lpa{ + Voucher: lpadata.Voucher{FirstNames: "V", LastName: "W"}, + } + provided := &voucherdata.Provided{LpaID: "lpa-id"} + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(lpa, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &yourDeclarationData{ + App: testAppData, + Lpa: lpa, + Voucher: provided, + Form: &yourDeclarationForm{}, + }). + Return(nil) + + err := YourDeclaration(template.Execute, lpaStoreResolvingService, nil, nil)(testAppData, w, r, provided) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetYourDeclarationWhenSigned(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + err := YourDeclaration(nil, nil, nil, nil)(testAppData, w, r, &voucherdata.Provided{ + LpaID: "lpa-id", + SignedAt: time.Now(), + }) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, voucher.PathTaskList.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestGetYourDeclarationWhenLpaStoreResolvingServiceErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(nil, expectedError) + + err := YourDeclaration(nil, lpaStoreResolvingService, nil, nil)(testAppData, w, r, &voucherdata.Provided{}) + + assert.Equal(t, expectedError, err) +} + +func TestGetYourDeclarationWhenTemplateErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{}, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, mock.Anything). + Return(expectedError) + + err := YourDeclaration(template.Execute, lpaStoreResolvingService, nil, nil)(testAppData, w, r, &voucherdata.Provided{}) + + assert.Equal(t, expectedError, err) +} + +func TestPostYourDeclaration(t *testing.T) { + f := url.Values{ + "confirm": {"1"}, + } + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{Donor: lpadata.Donor{LastName: "Smith"}}, nil) + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(r.Context(), &voucherdata.Provided{ + LpaID: "lpa-id", + SignedAt: testNow, + Tasks: voucherdata.Tasks{SignTheDeclaration: task.StateCompleted}, + }). + Return(nil) + + err := YourDeclaration(nil, lpaStoreResolvingService, voucherStore, testNowFn)(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, voucher.PathTaskList.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestPostYourDeclarationWhenValidationError(t *testing.T) { + f := url.Values{ + "confirm": {"2"}, + } + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{Donor: lpadata.Donor{LastName: "Smith"}}, nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, mock.MatchedBy(func(d *yourDeclarationData) bool { + return assert.Equal(t, validation.With("confirm", validation.CustomError{Label: "youMustSelectTheBoxToVouch"}), d.Errors) + })). + Return(nil) + + err := YourDeclaration(template.Execute, lpaStoreResolvingService, nil, nil)(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostYourDeclarationWhenStoreErrors(t *testing.T) { + f := url.Values{ + "confirm": {"1"}, + } + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{}, nil) + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(r.Context(), mock.Anything). + Return(expectedError) + + err := YourDeclaration(nil, lpaStoreResolvingService, voucherStore, testNowFn)(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + assert.Equal(t, expectedError, err) +} + +func TestReadYourDeclarationForm(t *testing.T) { + form := url.Values{ + "confirm": {"1"}, + } + + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + result := readYourDeclarationForm(r) + assert.Equal(t, true, result.Confirm) +} + +func TestYourDeclarationFormValidate(t *testing.T) { + testCases := map[string]struct { + form *yourDeclarationForm + errors validation.List + }{ + "valid": { + form: &yourDeclarationForm{ + Confirm: true, + }, + }, + "not selected": { + form: &yourDeclarationForm{}, + errors: validation.With("confirm", validation.CustomError{Label: "youMustSelectTheBoxToVouch"}), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.errors, tc.form.Validate()) + }) + } +} diff --git a/lang/cy.json b/lang/cy.json index 748644d229..0d70ea41be 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -337,6 +337,7 @@ "wantToApply": "Fy mod eisiau gwneud cais i gofrestru’r LPA hon", "howTickingActsAsSignature": "Sut mae ticio’r blychau yn gweithredu fel eich llofnod cyfreithiol", "howTickingActsAsSignatureContent": "
Mae llofnodion electronig yn cael eu cydnabod mewn cyfraith yng Nghymru a Lloegr. Maent yn ei gwneud yn haws i chi lofnodi dogfennau ar-lein.
Drwy roi tic yn y ddau flwch a dewis ‘Cyflwyno fy llofnod’, rydych yn darparu eich llofnod cyfreithiol. Mae hyn yr un mor ddiogel a rhwymol mewn cyfraith â llofnodi’ch LPA â llaw.
", + "howTickingActsAsSignatureDeclarationContent": "Welsh
", "submitMySignature": "Cyflwyno fy llofnod", "bothBoxesToSignAndApply": "y ddau flwch i lofnodi ac i wneud cais i gofrestru eich LPA", "iWantToSignThisLpa": "Fy mod eisiau llofnodi’r LPA hon fel gweithred", @@ -1338,5 +1339,10 @@ "youHaveToldUsDetailsDoNotMatchIdentity": "Welsh {{.DonorFullNamePossessive}}", "youHaveToldUsDetailsDoNotMatchIdentityContent": "Welsh {{.DonorFirstNames}}
", "voucherConfirmYourIdentityContent": "Welsh {{.DonorFullName}}
", - "voucherUnableToConfirmIdentityContent": "Welsh {{.DonorFullName}} {{.DonorFirstNames}}
" + "voucherUnableToConfirmIdentityContent": "Welsh {{.DonorFullName}} {{.DonorFirstNames}}
", + "yourDeclaration": "Welsh", + "yourDeclarationContent": "Welsh {{.DonorFullNamePossessive}} {{.DonorFirstNames}} {{.DonorFirstNamesPossessive}}
", + "youMustSelectTheBoxToVouch": "Welsh", + "iAmVouchingThat": "Welsh {{.VoucherFullName}}", + "toTheBestOfMyKnowledgeDeclaration": "Welsh {{.DonorFullName}}" } diff --git a/lang/en.json b/lang/en.json index 5199546c5a..33220ce20c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -291,6 +291,7 @@ "wantToApply": "I want to apply to register this LPA", "howTickingActsAsSignature": "How ticking the boxes acts as your legal signature", "howTickingActsAsSignatureContent": "Electronic signatures are legally recognised in England and Wales. They make it easier for you to sign documents online.
By ticking both boxes and selecting ‘Submit my signature’, you are providing your legal signature. This is just as safe and legally binding as signing your LPA by hand.
", + "howTickingActsAsSignatureDeclarationContent": "Electronic signatures are legally recognised in England and Wales. They make it easier for you to sign documents online.
By ticking both boxes and selecting ‘Submit my signature’, you are providing your legal signature. This is just as safe and legally binding as signing your declaration by hand.
", "submitMySignature": "Submit my signature", "bothBoxesToSignAndApply": "both boxes to sign and apply to register your LPA", "iWantToSignThisLpa": "I want to sign this LPA as a deed", @@ -1267,5 +1268,10 @@ "youHaveToldUsDetailsDoNotMatchIdentity": "You have told us that the details do not match {{.DonorFullNamePossessive}} identity.", "youHaveToldUsDetailsDoNotMatchIdentityContent": "We have contacted {{.DonorFirstNames}} to let them know. You do not need to do anything else.
You can now close this browsing window.
", "voucherConfirmYourIdentityContent": "Before you can vouch for {{.DonorFullName}}, you need to confirm your own identity.
You can do this either:
You will need an identity document, for example your passport or driving licence.
It takes longer to do it at a Post Office, but you can usually get an outcome within a day.
When you continue, you’ll go to GOV.UK One Login where you can choose if you want to confirm your identity online or in person.
", - "voucherUnableToConfirmIdentityContent": "This means you cannot vouch for {{.DonorFullName}}. We have contacted {{.DonorFirstNames}} to let them know.
You do not need to do anything else.
You can now close this browsing window.
" + "voucherUnableToConfirmIdentityContent": "This means you cannot vouch for {{.DonorFullName}}. We have contacted {{.DonorFirstNames}} to let them know.
You do not need to do anything else.
You can now close this browsing window.
", + "yourDeclaration": "Your declaration", + "yourDeclarationContent": "Sign your declaration to vouch for {{.DonorFullNamePossessive}} identity.
By signing this declaration, I confirm all of the following: