diff --git a/pkg/probo/actions.go b/pkg/probo/actions.go index 19564b648..96faea13c 100644 --- a/pkg/probo/actions.go +++ b/pkg/probo/actions.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -242,12 +242,14 @@ const ( ActionReportGetReportUrl = "core:report:get-report-url" ActionReportDownloadUrlGet = "core:report:get-download-url" - // Nonconformity actions - ActionNonconformityGet = "core:nonconformity:get" - ActionNonconformityList = "core:nonconformity:list" - ActionNonconformityCreate = "core:nonconformity:create" - ActionNonconformityUpdate = "core:nonconformity:update" - ActionNonconformityDelete = "core:nonconformity:delete" + // Finding actions + ActionFindingGet = "core:finding:get" + ActionFindingList = "core:finding:list" + ActionFindingCreate = "core:finding:create" + ActionFindingUpdate = "core:finding:update" + ActionFindingDelete = "core:finding:delete" + ActionFindingAuditMappingCreate = "core:finding:create-audit-mapping" + ActionFindingAuditMappingDelete = "core:finding:delete-audit-mapping" // Obligation actions ActionObligationGet = "core:obligation:get" @@ -256,13 +258,6 @@ const ( ActionObligationUpdate = "core:obligation:update" ActionObligationDelete = "core:obligation:delete" - // ContinualImprovement actions - ActionContinualImprovementGet = "core:continual-improvement:get" - ActionContinualImprovementList = "core:continual-improvement:list" - ActionContinualImprovementCreate = "core:continual-improvement:create" - ActionContinualImprovementUpdate = "core:continual-improvement:update" - ActionContinualImprovementDelete = "core:continual-improvement:delete" - // ProcessingActivity actions ActionProcessingActivityList = "core:processing-activity:list" ActionProcessingActivityGet = "core:processing-activity:get" diff --git a/pkg/probo/audit_service.go b/pkg/probo/audit_service.go index 5c61ee265..4118b5e68 100644 --- a/pkg/probo/audit_service.go +++ b/pkg/probo/audit_service.go @@ -485,3 +485,86 @@ func (s AuditService) ListForControlID( return page.NewPage(audits, cursor), nil } + +func (s AuditService) CountForControlID( + ctx context.Context, + controlID gid.GID, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + audits := coredata.Audits{} + count, err = audits.CountByControlID(ctx, conn, s.svc.scope, controlID) + if err != nil { + return fmt.Errorf("cannot count audits: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +} + +func (s AuditService) CountForFindingID( + ctx context.Context, + findingID gid.GID, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + audits := coredata.Audits{} + count, err = audits.CountByFindingID(ctx, conn, s.svc.scope, findingID) + if err != nil { + return fmt.Errorf("cannot count audits: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +} + +func (s AuditService) ListForFindingID( + ctx context.Context, + findingID gid.GID, + cursor *page.Cursor[coredata.AuditOrderField], +) (*page.Page[*coredata.Audit, coredata.AuditOrderField], error) { + var audits coredata.Audits + finding := &coredata.Finding{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := finding.LoadByID(ctx, conn, s.svc.scope, findingID); err != nil { + return fmt.Errorf("cannot load finding: %w", err) + } + + err := audits.LoadByFindingID(ctx, conn, s.svc.scope, finding.ID, cursor) + if err != nil { + return fmt.Errorf("cannot load audits: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(audits, cursor), nil +} diff --git a/pkg/probo/continual_improvement_service.go b/pkg/probo/continual_improvement_service.go deleted file mode 100644 index af08547e1..000000000 --- a/pkg/probo/continual_improvement_service.go +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package probo - -import ( - "context" - "fmt" - "time" - - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" - "go.probo.inc/probo/pkg/validator" -) - -type ContinualImprovementService struct { - svc *TenantService -} - -type ( - CreateContinualImprovementRequest struct { - OrganizationID gid.GID - ReferenceID string - Description *string - Source *string - OwnerID gid.GID - TargetDate *time.Time - Status *coredata.ContinualImprovementStatus - Priority *coredata.ContinualImprovementPriority - } - - UpdateContinualImprovementRequest struct { - ID gid.GID - ReferenceID *string - Description **string - Source **string - OwnerID *gid.GID - TargetDate **time.Time - Status *coredata.ContinualImprovementStatus - Priority *coredata.ContinualImprovementPriority - } -) - -func (ccir *CreateContinualImprovementRequest) Validate() error { - v := validator.New() - - v.Check(ccir.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) - v.Check(ccir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength)) - v.Check(ccir.Description, "description", validator.SafeText(ContentMaxLength)) - v.Check(ccir.Source, "source", validator.SafeText(ContentMaxLength)) - v.Check(ccir.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.MembershipProfileEntityType)) - v.Check(ccir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses())) - v.Check(ccir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities())) - - return v.Error() -} - -func (ucir *UpdateContinualImprovementRequest) Validate() error { - v := validator.New() - - v.Check(ucir.ID, "id", validator.Required(), validator.GID(coredata.ContinualImprovementEntityType)) - v.Check(ucir.ReferenceID, "reference_id", validator.SafeText(NameMaxLength)) - v.Check(ucir.Description, "description", validator.SafeText(ContentMaxLength)) - v.Check(ucir.Source, "source", validator.SafeText(ContentMaxLength)) - v.Check(ucir.OwnerID, "owner_id", validator.GID(coredata.MembershipProfileEntityType)) - v.Check(ucir.Status, "status", validator.OneOfSlice(coredata.ContinualImprovementStatuses())) - v.Check(ucir.Priority, "priority", validator.OneOfSlice(coredata.ContinualImprovementPriorities())) - - return v.Error() -} - -func (s ContinualImprovementService) Get( - ctx context.Context, - continualImprovementID gid.GID, -) (*coredata.ContinualImprovement, error) { - improvement := &coredata.ContinualImprovement{} - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - if err := improvement.LoadByID(ctx, conn, s.svc.scope, continualImprovementID); err != nil { - return fmt.Errorf("cannot load continual improvement: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return improvement, nil -} - -func (s *ContinualImprovementService) Create( - ctx context.Context, - req *CreateContinualImprovementRequest, -) (*coredata.ContinualImprovement, error) { - if err := req.Validate(); err != nil { - return nil, err - } - - now := time.Now() - - improvement := &coredata.ContinualImprovement{ - ID: gid.New(s.svc.scope.GetTenantID(), coredata.ContinualImprovementEntityType), - OrganizationID: req.OrganizationID, - ReferenceID: req.ReferenceID, - Description: req.Description, - Source: req.Source, - OwnerID: req.OwnerID, - TargetDate: req.TargetDate, - Status: *req.Status, - Priority: *req.Priority, - CreatedAt: now, - UpdatedAt: now, - } - - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - organization := &coredata.Organization{} - if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - owner := &coredata.MembershipProfile{} - if err := owner.LoadByID(ctx, conn, s.svc.scope, req.OwnerID); err != nil { - return fmt.Errorf("cannot load owner profile: %w", err) - } - - if err := improvement.Insert(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert continual improvement: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return improvement, nil -} - -func (s *ContinualImprovementService) Update( - ctx context.Context, - req *UpdateContinualImprovementRequest, -) (*coredata.ContinualImprovement, error) { - improvement := &coredata.ContinualImprovement{} - - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - if err := improvement.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { - return fmt.Errorf("cannot load continual improvement: %w", err) - } - - if req.ReferenceID != nil { - improvement.ReferenceID = *req.ReferenceID - } - - if req.Description != nil { - improvement.Description = *req.Description - } - - if req.Source != nil { - improvement.Source = *req.Source - } - - if req.OwnerID != nil { - owner := &coredata.MembershipProfile{} - if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { - return fmt.Errorf("cannot load owner profile: %w", err) - } - improvement.OwnerID = *req.OwnerID - } - - if req.TargetDate != nil { - improvement.TargetDate = *req.TargetDate - } - - if req.Status != nil { - improvement.Status = *req.Status - } - - if req.Priority != nil { - improvement.Priority = *req.Priority - } - - improvement.UpdatedAt = time.Now() - - if err := improvement.Update(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot update continual improvement: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return improvement, nil -} - -func (s *ContinualImprovementService) Delete( - ctx context.Context, - continualImprovementID gid.GID, -) error { - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - improvement := &coredata.ContinualImprovement{} - if err := improvement.LoadByID(ctx, conn, s.svc.scope, continualImprovementID); err != nil { - return fmt.Errorf("cannot load continual improvement: %w", err) - } - - if err := improvement.Delete(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot delete continual improvement: %w", err) - } - - return nil - }, - ) - - return err -} - -func (s ContinualImprovementService) CountByOrganizationID( - ctx context.Context, - organizationID gid.GID, - filter *coredata.ContinualImprovementFilter, -) (int, error) { - var count int - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) (err error) { - improvements := coredata.ContinualImprovements{} - count, err = improvements.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) - if err != nil { - return fmt.Errorf("cannot count continual improvements: %w", err) - } - - return nil - }, - ) - - if err != nil { - return 0, err - } - - return count, nil -} - -func (s ContinualImprovementService) ListForOrganizationID( - ctx context.Context, - organizationID gid.GID, - cursor *page.Cursor[coredata.ContinualImprovementOrderField], - filter *coredata.ContinualImprovementFilter, -) (*page.Page[*coredata.ContinualImprovement, coredata.ContinualImprovementOrderField], error) { - var improvements coredata.ContinualImprovements - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - err := improvements.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) - if err != nil { - return fmt.Errorf("cannot load continual improvements: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return page.NewPage(improvements, cursor), nil -} diff --git a/pkg/probo/finding_service.go b/pkg/probo/finding_service.go new file mode 100644 index 000000000..77a9eb6da --- /dev/null +++ b/pkg/probo/finding_service.go @@ -0,0 +1,476 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package probo + +import ( + "context" + "fmt" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/validator" +) + +type FindingService struct { + svc *TenantService +} + +type ( + CreateFindingRequest struct { + OrganizationID gid.GID + Kind coredata.FindingKind + Description *string + Source *string + IdentifiedOn *time.Time + RootCause *string + CorrectiveAction *string + OwnerID *gid.GID + DueDate *time.Time + Status *coredata.FindingStatus + Priority *coredata.FindingPriority + RiskID *gid.GID + EffectivenessCheck *string + } + + UpdateFindingRequest struct { + ID gid.GID + Description **string + Source **string + IdentifiedOn **time.Time + RootCause **string + CorrectiveAction **string + OwnerID *gid.GID + DueDate **time.Time + Status *coredata.FindingStatus + Priority *coredata.FindingPriority + RiskID **gid.GID + EffectivenessCheck **string + } +) + +func (r *CreateFindingRequest) Validate() error { + v := validator.New() + + v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) + v.Check(r.Kind, "kind", validator.Required(), validator.OneOfSlice(coredata.FindingKinds())) + v.Check(r.Description, "description", validator.SafeText(ContentMaxLength)) + v.Check(r.Source, "source", validator.SafeText(ContentMaxLength)) + v.Check(r.RootCause, "root_cause", validator.SafeText(ContentMaxLength)) + v.Check(r.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength)) + v.Check(r.OwnerID, "owner_id", validator.GID(coredata.MembershipProfileEntityType)) + v.Check(r.Status, "status", validator.OneOfSlice(coredata.FindingStatuses())) + v.Check(r.Priority, "priority", validator.OneOfSlice(coredata.FindingPriorities())) + v.Check(r.RiskID, "risk_id", validator.GID(coredata.RiskEntityType)) + v.Check(r.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength)) + + if r.Status != nil && *r.Status == coredata.FindingStatusRiskAccepted { + v.Check(r.RiskID, "risk_id", validator.Required()) + } + + return v.Error() +} + +func (r *UpdateFindingRequest) Validate() error { + v := validator.New() + + v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.FindingEntityType)) + v.Check(r.Description, "description", validator.SafeText(ContentMaxLength)) + v.Check(r.Source, "source", validator.SafeText(ContentMaxLength)) + v.Check(r.RootCause, "root_cause", validator.SafeText(ContentMaxLength)) + v.Check(r.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength)) + v.Check(r.OwnerID, "owner_id", validator.GID(coredata.MembershipProfileEntityType)) + v.Check(r.Status, "status", validator.OneOfSlice(coredata.FindingStatuses())) + v.Check(r.Priority, "priority", validator.OneOfSlice(coredata.FindingPriorities())) + v.Check(r.RiskID, "risk_id", validator.GID(coredata.RiskEntityType)) + v.Check(r.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength)) + + return v.Error() +} + +func (s FindingService) Get( + ctx context.Context, + findingID gid.GID, +) (*coredata.Finding, error) { + finding := &coredata.Finding{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return finding.LoadByID(ctx, conn, s.svc.scope, findingID) + }, + ) + + if err != nil { + return nil, fmt.Errorf("cannot get finding: %w", err) + } + + return finding, nil +} + +func (s *FindingService) Create( + ctx context.Context, + req *CreateFindingRequest, +) (*coredata.Finding, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + now := time.Now() + + finding := &coredata.Finding{ + ID: gid.New(s.svc.scope.GetTenantID(), coredata.FindingEntityType), + OrganizationID: req.OrganizationID, + Kind: req.Kind, + Description: req.Description, + Source: req.Source, + IdentifiedOn: req.IdentifiedOn, + RootCause: req.RootCause, + CorrectiveAction: req.CorrectiveAction, + OwnerID: req.OwnerID, + DueDate: req.DueDate, + Status: coredata.FindingStatusOpen, + Priority: coredata.FindingPriorityMedium, + RiskID: req.RiskID, + EffectivenessCheck: req.EffectivenessCheck, + CreatedAt: now, + UpdatedAt: now, + } + + if req.Status != nil { + finding.Status = *req.Status + } + + if req.Priority != nil { + finding.Priority = *req.Priority + } + + err := s.svc.pg.WithTx( + ctx, + func(conn pg.Conn) error { + organization := &coredata.Organization{} + if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + if req.OwnerID != nil { + owner := &coredata.MembershipProfile{} + if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { + return fmt.Errorf("cannot load owner profile: %w", err) + } + } + + if err := finding.Insert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert finding: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return finding, nil +} + +func (s *FindingService) Update( + ctx context.Context, + req *UpdateFindingRequest, +) (*coredata.Finding, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + finding := &coredata.Finding{} + + err := s.svc.pg.WithTx( + ctx, + func(conn pg.Conn) error { + if err := finding.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { + return fmt.Errorf("cannot load finding: %w", err) + } + + if req.Description != nil { + finding.Description = *req.Description + } + if req.Source != nil { + finding.Source = *req.Source + } + if req.IdentifiedOn != nil { + finding.IdentifiedOn = *req.IdentifiedOn + } + if req.RootCause != nil { + finding.RootCause = *req.RootCause + } + if req.CorrectiveAction != nil { + finding.CorrectiveAction = *req.CorrectiveAction + } + if req.OwnerID != nil { + owner := &coredata.MembershipProfile{} + if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { + return fmt.Errorf("cannot load owner profile: %w", err) + } + finding.OwnerID = req.OwnerID + } + if req.DueDate != nil { + finding.DueDate = *req.DueDate + } + if req.Status != nil { + finding.Status = *req.Status + } + if req.Priority != nil { + finding.Priority = *req.Priority + } + if req.RiskID != nil { + finding.RiskID = *req.RiskID + } + if req.EffectivenessCheck != nil { + finding.EffectivenessCheck = *req.EffectivenessCheck + } + + if finding.Status == coredata.FindingStatusRiskAccepted && finding.RiskID == nil { + return fmt.Errorf("cannot update finding: risk_id is required when status is RISK_ACCEPTED") + } + + finding.UpdatedAt = time.Now() + + if err := finding.Update(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot update finding: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return finding, nil +} + +func (s FindingService) Delete( + ctx context.Context, + findingID gid.GID, +) error { + finding := coredata.Finding{ID: findingID} + return s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := finding.Delete(ctx, conn, s.svc.scope) + if err != nil { + return fmt.Errorf("cannot delete finding: %w", err) + } + return nil + }, + ) +} + +func (s FindingService) ListForOrganizationID( + ctx context.Context, + organizationID gid.GID, + cursor *page.Cursor[coredata.FindingOrderField], + filter *coredata.FindingFilter, +) (*page.Page[*coredata.Finding, coredata.FindingOrderField], error) { + var findings coredata.Findings + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := findings.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) + if err != nil { + return fmt.Errorf("cannot load findings: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(findings, cursor), nil +} + +func (s FindingService) CountForOrganizationID( + ctx context.Context, + organizationID gid.GID, + filter *coredata.FindingFilter, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + findings := coredata.Findings{} + count, err = findings.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) + if err != nil { + return fmt.Errorf("cannot count findings: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +} + +func (s FindingService) CreateAuditMapping( + ctx context.Context, + findingID gid.GID, + auditID gid.GID, + referenceID string, +) (*coredata.Finding, *coredata.Audit, error) { + finding := &coredata.Finding{} + audit := &coredata.Audit{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := finding.LoadByID(ctx, conn, s.svc.scope, findingID); err != nil { + return fmt.Errorf("cannot load finding: %w", err) + } + + if err := audit.LoadByID(ctx, conn, s.svc.scope, auditID); err != nil { + return fmt.Errorf("cannot load audit: %w", err) + } + + if finding.OrganizationID != audit.OrganizationID { + return fmt.Errorf("cannot create finding audit mapping: finding and audit belong to different organizations") + } + + findingAudit := &coredata.FindingAudit{ + FindingID: findingID, + AuditID: auditID, + ReferenceID: referenceID, + OrganizationID: finding.OrganizationID, + CreatedAt: time.Now(), + } + + if err := findingAudit.Upsert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot create finding audit mapping: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, err + } + + return finding, audit, nil +} + +func (s FindingService) DeleteAuditMapping( + ctx context.Context, + findingID gid.GID, + auditID gid.GID, +) (*coredata.Finding, *coredata.Audit, error) { + finding := &coredata.Finding{} + audit := &coredata.Audit{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := finding.LoadByID(ctx, conn, s.svc.scope, findingID); err != nil { + return fmt.Errorf("cannot load finding: %w", err) + } + + if err := audit.LoadByID(ctx, conn, s.svc.scope, auditID); err != nil { + return fmt.Errorf("cannot load audit: %w", err) + } + + findingAudit := &coredata.FindingAudit{} + if err := findingAudit.Delete(ctx, conn, s.svc.scope, finding.ID, audit.ID); err != nil { + return fmt.Errorf("cannot delete finding audit mapping: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, fmt.Errorf("cannot delete finding audit mapping: %w", err) + } + + return finding, audit, nil +} + +func (s FindingService) ListForAuditID( + ctx context.Context, + auditID gid.GID, + cursor *page.Cursor[coredata.FindingOrderField], + filter *coredata.FindingFilter, +) (*page.Page[*coredata.Finding, coredata.FindingOrderField], error) { + var findings coredata.Findings + audit := &coredata.Audit{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := audit.LoadByID(ctx, conn, s.svc.scope, auditID); err != nil { + return fmt.Errorf("cannot load audit: %w", err) + } + if err := findings.LoadByAuditID(ctx, conn, s.svc.scope, auditID, cursor, filter); err != nil { + return fmt.Errorf("cannot load findings: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage([]*coredata.Finding(findings), cursor), nil +} + +func (s FindingService) CountForAuditID( + ctx context.Context, + auditID gid.GID, + filter *coredata.FindingFilter, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + findings := coredata.Findings{} + count, err = findings.CountByAuditID(ctx, conn, s.svc.scope, auditID, filter) + if err != nil { + return fmt.Errorf("cannot count findings: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/pkg/probo/nonconformity_service.go b/pkg/probo/nonconformity_service.go deleted file mode 100644 index ce9664754..000000000 --- a/pkg/probo/nonconformity_service.go +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package probo - -import ( - "context" - "fmt" - "time" - - "go.gearno.de/kit/pg" - "go.probo.inc/probo/pkg/coredata" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" - "go.probo.inc/probo/pkg/validator" -) - -type NonconformityService struct { - svc *TenantService -} - -type ( - CreateNonconformityRequest struct { - OrganizationID gid.GID - ReferenceID string - Description *string - AuditID *gid.GID - DateIdentified *time.Time - RootCause string - CorrectiveAction *string - OwnerID gid.GID - DueDate *time.Time - Status *coredata.NonconformityStatus - EffectivenessCheck *string - } - - UpdateNonconformityRequest struct { - ID gid.GID - ReferenceID *string - Description **string - DateIdentified **time.Time - RootCause *string - CorrectiveAction **string - OwnerID *gid.GID - AuditID **gid.GID - DueDate **time.Time - Status *coredata.NonconformityStatus - EffectivenessCheck **string - } -) - -func (cnr *CreateNonconformityRequest) Validate() error { - v := validator.New() - - v.Check(cnr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) - v.Check(cnr.ReferenceID, "reference_id", validator.Required(), validator.SafeText(NameMaxLength)) - v.Check(cnr.Description, "description", validator.SafeText(ContentMaxLength)) - v.Check(cnr.AuditID, "audit_id", validator.GID(coredata.AuditEntityType)) - v.Check(cnr.RootCause, "root_cause", validator.Required(), validator.SafeText(ContentMaxLength)) - v.Check(cnr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength)) - v.Check(cnr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.MembershipProfileEntityType)) - v.Check(cnr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses())) - v.Check(cnr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength)) - - return v.Error() -} - -func (unr *UpdateNonconformityRequest) Validate() error { - v := validator.New() - - v.Check(unr.ID, "id", validator.Required(), validator.GID(coredata.NonconformityEntityType)) - v.Check(unr.ReferenceID, "reference_id", validator.SafeText(NameMaxLength)) - v.Check(unr.Description, "description", validator.SafeText(ContentMaxLength)) - v.Check(unr.RootCause, "root_cause", validator.SafeText(ContentMaxLength)) - v.Check(unr.CorrectiveAction, "corrective_action", validator.SafeText(ContentMaxLength)) - v.Check(unr.OwnerID, "owner_id", validator.GID(coredata.MembershipProfileEntityType)) - v.Check(unr.Status, "status", validator.OneOfSlice(coredata.NonconformityStatuses())) - v.Check(unr.EffectivenessCheck, "effectiveness_check", validator.SafeText(ContentMaxLength)) - - return v.Error() -} -func (s NonconformityService) Get( - ctx context.Context, - nonconformityID gid.GID, -) (*coredata.Nonconformity, error) { - nonconformity := &coredata.Nonconformity{} - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - return nonconformity.LoadByID(ctx, conn, s.svc.scope, nonconformityID) - }, - ) - - if err != nil { - return nil, err - } - - return nonconformity, nil -} - -func (s *NonconformityService) Create( - ctx context.Context, - req *CreateNonconformityRequest, -) (*coredata.Nonconformity, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("invalid request: %w", err) - } - - now := time.Now() - - nonconformity := &coredata.Nonconformity{ - ID: gid.New(s.svc.scope.GetTenantID(), coredata.NonconformityEntityType), - OrganizationID: req.OrganizationID, - ReferenceID: req.ReferenceID, - Description: req.Description, - AuditID: req.AuditID, - DateIdentified: req.DateIdentified, - RootCause: req.RootCause, - CorrectiveAction: req.CorrectiveAction, - OwnerID: req.OwnerID, - DueDate: req.DueDate, - Status: coredata.NonconformityStatusOpen, - EffectivenessCheck: req.EffectivenessCheck, - CreatedAt: now, - UpdatedAt: now, - } - - if req.Status != nil { - nonconformity.Status = *req.Status - } - - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - organization := &coredata.Organization{} - if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } - - if req.AuditID != nil { - audit := &coredata.Audit{} - if err := audit.LoadByID(ctx, conn, s.svc.scope, *req.AuditID); err != nil { - return fmt.Errorf("cannot load audit: %w", err) - } - } - - owner := &coredata.MembershipProfile{} - if err := owner.LoadByID(ctx, conn, s.svc.scope, req.OwnerID); err != nil { - return fmt.Errorf("cannot load owner profile: %w", err) - } - - if err := nonconformity.Insert(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert nonconformity: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return nonconformity, nil -} - -func (s *NonconformityService) Update( - ctx context.Context, - req *UpdateNonconformityRequest, -) (*coredata.Nonconformity, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("invalid request: %w", err) - } - - nonconformity := &coredata.Nonconformity{} - - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - if err := nonconformity.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { - return fmt.Errorf("cannot load nonconformity: %w", err) - } - - if req.ReferenceID != nil { - nonconformity.ReferenceID = *req.ReferenceID - } - if req.Description != nil { - nonconformity.Description = *req.Description - } - if req.DateIdentified != nil { - nonconformity.DateIdentified = *req.DateIdentified - } - if req.RootCause != nil { - nonconformity.RootCause = *req.RootCause - } - if req.CorrectiveAction != nil { - nonconformity.CorrectiveAction = *req.CorrectiveAction - } - if req.OwnerID != nil { - owner := &coredata.MembershipProfile{} - if err := owner.LoadByID(ctx, conn, s.svc.scope, *req.OwnerID); err != nil { - return fmt.Errorf("cannot load owner profile: %w", err) - } - nonconformity.OwnerID = *req.OwnerID - } - if req.AuditID != nil { - if *req.AuditID != nil { - audit := &coredata.Audit{} - if err := audit.LoadByID(ctx, conn, s.svc.scope, **req.AuditID); err != nil { - return fmt.Errorf("cannot load audit: %w", err) - } - } - nonconformity.AuditID = *req.AuditID - } - if req.DueDate != nil { - nonconformity.DueDate = *req.DueDate - } - if req.Status != nil { - nonconformity.Status = *req.Status - } - if req.EffectivenessCheck != nil { - nonconformity.EffectivenessCheck = *req.EffectivenessCheck - } - - nonconformity.UpdatedAt = time.Now() - - if err := nonconformity.Update(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot update nonconformity: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return nonconformity, nil -} - -func (s NonconformityService) Delete( - ctx context.Context, - nonconformityID gid.GID, -) error { - nonconformity := coredata.Nonconformity{ID: nonconformityID} - return s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - err := nonconformity.Delete(ctx, conn, s.svc.scope) - if err != nil { - return fmt.Errorf("cannot delete nonconformity: %w", err) - } - return nil - }, - ) -} - -func (s NonconformityService) ListForOrganizationID( - ctx context.Context, - organizationID gid.GID, - cursor *page.Cursor[coredata.NonconformityOrderField], - filter *coredata.NonconformityFilter, -) (*page.Page[*coredata.Nonconformity, coredata.NonconformityOrderField], error) { - var nonconformities coredata.Nonconformities - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - err := nonconformities.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) - if err != nil { - return fmt.Errorf("cannot load nonconformities: %w", err) - } - - return nil - }, - ) - - if err != nil { - return nil, err - } - - return page.NewPage(nonconformities, cursor), nil -} - -func (s NonconformityService) CountForOrganizationID( - ctx context.Context, - organizationID gid.GID, - filter *coredata.NonconformityFilter, -) (int, error) { - var count int - - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) (err error) { - nonconformities := coredata.Nonconformities{} - count, err = nonconformities.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) - if err != nil { - return fmt.Errorf("cannot count nonconformities: %w", err) - } - - return nil - }, - ) - - if err != nil { - return 0, err - } - - return count, nil -} diff --git a/pkg/probo/policies.go b/pkg/probo/policies.go index 075e1dd9c..5d01afabf 100644 --- a/pkg/probo/policies.go +++ b/pkg/probo/policies.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -66,9 +66,8 @@ var ViewerPolicy = policy.NewPolicy( ActionDatumGet, ActionDatumList, ActionAuditGet, ActionAuditList, ActionReportGet, ActionReportGetReportUrl, ActionReportDownloadUrlGet, - ActionNonconformityGet, ActionNonconformityList, + ActionFindingGet, ActionFindingList, ActionObligationGet, ActionObligationList, - ActionContinualImprovementGet, ActionContinualImprovementList, ActionProcessingActivityGet, ActionProcessingActivityList, ActionDataProtectionImpactAssessmentGet, ActionDataProtectionImpactAssessmentList, ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList, @@ -134,9 +133,8 @@ var AuditorPolicy = policy.NewPolicy( ActionDatumGet, ActionDatumList, ActionAuditGet, ActionAuditList, ActionReportGet, ActionReportGetReportUrl, ActionReportDownloadUrlGet, - ActionNonconformityGet, ActionNonconformityList, + ActionFindingGet, ActionFindingList, ActionObligationGet, ActionObligationList, - ActionContinualImprovementGet, ActionContinualImprovementList, ActionProcessingActivityGet, ActionProcessingActivityList, ActionDataProtectionImpactAssessmentGet, ActionTransferImpactAssessmentGet, ActionTransferImpactAssessmentList, diff --git a/pkg/probo/service.go b/pkg/probo/service.go index 3511cf5ba..b2d670c36 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Probo Inc . +// Copyright (c) 2025-2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -107,10 +107,9 @@ type ( TrustCenterFiles *TrustCenterFileService ComplianceFrameworks *ComplianceFrameworkService ComplianceExternalURLs *ComplianceExternalURLService - Nonconformities *NonconformityService + Findings *FindingService Obligations *ObligationService Snapshots *SnapshotService - ContinualImprovements *ContinualImprovementService RightsRequests *RightsRequestService ProcessingActivities *ProcessingActivityService DataProtectionImpactAssessments *DataProtectionImpactAssessmentService @@ -257,10 +256,9 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { filevalidation.WithMaxFileSize(10*1024*1024), // 10MB ), } - tenantService.Nonconformities = &NonconformityService{svc: tenantService} + tenantService.Findings = &FindingService{svc: tenantService} tenantService.Obligations = &ObligationService{svc: tenantService} tenantService.Snapshots = &SnapshotService{svc: tenantService} - tenantService.ContinualImprovements = &ContinualImprovementService{svc: tenantService} tenantService.RightsRequests = &RightsRequestService{svc: tenantService} tenantService.ProcessingActivities = &ProcessingActivityService{ svc: tenantService,