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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(), "")
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
// IsDuplicateKeyError reports whether err is a MongoDB E11000 duplicate key error.
|
||||
func IsDuplicateKeyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(err.Error(), "E11000") || strings.Contains(err.Error(), "duplicate key") {
|
||||
return true
|
||||
}
|
||||
var we mongodriver.WriteException
|
||||
if errors.As(err, &we) {
|
||||
for _, wErr := range we.WriteErrors {
|
||||
if wErr.Code == 11000 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DuplicateKeyMatchesField reports whether a duplicate key error involves the given field
|
||||
// (e.g. "phone" matches index phone_idx or dup key { phone: "..." }).
|
||||
func DuplicateKeyMatchesField(err error, field string) bool {
|
||||
if err == nil || field == "" {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
field = strings.ToLower(field)
|
||||
if strings.Contains(msg, field+"_idx") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(msg, "dup key") && strings.Contains(msg, field) {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(msg, `"`+field+`"`) || strings.Contains(msg, field+":")
|
||||
}
|
||||
Reference in New Issue
Block a user