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
+}