You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
681 lines
19 KiB
681 lines
19 KiB
package dbManager
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/Cyclops-Labs/cyclops-4-hpc.git/services/customerdb/models"
|
|
l "gitlab.com/cyclops-utilities/logging"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
statusDuplicated = iota
|
|
statusFail
|
|
statusMissing
|
|
statusOK
|
|
)
|
|
|
|
// DbParameter is the struct defined to group and contain all the methods
|
|
// that interact with the database.
|
|
// On it there is the following parameters:
|
|
// - connStr: strings with the connection information to the database
|
|
// - Db: a gorm.DB pointer to the db to invoke all the db methods
|
|
type DbParameter struct {
|
|
connStr string
|
|
Db *gorm.DB
|
|
Metrics map[string]*prometheus.GaugeVec
|
|
}
|
|
|
|
// New is the function to create the struct DbParameter.
|
|
// Parameters:
|
|
// - dbConn: strings with the connection information to the database
|
|
// - tables: array of interfaces that will contains the models to migrate
|
|
// to the database on initialization
|
|
// Returns:
|
|
// - DbParameter: struct to interact with dbManager functionalities
|
|
func New(dbConn string, tables ...interface{}) *DbParameter {
|
|
|
|
l.Trace.Printf("[DB] Gerenating new DBParameter.\n")
|
|
|
|
var (
|
|
dp DbParameter
|
|
err error
|
|
)
|
|
|
|
dp.connStr = dbConn
|
|
|
|
dp.Db, err = gorm.Open(postgres.Open(dbConn), &gorm.Config{})
|
|
|
|
if err != nil {
|
|
|
|
l.Error.Printf("[DB] Error opening connection. Error: %v\n", err)
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Migrating tables.\n")
|
|
|
|
//Database migration, it handles everything
|
|
dp.Db.AutoMigrate(tables...)
|
|
|
|
//l.Trace.Printf("[DB] Generating hypertables.\n")
|
|
|
|
// Hypertables creation for timescaledb in case of needed
|
|
//dp.Db.Exec("SELECT create_hypertable('" + dp.Db.NewScope(&models.TABLE).TableName() + "', 'TIMESCALE-ROW-INDEX');")
|
|
|
|
return &dp
|
|
|
|
}
|
|
|
|
// ListResellers function extracts from the db all the resellers in the system.
|
|
// It only list the resellers, not their linked customers neither the linked
|
|
// products to each customer.
|
|
// Returns:
|
|
// - r: slice of the reseller model with all the resellers in the system.
|
|
func (d *DbParameter) ListResellers() (r []*models.Reseller, e error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to fetch reseller list now.\n")
|
|
|
|
if e = d.Db.Find(&r).Error; e != nil {
|
|
|
|
l.Warning.Printf("[DB] Error in DB operation. Error: %v\n", e)
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Found [ %d ] resellers in the db.\n", len(r))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// ListCustomers function extracts from the db all the customers in the system.
|
|
// It only list the customerss, not their linked products.
|
|
// Returns:
|
|
// - c: slice of the customers model with all the customers in the system.
|
|
func (d *DbParameter) ListCustomers() (c []*models.Customer, e error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to fetch customers list now.\n")
|
|
|
|
if e = d.Db.Find(&c).Error; e != nil {
|
|
|
|
l.Warning.Printf("[DB] Error in DB operation. Error: %v\n", e)
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Found [ %d ] customers in the db.\n", len(c))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// ListProducts function extracts from the db all the products in the system.
|
|
// Returns:
|
|
// - p: slice of the products model with all the products in the system.
|
|
func (d *DbParameter) ListProducts() (p []*models.Product, e error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to fetch products list now.\n")
|
|
|
|
if e = d.Db.Find(&p).Error; e != nil {
|
|
|
|
l.Warning.Printf("[DB] Error in DB operation. Error: %v\n", e)
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Found [ %d ] products in the db.\n", len(p))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// GetReseller function extracts from the db the information that the system
|
|
// has about the specified reseller.
|
|
// Internally this function makes use of GetCustomers() to fill the information
|
|
// about all the customers linked to the specified reseller.
|
|
// Parameters:
|
|
// - id: string containing the id of the reseller to be gotten.
|
|
// - host: string containing the basepath of the server for filling out the APILink
|
|
// field of the linked customers and products.
|
|
// Returns:
|
|
// - A reference to the reseller's model containing all the information in the
|
|
// system linked to the specified reseller.
|
|
func (d *DbParameter) GetReseller(id string, host string) (*models.Reseller, error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to get reseller [ %v ] now.\n", id)
|
|
|
|
var r models.Reseller
|
|
|
|
e := d.Db.Where(&models.Reseller{ResellerID: id}).First(&r).Error
|
|
|
|
if errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
l.Warning.Printf("[DB] Unable to fetch existing record for reseller [ %v ], check with administrator.\n", id)
|
|
|
|
} else {
|
|
|
|
l.Info.Printf("[DB] Found existing record for reseller [ %v ] successfully.\n", r.Name)
|
|
|
|
r.Customers = d.GetCustomers(id, host)
|
|
|
|
}
|
|
|
|
return &r, e
|
|
|
|
}
|
|
|
|
// GetCustomer function extracts from the db the information that the system
|
|
// has about the specified customer.
|
|
// Internally this function makes use of GetProducts() to fill the information
|
|
// about all the products linked to the specified customer.
|
|
// Parameters:
|
|
// - id: string containing the id of the customer to be gotten.
|
|
// - host: string containing the basepath of the server for filling out the APILink
|
|
// field of the linked products.
|
|
// Returns:
|
|
// - A reference to the customer's model containing all the information in the
|
|
// system linked to the specified customer.
|
|
func (d *DbParameter) GetCustomer(id string, host string) (*models.Customer, error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to get customer [ %v ] now.\n", id)
|
|
|
|
var c models.Customer
|
|
|
|
e := d.Db.Where(&models.Customer{CustomerID: id}).First(&c).Error
|
|
|
|
if errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
l.Warning.Printf("[DB] Unable to fetch existing record for customer [ %v ], check with administrator.\n", id)
|
|
|
|
} else {
|
|
|
|
l.Info.Printf("[DB] Found existing record for customer [ %v ] successfully.\n", c.Name)
|
|
|
|
c.Products = d.GetProducts(id, host)
|
|
|
|
}
|
|
|
|
return &c, e
|
|
|
|
}
|
|
|
|
// GetProduct function extracts from the db the information that the system
|
|
// has about the specified product.
|
|
// Parameters:
|
|
// - id: string containing the id of the product to be gotten.
|
|
// Returns:
|
|
// - A reference to the product's model containing all the information in the
|
|
// system linked to the specified product.
|
|
func (d *DbParameter) GetProduct(id string) (*models.Product, error) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to get product [ %v ] now.\n", id)
|
|
|
|
var p models.Product
|
|
|
|
e := d.Db.Where(&models.Product{ProductID: id}).First(&p).Error
|
|
|
|
if errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
l.Warning.Printf("[DB] Unable to fetch existing record for product [ %v ], check with administrator.\n", id)
|
|
|
|
} else {
|
|
|
|
l.Info.Printf("[DB] Found existing record for product [ %v ] successfully.\n", p.Name)
|
|
|
|
}
|
|
|
|
return &p, e
|
|
|
|
}
|
|
|
|
// GetCustomers function extracts from the db the information that the system
|
|
// has about all the customers linked to the specified reseller id.
|
|
// Internally this function makes use of GetProducts() to fill the information
|
|
// about all the products linked to each customer.
|
|
// Parameters:
|
|
// - id: string containing the id of the customer to be gotten.
|
|
// - host: string containing the basepath of the server for filling out the APILink
|
|
// field of the customers and the linked products.
|
|
// Returns:
|
|
// - A slice of the customer's model containing all the information in the
|
|
// system linked to the specified reseller id.
|
|
func (d *DbParameter) GetCustomers(id string, host string) (c []*models.Customer) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to fetch customers for reseller [ %v ] now.\n", id)
|
|
|
|
var custs, customers []models.Customer
|
|
|
|
if e := d.Db.Where(&models.Customer{ResellerID: id}).Find(&customers).Error; e != nil {
|
|
|
|
l.Warning.Printf("[DB] Error in DB operation while retrieving linked custommers. Error: %v\n", e)
|
|
|
|
}
|
|
|
|
for _, customer := range customers {
|
|
|
|
customer.Products = d.GetProducts(customer.CustomerID, host)
|
|
customer.APILink = host + "/customer/" + customer.CustomerID
|
|
|
|
custs = append(custs, customer)
|
|
|
|
}
|
|
|
|
//This is the only way to avoid the issue with the unproper assigning of
|
|
//&Elemnt when using range over and array
|
|
for id := range custs {
|
|
|
|
c = append(c, &custs[id])
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Found [ %d ] customers for reseller [ %v ].\n", len(c), id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// GetProducts function extracts from the db the information that the system
|
|
// has about all the products linked to the specified customer id.
|
|
// Parameters:
|
|
// - id: string containing the id of the customer to be gotten.
|
|
// - host: string containing the basepath of the server for filling out the APILink
|
|
// field of products.
|
|
// Returns:
|
|
// - A slice of the product's model containing all the information in the
|
|
// system linked to the specified customer id.
|
|
func (d *DbParameter) GetProducts(id string, host string) (p []*models.Product) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to fetch products for customer [ %v ] now.\n", id)
|
|
|
|
var products []models.Product
|
|
|
|
if e := d.Db.Where(&models.Product{CustomerID: id}).Find(&products).Error; e != nil {
|
|
|
|
l.Warning.Printf("[DB] Error in DB operation while retrieving linked products. Error: %v\n", e)
|
|
|
|
}
|
|
|
|
for i := range products {
|
|
|
|
product := products[i]
|
|
|
|
product.APILink = host + "/product/" + product.ProductID
|
|
|
|
p = append(p, &product)
|
|
|
|
}
|
|
|
|
l.Trace.Printf("[DB] Found [ %d ] products for customer [ %v ].\n", len(p), id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// AddReseller function inserts the reseller and its linked customers and products
|
|
// into the system.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the reseller or any of its linked
|
|
// components in the db.
|
|
// Parameters:
|
|
// - r: reseller's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - rs: integer with the state of adding the reseller itself.
|
|
// - cs: integer with the state of adding the linked customers.
|
|
// - ps: integer with the state of adding the linked products.
|
|
func (d *DbParameter) AddReseller(r *models.Reseller) (id string, rs int, cs int, ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to add reseller [ %v ] now.\n", r.ResellerID)
|
|
|
|
var r0 models.Reseller
|
|
|
|
if e := d.Db.Where(r).First(&r0).Error; errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Create(r); e.Error == nil {
|
|
|
|
l.Info.Printf("[DB] Inserted new record for reseller [ %v ] successfully.\n", r.Name)
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Resellers added"}).Inc()
|
|
|
|
rs, cs, ps = statusOK, statusOK, statusOK
|
|
|
|
id = (*e.Statement.Model.(*models.Reseller)).ResellerID
|
|
|
|
for _, c := range r.Customers {
|
|
|
|
c.ResellerID = id
|
|
|
|
_, a, b := d.AddCustomer(c)
|
|
|
|
if a != statusOK {
|
|
|
|
cs = statusFail
|
|
|
|
}
|
|
|
|
if b != statusOK {
|
|
|
|
ps = statusFail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to insert the record for reseller [ %v ], check with administrator.\n", r.Name)
|
|
|
|
rs = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for reseller [ %v ] already exists, check with administrator.\n", r.Name)
|
|
|
|
rs = statusDuplicated
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// AddCustomer function inserts the customer and its linked products into the system.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the customer or any of its linked
|
|
// components in the db.
|
|
// Parameters:
|
|
// - c: customer's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - cs: integer with the state of adding the customer itself.
|
|
// - ps: integer with the state of adding the linked products.
|
|
func (d *DbParameter) AddCustomer(c *models.Customer) (id string, cs int, ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to add customer [ %v ] now.\n", c.CustomerID)
|
|
|
|
var c0 models.Customer
|
|
|
|
if e := d.Db.Where(c).First(&c0).Error; errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Create(c); e.Error == nil {
|
|
|
|
l.Info.Printf("[DB] Inserted new record for customer [ %v ] successfully.\n", c.Name)
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Customers added"}).Inc()
|
|
|
|
cs, ps = statusOK, statusOK
|
|
|
|
id = (*e.Statement.Model.(*models.Customer)).CustomerID
|
|
|
|
for _, p := range c.Products {
|
|
|
|
p.CustomerID = id
|
|
|
|
_, a := d.AddProduct(p)
|
|
|
|
if a != statusOK {
|
|
|
|
ps = statusFail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to insert the record for customer [ %v ], check with administrator.\n", c.Name)
|
|
|
|
cs = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for customer [ %v ] already exists, check with administrator.\n", c.Name)
|
|
|
|
cs = statusDuplicated
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// AddProduct function inserts the product into the system.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the product in the db.
|
|
// Parameters:
|
|
// - p: product's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - ps: integer with the state of adding the product.
|
|
func (d *DbParameter) AddProduct(p *models.Product) (id string, ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to add product [ %v ] now.\n", p.ProductID)
|
|
|
|
var p0 models.Product
|
|
|
|
if e := d.Db.Where(p).First(&p0).Error; errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Create(p); e.Error == nil {
|
|
|
|
l.Info.Printf("[DB] Inserted new record for prolduct [ %v ] successfully.\n", p.Name)
|
|
|
|
id = (*e.Statement.Model.(*models.Product)).ProductID
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Products added"}).Inc()
|
|
|
|
ps = statusOK
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to insert the record for product [ %v ], check with administrator.\n", p.Name)
|
|
|
|
ps = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for product [ %v ] already exists, check with administrator.\n", p.Name)
|
|
|
|
ps = statusDuplicated
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// UpdateReseller function inserts the updated reseller information and its
|
|
// linked customers and products into the system, creating new ones in case of
|
|
// no previous version found in the database.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the updated reseller or any of its linked
|
|
// components in the db.
|
|
// Parameters:
|
|
// - r: reseller's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - rs: integer with the state of updating the reseller itself.
|
|
// - cs: integer with the state of updating the linked customers.
|
|
// - ps: integer with the state of updating the linked products.
|
|
func (d *DbParameter) UpdateReseller(r *models.Reseller) (rs int, cs int, ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to update reseller [ %v ] now.\n", r.ResellerID)
|
|
|
|
var r0 models.Reseller
|
|
|
|
if e := d.Db.Where(models.Reseller{ResellerID: r.ResellerID}).First(&r0).Error; !errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Model(&r0).Updates(*r).Error; e == nil {
|
|
|
|
l.Info.Printf("[DB] Updated record for reseller [ %v ] successfully.\n", r.Name)
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Resellers updated"}).Inc()
|
|
|
|
rs, cs, ps = statusOK, statusOK, statusOK
|
|
|
|
for _, c := range r.Customers {
|
|
|
|
c.ResellerID = r.ResellerID
|
|
|
|
if a, _ := d.UpdateCustomer(c); a != statusOK {
|
|
|
|
l.Trace.Printf("[DB] Attempt to update customer [ %v ] failed, considering it as new customer to be added.\n", c.CustomerID)
|
|
|
|
_, b, d := d.AddCustomer(c)
|
|
|
|
if b != statusOK {
|
|
|
|
l.Trace.Printf("[DB] Attempt to add customer [ %v ] failed.\n", c.CustomerID)
|
|
|
|
cs = statusFail
|
|
|
|
}
|
|
|
|
if d != statusOK {
|
|
|
|
ps = statusFail
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to update record for reseller [ %v ], check with administrator.\n", r.Name)
|
|
|
|
rs = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for reseller [ %v ] not found, check with administrator.\n", r.Name)
|
|
|
|
rs = statusMissing
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// UpdateCustomer function inserts the updated customer information and its
|
|
// linked products into the system, creating new ones in case of no previous
|
|
// version found in the database.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the updated customer or any of its linked
|
|
// components in the db.
|
|
// Parameters:
|
|
// - c: customer's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - cs: integer with the state of updating the customer itself.
|
|
// - ps: integer with the state of updating the linked products.
|
|
func (d *DbParameter) UpdateCustomer(c *models.Customer) (cs int, ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to update customer [ %v ] now.\n", c.CustomerID)
|
|
|
|
var c0 models.Customer
|
|
|
|
if e := d.Db.Where(&models.Customer{CustomerID: c.CustomerID}).First(&c0).Error; !errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Model(&c0).Updates(*c).Error; e == nil {
|
|
|
|
l.Info.Printf("[DB] Updated record for customer [ %v ] successfully.\n", c.Name)
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Customers updated"}).Inc()
|
|
|
|
cs, ps = statusOK, statusOK
|
|
|
|
for _, p := range c.Products {
|
|
|
|
p.CustomerID = c.CustomerID
|
|
|
|
if d.UpdateProduct(p) != statusOK {
|
|
|
|
l.Trace.Printf("[DB] Attempt to update product [ %v ] failed, considering it as new product to be added.\n", p.ProductID)
|
|
|
|
_, a := d.AddProduct(p)
|
|
|
|
if a != statusOK {
|
|
|
|
l.Trace.Printf("[DB] Attempt to add product [ %v ] failed.\n", p.ProductID)
|
|
|
|
cs = statusFail
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to update record for customer [ %v ], check with administrator.\n", c.Name)
|
|
|
|
cs = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for customer [ %v ] not found, check with administrator.\n", c.Name)
|
|
|
|
cs = statusMissing
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// UpdateProduct function inserts the updated product information in the system.
|
|
// Since the system is designed to not raised a problem when it fails to add new
|
|
// items in the db, the method constains a basic feedback control to know if
|
|
// there was a problem when inserting the updated product in the db.
|
|
// Parameters:
|
|
// - p: product's model containing the information to be imported in the db.
|
|
// Returns:
|
|
// - ps: integer with the state of updating the product.
|
|
func (d *DbParameter) UpdateProduct(p *models.Product) (ps int) {
|
|
|
|
l.Trace.Printf("[DB] Attempting to update product [ %v ] now.\n", p.ProductID)
|
|
|
|
var p0 models.Product
|
|
|
|
if e := d.Db.Where(&models.Product{ProductID: p.ProductID}).First(&p0).Error; !errors.Is(e, gorm.ErrRecordNotFound) {
|
|
|
|
if e := d.Db.Model(&p0).Updates(*p).Error; e == nil {
|
|
|
|
l.Info.Printf("[DB] Updated record for product [ %v ] successfully.\n", p.Name)
|
|
|
|
d.Metrics["count"].With(prometheus.Labels{"type": "Products updated"}).Inc()
|
|
|
|
ps = statusOK
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Unable to update record for product [ %v ], check with administrator.\n", p.Name)
|
|
|
|
ps = statusFail
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
l.Warning.Printf("[DB] Record for product [ %v ] not found, check with administrator.\n", p.Name)
|
|
|
|
ps = statusMissing
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|