From 21f36266484be7cb3f059497b7559d91b02dc9db Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell Date: Tue, 3 Sep 2024 10:33:29 +0100 Subject: [PATCH] MLPAB-2416 MLPAB-2417 Trust corporation opt-out (#256) * Add test to show replacement attorney opt-out works * Add trust corporation opt-out update --- internal/shared/lpa.go | 39 ---- internal/shared/lpa_test.go | 111 ------------ lambda/update/attorney_opt_out.go | 21 +-- lambda/update/attorney_opt_out_test.go | 22 +++ lambda/update/trust_corporation_opt_out.go | 39 ++++ .../update/trust_corporation_opt_out_test.go | 171 ++++++++++++++++++ lambda/update/validate.go | 2 + 7 files changed, 244 insertions(+), 161 deletions(-) create mode 100644 lambda/update/trust_corporation_opt_out.go create mode 100644 lambda/update/trust_corporation_opt_out_test.go diff --git a/internal/shared/lpa.go b/internal/shared/lpa.go index 932d0889..0fa3a6fa 100644 --- a/internal/shared/lpa.go +++ b/internal/shared/lpa.go @@ -1,7 +1,6 @@ package shared import ( - "slices" "time" ) @@ -27,44 +26,6 @@ type LpaInit struct { CertificateProviderNotRelatedConfirmedAt *time.Time `json:"certificateProviderNotRelatedConfirmedAt,omitempty"` } -func (l *Lpa) GetAttorney(uid string) (Attorney, bool) { - idx := slices.IndexFunc(l.Attorneys, func(a Attorney) bool { return a.UID == uid }) - if idx == -1 { - return Attorney{}, false - } - - return l.Attorneys[idx], true -} - -func (l *Lpa) PutAttorney(attorney Attorney) { - idx := slices.IndexFunc(l.Attorneys, func(a Attorney) bool { return a.UID == attorney.UID }) - if idx == -1 { - l.Attorneys = append(l.Attorneys, attorney) - } else { - l.Attorneys[idx] = attorney - } -} - -func (l *Lpa) ActiveAttorneys() (attorneys []Attorney) { - for _, a := range l.Attorneys { - if a.Status == AttorneyStatusActive { - attorneys = append(attorneys, a) - } - } - - return attorneys -} - -func (l *Lpa) ActiveTrustCorporations() (trustCorporations []TrustCorporation) { - for _, tc := range l.TrustCorporations { - if tc.Status == AttorneyStatusActive { - trustCorporations = append(trustCorporations, tc) - } - } - - return trustCorporations -} - type Lpa struct { LpaInit Uid string `json:"uid"` diff --git a/internal/shared/lpa_test.go b/internal/shared/lpa_test.go index 996ad995..4cb2e1a2 100644 --- a/internal/shared/lpa_test.go +++ b/internal/shared/lpa_test.go @@ -19,114 +19,3 @@ func TestLpaInitMarshalJSON(t *testing.T) { data, _ := json.Marshal(LpaInit{}) assert.JSONEq(t, expected, string(data)) } - -func TestAttorneysGet(t *testing.T) { - testCases := map[string]struct { - attorneys []Attorney - expectedAttorney Attorney - uid string - expectedFound bool - }{ - "found": { - attorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - {Person: Person{UID: "xyz", FirstNames: "b"}}, - }, - expectedAttorney: Attorney{Person: Person{UID: "xyz", FirstNames: "b"}}, - uid: "xyz", - expectedFound: true, - }, - "not found": { - attorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - {Person: Person{UID: "xyz", FirstNames: "b"}}, - }, - expectedAttorney: Attorney{}, - uid: "not-a-match", - expectedFound: false, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - lpa := &Lpa{LpaInit: LpaInit{Attorneys: tc.attorneys}} - a, found := lpa.GetAttorney(tc.uid) - - assert.Equal(t, tc.expectedFound, found) - assert.Equal(t, tc.expectedAttorney, a) - }) - } -} - -func TestAttorneysPut(t *testing.T) { - testCases := map[string]struct { - attorneys []Attorney - expectedAttorneys []Attorney - updatedAttorney Attorney - }{ - "does not exist": { - attorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - }, - expectedAttorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - {Person: Person{UID: "xyz", FirstNames: "b"}}, - }, - updatedAttorney: Attorney{Person: Person{UID: "xyz", FirstNames: "b"}}, - }, - "exists": { - attorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - {Person: Person{UID: "xyz", FirstNames: "b"}}, - }, - expectedAttorneys: []Attorney{ - {Person: Person{UID: "abc", FirstNames: "a"}}, - {Person: Person{UID: "xyz", FirstNames: "z"}}, - }, - updatedAttorney: Attorney{Person: Person{UID: "xyz", FirstNames: "z"}}, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - lpa := &Lpa{LpaInit: LpaInit{Attorneys: tc.attorneys}} - lpa.PutAttorney(tc.updatedAttorney) - - assert.Equal(t, tc.expectedAttorneys, lpa.Attorneys) - }) - } -} - -func TestActiveAttorneys(t *testing.T) { - lpa := &Lpa{LpaInit: LpaInit{ - Attorneys: []Attorney{ - {Person: Person{FirstNames: "a"}}, - {Person: Person{FirstNames: "b"}, Status: AttorneyStatusActive}, - {Person: Person{FirstNames: "c"}, Status: AttorneyStatusReplacement}, - {Person: Person{FirstNames: "d"}, Status: AttorneyStatusRemoved}, - {Person: Person{FirstNames: "e"}, Status: AttorneyStatusActive}, - }, - }} - - assert.Equal(t, []Attorney{ - {Person: Person{FirstNames: "b"}, Status: AttorneyStatusActive}, - {Person: Person{FirstNames: "e"}, Status: AttorneyStatusActive}, - }, lpa.ActiveAttorneys()) -} - -func TestActiveTrustCorporations(t *testing.T) { - lpa := &Lpa{LpaInit: LpaInit{ - TrustCorporations: []TrustCorporation{ - {Name: "a"}, - {Name: "b", Status: AttorneyStatusActive}, - {Name: "c", Status: AttorneyStatusReplacement}, - {Name: "d", Status: AttorneyStatusRemoved}, - {Name: "e", Status: AttorneyStatusActive}, - }, - }} - - assert.Equal(t, []TrustCorporation{ - {Name: "b", Status: AttorneyStatusActive}, - {Name: "e", Status: AttorneyStatusActive}, - }, lpa.ActiveTrustCorporations()) -} diff --git a/lambda/update/attorney_opt_out.go b/lambda/update/attorney_opt_out.go index 107e08a3..d9952760 100644 --- a/lambda/update/attorney_opt_out.go +++ b/lambda/update/attorney_opt_out.go @@ -10,19 +10,18 @@ type AttorneyOptOut struct { } func (c AttorneyOptOut) Apply(lpa *shared.Lpa) []shared.FieldError { - attorney, ok := lpa.GetAttorney(c.AttorneyUID) - if !ok { - return []shared.FieldError{{Source: "/type", Detail: "attorney not found"}} + for i := range lpa.Attorneys { + if lpa.Attorneys[i].UID == c.AttorneyUID { + if lpa.Attorneys[i].SignedAt != nil && !lpa.Attorneys[i].SignedAt.IsZero() { + return []shared.FieldError{{Source: "/type", Detail: "attorney cannot opt out after signing"}} + } + + lpa.Attorneys[i].Status = shared.AttorneyStatusRemoved + return nil + } } - if attorney.SignedAt != nil && !attorney.SignedAt.IsZero() { - return []shared.FieldError{{Source: "/type", Detail: "attorney cannot opt out after signing"}} - } - - attorney.Status = shared.AttorneyStatusRemoved - lpa.PutAttorney(attorney) - - return nil + return []shared.FieldError{{Source: "/type", Detail: "attorney not found"}} } func validateAttorneyOptOut(update shared.Update) (AttorneyOptOut, []shared.FieldError) { diff --git a/lambda/update/attorney_opt_out_test.go b/lambda/update/attorney_opt_out_test.go index 9c249d1c..b7fa9c60 100644 --- a/lambda/update/attorney_opt_out_test.go +++ b/lambda/update/attorney_opt_out_test.go @@ -39,6 +39,28 @@ func TestAttorneyOptOutApply(t *testing.T) { }, }, }, + "successful apply to replacement": { + lpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + Attorneys: []shared.Attorney{ + {Person: shared.Person{UID: "a"}, Status: shared.AttorneyStatusActive}, + {Person: shared.Person{UID: "b"}, Status: shared.AttorneyStatusReplacement}, + {Person: shared.Person{UID: "c"}, Status: shared.AttorneyStatusActive}, + }, + }, + }, + expectedLpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + Attorneys: []shared.Attorney{ + {Person: shared.Person{UID: "a"}, Status: shared.AttorneyStatusActive}, + {Person: shared.Person{UID: "b"}, Status: shared.AttorneyStatusRemoved}, + {Person: shared.Person{UID: "c"}, Status: shared.AttorneyStatusActive}, + }, + }, + }, + }, "not found": { lpa: &shared.Lpa{ Status: shared.LpaStatusInProgress, diff --git a/lambda/update/trust_corporation_opt_out.go b/lambda/update/trust_corporation_opt_out.go new file mode 100644 index 00000000..052ff642 --- /dev/null +++ b/lambda/update/trust_corporation_opt_out.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/ministryofjustice/opg-data-lpa-store/internal/shared" + "github.com/ministryofjustice/opg-data-lpa-store/internal/validate" +) + +type TrustCorporationOptOut struct { + trustCorporationUID string +} + +func (c TrustCorporationOptOut) Apply(lpa *shared.Lpa) []shared.FieldError { + for i := range lpa.TrustCorporations { + if lpa.TrustCorporations[i].UID == c.trustCorporationUID { + if len(lpa.TrustCorporations[i].Signatories) > 0 && !lpa.TrustCorporations[i].Signatories[0].SignedAt.IsZero() { + return []shared.FieldError{{Source: "/type", Detail: "trust corporation cannot opt out after signing"}} + } + + lpa.TrustCorporations[i].Status = shared.AttorneyStatusRemoved + return nil + } + } + + return []shared.FieldError{{Source: "/type", Detail: "trust corporation not found"}} +} + +func validateTrustCorporationOptOut(update shared.Update) (TrustCorporationOptOut, []shared.FieldError) { + if len(update.Changes) > 0 { + return TrustCorporationOptOut{}, []shared.FieldError{{Source: "/changes", Detail: "expected empty"}} + } + + author := update.Author.Details() + + if errs := validate.UUID("/author", author.UID); len(errs) > 0 { + return TrustCorporationOptOut{}, errs + } + + return TrustCorporationOptOut{trustCorporationUID: author.UID}, nil +} diff --git a/lambda/update/trust_corporation_opt_out_test.go b/lambda/update/trust_corporation_opt_out_test.go new file mode 100644 index 00000000..d1110c92 --- /dev/null +++ b/lambda/update/trust_corporation_opt_out_test.go @@ -0,0 +1,171 @@ +package main + +import ( + "encoding/json" + "testing" + "time" + + "github.com/ministryofjustice/opg-data-lpa-store/internal/shared" + "github.com/stretchr/testify/assert" +) + +func TestTrustCorporationOptOutApply(t *testing.T) { + now := time.Now() + + testcases := map[string]struct { + lpa *shared.Lpa + expectedLpa *shared.Lpa + errors []shared.FieldError + }{ + "successful apply": { + lpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + {UID: "b", Status: shared.AttorneyStatusActive}, + {UID: "c", Status: shared.AttorneyStatusActive}, + }, + }, + }, + expectedLpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + {UID: "b", Status: shared.AttorneyStatusRemoved}, + {UID: "c", Status: shared.AttorneyStatusActive}, + }, + }, + }, + }, + "successful apply to replacement": { + lpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + {UID: "b", Status: shared.AttorneyStatusReplacement}, + {UID: "c", Status: shared.AttorneyStatusActive}, + }, + }, + }, + expectedLpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + {UID: "b", Status: shared.AttorneyStatusRemoved}, + {UID: "c", Status: shared.AttorneyStatusActive}, + }, + }, + }, + }, + "not found": { + lpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + }, + }, + }, + expectedLpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "a", Status: shared.AttorneyStatusActive}, + }, + }, + }, + errors: []shared.FieldError{ + {Source: "/type", Detail: "trust corporation not found"}, + }, + }, + "already signed": { + lpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "b", Status: shared.AttorneyStatusActive, Signatories: []shared.Signatory{{SignedAt: now}}}, + }, + }, + }, + expectedLpa: &shared.Lpa{ + Status: shared.LpaStatusInProgress, + LpaInit: shared.LpaInit{ + TrustCorporations: []shared.TrustCorporation{ + {UID: "b", Status: shared.AttorneyStatusActive, Signatories: []shared.Signatory{{SignedAt: now}}}, + }, + }, + }, + errors: []shared.FieldError{ + {Source: "/type", Detail: "trust corporation cannot opt out after signing"}, + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + c := TrustCorporationOptOut{trustCorporationUID: "b"} + + errors := c.Apply(tc.lpa) + + assert.Equal(t, tc.errors, errors) + assert.Equal(t, tc.expectedLpa, tc.lpa) + }) + } +} + +func TestValidateUpdateTrustCorporationOptOut(t *testing.T) { + testcases := map[string]struct { + update shared.Update + errors []shared.FieldError + expected TrustCorporationOptOut + }{ + "valid": { + update: shared.Update{ + Author: "urn:opg:poas:makeregister:users:dc487ebb-b39d-45ed-bb6a-7f950fd355c9", + Type: "TRUST_CORPORATION_OPT_OUT", + Changes: []shared.Change{}, + }, + expected: TrustCorporationOptOut{trustCorporationUID: "dc487ebb-b39d-45ed-bb6a-7f950fd355c9"}, + }, + "with changes": { + update: shared.Update{ + Author: "urn:opg:poas:makeregister:users:dc487ebb-b39d-45ed-bb6a-7f950fd355c9", + Type: "TRUST_CORPORATION_OPT_OUT", + Changes: []shared.Change{ + { + Key: "/something/someValue", + New: json.RawMessage(`"not expected"`), + Old: jsonNull, + }, + }, + }, + expected: TrustCorporationOptOut{}, + errors: []shared.FieldError{ + {Source: "/changes", Detail: "expected empty"}, + }, + }, + "author UID not valid": { + update: shared.Update{ + Author: "urn:opg:poas:makeregister:users:not-a-uid", + Type: "TRUST_CORPORATION_OPT_OUT", + Changes: []shared.Change{}, + }, + expected: TrustCorporationOptOut{}, + errors: []shared.FieldError{ + {Source: "/author", Detail: "invalid format"}, + }, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + data, errors := validateUpdate(tc.update, &shared.Lpa{}) + assert.Equal(t, tc.expected, data) + assert.ElementsMatch(t, tc.errors, errors) + }) + } +} diff --git a/lambda/update/validate.go b/lambda/update/validate.go index 5a710c9f..1e925fb0 100644 --- a/lambda/update/validate.go +++ b/lambda/update/validate.go @@ -30,6 +30,8 @@ func validateUpdate(update shared.Update, lpa *shared.Lpa) (Applyable, []shared. return validateDonorWithdrawLPA(update.Changes) case "ATTORNEY_OPT_OUT": return validateAttorneyOptOut(update) + case "TRUST_CORPORATION_OPT_OUT": + return validateTrustCorporationOptOut(update) default: return nil, []shared.FieldError{{Source: "/type", Detail: "invalid value"}} }