package services import ( "context" "database/sql" "errors" "fmt" "net/http" "time" "edge-infra.dev/pkg/edge/api/bsl/types" "edge-infra.dev/pkg/edge/api/graph/mapper" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/middleware" sqlquery "edge-infra.dev/pkg/edge/api/sql" "edge-infra.dev/pkg/edge/api/utils" "edge-infra.dev/pkg/edge/bsl" "edge-infra.dev/pkg/edge/constants/api/banner" ) const ( // BslSitePath the request path to create a bsl site. BslSitePath = "/site/v1/sites" // BslSitePath the request path to delete a bsl site. BslUpdateSitePath = "/site/v1/sites/%s" // BslOrgPath the request path to get a bsl org. BslOrgPath = "/provisioning/organizations" // ActiveStatus active status for bsl site ActiveStatus = "ACTIVE" // InActiveStatus inactive status for bsl site InActiveStatus = "INACTIVE" // DefaultLatitude is the default latitude value used when a latitude is not provided. DefaultLatitude = 90.0 // DefaultLongitude is the default longitude value used when a longitude is not provided. DefaultLongitude = 180.0 deletedSuffix = "-deleted-" ) //go:generate mockgen -destination=../mocks/mock_bsp_site_service.go -package=mocks edge-infra.dev/pkg/edge/api/services BSLSiteService type BSLSiteService interface { CreateBSLSite(ctx context.Context, storeName, enterpriseUnitName, status string, latitude, longitude float64, clusterBanner *model.Banner, referenceID string) (*bsl.BSLInfo, error) GetOrCreateSite(ctx context.Context, storeInfo *model.StoreInfo, clusterBanner *model.Banner, storeName, clusterGUID string) (*bsl.BSLInfo, bool, error) UpdateBSLSiteByID(ctx context.Context, clusterBanner *model.Banner, siteID string, site model.Site) (*model.StoreSiteInfo, error) GetBSLSiteByID(ctx context.Context, siteID string, clusterBanner *model.Banner) (*bsl.BSLInfo, error) DeleteBSLSite(ctx context.Context, bslSite *bsl.BSLInfo, siteBanner *model.Banner) error DoesSiteHaveAttachedCluster(ctx context.Context, siteID string) (bool, error) } type bslSiteService struct { BSLClient *bsl.Client SQLDB *sql.DB } func (s *bslSiteService) DoesSiteHaveAttachedCluster(ctx context.Context, siteID string) (bool, error) { var exists bool row := s.SQLDB.QueryRowContext(ctx, sqlquery.DoesClusterWithSiteIDExist, siteID) if err := row.Scan(&exists); err != nil { return false, err } return exists, nil } func (s *bslSiteService) GetBSLSiteByID(ctx context.Context, siteID string, clusterBanner *model.Banner) (*bsl.BSLInfo, error) { user := middleware.ForContext(ctx) organization := bsl.GetOrgShortName(user.Organization) client, err := s.BSLClient.WithBackendOrgAccessKey(ctx, organization) if err != nil { return nil, err } if banner.Type(clusterBanner.BannerType) == banner.Org { client.SetOrgID(clusterBanner.BannerBSLId) } storeSite := bsl.NewBSLInfo() err = getBSLSite(client, siteID, storeSite) return storeSite, err } func (s *bslSiteService) UpdateBSLSiteByID(ctx context.Context, clusterBanner *model.Banner, siteID string, site model.Site) (*model.StoreSiteInfo, error) { user := middleware.ForContext(ctx) organization := bsl.GetOrgShortName(user.Organization) client, errs := s.BSLClient.WithBackendOrgAccessKey(ctx, organization) if errs != nil { return nil, errs } if banner.Type(clusterBanner.BannerType) == banner.Org { client.SetOrgID(clusterBanner.BannerBSLId) } siteResponse := types.NewBslSite() err := getBSLSite(client, siteID, siteResponse) if err != nil { return nil, err } siteContacts := make(map[string]*types.Contact, 0) for _, v := range site.Contacts { if v != nil { c := &types.Contact{} if v.Contact.ContactPerson != nil { c.ContactPerson = v.Contact.ContactPerson } if v.Contact.Email != nil { c.Email = v.Contact.Email } if v.Contact.PhoneNumber != nil { c.PhoneNumber = v.Contact.PhoneNumber } if v.Contact.PhoneNumberCountryCode != nil { c.PhoneNumberCountryCode = v.Contact.PhoneNumberCountryCode } siteContacts[v.Identifier] = c } } siteResponse.Contacts = siteContacts return updateBSLSite(client, siteID, mapper.ToBSLSite(&site, siteResponse)) } // GetOrCreateSite check if site exists and create one if not func (s *bslSiteService) GetOrCreateSite(ctx context.Context, storeInfo *model.StoreInfo, clusterBanner *model.Banner, storeName, clusterGUID string) (*bsl.BSLInfo, bool, error) { if storeInfo == nil { return &bsl.BSLInfo{}, false, nil } // check and get existed site if siteID is provided if !utils.IsNullOrEmpty(storeInfo.SiteID) { bslSite, err := s.checkIfSiteExists(ctx, *storeInfo.SiteID, clusterBanner) return bslSite, false, err } //create site if siteID is not provided createSite := storeInfo.CreateSite != nil && *storeInfo.CreateSite if !createSite { return &bsl.BSLInfo{}, false, nil } bslSite, err := s.registerSite(ctx, storeInfo, storeName, clusterGUID, clusterBanner) if err != nil { return bslSite, false, err } return bslSite, true, nil } func (s *bslSiteService) checkIfSiteExists(ctx context.Context, siteID string, clusterBanner *model.Banner) (*bsl.BSLInfo, error) { //check if the provided site id has been used by a non-deleted cluster exists, err := s.DoesSiteHaveAttachedCluster(ctx, siteID) if err != nil { return nil, err } if exists { return nil, errors.New("bsl site id has already been registered with a cluster") } //get existed site return s.GetSiteIfItExists(ctx, siteID, clusterBanner) } func (s *bslSiteService) registerSite(ctx context.Context, storeInfo *model.StoreInfo, storeName, clusterEdgeID string, clusterBanner *model.Banner) (*bsl.BSLInfo, error) { latitude := VerifyCoordinate(storeInfo.Latitude, DefaultLatitude) longitude := VerifyCoordinate(storeInfo.Longitude, DefaultLongitude) referenceID := "" if !utils.IsNullOrEmpty(storeInfo.ReferenceID) { referenceID = *storeInfo.ReferenceID } return s.CreateBSLSite(ctx, storeName, clusterEdgeID, ActiveStatus, latitude, longitude, clusterBanner, referenceID) } func (s *bslSiteService) DeleteBSLSite(ctx context.Context, bslSite *bsl.BSLInfo, siteBanner *model.Banner) error { user := middleware.ForContext(ctx) organization := bsl.GetOrgShortName(user.Organization) client, err := s.BSLClient.WithBackendOrgAccessKey(ctx, organization) if err != nil { return err } if banner.Type(siteBanner.BannerType) == banner.Org { client.SetOrgID(siteBanner.BannerBSLId) } suffix := deletedSuffix + time.Now().UTC().Format(mapper.TimeFormat) sitePayload := &types.BSLSite{ SiteName: bslSite.SiteName + suffix, EnterpriseUnitName: bslSite.EnterpriseUnitName, Status: InActiveStatus, Coordinates: types.Coordinates{ Latitude: bslSite.Coordinates.Latitude, Longitude: bslSite.Coordinates.Longitude, }, } return client.SetPayload(sitePayload).Put(fmt.Sprintf(BslUpdateSitePath, bslSite.ID)) } func (s *bslSiteService) CreateBSLSite(ctx context.Context, storeName, enterpriseUnitName, status string, latitude, longitude float64, clusterBanner *model.Banner, referenceID string) (*bsl.BSLInfo, error) { client := s.BSLClient.WithUserTokenCredentials(ctx) payload := &types.BSLSite{ SiteName: storeName, EnterpriseUnitName: enterpriseUnitName, Status: status, Coordinates: types.Coordinates{ Latitude: latitude, Longitude: longitude, }, ReferenceID: referenceID, } return createBSLSite(client, payload, clusterBanner) } func (s *bslSiteService) GetSiteIfItExists(ctx context.Context, euid string, clusterBanner *model.Banner) (*bsl.BSLInfo, error) { client := s.BSLClient.WithUserTokenCredentials(ctx) if banner.Type(clusterBanner.BannerType) == banner.Org { client.SetOrgID(clusterBanner.BannerBSLId) } site := &bsl.BSLInfo{} err := getBSLSite(client, euid, site) return site, err } // createBSLSite - creates a new BSL Site and returns the enterprise unit id of the site. func createBSLSite(client *bsl.Request, payload *types.BSLSite, clusterBanner *model.Banner) (*bsl.BSLInfo, error) { bslSite := &bsl.BSLInfo{} if banner.Type(clusterBanner.BannerType) == banner.Org { client.SetOrgID(clusterBanner.BannerBSLId) } err := client.SetPayload(payload).JSON(http.MethodPost, BslSitePath, bslSite) if err != nil { return bslSite, err } return bslSite, nil } // getBSLSite checks if the BSL Site with the provided euid exists and returns it. func getBSLSite(client *bsl.Request, bslID string, siteResponse interface{}) error { return client.JSON(http.MethodGet, fmt.Sprintf("%s/%s", BslSitePath, bslID), siteResponse) } type UpdateSiteResponse struct { model.StoreSiteInfo Contacts map[string]model.Contact `json:"contacts"` } // updateBSLSite updates a bsl site with the specified euid. func updateBSLSite(client *bsl.Request, bslID string, site *types.BslSite) (*model.StoreSiteInfo, error) { response := &UpdateSiteResponse{} err := client.SetPayload(site).JSON(http.MethodPut, fmt.Sprintf("%s/%s", BslSitePath, bslID), response) return response.toStoreSiteInfo(), err } func (u *UpdateSiteResponse) toStoreSiteInfo() *model.StoreSiteInfo { for k, v := range u.Contacts { u.StoreSiteInfo.Contacts = append(u.StoreSiteInfo.Contacts, &model.Contacts{ Identifier: k, Details: &model.Contact{ Email: v.Email, ContactPerson: v.ContactPerson, PhoneNumber: v.PhoneNumber, PhoneNumberCountryCode: v.PhoneNumberCountryCode, }, }) } return &u.StoreSiteInfo } // VerifyCoordinate checks the latitude or longitude value to ensure it is not nil, // // if nil it sets a default value. // // if the value is not nil it returns the initial value. func VerifyCoordinate(coordinate *float64, defaultCoordinate float64) float64 { if coordinate == nil { return defaultCoordinate } return *coordinate } // NewBSLSiteService creates a new BSL Site Service. func NewBSLSiteService(client *bsl.Client, sqlDB *sql.DB) BSLSiteService { //nolint return &bslSiteService{ BSLClient: client, SQLDB: sqlDB, } }