// Copyright (c) 2025-2026 Probo Inc . // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package coredata import ( "context" "errors" "fmt" "maps" "time" "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam/policy" "go.probo.inc/probo/pkg/page" ) // MaxThirdPartyLevel is the deepest sub-third-party nesting allowed. Level 1 is // a direct third party; each descendant adds one level, so the chain may not go // beyond level 4. const MaxThirdPartyLevel = 4 func (v ThirdParty) GetGeneratedDocumentID( ctx context.Context, conn pg.Querier, organizationID gid.GID, ) (*gid.GID, error) { var documentID *gid.GID err := conn.QueryRow( ctx, ` SELECT third_parties_document_id FROM generated_documents WHERE organization_id = @organization_id `, pgx.NamedArgs{"organization_id": organizationID}, ).Scan(&documentID) if errors.Is(err, pgx.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("cannot get thirdParty list document ID: %w", err) } return documentID, nil } func (v ThirdParty) UpsertGeneratedDocumentID( ctx context.Context, conn pg.Tx, organizationID gid.GID, tenantID gid.TenantID, documentID gid.GID, ) error { now := time.Now() _, err := conn.Exec( ctx, ` INSERT INTO generated_documents ( organization_id, tenant_id, third_parties_document_id, created_at, updated_at ) VALUES ( @organization_id, @tenant_id, @third_parties_document_id, @created_at, @updated_at ) ON CONFLICT (organization_id) DO UPDATE SET third_parties_document_id = @third_parties_document_id, updated_at = @updated_at `, pgx.NamedArgs{ "organization_id": organizationID, "tenant_id": tenantID, "third_parties_document_id": documentID, "created_at": now, "updated_at": now, }, ) if err != nil { return fmt.Errorf("cannot upsert thirdParty list document ID: %w", err) } return nil } func (v ThirdParty) ClearGeneratedDocumentID( ctx context.Context, conn pg.Tx, documentIDs []gid.GID, ) error { ids := make([]string, len(documentIDs)) for i, id := range documentIDs { ids[i] = id.String() } _, err := conn.Exec( ctx, ` UPDATE generated_documents SET third_parties_document_id = NULL, updated_at = @now WHERE third_parties_document_id = ANY(@ids) `, pgx.NamedArgs{ "ids": ids, "now": time.Now(), }, ) if err != nil { return fmt.Errorf("cannot clear thirdParty list document references: %w", err) } return nil } type ( ThirdParty struct { ID gid.GID `db:"id"` OrganizationID gid.GID `db:"organization_id"` ParentThirdPartyID *gid.GID `db:"parent_third_party_id"` CommonThirdPartyID *gid.GID `db:"common_third_party_id"` Name string `db:"name"` Description *string `db:"description"` Category ThirdPartyCategory `db:"category"` HeadquarterAddress *string `db:"headquarter_address"` LegalName *string `db:"legal_name"` WebsiteURL *string `db:"website_url"` PrivacyPolicyURL *string `db:"privacy_policy_url"` ServiceLevelAgreementURL *string `db:"service_level_agreement_url"` DataProcessingAgreementURL *string `db:"data_processing_agreement_url"` BusinessAssociateAgreementURL *string `db:"business_associate_agreement_url"` SubprocessorsListURL *string `db:"subprocessors_list_url"` Certifications []string `db:"certifications"` Countries CountryCodes `db:"countries"` BusinessOwnerID *gid.GID `db:"business_owner_profile_id"` SecurityOwnerID *gid.GID `db:"security_owner_profile_id"` StatusPageURL *string `db:"status_page_url"` TermsOfServiceURL *string `db:"terms_of_service_url"` SecurityPageURL *string `db:"security_page_url"` TrustPageURL *string `db:"trust_page_url"` ShowOnCompliancePortal bool `db:"show_on_trust_center"` Level int `db:"level"` VettingStatus *ThirdPartyVettingStatus `db:"vetting_status"` VettingWebsiteURL *string `db:"vetting_website_url"` VettingProcedure *string `db:"vetting_procedure"` VettingProcessingStartedAt *time.Time `db:"vetting_processing_started_at"` VettingErrorMessage *string `db:"vetting_error_message"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } ThirdParties []*ThirdParty ) func (v ThirdParty) CursorKey(orderBy ThirdPartyOrderField) page.CursorKey { switch orderBy { case ThirdPartyOrderFieldCreatedAt: return page.NewCursorKey(v.ID, v.CreatedAt) case ThirdPartyOrderFieldUpdatedAt: return page.NewCursorKey(v.ID, v.UpdatedAt) case ThirdPartyOrderFieldName: return page.NewCursorKey(v.ID, v.Name) } panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } func (v *ThirdParty) AuthorizationAttributes( ctx context.Context, conn pg.Querier, resourceIDs []gid.GID, ) (policy.AttributesByID, error) { q := `SELECT id, organization_id FROM third_parties WHERE id = ANY(@resource_ids::text[])` args := pgx.StrictNamedArgs{ "resource_ids": resourceIDs, } rows, err := conn.Query(ctx, q, args) if err != nil { return nil, fmt.Errorf("cannot query authorization attributes: %w", err) } defer rows.Close() attrsByID := make(policy.AttributesByID) for rows.Next() { var id, organizationID gid.GID if err := rows.Scan(&id, &organizationID); err != nil { return nil, fmt.Errorf("cannot scan authorization attributes: %w", err) } attrsByID[id] = policy.Attributes{ "organization_id": organizationID.String(), } } if err := rows.Err(); err != nil { return nil, fmt.Errorf("cannot iterate authorization attributes: %w", err) } return attrsByID, nil } func (v *ThirdParty) LoadByID( ctx context.Context, conn pg.Querier, scope Scoper, thirdPartyID gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND id = @third_party_id LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"third_party_id": thirdPartyID} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParty: %w", err) } defer rows.Close() thirdParty, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } return fmt.Errorf("cannot collect thirdParty: %w", err) } *v = thirdParty return nil } func (v *ThirdParty) LoadByIDForUpdate( ctx context.Context, conn pg.Tx, scope Scoper, thirdPartyID gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND id = @third_party_id LIMIT 1 FOR UPDATE; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"third_party_id": thirdPartyID} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParty: %w", err) } defer rows.Close() thirdParty, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } return fmt.Errorf("cannot collect thirdParty: %w", err) } *v = thirdParty return nil } func (v *ThirdParty) LoadByNameAndOrganizationID( ctx context.Context, conn pg.Querier, scope Scoper, name string, organizationID gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND organization_id = @organization_id AND name = @name LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "organization_id": organizationID, "name": name, } maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParty by name: %w", err) } defer rows.Close() thirdParty, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } return fmt.Errorf("cannot collect thirdParty: %w", err) } *v = thirdParty return nil } func (v *ThirdParty) LoadByNameAndParentThirdPartyID( ctx context.Context, conn pg.Querier, scope Scoper, name string, parentThirdPartyID gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND parent_third_party_id = @parent_third_party_id AND name = @name LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "parent_third_party_id": parentThirdPartyID, "name": name, } maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParty by name and parent: %w", err) } defer rows.Close() thirdParty, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } return fmt.Errorf("cannot collect thirdParty: %w", err) } *v = thirdParty return nil } func (v *ThirdParties) LoadByIDs( ctx context.Context, conn pg.Querier, scope Scoper, thirdPartyIDs []gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND id = ANY(@third_party_ids) ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"third_party_ids": thirdPartyIDs} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *v = thirdParties if len(thirdParties) != len(gid.NewSet(thirdPartyIDs...)) { return ErrResourceNotFound } return nil } func (v ThirdParty) Insert( ctx context.Context, conn pg.Tx, scope Scoper, ) error { q := ` INSERT INTO third_parties ( tenant_id, id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at ) VALUES ( @tenant_id, @third_party_id, @organization_id, @parent_third_party_id, @common_third_party_id, @name, @description, @category, @headquarter_address, @legal_name, @website_url, @privacy_policy_url, @service_level_agreement_url, @data_processing_agreement_url, @business_associate_agreement_url, @subprocessors_list_url, @certifications, @countries, @business_owner_profile_id, @security_owner_profile_id, @status_page_url, @terms_of_service_url, @security_page_url, @trust_page_url, @show_on_trust_center, @level, @vetting_status, @vetting_website_url, @vetting_procedure, @vetting_processing_started_at, @vetting_error_message, @created_at, @updated_at ) ` args := pgx.StrictNamedArgs{ "tenant_id": scope.GetTenantID(), "third_party_id": v.ID, "organization_id": v.OrganizationID, "parent_third_party_id": v.ParentThirdPartyID, "common_third_party_id": v.CommonThirdPartyID, "name": v.Name, "description": v.Description, "category": v.Category, "headquarter_address": v.HeadquarterAddress, "legal_name": v.LegalName, "website_url": v.WebsiteURL, "privacy_policy_url": v.PrivacyPolicyURL, "service_level_agreement_url": v.ServiceLevelAgreementURL, "data_processing_agreement_url": v.DataProcessingAgreementURL, "business_associate_agreement_url": v.BusinessAssociateAgreementURL, "subprocessors_list_url": v.SubprocessorsListURL, "certifications": v.Certifications, "countries": v.Countries, "business_owner_profile_id": v.BusinessOwnerID, "security_owner_profile_id": v.SecurityOwnerID, "status_page_url": v.StatusPageURL, "terms_of_service_url": v.TermsOfServiceURL, "security_page_url": v.SecurityPageURL, "trust_page_url": v.TrustPageURL, "show_on_trust_center": v.ShowOnCompliancePortal, "level": v.Level, "vetting_status": v.VettingStatus, "vetting_website_url": v.VettingWebsiteURL, "vetting_procedure": v.VettingProcedure, "vetting_processing_started_at": v.VettingProcessingStartedAt, "vetting_error_message": v.VettingErrorMessage, "created_at": v.CreatedAt, "updated_at": v.UpdatedAt, } _, err := conn.Exec(ctx, q, args) return err } func (v ThirdParty) Delete( ctx context.Context, conn pg.Tx, scope Scoper, ) error { q := ` DELETE FROM third_parties WHERE %s AND id = @third_party_id ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"third_party_id": v.ID} maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) return err } func (v *ThirdParties) CountByOrganizationID( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, filter *ThirdPartyFilter, ) (int, error) { q := ` SELECT COUNT(id) FROM third_parties WHERE %s AND organization_id = @organization_id AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) var count int err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) } return count, nil } func (v *ThirdParties) LoadDistinctCompliancePortalCategoriesByOrganizationID( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, ) ([]ThirdPartyCategory, error) { q := ` SELECT DISTINCT category FROM third_parties WHERE %s AND organization_id = @organization_id AND show_on_trust_center = true ORDER BY category ASC ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return nil, fmt.Errorf("cannot query thirdParty categories: %w", err) } categories, err := pgx.CollectRows(rows, pgx.RowTo[ThirdPartyCategory]) if err != nil { return nil, fmt.Errorf("cannot collect thirdParty categories: %w", err) } return categories, nil } func (v *ThirdParties) LoadDistinctCompliancePortalCountriesByOrganizationID( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, ) ([]CountryCode, error) { q := ` SELECT DISTINCT unnest(countries) AS country FROM third_parties WHERE %s AND organization_id = @organization_id AND show_on_trust_center = true ORDER BY country ASC ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return nil, fmt.Errorf("cannot query thirdParty countries: %w", err) } countries, err := pgx.CollectRows(rows, pgx.RowTo[CountryCode]) if err != nil { return nil, fmt.Errorf("cannot collect thirdParty countries: %w", err) } return countries, nil } func (v *ThirdParties) LoadByOrganizationID( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], filter *ThirdPartyFilter, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND organization_id = @organization_id AND %s AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, filter.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *v = thirdParties return nil } func (v *ThirdParty) Update( ctx context.Context, conn pg.Tx, scope Scoper, ) error { q := ` UPDATE third_parties SET common_third_party_id = @common_third_party_id, name = @name, description = @description, category = @category, parent_third_party_id = @parent_third_party_id, headquarter_address = @headquarter_address, legal_name = @legal_name, website_url = @website_url, privacy_policy_url = @privacy_policy_url, service_level_agreement_url = @service_level_agreement_url, data_processing_agreement_url = @data_processing_agreement_url, business_associate_agreement_url = @business_associate_agreement_url, subprocessors_list_url = @subprocessors_list_url, certifications = @certifications, countries = @countries, status_page_url = @status_page_url, terms_of_service_url = @terms_of_service_url, security_page_url = @security_page_url, trust_page_url = @trust_page_url, business_owner_profile_id = @business_owner_profile_id, security_owner_profile_id = @security_owner_profile_id, show_on_trust_center = @show_on_trust_center, level = @level, vetting_status = @vetting_status, vetting_website_url = @vetting_website_url, vetting_procedure = @vetting_procedure, vetting_processing_started_at = @vetting_processing_started_at, vetting_error_message = @vetting_error_message, updated_at = @updated_at WHERE %s AND id = @third_party_id ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "third_party_id": v.ID, "updated_at": time.Now(), "common_third_party_id": v.CommonThirdPartyID, "name": v.Name, "description": v.Description, "category": v.Category, "parent_third_party_id": v.ParentThirdPartyID, "headquarter_address": v.HeadquarterAddress, "legal_name": v.LegalName, "website_url": v.WebsiteURL, "privacy_policy_url": v.PrivacyPolicyURL, "service_level_agreement_url": v.ServiceLevelAgreementURL, "data_processing_agreement_url": v.DataProcessingAgreementURL, "business_associate_agreement_url": v.BusinessAssociateAgreementURL, "subprocessors_list_url": v.SubprocessorsListURL, "certifications": v.Certifications, "countries": v.Countries, "status_page_url": v.StatusPageURL, "terms_of_service_url": v.TermsOfServiceURL, "security_page_url": v.SecurityPageURL, "trust_page_url": v.TrustPageURL, "business_owner_profile_id": v.BusinessOwnerID, "security_owner_profile_id": v.SecurityOwnerID, "show_on_trust_center": v.ShowOnCompliancePortal, "level": v.Level, "vetting_status": v.VettingStatus, "vetting_website_url": v.VettingWebsiteURL, "vetting_procedure": v.VettingProcedure, "vetting_processing_started_at": v.VettingProcessingStartedAt, "vetting_error_message": v.VettingErrorMessage, } maps.Copy(args, scope.SQLArguments()) result, err := conn.Exec(ctx, q, args) if err != nil { return err } if result.RowsAffected() == 0 { return ErrResourceNotFound } return nil } func (v ThirdParty) ExpireNonExpiredRiskAssessments( ctx context.Context, conn pg.Querier, scope Scoper, ) error { now := time.Now() q := ` UPDATE third_party_risk_assessments SET expires_at = @now, updated_at = @now WHERE %s AND third_party_id = @third_party_id AND expires_at > @now ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "third_party_id": v.ID, "now": now, } maps.Copy(args, scope.SQLArguments()) _, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot expire existing risk assessments: %w", err) } return nil } func (v *ThirdParties) CountByAssetID( ctx context.Context, conn pg.Querier, scope Scoper, assetID gid.GID, ) (int, error) { q := ` WITH vend AS ( SELECT v.id FROM third_parties v INNER JOIN asset_third_parties av ON v.id = av.third_party_id WHERE av.asset_id = @asset_id ) SELECT COUNT(id) FROM vend WHERE %s ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"asset_id": assetID} maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) var count int err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) } return count, nil } func (v *ThirdParties) LoadByAssetID( ctx context.Context, conn pg.Querier, scope Scoper, assetID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], ) error { q := ` WITH vend AS ( SELECT v.id, v.tenant_id, v.organization_id, v.parent_third_party_id, v.common_third_party_id, v.name, v.description, v.category, v.headquarter_address, v.legal_name, v.website_url, v.privacy_policy_url, v.service_level_agreement_url, v.data_processing_agreement_url, v.business_associate_agreement_url, v.subprocessors_list_url, v.certifications, v.countries, v.business_owner_profile_id, v.security_owner_profile_id, v.status_page_url, v.terms_of_service_url, v.security_page_url, v.trust_page_url, v.show_on_trust_center, v.level, v.vetting_status, v.vetting_website_url, v.vetting_procedure, v.vetting_processing_started_at, v.vetting_error_message, v.created_at, v.updated_at FROM third_parties v INNER JOIN asset_third_parties av ON v.id = av.third_party_id WHERE av.asset_id = @asset_id ) SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM vend WHERE %s AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"asset_id": assetID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *v = thirdParties return nil } func (v *ThirdParties) CountByDatumID( ctx context.Context, conn pg.Querier, scope Scoper, datumID gid.GID, ) (int, error) { q := ` WITH vend AS ( SELECT v.id FROM third_parties v INNER JOIN data_third_parties dv ON v.id = dv.third_party_id WHERE dv.datum_id = @datum_id ) SELECT COUNT(id) FROM vend WHERE %s ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"datum_id": datumID} maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) var count int err := row.Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) } return count, nil } func (vs *ThirdParties) LoadByDatumID( ctx context.Context, conn pg.Querier, scope Scoper, datumID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], ) error { q := ` WITH vend AS ( SELECT v.id, v.tenant_id, v.organization_id, v.parent_third_party_id, v.common_third_party_id, v.name, v.description, v.category, v.headquarter_address, v.legal_name, v.website_url, v.privacy_policy_url, v.service_level_agreement_url, v.data_processing_agreement_url, v.business_associate_agreement_url, v.subprocessors_list_url, v.certifications, v.countries, v.business_owner_profile_id, v.security_owner_profile_id, v.status_page_url, v.terms_of_service_url, v.security_page_url, v.trust_page_url, v.show_on_trust_center, v.level, v.vetting_status, v.vetting_website_url, v.vetting_procedure, v.vetting_processing_started_at, v.vetting_error_message, v.created_at, v.updated_at FROM third_parties v INNER JOIN data_third_parties dv ON v.id = dv.third_party_id WHERE dv.datum_id = @datum_id ) SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM vend WHERE %s AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"datum_id": datumID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *vs = thirdParties return nil } func (v *ThirdParties) LoadByProcessingActivityID( ctx context.Context, conn pg.Querier, scope Scoper, processingActivityID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], ) error { q := ` WITH vend AS ( SELECT v.id, v.tenant_id, v.organization_id, v.parent_third_party_id, v.common_third_party_id, v.name, v.description, v.category, v.headquarter_address, v.legal_name, v.website_url, v.privacy_policy_url, v.service_level_agreement_url, v.data_processing_agreement_url, v.business_associate_agreement_url, v.subprocessors_list_url, v.certifications, v.countries, v.business_owner_profile_id, v.security_owner_profile_id, v.status_page_url, v.terms_of_service_url, v.security_page_url, v.trust_page_url, v.show_on_trust_center, v.level, v.vetting_status, v.vetting_website_url, v.vetting_procedure, v.vetting_processing_started_at, v.vetting_error_message, v.created_at, v.updated_at FROM third_parties v INNER JOIN processing_activity_third_parties pav ON v.id = pav.third_party_id WHERE pav.processing_activity_id = @processing_activity_id ) SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM vend WHERE %s AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"processing_activity_id": processingActivityID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *v = thirdParties return nil } func (v *ThirdParties) LoadAllByProcessingActivities( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, ) (map[gid.GID][]string, error) { q := ` WITH filtered_processing_activities AS ( SELECT pa.id FROM processing_activities pa WHERE pa.tenant_id = @tenant_id AND pa.organization_id = @organization_id ), filtered_third_parties AS ( SELECT v.id, v.name FROM third_parties v WHERE v.tenant_id = @tenant_id ) SELECT pav.processing_activity_id, fv.name FROM processing_activity_third_parties pav INNER JOIN filtered_third_parties fv ON fv.id = pav.third_party_id INNER JOIN filtered_processing_activities fpa ON fpa.id = pav.processing_activity_id WHERE pav.tenant_id = @tenant_id ORDER BY pav.processing_activity_id, fv.name ` args := pgx.StrictNamedArgs{ "organization_id": organizationID, } maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return nil, fmt.Errorf("cannot query thirdParties: %w", err) } defer rows.Close() thirdPartyMap := make(map[gid.GID][]string) for rows.Next() { var ( processingActivityID gid.GID thirdPartyName string ) if err := rows.Scan(&processingActivityID, &thirdPartyName); err != nil { return nil, fmt.Errorf("cannot scan thirdParty: %w", err) } thirdPartyMap[processingActivityID] = append(thirdPartyMap[processingActivityID], thirdPartyName) } return thirdPartyMap, nil } func (v *ThirdParty) LoadByOrganizationIDAndCommonThirdPartyID( ctx context.Context, conn pg.Querier, scope Scoper, organizationID gid.GID, commonThirdPartyID gid.GID, ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND organization_id = @organization_id AND common_third_party_id = @common_third_party_id ORDER BY id ASC LIMIT 1; ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "organization_id": organizationID, "common_third_party_id": commonThirdPartyID, } maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query vendor by common third party: %w", err) } defer rows.Close() vendor, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ThirdParty]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrResourceNotFound } return fmt.Errorf("cannot collect vendor by common third party: %w", err) } *v = vendor return nil } func (v *ThirdParties) CountByMeasureID( ctx context.Context, conn pg.Querier, scope Scoper, measureID gid.GID, ) (int, error) { q := ` WITH tps AS ( SELECT v.id, v.tenant_id FROM third_parties v INNER JOIN measures_third_parties mtp ON v.id = mtp.third_party_id WHERE mtp.measure_id = @measure_id ) SELECT COUNT(id) FROM tps WHERE %s ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"measure_id": measureID} maps.Copy(args, scope.SQLArguments()) row := conn.QueryRow(ctx, q, args) var count int if err := row.Scan(&count); err != nil { return 0, fmt.Errorf("cannot count thirdParties: %w", err) } return count, nil } func (v *ThirdParties) LoadByMeasureID( ctx context.Context, conn pg.Querier, scope Scoper, measureID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], ) error { q := ` WITH tps AS ( SELECT v.id, v.tenant_id, v.organization_id, v.parent_third_party_id, v.common_third_party_id, v.name, v.description, v.category, v.headquarter_address, v.legal_name, v.website_url, v.privacy_policy_url, v.service_level_agreement_url, v.data_processing_agreement_url, v.business_associate_agreement_url, v.subprocessors_list_url, v.certifications, v.countries, v.business_owner_profile_id, v.security_owner_profile_id, v.status_page_url, v.terms_of_service_url, v.security_page_url, v.trust_page_url, v.show_on_trust_center, v.level, v.vetting_status, v.vetting_website_url, v.vetting_procedure, v.vetting_processing_started_at, v.vetting_error_message, v.created_at, v.updated_at FROM third_parties v INNER JOIN measures_third_parties mtp ON v.id = mtp.third_party_id WHERE mtp.measure_id = @measure_id ) SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM tps WHERE %s AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"measure_id": measureID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query thirdParties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect thirdParties: %w", err) } *v = thirdParties return nil } func (v *ThirdParties) CountByParentThirdPartyID( ctx context.Context, conn pg.Querier, scope Scoper, parentThirdPartyID gid.GID, ) (int, error) { q := ` SELECT COUNT(id) FROM third_parties WHERE %s AND parent_third_party_id = @parent_third_party_id ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{"parent_third_party_id": parentThirdPartyID} maps.Copy(args, scope.SQLArguments()) var count int err := conn.QueryRow(ctx, q, args).Scan(&count) if err != nil { return 0, fmt.Errorf("cannot count child third parties: %w", err) } return count, nil } func (v *ThirdParties) LoadAllAncestorsByThirdPartyID( ctx context.Context, conn pg.Querier, scope Scoper, thirdPartyID gid.GID, ) error { q := ` WITH RECURSIVE ancestor_chain AS ( SELECT tp.id, tp.tenant_id, tp.organization_id, tp.parent_third_party_id, tp.common_third_party_id, tp.name, tp.description, tp.category, tp.headquarter_address, tp.legal_name, tp.website_url, tp.privacy_policy_url, tp.service_level_agreement_url, tp.data_processing_agreement_url, tp.business_associate_agreement_url, tp.subprocessors_list_url, tp.certifications, tp.countries, tp.business_owner_profile_id, tp.security_owner_profile_id, tp.status_page_url, tp.terms_of_service_url, tp.security_page_url, tp.trust_page_url, tp.show_on_trust_center, tp.level, tp.vetting_status, tp.vetting_website_url, tp.vetting_procedure, tp.vetting_processing_started_at, tp.vetting_error_message, tp.created_at, tp.updated_at, 1 AS depth FROM third_parties tp WHERE %s AND tp.id = ( SELECT parent_third_party_id FROM third_parties WHERE id = @third_party_id ) UNION ALL SELECT tp.id, tp.tenant_id, tp.organization_id, tp.parent_third_party_id, tp.common_third_party_id, tp.name, tp.description, tp.category, tp.headquarter_address, tp.legal_name, tp.website_url, tp.privacy_policy_url, tp.service_level_agreement_url, tp.data_processing_agreement_url, tp.business_associate_agreement_url, tp.subprocessors_list_url, tp.certifications, tp.countries, tp.business_owner_profile_id, tp.security_owner_profile_id, tp.status_page_url, tp.terms_of_service_url, tp.security_page_url, tp.trust_page_url, tp.show_on_trust_center, tp.level, tp.vetting_status, tp.vetting_website_url, tp.vetting_procedure, tp.vetting_processing_started_at, tp.vetting_error_message, tp.created_at, tp.updated_at, ac.depth + 1 FROM third_parties tp JOIN ancestor_chain ac ON tp.id = ac.parent_third_party_id WHERE ac.depth < @max_depth AND tp.tenant_id = ac.tenant_id ) SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM ancestor_chain ORDER BY depth DESC ` q = fmt.Sprintf(q, scope.SQLFragment()) args := pgx.StrictNamedArgs{ "third_party_id": thirdPartyID, "max_depth": MaxThirdPartyLevel, } maps.Copy(args, scope.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query ancestors: %w", err) } ancestors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect ancestors: %w", err) } *v = ancestors return nil } func (v *ThirdParties) LoadByParentThirdPartyID( ctx context.Context, conn pg.Querier, scope Scoper, parentThirdPartyID gid.GID, cursor *page.Cursor[ThirdPartyOrderField], ) error { q := ` SELECT id, organization_id, parent_third_party_id, common_third_party_id, name, description, category, headquarter_address, legal_name, website_url, privacy_policy_url, service_level_agreement_url, data_processing_agreement_url, business_associate_agreement_url, subprocessors_list_url, certifications, countries, business_owner_profile_id, security_owner_profile_id, status_page_url, terms_of_service_url, security_page_url, trust_page_url, show_on_trust_center, level, vetting_status, vetting_website_url, vetting_procedure, vetting_processing_started_at, vetting_error_message, created_at, updated_at FROM third_parties WHERE %s AND parent_third_party_id = @parent_third_party_id AND %s ` q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"parent_third_party_id": parentThirdPartyID} maps.Copy(args, scope.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) if err != nil { return fmt.Errorf("cannot query child third parties: %w", err) } thirdParties, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ThirdParty]) if err != nil { return fmt.Errorf("cannot collect child third parties: %w", err) } *v = thirdParties return nil }