fix(user,customer): return 409 on phone duplicate-key races

Map E11000 from the per-collection phone index to friendly conflict
errors and skip redundant GetByPhone when the phone is unchanged.

Not blocking: IAT test DB may need a one-off dedupe for index build;
phone uniqueness is per collection (users vs customers), not global.
This commit is contained in:
Mazyar
2026-06-06 20:34:45 +03:30
parent 0da0c8b8c9
commit 22487eaef5
7 changed files with 143 additions and 20 deletions
+38 -3
View File
@@ -1,7 +1,42 @@
package customer
import "errors"
import (
"errors"
var (
ErrCustomerNotFound = errors.New("customer not found")
orm "tm/pkg/mongo"
)
var ErrCustomerNotFound = errors.New("customer not found")
func mapCustomerWriteError(err error) error {
if err == nil || !orm.IsDuplicateKeyError(err) {
return err
}
switch {
case orm.DuplicateKeyMatchesField(err, "phone"):
return errors.New("customer with this phone number already exists")
case orm.DuplicateKeyMatchesField(err, "email"):
return errors.New("customer with this email already exists")
case orm.DuplicateKeyMatchesField(err, "username"):
return errors.New("customer with this username already exists")
default:
return err
}
}
func isCustomerConflictError(err error) bool {
if err == nil {
return false
}
switch err.Error() {
case "customer with this phone number already exists",
"customer with this email already exists",
"customer with this username already exists",
"company with this name already exists",
"company with this registration number already exists",
"company with this tax ID already exists":
return true
default:
return false
}
}
+2 -12
View File
@@ -53,12 +53,7 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
customer, err := h.service.Register(c.Request().Context(), form)
if err != nil {
if err.Error() == "customer with this email already exists" ||
err.Error() == "customer with this username already exists" ||
err.Error() == "customer with this phone number already exists" ||
err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
err.Error() == "company with this tax ID already exists" {
if isCustomerConflictError(err) {
return response.Conflict(c, err.Error())
}
if strings.HasPrefix(err.Error(), "invalid company ID") || err.Error() == "invalid username format" {
@@ -127,12 +122,7 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
if err.Error() == "customer with this email already exists" ||
err.Error() == "customer with this username already exists" ||
err.Error() == "customer with this phone number already exists" ||
err.Error() == "company with this name already exists" ||
err.Error() == "company with this registration number already exists" ||
err.Error() == "company with this tax ID already exists" {
if isCustomerConflictError(err) {
return response.Conflict(c, err.Error())
}
if strings.HasPrefix(err.Error(), "invalid company ID") {
+6
View File
@@ -156,6 +156,9 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
// Save to database
err = s.repository.Register(ctx, customer)
if err != nil {
if mapped := mapCustomerWriteError(err); mapped != err {
return nil, mapped
}
s.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
@@ -424,6 +427,9 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
// Update in database
err = s.repository.Update(ctx, customer)
if err != nil {
if mapped := mapCustomerWriteError(err); mapped != err {
return nil, mapped
}
s.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
+33 -3
View File
@@ -1,7 +1,37 @@
package user
import "errors"
import (
"errors"
var (
ErrUserNotFound = errors.New("user not found")
orm "tm/pkg/mongo"
)
var ErrUserNotFound = errors.New("user not found")
func mapUserWriteError(err error) error {
if err == nil || !orm.IsDuplicateKeyError(err) {
return err
}
switch {
case orm.DuplicateKeyMatchesField(err, "phone"):
return errors.New("phone number already exists")
case orm.DuplicateKeyMatchesField(err, "email"):
return errors.New("email already exists")
case orm.DuplicateKeyMatchesField(err, "username"):
return errors.New("username already exists")
default:
return err
}
}
func isUserConflictError(err error) bool {
if err == nil {
return false
}
switch err.Error() {
case "phone number already exists", "email already exists", "username already exists":
return true
default:
return false
}
}
+9
View File
@@ -170,6 +170,9 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
user, err := h.service.Update(c.Request().Context(), userID, form)
if err != nil {
if isUserConflictError(err) {
return response.Conflict(c, err.Error())
}
return response.BadRequest(c, err.Error(), "")
}
@@ -274,6 +277,9 @@ func (h *Handler) CreateUser(c echo.Context) error {
// Call service
user, err := h.service.Register(c.Request().Context(), form)
if err != nil {
if isUserConflictError(err) {
return response.Conflict(c, err.Error())
}
return response.BadRequest(c, err.Error(), "")
}
@@ -386,6 +392,9 @@ func (h *Handler) UpdateUser(c echo.Context) error {
// Call service
user, err := h.service.Update(c.Request().Context(), idStr, form)
if err != nil {
if isUserConflictError(err) {
return response.Conflict(c, err.Error())
}
return response.BadRequest(c, err.Error(), "")
}
+11 -2
View File
@@ -117,6 +117,9 @@ func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User
// Save to database
err = s.repository.Create(ctx, user)
if err != nil {
if mapped := mapUserWriteError(err); mapped != err {
return nil, mapped
}
s.logger.Error("Failed to register user", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
@@ -191,7 +194,7 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor
user.Position = form.Position
}
if form.Phone != nil {
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
if *form.Phone != "" {
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
if existingUser != nil && existingUser.ID.Hex() != id {
@@ -208,6 +211,9 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor
// Update in database
err = s.repository.Update(ctx, user)
if err != nil {
if mapped := mapUserWriteError(err); mapped != err {
return nil, mapped
}
s.logger.Error("Failed to update user", map[string]interface{}{
"error": err.Error(),
"user_id": id,
@@ -586,7 +592,7 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up
if form.Position != nil {
user.Position = form.Position
}
if form.Phone != nil {
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
if *form.Phone != "" {
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
if existingUser != nil && existingUser.ID.Hex() != userID {
@@ -605,6 +611,9 @@ func (s *userService) UpdateProfile(ctx context.Context, userID string, form *Up
// Save to database
err = s.repository.Update(ctx, user)
if err != nil {
if mapped := mapUserWriteError(err); mapped != err {
return mapped
}
s.logger.Error("Failed to update user profile", map[string]interface{}{
"error": err.Error(),
"user_id": userID,