Merge branch 'develop' into TM-487
This commit is contained in:
@@ -24,11 +24,12 @@ type UpdateCustomerForm struct {
|
|||||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"`
|
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"User"`
|
||||||
Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"`
|
Type string `json:"type" valid:"required,in(individual|company|government)" example:"individual"`
|
||||||
Role string `json:"role" valid:"required,in(admin|analyst)" example:"analyst"`
|
Role string `json:"role" valid:"required,in(admin|analyst)" example:"analyst"`
|
||||||
Companies []string `json:"companies,omitempty" valid:"optional"`
|
// Companies is a pointer so omitted/null leaves assignments unchanged; an empty JSON array clears them.
|
||||||
Username string `json:"username" valid:"required,username,length(3|30)" example:"user"`
|
Companies *[]string `json:"companies,omitempty" valid:"optional"`
|
||||||
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
|
Username string `json:"username" valid:"required,username,length(3|30)" example:"user"`
|
||||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
Email string `json:"email" valid:"required,email" example:"user@opplens.com"`
|
||||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"`
|
||||||
|
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomerStatusForm represents the form for suspending a customer
|
// CustomerStatusForm represents the form for suspending a customer
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
|
|||||||
err.Error() == "company with this tax ID already exists" {
|
err.Error() == "company with this tax ID already exists" {
|
||||||
return response.Conflict(c, err.Error())
|
return response.Conflict(c, err.Error())
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(err.Error(), "invalid company ID") {
|
||||||
|
return response.BadRequest(c, err.Error(), err.Error())
|
||||||
|
}
|
||||||
return response.InternalServerError(c, "Failed to update customer")
|
return response.InternalServerError(c, "Failed to update customer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,23 +100,13 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
|
|||||||
return nil, errors.New("invalid username format")
|
return nil, errors.New("invalid username format")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that all company IDs exist
|
|
||||||
var Companies []string
|
var Companies []string
|
||||||
if len(form.Companies) > 0 {
|
if len(form.Companies) > 0 {
|
||||||
for _, companyID := range form.Companies {
|
validated, err := s.validateCompanyIDs(ctx, form.Companies)
|
||||||
if _, err := bson.ObjectIDFromHex(companyID); err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid company ID format: " + companyID)
|
return nil, err
|
||||||
}
|
|
||||||
_, err := s.companyService.GetByID(ctx, companyID)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("Invalid company ID provided", map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
"company_id": companyID,
|
|
||||||
})
|
|
||||||
return nil, errors.New("invalid company ID: " + companyID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Companies = form.Companies
|
Companies = validated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
// Hash password
|
||||||
@@ -362,36 +352,21 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
|||||||
changeUsername = true
|
changeUsername = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle company assignments
|
|
||||||
companies := make([]*CompanySummary, 0)
|
|
||||||
companiesChanged := false
|
companiesChanged := false
|
||||||
if len(form.Companies) > 0 {
|
if form.Companies != nil {
|
||||||
// Check if companies changed
|
companyIDs, validateErr := s.validateCompanyIDs(ctx, *form.Companies)
|
||||||
if len(form.Companies) != len(customer.Companies) {
|
if validateErr != nil {
|
||||||
companiesChanged = true
|
return nil, validateErr
|
||||||
} else {
|
|
||||||
companyMap := make(map[string]bool)
|
|
||||||
for _, id := range customer.Companies {
|
|
||||||
companyMap[id] = true
|
|
||||||
}
|
|
||||||
for _, id := range form.Companies {
|
|
||||||
if !companyMap[id] {
|
|
||||||
companiesChanged = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customer.Companies = form.Companies
|
|
||||||
companies, err = s.loadCompaniesForCustomer(ctx, customer)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
"customer_id": customer.ID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
companiesChanged = !companyIDsEqual(customer.Companies, companyIDs)
|
||||||
|
customer.Companies = companyIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
infoChanged := false
|
infoChanged := false
|
||||||
|
if form.Type != "" && customer.Type != CustomerType(form.Type) {
|
||||||
|
customer.Type = CustomerType(form.Type)
|
||||||
|
infoChanged = true
|
||||||
|
}
|
||||||
if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) {
|
if form.FullName != nil && (customer.FullName == nil || *form.FullName != *customer.FullName) {
|
||||||
customer.FullName = form.FullName
|
customer.FullName = form.FullName
|
||||||
infoChanged = true
|
infoChanged = true
|
||||||
@@ -458,7 +433,16 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
|
|||||||
"email": customer.Email,
|
"email": customer.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
return customer.ToResponse(companies), nil
|
responseCompanies, err := s.loadCompaniesForCustomer(ctx, customer)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to load companies for customer response", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"customer_id": customer.ID,
|
||||||
|
})
|
||||||
|
responseCompanies = []*CompanySummary{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return customer.ToResponse(responseCompanies), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes a customer (soft delete)
|
// Delete deletes a customer (soft delete)
|
||||||
@@ -1269,6 +1253,61 @@ func (s *customerService) isValidPassword(password string) bool {
|
|||||||
return hasUpper && hasLower && hasSpecial
|
return hasUpper && hasLower && hasSpecial
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *customerService) validateCompanyIDs(ctx context.Context, companyIDs []string) ([]string, error) {
|
||||||
|
if len(companyIDs) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validated := make([]string, 0, len(companyIDs))
|
||||||
|
seen := make(map[string]struct{}, len(companyIDs))
|
||||||
|
for _, companyID := range companyIDs {
|
||||||
|
if companyID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[companyID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := bson.ObjectIDFromHex(companyID); err != nil {
|
||||||
|
return nil, errors.New("invalid company ID format: " + companyID)
|
||||||
|
}
|
||||||
|
if _, err := s.companyService.GetByID(ctx, companyID); err != nil {
|
||||||
|
s.logger.Error("Invalid company ID provided", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"company_id": companyID,
|
||||||
|
})
|
||||||
|
return nil, errors.New("invalid company ID: " + companyID)
|
||||||
|
}
|
||||||
|
seen[companyID] = struct{}{}
|
||||||
|
validated = append(validated, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func companyIDsEqual(existing, updated []string) bool {
|
||||||
|
if len(existing) != len(updated) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := make(map[string]int, len(existing))
|
||||||
|
for _, id := range existing {
|
||||||
|
counts[id]++
|
||||||
|
}
|
||||||
|
for _, id := range updated {
|
||||||
|
counts[id]--
|
||||||
|
if counts[id] < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, count := range counts {
|
||||||
|
if count != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// loadCompaniesForCustomer loads company summaries for a customer
|
// loadCompaniesForCustomer loads company summaries for a customer
|
||||||
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
|
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
|
||||||
if len(customer.Companies) == 0 {
|
if len(customer.Companies) == 0 {
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import (
|
|||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
const documentScraperCountryCode = "SWE"
|
var documentScraperCountryCodes = []string{"SWE", "FIN", "NOR"}
|
||||||
|
|
||||||
|
func isDocumentScraperCountry(countryCode string) bool {
|
||||||
|
for _, code := range documentScraperCountryCodes {
|
||||||
|
if countryCode == code {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Service defines business logic for the document scraper API
|
// Service defines business logic for the document scraper API
|
||||||
type Service interface {
|
type Service interface {
|
||||||
@@ -33,7 +42,7 @@ func NewService(tenderRepo tender.TenderRepository, logger logger.Logger) Servic
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListPendingTenders retrieves tenders that have not yet been scraped for documents.
|
// ListPendingTenders retrieves tenders that have not yet been scraped for documents.
|
||||||
// Only returns tenders from Sweden with active deadlines (deadline not yet reached).
|
// Only returns tenders from Sweden, Finland, and Norway with active deadlines (deadline not yet reached).
|
||||||
func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) {
|
func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) {
|
||||||
s.logger.Info("Listing pending tenders for document scraper", map[string]interface{}{
|
s.logger.Info("Listing pending tenders for document scraper", map[string]interface{}{
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
@@ -42,7 +51,7 @@ func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*D
|
|||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenders, hasMore, err := s.tenderRepo.GetPendingDocumentScrapeTenders(ctx, documentScraperCountryCode, now, limit, offset)
|
tenders, hasMore, err := s.tenderRepo.GetPendingDocumentScrapeTenders(ctx, documentScraperCountryCodes, now, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to list pending tenders", map[string]interface{}{
|
s.logger.Error("Failed to list pending tenders", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -77,7 +86,7 @@ func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*D
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTenderByNoticeID retrieves a specific tender by its notice publication ID.
|
// GetTenderByNoticeID retrieves a specific tender by its notice publication ID.
|
||||||
// Only returns tenders from Sweden with active deadlines (deadline not yet reached).
|
// Only returns tenders from Sweden, Finland, and Norway with active deadlines (deadline not yet reached).
|
||||||
func (s *service) GetTenderByNoticeID(ctx context.Context, noticeID string) (*DocumentScraperTenderResponse, error) {
|
func (s *service) GetTenderByNoticeID(ctx context.Context, noticeID string) (*DocumentScraperTenderResponse, error) {
|
||||||
s.logger.Info("Getting tender by notice ID for document scraper", map[string]interface{}{
|
s.logger.Info("Getting tender by notice ID for document scraper", map[string]interface{}{
|
||||||
"notice_id": noticeID,
|
"notice_id": noticeID,
|
||||||
@@ -97,8 +106,8 @@ func (s *service) GetTenderByNoticeID(ctx context.Context, noticeID string) (*Do
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if t.CountryCode != documentScraperCountryCode || t.TenderDeadline <= now {
|
if !isDocumentScraperCountry(t.CountryCode) || t.TenderDeadline <= now {
|
||||||
s.logger.Info("Tender filtered out: not Sweden or deadline passed", map[string]interface{}{
|
s.logger.Info("Tender filtered out: unsupported country or deadline passed", map[string]interface{}{
|
||||||
"notice_id": noticeID,
|
"notice_id": noticeID,
|
||||||
"country_code": t.CountryCode,
|
"country_code": t.CountryCode,
|
||||||
"tender_deadline": t.TenderDeadline,
|
"tender_deadline": t.TenderDeadline,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type TenderRepository interface {
|
|||||||
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error)
|
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error)
|
||||||
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
|
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
|
||||||
GetUnScrapedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
GetUnScrapedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
||||||
GetPendingDocumentScrapeTenders(ctx context.Context, countryCode string, minDeadline int64, limit, skip int) ([]Tender, bool, error)
|
GetPendingDocumentScrapeTenders(ctx context.Context, countryCodes []string, minDeadline int64, limit, skip int) ([]Tender, bool, error)
|
||||||
GetUnSummarizedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
GetUnSummarizedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
|
||||||
GetUnTranslatedTenders(ctx context.Context, language string, limit, skip int) ([]Tender, int64, error)
|
GetUnTranslatedTenders(ctx context.Context, language string, limit, skip int) ([]Tender, int64, error)
|
||||||
Update(ctx context.Context, tender *Tender) error
|
Update(ctx context.Context, tender *Tender) error
|
||||||
@@ -579,10 +579,14 @@ func (r *tenderRepository) GetUnScrapedTenders(ctx context.Context, limit, skip
|
|||||||
return result.Items, result.TotalCount, nil
|
return result.Items, result.TotalCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPendingDocumentScrapeTenders returns unscraped tenders for one country with an active deadline.
|
// GetPendingDocumentScrapeTenders returns unscraped tenders for the given countries with an active deadline.
|
||||||
func (r *tenderRepository) GetPendingDocumentScrapeTenders(ctx context.Context, countryCode string, minDeadline int64, limit, skip int) ([]Tender, bool, error) {
|
func (r *tenderRepository) GetPendingDocumentScrapeTenders(ctx context.Context, countryCodes []string, minDeadline int64, limit, skip int) ([]Tender, bool, error) {
|
||||||
|
if len(countryCodes) == 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
filter := bson.M{
|
filter := bson.M{
|
||||||
"country_code": countryCode,
|
"country_code": bson.M{"$in": countryCodes},
|
||||||
"tender_deadline": bson.M{
|
"tender_deadline": bson.M{
|
||||||
"$gt": minDeadline,
|
"$gt": minDeadline,
|
||||||
},
|
},
|
||||||
@@ -601,7 +605,7 @@ func (r *tenderRepository) GetPendingDocumentScrapeTenders(ctx context.Context,
|
|||||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to get pending document scrape tenders", map[string]interface{}{
|
r.logger.Error("Failed to get pending document scrape tenders", map[string]interface{}{
|
||||||
"country_code": countryCode,
|
"country_codes": countryCodes,
|
||||||
"min_deadline": minDeadline,
|
"min_deadline": minDeadline,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"skip": skip,
|
"skip": skip,
|
||||||
|
|||||||
Reference in New Issue
Block a user