package validator import ( "errors" "strings" "time" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "github.com/google/uuid" ) type CashAdvanceValidator interface { ValidateCreateCashAdvanceRequest(req *contract.CreateCashAdvanceRequest) (error, string) ValidateUpdateCashAdvanceRequest(req *contract.UpdateCashAdvanceRequest) (error, string) ValidateListCashAdvancesRequest(req *contract.ListCashAdvancesRequest) (error, string) } type CashAdvanceValidatorImpl struct{} func NewCashAdvanceValidator() *CashAdvanceValidatorImpl { return &CashAdvanceValidatorImpl{} } func (v *CashAdvanceValidatorImpl) ValidateCreateCashAdvanceRequest(req *contract.CreateCashAdvanceRequest) (error, string) { if req == nil { return errors.New("request body is required"), constants.MissingFieldErrorCode } if strings.TrimSpace(req.CodeNumber) == "" { return errors.New("code_number is required"), constants.MissingFieldErrorCode } if len(req.CodeNumber) > 50 { return errors.New("code_number must be at most 50 characters"), constants.MalformedFieldErrorCode } if req.OutletID != nil && *req.OutletID == uuid.Nil { return errors.New("outlet_id cannot be empty"), constants.MalformedFieldErrorCode } // A cash advance is cash handed to a team, so the team is not optional here the way // it is on a purchase order: allowClear stays false and an empty scope is rejected. if err, code := validatePurchaseTeamSelection(&req.TeamScope, req.TeamCategoryID, false); err != nil { return err, code } if req.Amount <= 0 { return errors.New("amount must be greater than 0"), constants.MalformedFieldErrorCode } issuedDate, err := time.Parse("2006-01-02", strings.TrimSpace(req.IssuedDate)) if err != nil { return errors.New("issued_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode } if req.DueDate != nil { if strings.TrimSpace(*req.DueDate) == "" { return errors.New("due_date cannot be empty"), constants.MalformedFieldErrorCode } dueDate, err := time.Parse("2006-01-02", *req.DueDate) if err != nil { return errors.New("due_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode } if dueDate.Before(issuedDate) { return errors.New("due_date must be after issued_date"), constants.MalformedFieldErrorCode } } if req.Status != nil && !constants.IsValidCashAdvanceStatus(*req.Status) { return errors.New("status must be one of: " + strings.Join(constants.GetAllCashAdvanceStatuses(), ", ")), constants.MalformedFieldErrorCode } return nil, "" } func (v *CashAdvanceValidatorImpl) ValidateUpdateCashAdvanceRequest(req *contract.UpdateCashAdvanceRequest) (error, string) { if req == nil { return errors.New("request body is required"), constants.MissingFieldErrorCode } if req.CodeNumber != nil { if strings.TrimSpace(*req.CodeNumber) == "" { return errors.New("code_number cannot be empty"), constants.MalformedFieldErrorCode } if len(*req.CodeNumber) > 50 { return errors.New("code_number must be at most 50 characters"), constants.MalformedFieldErrorCode } } // The team can be moved but never dropped, so clearing is not allowed here either. if err, code := validatePurchaseTeamSelection(req.TeamScope, req.TeamCategoryID, false); err != nil { return err, code } if req.Amount != nil && *req.Amount <= 0 { return errors.New("amount must be greater than 0"), constants.MalformedFieldErrorCode } if req.ReturnedAmount != nil && *req.ReturnedAmount < 0 { return errors.New("returned_amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode } var issuedDate *time.Time if req.IssuedDate != nil { if strings.TrimSpace(*req.IssuedDate) == "" { return errors.New("issued_date cannot be empty"), constants.MalformedFieldErrorCode } parsed, err := time.Parse("2006-01-02", *req.IssuedDate) if err != nil { return errors.New("issued_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode } issuedDate = &parsed } if req.DueDate != nil { if strings.TrimSpace(*req.DueDate) == "" { return errors.New("due_date cannot be empty"), constants.MalformedFieldErrorCode } dueDate, err := time.Parse("2006-01-02", *req.DueDate) if err != nil { return errors.New("due_date must be in YYYY-MM-DD format"), constants.MalformedFieldErrorCode } if issuedDate != nil && dueDate.Before(*issuedDate) { return errors.New("due_date must be after issued_date"), constants.MalformedFieldErrorCode } } if req.Status != nil && !constants.IsValidCashAdvanceStatus(*req.Status) { return errors.New("status must be one of: " + strings.Join(constants.GetAllCashAdvanceStatuses(), ", ")), constants.MalformedFieldErrorCode } return nil, "" } func (v *CashAdvanceValidatorImpl) ValidateListCashAdvancesRequest(req *contract.ListCashAdvancesRequest) (error, string) { if req == nil { return errors.New("request body is required"), constants.MissingFieldErrorCode } if req.Page < 1 { return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode } if req.Limit < 1 || req.Limit > 100 { return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode } if req.Status != "" && !constants.IsValidCashAdvanceStatus(req.Status) { return errors.New("status must be one of: " + strings.Join(constants.GetAllCashAdvanceStatuses(), ", ")), constants.MalformedFieldErrorCode } if req.SettlementStatus != "" && !constants.IsValidCashAdvanceSettlementStatus(req.SettlementStatus) { return errors.New("settlement_status must be one of: " + strings.Join(constants.GetAllCashAdvanceSettlementStatuses(), ", ")), constants.MalformedFieldErrorCode } if req.Team != "" { if req.TeamScope != "" || req.TeamCategoryID != nil { return errors.New("team cannot be combined with team_scope or team_category_id"), constants.MalformedFieldErrorCode } // Every cash advance has a team, so unlike purchases there is nothing to filter // for "no team yet": only Pusat or a category id make sense here. if req.Team != constants.PurchaseTeamScopeCentral { if categoryID, err := uuid.Parse(req.Team); err != nil || categoryID == uuid.Nil { return errors.New("team must be either central or a category id"), constants.MalformedFieldErrorCode } } } if req.TeamScope != "" { validScopes := []string{constants.PurchaseTeamScopeCategory, constants.PurchaseTeamScopeCentral} if !contains(validScopes, req.TeamScope) { return errors.New("team_scope must be one of: category, central"), constants.MalformedFieldErrorCode } if req.TeamScope == constants.PurchaseTeamScopeCentral && req.TeamCategoryID != nil { return errors.New("team_category_id must be empty when team_scope is central"), constants.MalformedFieldErrorCode } } if req.TeamCategoryID != nil && *req.TeamCategoryID == uuid.Nil { return errors.New("team_category_id cannot be empty"), constants.MalformedFieldErrorCode } if req.StartDate != nil && req.EndDate != nil && req.EndDate.Before(*req.StartDate) { return errors.New("end_date must be after start_date"), constants.MalformedFieldErrorCode } return nil, "" }