From a37da016594a4c97983849e4a8934b7f5889b4f3 Mon Sep 17 00:00:00 2001 From: Grady Berry Ward Date: Tue, 16 Jan 2024 12:32:01 -0700 Subject: [PATCH] Implements PortfolioProperties across the stack (#139) --- azure/azevents/azevents.go | 19 +++-- cmd/server/pactasrv/conv/oapi_to_pacta.go | 10 +++ cmd/server/pactasrv/conv/pacta_to_oapi.go | 76 ++++++++++++------- cmd/server/pactasrv/portfolio.go | 9 +++ cmd/server/pactasrv/upload.go | 21 +++-- db/db.go | 50 +++++++++++- db/sqldb/golden/human_readable_schema.sql | 6 +- db/sqldb/golden/schema_dump.sql | 8 +- db/sqldb/incomplete_upload.go | 30 +++----- db/sqldb/incomplete_upload_test.go | 33 +++++--- .../0012_portfolio_properties.down.sql | 13 ++++ .../0012_portfolio_properties.up.sql | 13 ++++ db/sqldb/portfolio.go | 32 ++------ db/sqldb/portfolio_test.go | 22 ++++-- db/sqldb/sqldb.go | 4 + db/sqldb/sqldb_test.go | 2 +- .../components/ExplicitTriStateCheckbox.vue | 56 ++++++++++++++ .../components/inputs/EngagementStrategy.vue | 33 ++++++++ frontend/components/inputs/Esg.vue | 33 ++++++++ frontend/components/inputs/External.vue | 33 ++++++++ frontend/components/inputs/HoldingDate.vue | 29 +++++++ frontend/components/portfolio/Editor.vue | 24 ++++++ frontend/components/portfolio/ListView.vue | 5 -- frontend/lib/editor/incomplete_upload.ts | 20 ++++- frontend/lib/editor/portfolio.ts | 19 ++++- frontend/openapi/generated/pacta/index.ts | 1 + .../pacta/models/IncompleteUpload.ts | 15 +++- .../generated/pacta/models/OptionalBoolean.ts | 10 +++ .../generated/pacta/models/Portfolio.ts | 15 +++- .../pacta/models/PortfolioChanges.ts | 16 ++++ .../pacta/models/StartPortfolioUploadReq.ts | 15 +++- frontend/pages/upload.vue | 71 +++++++++++++---- frontend/plugins/primevue.ts | 2 + openapi/pacta.yaml | 63 +++++++++++++-- pacta/pacta.go | 32 +++++++- 35 files changed, 684 insertions(+), 156 deletions(-) create mode 100644 db/sqldb/migrations/0012_portfolio_properties.down.sql create mode 100644 db/sqldb/migrations/0012_portfolio_properties.up.sql create mode 100644 frontend/components/ExplicitTriStateCheckbox.vue create mode 100644 frontend/components/inputs/EngagementStrategy.vue create mode 100644 frontend/components/inputs/Esg.vue create mode 100644 frontend/components/inputs/External.vue create mode 100644 frontend/components/inputs/HoldingDate.vue create mode 100644 frontend/openapi/generated/pacta/models/OptionalBoolean.ts diff --git a/azure/azevents/azevents.go b/azure/azevents/azevents.go index 004d676..8d0e262 100644 --- a/azure/azevents/azevents.go +++ b/azure/azevents/azevents.go @@ -299,7 +299,7 @@ func (s *Server) handleParsedPortfolio(id string, resp *task.ParsePortfolioRespo if len(incompleteUploads) == 0 { return fmt.Errorf("no incomplete uploads found for ids: %v", resp.Request.IncompleteUploadIDs) } - var holdingsDate *pacta.HoldingsDate + var properties *pacta.PortfolioProperties var ownerID pacta.OwnerID for _, iu := range incompleteUploads { if ownerID == "" { @@ -307,14 +307,13 @@ func (s *Server) handleParsedPortfolio(id string, resp *task.ParsePortfolioRespo } else if ownerID != iu.Owner.ID { return fmt.Errorf("multiple owners found for incomplete uploads: %+v", incompleteUploads) } - if iu.HoldingsDate == nil { - return fmt.Errorf("incomplete upload %s had no holdings date", iu.ID) - } - if holdingsDate == nil { - holdingsDate = iu.HoldingsDate - } else if iu.HoldingsDate != nil && *holdingsDate != *iu.HoldingsDate { - // Question for Grady: can iu.HoldingsDate ever be nil? - return fmt.Errorf("multiple holdings dates found for incomplete uploads: %+v", incompleteUploads) + + if properties == nil { + properties = &iu.Properties + } else if *properties != iu.Properties { + // TODO(#75) We currently don't support merging portfolios with different properties. + // but we could change that if we get better input output correlation information. + return fmt.Errorf("multiple properties found for incomplete uploads: %+v", incompleteUploads) } if iu.RanAt.After(ranAt) { ranAt = iu.RanAt @@ -330,7 +329,7 @@ func (s *Server) handleParsedPortfolio(id string, resp *task.ParsePortfolioRespo Name: output.Blob.FileName, NumberOfRows: output.LineCount, Blob: &pacta.Blob{ID: blobID}, - HoldingsDate: holdingsDate, + Properties: *properties, }) if err != nil { return fmt.Errorf("creating portfolio %d: %w", i, err) diff --git a/cmd/server/pactasrv/conv/oapi_to_pacta.go b/cmd/server/pactasrv/conv/oapi_to_pacta.go index ee2c4e0..bd2bf5c 100644 --- a/cmd/server/pactasrv/conv/oapi_to_pacta.go +++ b/cmd/server/pactasrv/conv/oapi_to_pacta.go @@ -27,6 +27,16 @@ func LanguageFromOAPI(l api.Language) (pacta.Language, error) { return "", oapierr.BadRequest("unknown language", zap.String("language", string(l))) } +func OptionalBoolFromOAPI(b api.OptionalBoolean) *bool { + switch b { + case api.OptionalBooleanFALSE: + return ptr(false) + case api.OptionalBooleanTRUE: + return ptr(true) + } + return nil +} + func InitiativeCreateFromOAPI(i *api.InitiativeCreate) (*pacta.Initiative, error) { if i == nil { return nil, oapierr.BadRequest("InitiativeCreate cannot be nil") diff --git a/cmd/server/pactasrv/conv/pacta_to_oapi.go b/cmd/server/pactasrv/conv/pacta_to_oapi.go index 981368d..d6511cf 100644 --- a/cmd/server/pactasrv/conv/pacta_to_oapi.go +++ b/cmd/server/pactasrv/conv/pacta_to_oapi.go @@ -24,6 +24,16 @@ func LanguageToOAPI(l pacta.Language) (api.Language, error) { } } +func optionalBoolToOAPI(b *bool) api.OptionalBoolean { + if b == nil { + return api.OptionalBooleanUNSET + } else if *b { + return api.OptionalBooleanTRUE + } else { + return api.OptionalBooleanFALSE + } +} + func InitiativeToOAPI(i *pacta.Initiative) (*api.Initiative, error) { if i == nil { return nil, oapierr.Internal("initiativeToOAPI: can't convert nil pointer") @@ -190,25 +200,31 @@ func IncompleteUploadToOAPI(iu *pacta.IncompleteUpload) (*api.IncompleteUpload, if iu == nil { return nil, oapierr.Internal("incompleteUploadToOAPI: can't convert nil pointer") } - hd, err := HoldingsDateToOAPI(iu.HoldingsDate) - if err != nil { - return nil, oapierr.Internal("incompleteUploadToOAPI: holdingsDateToOAPI failed", zap.Error(err)) - } fc, err := FailureCodeToOAPI(iu.FailureCode) if err != nil { return nil, oapierr.Internal("incompleteUploadToOAPI: failureCodeToOAPI failed", zap.Error(err)) } + var hd *api.HoldingsDate + if iu.Properties.HoldingsDate != nil { + hd, err = HoldingsDateToOAPI(iu.Properties.HoldingsDate) + if err != nil { + return nil, oapierr.Internal("incompleteUploadToOAPI: holdingsDateToOAPI failed", zap.Error(err)) + } + } return &api.IncompleteUpload{ - Id: string(iu.ID), - Name: iu.Name, - Description: iu.Description, - HoldingsDate: hd, - CreatedAt: iu.CreatedAt, - RanAt: timeToNilable(iu.RanAt), - CompletedAt: timeToNilable(iu.CompletedAt), - FailureCode: fc, - FailureMessage: stringToNilable(iu.FailureMessage), - AdminDebugEnabled: iu.AdminDebugEnabled, + Id: string(iu.ID), + Name: iu.Name, + Description: iu.Description, + CreatedAt: iu.CreatedAt, + RanAt: timeToNilable(iu.RanAt), + CompletedAt: timeToNilable(iu.CompletedAt), + FailureCode: fc, + FailureMessage: stringToNilable(iu.FailureMessage), + AdminDebugEnabled: iu.AdminDebugEnabled, + PropertyHoldingsDate: hd, + PropertyESG: optionalBoolToOAPI(iu.Properties.ESG), + PropertyExternal: optionalBoolToOAPI(iu.Properties.External), + PropertyEngagementStrategy: optionalBoolToOAPI(iu.Properties.EngagementStrategy), }, nil } @@ -220,10 +236,6 @@ func PortfolioToOAPI(p *pacta.Portfolio) (*api.Portfolio, error) { if p == nil { return nil, oapierr.Internal("portfolioToOAPI: can't convert nil pointer") } - hd, err := HoldingsDateToOAPI(p.HoldingsDate) - if err != nil { - return nil, oapierr.Internal("portfolioToOAPI: holdingsDateToOAPI failed", zap.Error(err)) - } portfolioGroupMemberships := []api.PortfolioGroupMembershipPortfolioGroup{} for _, m := range p.PortfolioGroupMemberships { pg, err := PortfolioGroupToOAPI(m.PortfolioGroup) @@ -239,16 +251,26 @@ func PortfolioToOAPI(p *pacta.Portfolio) (*api.Portfolio, error) { if err != nil { return nil, oapierr.Internal("initiativeToOAPI: portfolioInitiativeMembershipToOAPIInitiative failed", zap.Error(err)) } + var hd *api.HoldingsDate + if p.Properties.HoldingsDate != nil { + hd, err = HoldingsDateToOAPI(p.Properties.HoldingsDate) + if err != nil { + return nil, oapierr.Internal("portfolioToOAPI: holdingsDateToOAPI failed", zap.Error(err)) + } + } return &api.Portfolio{ - Id: string(p.ID), - Name: p.Name, - Description: p.Description, - HoldingsDate: hd, - CreatedAt: p.CreatedAt, - NumberOfRows: p.NumberOfRows, - AdminDebugEnabled: p.AdminDebugEnabled, - Groups: &portfolioGroupMemberships, - Initiatives: &pims, + Id: string(p.ID), + Name: p.Name, + Description: p.Description, + CreatedAt: p.CreatedAt, + NumberOfRows: p.NumberOfRows, + AdminDebugEnabled: p.AdminDebugEnabled, + Groups: &portfolioGroupMemberships, + Initiatives: &pims, + PropertyHoldingsDate: hd, + PropertyESG: optionalBoolToOAPI(p.Properties.ESG), + PropertyExternal: optionalBoolToOAPI(p.Properties.External), + PropertyEngagementStrategy: optionalBoolToOAPI(p.Properties.EngagementStrategy), }, nil } diff --git a/cmd/server/pactasrv/portfolio.go b/cmd/server/pactasrv/portfolio.go index 6618303..b93f5a2 100644 --- a/cmd/server/pactasrv/portfolio.go +++ b/cmd/server/pactasrv/portfolio.go @@ -90,6 +90,15 @@ func (s *Server) UpdatePortfolio(ctx context.Context, request api.UpdatePortfoli if request.Body.Description != nil { mutations = append(mutations, db.SetPortfolioDescription(*request.Body.Description)) } + if request.Body.PropertyESG != nil { + mutations = append(mutations, db.SetPortfolioPropertyESG(conv.OptionalBoolFromOAPI(*request.Body.PropertyESG))) + } + if request.Body.PropertyExternal != nil { + mutations = append(mutations, db.SetPortfolioPropertyExternal(conv.OptionalBoolFromOAPI(*request.Body.PropertyExternal))) + } + if request.Body.PropertyEngagementStrategy != nil { + mutations = append(mutations, db.SetPortfolioPropertyEngagementStrategy(conv.OptionalBoolFromOAPI(*request.Body.PropertyEngagementStrategy))) + } if request.Body.AdminDebugEnabled != nil { if *request.Body.AdminDebugEnabled { if err := s.portfolioDoAuthzAndAuditLog(ctx, id, pacta.AuditLogAction_EnableAdminDebug); err != nil { diff --git a/cmd/server/pactasrv/upload.go b/cmd/server/pactasrv/upload.go index f5371ad..2f4a59a 100644 --- a/cmd/server/pactasrv/upload.go +++ b/cmd/server/pactasrv/upload.go @@ -25,10 +25,17 @@ func (s *Server) StartPortfolioUpload(ctx context.Context, request api.StartPort return nil, err } owner := &pacta.Owner{ID: actorInfo.OwnerID} - holdingsDate, err := conv.HoldingsDateFromOAPI(&request.Body.HoldingsDate) - if err != nil { - return nil, err + properties := pacta.PortfolioProperties{} + if request.Body.PropertyHoldingsDate != nil { + properties.HoldingsDate, err = conv.HoldingsDateFromOAPI(request.Body.PropertyHoldingsDate) + if err != nil { + return nil, err + } } + properties.ESG = conv.OptionalBoolFromOAPI(request.Body.PropertyESG) + properties.External = conv.OptionalBoolFromOAPI(request.Body.PropertyExternal) + properties.EngagementStrategy = conv.OptionalBoolFromOAPI(request.Body.PropertyEngagementStrategy) + n := len(request.Body.Items) if n > 25 { // TODO(#71) Implement basic limits @@ -76,10 +83,10 @@ func (s *Server) StartPortfolioUpload(ctx context.Context, request api.StartPort } blob.ID = blobID iuid, err := s.DB.CreateIncompleteUpload(tx, &pacta.IncompleteUpload{ - Blob: blob, - Name: blob.FileName, - HoldingsDate: holdingsDate, - Owner: owner, + Blob: blob, + Name: blob.FileName, + Properties: properties, + Owner: owner, }) if err != nil { return fmt.Errorf("creating incomplete upload %d: %w", i, err) diff --git a/db/db.go b/db/db.go index 644fa18..17e752c 100644 --- a/db/db.go +++ b/db/db.go @@ -209,9 +209,30 @@ func SetPortfolioDescription(value string) UpdatePortfolioFn { } } -func SetPortfolioHoldingsDate(value *pacta.HoldingsDate) UpdatePortfolioFn { +func SetPortfolioPropertyHoldingsDate(value *pacta.HoldingsDate) UpdatePortfolioFn { return func(v *pacta.Portfolio) error { - v.HoldingsDate = value + v.Properties.HoldingsDate = value + return nil + } +} + +func SetPortfolioPropertyESG(value *bool) UpdatePortfolioFn { + return func(v *pacta.Portfolio) error { + v.Properties.ESG = value + return nil + } +} + +func SetPortfolioPropertyExternal(value *bool) UpdatePortfolioFn { + return func(v *pacta.Portfolio) error { + v.Properties.External = value + return nil + } +} + +func SetPortfolioPropertyEngagementStrategy(value *bool) UpdatePortfolioFn { + return func(v *pacta.Portfolio) error { + v.Properties.EngagementStrategy = value return nil } } @@ -385,9 +406,30 @@ func SetIncompleteUploadFailureMessage(value string) UpdateIncompleteUploadFn { } } -func SetIncompleteUploadHoldingsDate(value *pacta.HoldingsDate) UpdateIncompleteUploadFn { +func SetIncompleteUploadPropertyHoldingsDate(value *pacta.HoldingsDate) UpdateIncompleteUploadFn { + return func(v *pacta.IncompleteUpload) error { + v.Properties.HoldingsDate = value + return nil + } +} + +func SetIncompleteUploadPropertyESG(value *bool) UpdateIncompleteUploadFn { + return func(v *pacta.IncompleteUpload) error { + v.Properties.ESG = value + return nil + } +} + +func SetIncompleteUploadPropertyExternal(value *bool) UpdateIncompleteUploadFn { + return func(v *pacta.IncompleteUpload) error { + v.Properties.External = value + return nil + } +} + +func SetIncompleteUploadPropertyEngagementStrategy(value *bool) UpdateIncompleteUploadFn { return func(v *pacta.IncompleteUpload) error { - v.HoldingsDate = value + v.Properties.EngagementStrategy = value return nil } } diff --git a/db/sqldb/golden/human_readable_schema.sql b/db/sqldb/golden/human_readable_schema.sql index 093cda3..41fe6ec 100644 --- a/db/sqldb/golden/human_readable_schema.sql +++ b/db/sqldb/golden/human_readable_schema.sql @@ -125,10 +125,10 @@ CREATE TABLE incomplete_upload ( description text NOT NULL, failure_code failure_code, failure_message text, - holdings_date timestamp with time zone, id text NOT NULL, name text NOT NULL, owner_id text NOT NULL, + properties jsonb DEFAULT '{}'::jsonb NOT NULL, ran_at timestamp with time zone); ALTER TABLE ONLY incomplete_upload ADD CONSTRAINT incomplete_upload_pkey PRIMARY KEY (id); CREATE INDEX incomplete_upload_by_blob_id ON incomplete_upload USING btree (blob_id); @@ -226,11 +226,11 @@ CREATE TABLE portfolio ( blob_id text NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, description text NOT NULL, - holdings_date timestamp with time zone, id text NOT NULL, name text NOT NULL, number_of_rows integer, - owner_id text NOT NULL); + owner_id text NOT NULL, + properties jsonb DEFAULT '{}'::jsonb NOT NULL); ALTER TABLE ONLY portfolio ADD CONSTRAINT portfolio_pkey PRIMARY KEY (id); CREATE INDEX portfolio_by_blob_id ON portfolio USING btree (blob_id); ALTER TABLE ONLY portfolio ADD CONSTRAINT portfolio_blob_id_fkey FOREIGN KEY (blob_id) REFERENCES blob(id) ON DELETE RESTRICT; diff --git a/db/sqldb/golden/schema_dump.sql b/db/sqldb/golden/schema_dump.sql index a07ea70..c3c8187 100644 --- a/db/sqldb/golden/schema_dump.sql +++ b/db/sqldb/golden/schema_dump.sql @@ -250,12 +250,12 @@ CREATE TABLE public.incomplete_upload ( blob_id text, name text NOT NULL, description text NOT NULL, - holdings_date timestamp with time zone, created_at timestamp with time zone DEFAULT now() NOT NULL, ran_at timestamp with time zone, completed_at timestamp with time zone, failure_code public.failure_code, - failure_message text + failure_message text, + properties jsonb DEFAULT '{}'::jsonb NOT NULL ); @@ -387,10 +387,10 @@ CREATE TABLE public.portfolio ( name text NOT NULL, description text NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, - holdings_date timestamp with time zone, blob_id text NOT NULL, admin_debug_enabled boolean NOT NULL, - number_of_rows integer + number_of_rows integer, + properties jsonb DEFAULT '{}'::jsonb NOT NULL ); diff --git a/db/sqldb/incomplete_upload.go b/db/sqldb/incomplete_upload.go index 38699e5..c3e53b8 100644 --- a/db/sqldb/incomplete_upload.go +++ b/db/sqldb/incomplete_upload.go @@ -17,7 +17,7 @@ const incompleteUploadSelectColumns = ` incomplete_upload.blob_id, incomplete_upload.name, incomplete_upload.description, - incomplete_upload.holdings_date, + incomplete_upload.properties, incomplete_upload.created_at, incomplete_upload.ran_at, incomplete_upload.completed_at, @@ -79,17 +79,13 @@ func (d *DB) CreateIncompleteUpload(tx db.Tx, i *pacta.IncompleteUpload) (pacta. if err := validateIncompleteUploadForCreation(i); err != nil { return "", fmt.Errorf("validating incomplete_upload for creation: %w", err) } - hd, err := encodeHoldingsDate(i.HoldingsDate) - if err != nil { - return "", fmt.Errorf("validating holdings date: %w", err) - } i.ID = pacta.IncompleteUploadID(d.randomID("iu")) - err = d.exec(tx, ` + err := d.exec(tx, ` INSERT INTO incomplete_upload - (id, owner_id, admin_debug_enabled, blob_id, name, description, holdings_date) + (id, owner_id, admin_debug_enabled, blob_id, name, description, properties) VALUES ($1, $2, $3, $4, $5, $6, $7);`, - i.ID, i.Owner.ID, i.AdminDebugEnabled, i.Blob.ID, i.Name, i.Description, hd) + i.ID, i.Owner.ID, i.AdminDebugEnabled, i.Blob.ID, i.Name, i.Description, i.Properties) if err != nil { return "", fmt.Errorf("creating incomplete_upload: %w", err) } @@ -147,7 +143,7 @@ func rowToIncompleteUpload(row rowScanner) (*pacta.IncompleteUpload, error) { iu := &pacta.IncompleteUpload{Owner: &pacta.Owner{}, Blob: &pacta.Blob{}} var ( failureCode, failureMessage pgtype.Text - hd, ranAt, completedAt pgtype.Timestamptz + ranAt, completedAt pgtype.Timestamptz ) err := row.Scan( &iu.ID, @@ -156,7 +152,7 @@ func rowToIncompleteUpload(row rowScanner) (*pacta.IncompleteUpload, error) { &iu.Blob.ID, &iu.Name, &iu.Description, - &hd, + &iu.Properties, &iu.CreatedAt, &ranAt, &completedAt, @@ -166,10 +162,6 @@ func rowToIncompleteUpload(row rowScanner) (*pacta.IncompleteUpload, error) { if err != nil { return nil, fmt.Errorf("scanning into incomplete_upload: %w", err) } - iu.HoldingsDate, err = decodeHoldingsDate(hd) - if err != nil { - return nil, fmt.Errorf("decoding holdings date: %w", err) - } if failureCode.Valid { iu.FailureCode, err = pacta.ParseFailureCode(failureCode.String) if err != nil { @@ -193,24 +185,20 @@ func rowsToIncompleteUploads(rows pgx.Rows) ([]*pacta.IncompleteUpload, error) { } func (db *DB) putIncompleteUpload(tx db.Tx, iu *pacta.IncompleteUpload) error { - hd, err := encodeHoldingsDate(iu.HoldingsDate) - if err != nil { - return fmt.Errorf("validating holdings date: %w", err) - } - err = db.exec(tx, ` + err := db.exec(tx, ` UPDATE incomplete_upload SET owner_id = $2, admin_debug_enabled = $3, name = $4, description = $5, - holdings_date = $6, + properties = $6, ran_at = $7, completed_at = $8, failure_code = $9, failure_message = $10 WHERE id = $1; `, iu.ID, iu.Owner.ID, iu.AdminDebugEnabled, iu.Name, iu.Description, - hd, timeToNilable(iu.RanAt), timeToNilable(iu.CompletedAt), + iu.Properties, timeToNilable(iu.RanAt), timeToNilable(iu.CompletedAt), strToNilable(iu.FailureCode), strToNilable(iu.FailureMessage)) if err != nil { return fmt.Errorf("updating incomplete_upload writable fields: %w", err) diff --git a/db/sqldb/incomplete_upload_test.go b/db/sqldb/incomplete_upload_test.go index 5298087..50c91a9 100644 --- a/db/sqldb/incomplete_upload_test.go +++ b/db/sqldb/incomplete_upload_test.go @@ -23,11 +23,15 @@ func TestIncompleteUploadCRUD(t *testing.T) { cmpOpts := incompleteUploadCmpOpts() iu := &pacta.IncompleteUpload{ - Name: "i-u-name", - Description: "i-u-description", - HoldingsDate: exampleHoldingsDate, - Owner: &pacta.Owner{ID: o1.ID}, - Blob: &pacta.Blob{ID: b.ID}, + Name: "i-u-name", + Description: "i-u-description", + Properties: pacta.PortfolioProperties{ + HoldingsDate: exampleHoldingsDate, + ESG: ptr(true), + External: ptr(false), + }, + Owner: &pacta.Owner{ID: o1.ID}, + Blob: &pacta.Blob{ID: b.ID}, } id, err := tdb.CreateIncompleteUpload(tx, iu) if err != nil { @@ -68,7 +72,10 @@ func TestIncompleteUploadCRUD(t *testing.T) { db.SetIncompleteUploadCompletedAt(completedAt), db.SetIncompleteUploadAdminDebugEnabled(true), db.SetIncompleteUploadFailureMessage(failureMessage), - db.SetIncompleteUploadHoldingsDate(hd), + db.SetIncompleteUploadPropertyHoldingsDate(hd), + db.SetIncompleteUploadPropertyESG(ptr(false)), + db.SetIncompleteUploadPropertyEngagementStrategy(ptr(true)), + db.SetIncompleteUploadPropertyExternal(nil), ) if err != nil { t.Fatalf("updating incomplete upload: %v", err) @@ -81,7 +88,10 @@ func TestIncompleteUploadCRUD(t *testing.T) { iu.CompletedAt = completedAt iu.AdminDebugEnabled = true iu.FailureMessage = failureMessage - iu.HoldingsDate = hd + iu.Properties.HoldingsDate = hd + iu.Properties.ESG = ptr(false) + iu.Properties.EngagementStrategy = ptr(true) + iu.Properties.External = nil actual, err = tdb.IncompleteUpload(tx, iu.ID) if err != nil { @@ -144,11 +154,10 @@ func TestFailureCodePersistability(t *testing.T) { o := ownerUserForTesting(t, tdb, u) iu := &pacta.IncompleteUpload{ - Name: "i-u-name", - Description: "i-u-description", - HoldingsDate: exampleHoldingsDate, - Owner: &pacta.Owner{ID: o.ID}, - Blob: &pacta.Blob{ID: b.ID}, + Name: "i-u-name", + Description: "i-u-description", + Owner: &pacta.Owner{ID: o.ID}, + Blob: &pacta.Blob{ID: b.ID}, } id, err := tdb.CreateIncompleteUpload(tx, iu) if err != nil { diff --git a/db/sqldb/migrations/0012_portfolio_properties.down.sql b/db/sqldb/migrations/0012_portfolio_properties.down.sql new file mode 100644 index 0000000..cb39c39 --- /dev/null +++ b/db/sqldb/migrations/0012_portfolio_properties.down.sql @@ -0,0 +1,13 @@ +BEGIN; + +ALTER TABLE portfolio + ADD COLUMN holdings_date TIMESTAMPTZ; +ALTER TABLE portfolio + DROP COLUMN properties; + +ALTER TABLE incomplete_upload + ADD COLUMN holdings_date TIMESTAMPTZ; +ALTER TABLE incomplete_upload + DROP COLUMN properties; + +COMMIT; \ No newline at end of file diff --git a/db/sqldb/migrations/0012_portfolio_properties.up.sql b/db/sqldb/migrations/0012_portfolio_properties.up.sql new file mode 100644 index 0000000..02b7736 --- /dev/null +++ b/db/sqldb/migrations/0012_portfolio_properties.up.sql @@ -0,0 +1,13 @@ +BEGIN; + +ALTER TABLE incomplete_upload + ADD COLUMN properties JSONB NOT NULL DEFAULT '{}'; +ALTER TABLE incomplete_upload + DROP COLUMN holdings_date; + +ALTER TABLE portfolio + ADD COLUMN properties JSONB NOT NULL DEFAULT '{}'; +ALTER TABLE portfolio + DROP COLUMN holdings_date; + +COMMIT; \ No newline at end of file diff --git a/db/sqldb/portfolio.go b/db/sqldb/portfolio.go index 8383808..9827a33 100644 --- a/db/sqldb/portfolio.go +++ b/db/sqldb/portfolio.go @@ -23,7 +23,7 @@ func portfolioQueryStanza(where string) string { portfolio.name, portfolio.description, portfolio.created_at, - portfolio.holdings_date, + portfolio.properties, portfolio.blob_id, portfolio.admin_debug_enabled, portfolio.number_of_rows, @@ -103,17 +103,13 @@ func (d *DB) CreatePortfolio(tx db.Tx, p *pacta.Portfolio) (pacta.PortfolioID, e if err := validatePortfolioForCreation(p); err != nil { return "", fmt.Errorf("validating portfolio for creation: %w", err) } - hd, err := encodeHoldingsDate(p.HoldingsDate) - if err != nil { - return "", fmt.Errorf("validating holdings date: %w", err) - } p.ID = pacta.PortfolioID(d.randomID("pflo")) - err = d.exec(tx, ` + err := d.exec(tx, ` INSERT INTO portfolio - (id, owner_id, name, description, holdings_date, blob_id, admin_debug_enabled, number_of_rows) + (id, owner_id, name, description, properties, blob_id, admin_debug_enabled, number_of_rows) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);`, - p.ID, p.Owner.ID, p.Name, p.Description, hd, p.Blob.ID, p.AdminDebugEnabled, p.NumberOfRows) + p.ID, p.Owner.ID, p.Name, p.Description, p.Properties, p.Blob.ID, p.AdminDebugEnabled, p.NumberOfRows) if err != nil { return "", fmt.Errorf("creating portfolio: %w", err) } @@ -182,7 +178,6 @@ func (d *DB) DeletePortfolio(tx db.Tx, id pacta.PortfolioID) ([]pacta.BlobURI, e func rowToPortfolio(row rowScanner) (*pacta.Portfolio, error) { p := &pacta.Portfolio{Owner: &pacta.Owner{}, Blob: &pacta.Blob{}} - hd := pgtype.Timestamptz{} groupsIDs := []pgtype.Text{} groupsCreatedAts := []pgtype.Timestamptz{} initiativesIDs := []pgtype.Text{} @@ -194,7 +189,7 @@ func rowToPortfolio(row rowScanner) (*pacta.Portfolio, error) { &p.Name, &p.Description, &p.CreatedAt, - &hd, + &p.Properties, &p.Blob.ID, &p.AdminDebugEnabled, &p.NumberOfRows, @@ -207,10 +202,6 @@ func rowToPortfolio(row rowScanner) (*pacta.Portfolio, error) { if err != nil { return nil, fmt.Errorf("scanning into portfolio row: %w", err) } - p.HoldingsDate, err = decodeHoldingsDate(hd) - if err != nil { - return nil, fmt.Errorf("decoding holdings date: %w", err) - } if err := checkSizesEquivalent("groups", len(groupsIDs), len(groupsCreatedAts)); err != nil { return nil, err } @@ -264,20 +255,16 @@ func rowsToPortfolios(rows pgx.Rows) ([]*pacta.Portfolio, error) { } func (db *DB) putPortfolio(tx db.Tx, p *pacta.Portfolio) error { - hd, err := encodeHoldingsDate(p.HoldingsDate) - if err != nil { - return fmt.Errorf("validating holdings date: %w", err) - } - err = db.exec(tx, ` + err := db.exec(tx, ` UPDATE portfolio SET owner_id = $2, name = $3, description = $4, - holdings_date = $5, + properties = $5, admin_debug_enabled = $6, number_of_rows = $7 WHERE id = $1; - `, p.ID, p.Owner.ID, p.Name, p.Description, hd, p.AdminDebugEnabled, p.NumberOfRows) + `, p.ID, p.Owner.ID, p.Name, p.Description, p.Properties, p.AdminDebugEnabled, p.NumberOfRows) if err != nil { return fmt.Errorf("updating portfolio writable fields: %w", err) } @@ -303,8 +290,5 @@ func validatePortfolioForCreation(p *pacta.Portfolio) error { if p.NumberOfRows < 0 { return fmt.Errorf("portfolio number_of_rows must be non-negative") } - if p.HoldingsDate == nil || p.HoldingsDate.Time.IsZero() { - return fmt.Errorf("portfolio holdings_date must be non-nil and non-zero") - } return nil } diff --git a/db/sqldb/portfolio_test.go b/db/sqldb/portfolio_test.go index 01f0177..ca75acd 100644 --- a/db/sqldb/portfolio_test.go +++ b/db/sqldb/portfolio_test.go @@ -22,9 +22,14 @@ func TestPortfolioCRUD(t *testing.T) { o2 := ownerUserForTesting(t, tdb, u2) p := &pacta.Portfolio{ - Name: "portfolio-name", - Description: "portfolio-description", - HoldingsDate: exampleHoldingsDate, + Name: "portfolio-name", + Description: "portfolio-description", + Properties: pacta.PortfolioProperties{ + HoldingsDate: exampleHoldingsDate, + ESG: nil, + External: ptr(true), + EngagementStrategy: ptr(false), + }, Owner: &pacta.Owner{ID: o1.ID}, Blob: &pacta.Blob{ID: b.ID}, NumberOfRows: 10, @@ -58,7 +63,10 @@ func TestPortfolioCRUD(t *testing.T) { err = tdb.UpdatePortfolio(tx, p.ID, db.SetPortfolioName(nName), db.SetPortfolioDescription(nDesc), - db.SetPortfolioHoldingsDate(exampleHoldingsDate2), + db.SetPortfolioPropertyHoldingsDate(exampleHoldingsDate2), + db.SetPortfolioPropertyESG(ptr(true)), + db.SetPortfolioPropertyExternal(ptr(false)), + db.SetPortfolioPropertyEngagementStrategy(nil), db.SetPortfolioOwner(o2.ID), db.SetPortfolioAdminDebugEnabled(true), db.SetPortfolioNumberOfRows(nRows), @@ -68,7 +76,10 @@ func TestPortfolioCRUD(t *testing.T) { } p.Name = nName p.Description = nDesc - p.HoldingsDate = exampleHoldingsDate2 + p.Properties.HoldingsDate = exampleHoldingsDate2 + p.Properties.ESG = ptr(true) + p.Properties.External = ptr(false) + p.Properties.EngagementStrategy = nil p.Owner = &pacta.Owner{ID: o2.ID} p.AdminDebugEnabled = true p.NumberOfRows = nRows @@ -157,7 +168,6 @@ func portfolioForTestingWithKey(t *testing.T, tdb *DB, key string) *pacta.Portfo p := &pacta.Portfolio{ Name: "portfolio-name-" + key, Description: "portfolio-description-" + key, - HoldingsDate: exampleHoldingsDate, Owner: &pacta.Owner{ID: o.ID}, Blob: &pacta.Blob{ID: b.ID}, NumberOfRows: 10, diff --git a/db/sqldb/sqldb.go b/db/sqldb/sqldb.go index d4436a8..ca2a905 100644 --- a/db/sqldb/sqldb.go +++ b/db/sqldb/sqldb.go @@ -441,3 +441,7 @@ func checkSizesEquivalent(name string, sizes ...int) error { } return nil } + +func ptr[T any](t T) *T { + return &t +} diff --git a/db/sqldb/sqldb_test.go b/db/sqldb/sqldb_test.go index a5628a6..f888b04 100644 --- a/db/sqldb/sqldb_test.go +++ b/db/sqldb/sqldb_test.go @@ -93,7 +93,7 @@ func TestSchemaHistory(t *testing.T) { {ID: 9, Version: 9}, // 0009_support_user_merge {ID: 10, Version: 10}, // 0010_audit_log_enum_values {ID: 11, Version: 11}, // 0011_add_report_file_types - + {ID: 12, Version: 12}, // 0012_portfolio_properties } if diff := cmp.Diff(want, got); diff != "" { diff --git a/frontend/components/ExplicitTriStateCheckbox.vue b/frontend/components/ExplicitTriStateCheckbox.vue new file mode 100644 index 0000000..bdc7808 --- /dev/null +++ b/frontend/components/ExplicitTriStateCheckbox.vue @@ -0,0 +1,56 @@ + + + diff --git a/frontend/components/inputs/EngagementStrategy.vue b/frontend/components/inputs/EngagementStrategy.vue new file mode 100644 index 0000000..b24b0fe --- /dev/null +++ b/frontend/components/inputs/EngagementStrategy.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/components/inputs/Esg.vue b/frontend/components/inputs/Esg.vue new file mode 100644 index 0000000..eff6911 --- /dev/null +++ b/frontend/components/inputs/Esg.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/components/inputs/External.vue b/frontend/components/inputs/External.vue new file mode 100644 index 0000000..6d23a4f --- /dev/null +++ b/frontend/components/inputs/External.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/components/inputs/HoldingDate.vue b/frontend/components/inputs/HoldingDate.vue new file mode 100644 index 0000000..f212d13 --- /dev/null +++ b/frontend/components/inputs/HoldingDate.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/components/portfolio/Editor.vue b/frontend/components/portfolio/Editor.vue index c4a3e40..05bb713 100644 --- a/frontend/components/portfolio/Editor.vue +++ b/frontend/components/portfolio/Editor.vue @@ -40,6 +40,30 @@ const evs = computed({ auto-resize /> + + + + + + + + + Promise.all([selectedRows.value.map((row) => delete {{ tt('Number of Rows') }} {{ slotProps.data.editorValues.value.numberOfRows.originalValue }} -
- {{ tt('Holdings Date') }} - {{ humanReadableDateFromStandardString(slotProps.data.editorValues.value.holdingsDate.originalValue.time).value }} -

{{ tt('Memberships') }} diff --git a/frontend/lib/editor/incomplete_upload.ts b/frontend/lib/editor/incomplete_upload.ts index 754e87d..4523c33 100644 --- a/frontend/lib/editor/incomplete_upload.ts +++ b/frontend/lib/editor/incomplete_upload.ts @@ -28,9 +28,25 @@ const createEditorIncompleteUploadFields = (translation: Translation): EditorInc label: tt('Admin Debugging Enabled'), helpText: tt('When enabled, this upload can be accessed by administrators to help with debugging. Only turn this on if you\'re comfortable with system administrators accessing this data.'), }, - holdingsDate: { - name: 'holdingsDate', + propertyHoldingsDate: { + name: 'propertyHoldingsDate', label: tt('Holdings Date'), + helpText: tt('HoldingsDateHelpText'), + }, + propertyESG: { + name: 'propertyESG', + label: tt('Environmental, Social, and Governance (ESG)'), + helpText: tt('ESGHelpText'), + }, + propertyExternal: { + name: 'propertyExternal', + label: tt('External'), + helpText: tt('ExternalHelpText'), + }, + propertyEngagementStrategy: { + name: 'propertyEngagementStrategy', + label: tt('Engagement Strategy'), + helpText: tt('EngagementStrategyHelpText'), }, createdAt: { name: 'createdAt', diff --git a/frontend/lib/editor/portfolio.ts b/frontend/lib/editor/portfolio.ts index 4964e5c..a1dddad 100644 --- a/frontend/lib/editor/portfolio.ts +++ b/frontend/lib/editor/portfolio.ts @@ -28,11 +28,26 @@ const createEditorPortfolioFields = (translation: Translation): EditorPortfolioF label: tt('Admin Debugging Enabled'), helpText: tt('AdminDebuggingEnabledHelpText'), }, - holdingsDate: { - name: 'holdingsDate', + propertyHoldingsDate: { + name: 'propertyHoldingsDate', label: tt('Holdings Date'), helpText: tt('HoldingsDateHelpText'), }, + propertyESG: { + name: 'propertyESG', + label: tt('Environmental, Social, and Governance (ESG)'), + helpText: tt('ESGHelpText'), + }, + propertyExternal: { + name: 'propertyExternal', + label: tt('External'), + helpText: tt('ExternalHelpText'), + }, + propertyEngagementStrategy: { + name: 'propertyEngagementStrategy', + label: tt('Engagement Strategy'), + helpText: tt('EngagementStrategyHelpText'), + }, createdAt: { name: 'createdAt', label: tt('Created At'), diff --git a/frontend/openapi/generated/pacta/index.ts b/frontend/openapi/generated/pacta/index.ts index 47773e7..e84eeac 100644 --- a/frontend/openapi/generated/pacta/index.ts +++ b/frontend/openapi/generated/pacta/index.ts @@ -59,6 +59,7 @@ export type { ListPortfoliosResp } from './models/ListPortfoliosResp'; export type { MergeUsersReq } from './models/MergeUsersReq'; export type { MergeUsersResp } from './models/MergeUsersResp'; export type { NewPortfolioAsset } from './models/NewPortfolioAsset'; +export { OptionalBoolean } from './models/OptionalBoolean'; export type { PactaVersion } from './models/PactaVersion'; export type { PactaVersionChanges } from './models/PactaVersionChanges'; export type { PactaVersionCreate } from './models/PactaVersionCreate'; diff --git a/frontend/openapi/generated/pacta/models/IncompleteUpload.ts b/frontend/openapi/generated/pacta/models/IncompleteUpload.ts index 9940693..d02ba35 100644 --- a/frontend/openapi/generated/pacta/models/IncompleteUpload.ts +++ b/frontend/openapi/generated/pacta/models/IncompleteUpload.ts @@ -5,6 +5,7 @@ import type { FailureCode } from './FailureCode'; import type { HoldingsDate } from './HoldingsDate'; +import type { OptionalBoolean } from './OptionalBoolean'; export type IncompleteUpload = { /** @@ -19,7 +20,19 @@ export type IncompleteUpload = { * Description of the upload */ description: string; - holdingsDate?: HoldingsDate; + propertyHoldingsDate?: HoldingsDate; + /** + * If set, this portfolio represents ESG data + */ + propertyESG: OptionalBoolean; + /** + * If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + */ + propertyExternal: OptionalBoolean; + /** + * If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + */ + propertyEngagementStrategy: OptionalBoolean; /** * The time when the upload was created */ diff --git a/frontend/openapi/generated/pacta/models/OptionalBoolean.ts b/frontend/openapi/generated/pacta/models/OptionalBoolean.ts new file mode 100644 index 0000000..6bf3b68 --- /dev/null +++ b/frontend/openapi/generated/pacta/models/OptionalBoolean.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export enum OptionalBoolean { + OPTIONAL_BOOLEAN_TRUE = 'OptionalBooleanTRUE', + OPTIONAL_BOOLEAN_FALSE = 'OptionalBooleanFALSE', + OPTIONAL_BOOLEAN_UNSET = 'OptionalBooleanUNSET', +} diff --git a/frontend/openapi/generated/pacta/models/Portfolio.ts b/frontend/openapi/generated/pacta/models/Portfolio.ts index 6dbf017..729d843 100644 --- a/frontend/openapi/generated/pacta/models/Portfolio.ts +++ b/frontend/openapi/generated/pacta/models/Portfolio.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { HoldingsDate } from './HoldingsDate'; +import type { OptionalBoolean } from './OptionalBoolean'; import type { PortfolioGroupMembershipPortfolioGroup } from './PortfolioGroupMembershipPortfolioGroup'; import type { PortfolioInitiativeMembershipInitiative } from './PortfolioInitiativeMembershipInitiative'; @@ -24,7 +25,6 @@ export type Portfolio = { * The time at which this portfolio was successfully parsed from a raw */ createdAt: string; - holdingsDate?: HoldingsDate; /** * Whether the admin debug mode is enabled for this portfolio */ @@ -41,5 +41,18 @@ export type Portfolio = { * The list of initiatives that this portfolio is a member of */ initiatives?: Array; + propertyHoldingsDate?: HoldingsDate; + /** + * If set, this portfolio represents ESG data + */ + propertyESG: OptionalBoolean; + /** + * If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + */ + propertyExternal: OptionalBoolean; + /** + * If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + */ + propertyEngagementStrategy: OptionalBoolean; }; diff --git a/frontend/openapi/generated/pacta/models/PortfolioChanges.ts b/frontend/openapi/generated/pacta/models/PortfolioChanges.ts index 5070f84..47793ab 100644 --- a/frontend/openapi/generated/pacta/models/PortfolioChanges.ts +++ b/frontend/openapi/generated/pacta/models/PortfolioChanges.ts @@ -3,6 +3,9 @@ /* tslint:disable */ /* eslint-disable */ +import type { HoldingsDate } from './HoldingsDate'; +import type { OptionalBoolean } from './OptionalBoolean'; + export type PortfolioChanges = { /** * the human meaningful name of the portfolio @@ -16,5 +19,18 @@ export type PortfolioChanges = { * Whether the admin debug mode is enabled for this portfolio */ adminDebugEnabled?: boolean; + propertyHoldingsDate?: HoldingsDate; + /** + * If set, this portfolio represents ESG data + */ + propertyESG?: OptionalBoolean; + /** + * If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + */ + propertyExternal?: OptionalBoolean; + /** + * If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + */ + propertyEngagementStrategy?: OptionalBoolean; }; diff --git a/frontend/openapi/generated/pacta/models/StartPortfolioUploadReq.ts b/frontend/openapi/generated/pacta/models/StartPortfolioUploadReq.ts index bc0feaf..ce128a1 100644 --- a/frontend/openapi/generated/pacta/models/StartPortfolioUploadReq.ts +++ b/frontend/openapi/generated/pacta/models/StartPortfolioUploadReq.ts @@ -4,10 +4,23 @@ /* eslint-disable */ import type { HoldingsDate } from './HoldingsDate'; +import type { OptionalBoolean } from './OptionalBoolean'; import type { StartPortfolioUploadReqItem } from './StartPortfolioUploadReqItem'; export type StartPortfolioUploadReq = { items: Array; - holdings_date: HoldingsDate; + propertyHoldingsDate?: HoldingsDate; + /** + * If set, this portfolio represents ESG data + */ + propertyESG: OptionalBoolean; + /** + * If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + */ + propertyExternal: OptionalBoolean; + /** + * If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + */ + propertyEngagementStrategy: OptionalBoolean; }; diff --git a/frontend/pages/upload.vue b/frontend/pages/upload.vue index 9044cd4..27a5b5c 100644 --- a/frontend/pages/upload.vue +++ b/frontend/pages/upload.vue @@ -2,6 +2,7 @@ import { type FileUploadUploaderEvent } from 'primevue/fileupload' import { serializeError } from 'serialize-error' import { formatFileSize } from '@/lib/filesize' +import { OptionalBoolean, type HoldingsDate } from '@/openapi/generated/pacta' const pactaClient = usePACTA() const { $axios } = useNuxtApp() @@ -38,7 +39,10 @@ interface FileStateDetail extends FileState { effectiveError?: string | undefined } -const holdingsDate = useState(`${prefix}.holdingsDate`, () => new Date()) +const holdingsDate = useState(`${prefix}.holdingsDate`, () => undefined) +const esg = useState(`${prefix}.esg`, () => OptionalBoolean.OPTIONAL_BOOLEAN_UNSET) +const external = useState(`${prefix}.external`, () => OptionalBoolean.OPTIONAL_BOOLEAN_UNSET) +const engagementStrategy = useState(`${prefix}.engagementStrategy`, () => OptionalBoolean.OPTIONAL_BOOLEAN_UNSET) const errorCode = useState(`${prefix}.errorCode`, () => '') const errorMessage = useState(`${prefix}.errorMessage`, () => '') const startedProcessing = useState(`${prefix}.startedProcessing`, () => false) @@ -46,7 +50,10 @@ const isProcessing = useState(`${prefix}.isProcessing`, () => false) const fileStates = useState(`${prefix}.fileState`, () => []) const reset = () => { - holdingsDate.value = new Date() + holdingsDate.value = undefined + esg.value = OptionalBoolean.OPTIONAL_BOOLEAN_UNSET + external.value = OptionalBoolean.OPTIONAL_BOOLEAN_UNSET + engagementStrategy.value = OptionalBoolean.OPTIONAL_BOOLEAN_UNSET errorCode.value = '' errorMessage.value = '' startedProcessing.value = false @@ -159,9 +166,10 @@ const startUpload = async () => { file_name: fileState.file.name, // TODO(#79) consider adding file size here as a validation step. })), - holdings_date: { - time: holdingsDate.value.toISOString(), - }, + propertyHoldingsDate: holdingsDate.value, + propertyESG: esg.value, + propertyExternal: external.value, + propertyEngagementStrategy: engagementStrategy.value, }).catch(e => { console.log('error starting upload', e, e.body) if (e.body?.error_id) { @@ -290,17 +298,6 @@ const cleanUpIncompleteUploads = async () => { This is a page where you can upload portfolios to test out the PACTA platform. This Copy will need work, and will need to link to the documentation.

- - - { label="File States" /> + + +
+ + + + + + + + + + + + +
+
+
{ vueApp.component('PVTabView', TabView) vueApp.component('PVTextarea', Textarea) vueApp.component('PVToast', Toast) + vueApp.component('PVTriStateCheckbox', TriStateCheckbox) vueApp.directive('tooltip', Tooltip) }) diff --git a/openapi/pacta.yaml b/openapi/pacta.yaml index ea49ea2..2c2a7d8 100644 --- a/openapi/pacta.yaml +++ b/openapi/pacta.yaml @@ -1025,6 +1025,12 @@ components: type: string enum: - FailureCodeUNKNOWN + OptionalBoolean: + type: string + enum: + - OptionalBooleanTRUE + - OptionalBooleanFALSE + - OptionalBooleanUNSET PactaVersionCreate: type: object required: @@ -1480,14 +1486,25 @@ components: type: object required: - items - - holdings_date + - propertyESG + - propertyExternal + - propertyEngagementStrategy properties: items: type: array items: $ref: '#/components/schemas/StartPortfolioUploadReqItem' - holdings_date: + propertyHoldingsDate: $ref: '#/components/schemas/HoldingsDate' + propertyESG: + description: If set, this portfolio represents ESG data + $ref: '#/components/schemas/OptionalBoolean' + propertyExternal: + description: If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + $ref: '#/components/schemas/OptionalBoolean' + propertyEngagementStrategy: + description: If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + $ref: '#/components/schemas/OptionalBoolean' StartPortfolioUploadReqItem: type: object required: @@ -1602,6 +1619,9 @@ components: - description - createdAt - adminDebugEnabled + - propertyESG + - propertyExternal + - propertyEngagementStrategy properties: id: type: string # Assuming IncompleteUploadID is a string, otherwise define its structure @@ -1612,8 +1632,17 @@ components: description: type: string description: Description of the upload - holdingsDate: + propertyHoldingsDate: $ref: '#/components/schemas/HoldingsDate' + propertyESG: + description: If set, this portfolio represents ESG data + $ref: '#/components/schemas/OptionalBoolean' + propertyExternal: + description: If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + $ref: '#/components/schemas/OptionalBoolean' + propertyEngagementStrategy: + description: If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + $ref: '#/components/schemas/OptionalBoolean' createdAt: type: string format: date-time @@ -1657,6 +1686,9 @@ components: - createdAt - adminDebugEnabled - numberOfRows + - propertyESG + - propertyExternal + - propertyEngagementStrategy properties: id: type: string @@ -1671,8 +1703,6 @@ components: type: string format: date-time description: The time at which this portfolio was successfully parsed from a raw - holdingsDate: - $ref: '#/components/schemas/HoldingsDate' adminDebugEnabled: type: boolean description: Whether the admin debug mode is enabled for this portfolio @@ -1689,6 +1719,17 @@ components: description: The list of initiatives that this portfolio is a member of items: $ref: '#/components/schemas/PortfolioInitiativeMembershipInitiative' + propertyHoldingsDate: + $ref: '#/components/schemas/HoldingsDate' + propertyESG: + description: If set, this portfolio represents ESG data + $ref: '#/components/schemas/OptionalBoolean' + propertyExternal: + description: If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + $ref: '#/components/schemas/OptionalBoolean' + propertyEngagementStrategy: + description: If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + $ref: '#/components/schemas/OptionalBoolean' PortfolioChanges: type: object properties: @@ -1701,6 +1742,18 @@ components: adminDebugEnabled: type: boolean description: Whether the admin debug mode is enabled for this portfolio + propertyHoldingsDate: + $ref: '#/components/schemas/HoldingsDate' + propertyESG: + description: If set, this portfolio represents ESG data + $ref: '#/components/schemas/OptionalBoolean' + propertyExternal: + description: If set to false, this portfolio represents internal data, if set to false it represents external data, unset represents no user input + $ref: '#/components/schemas/OptionalBoolean' + propertyEngagementStrategy: + type: boolean + description: If set, this portfolio represents engagement strategy data or not, if unset it represents no user input + $ref: '#/components/schemas/OptionalBoolean' PortfolioSnapshot: type: object description: represents an immutable description of a collection of portfolios at a point in time, used to ensure reproducibility and change detection diff --git a/pacta/pacta.go b/pacta/pacta.go index fd733c8..276754b 100644 --- a/pacta/pacta.go +++ b/pacta/pacta.go @@ -352,13 +352,29 @@ func (o *HoldingsDate) Clone() *HoldingsDate { } } +type PortfolioProperties struct { + HoldingsDate *HoldingsDate + ESG *bool + External *bool + EngagementStrategy *bool +} + +func (o PortfolioProperties) Clone() PortfolioProperties { + return PortfolioProperties{ + HoldingsDate: o.HoldingsDate.Clone(), + ESG: clonePtr(o.ESG), + External: clonePtr(o.External), + EngagementStrategy: clonePtr(o.EngagementStrategy), + } +} + type IncompleteUploadID string type IncompleteUpload struct { ID IncompleteUploadID Name string Description string CreatedAt time.Time - HoldingsDate *HoldingsDate + Properties PortfolioProperties RanAt time.Time CompletedAt time.Time FailureCode FailureCode @@ -377,7 +393,7 @@ func (o *IncompleteUpload) Clone() *IncompleteUpload { Name: o.Name, Description: o.Description, CreatedAt: o.CreatedAt, - HoldingsDate: o.HoldingsDate.Clone(), + Properties: o.Properties.Clone(), RanAt: o.RanAt, CompletedAt: o.CompletedAt, FailureCode: o.FailureCode, @@ -394,7 +410,7 @@ type Portfolio struct { Name string Description string CreatedAt time.Time - HoldingsDate *HoldingsDate + Properties PortfolioProperties Owner *Owner Blob *Blob AdminDebugEnabled bool @@ -412,7 +428,7 @@ func (o *Portfolio) Clone() *Portfolio { Name: o.Name, Description: o.Description, CreatedAt: o.CreatedAt, - HoldingsDate: o.HoldingsDate.Clone(), + Properties: o.Properties.Clone(), Owner: o.Owner.Clone(), Blob: o.Blob.Clone(), AdminDebugEnabled: o.AdminDebugEnabled, @@ -782,3 +798,11 @@ func cloneAll[T cloneable[T]](in []T) []T { } return out } + +func clonePtr[T any](in *T) *T { + if in == nil { + return nil + } + out := *in + return &out +}