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
+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,