From a3cb7dd5fd27649eba533202751437ab29ff49a8 Mon Sep 17 00:00:00 2001 From: efrilm Date: Wed, 30 Sep 2026 18:12:26 +0700 Subject: [PATCH] fix(customer-auth): register customers into the app's organization Registration put every new customer into a hardcoded organization id, which does not exist in staging, so set-password failed on the fk_customers_organization foreign key with a 500. POST /customer-auth/register/start now takes organization_id, the organization the app is built for. It must be a UUID of an existing organization, checked before the OTP is sent; it is kept in the OTP session and set-password creates the customer there. A registration started before this change has no organization in its session and is asked to start again. Co-Authored-By: Claude Opus 5.5 --- internal/contract/customer_auth_contract.go | 3 +++ internal/processor/customer_auth_processor.go | 24 +++++++++++++++++-- .../repository/customer_auth_repository.go | 12 ++++++++++ internal/validator/customer_auth_validator.go | 7 ++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/contract/customer_auth_contract.go b/internal/contract/customer_auth_contract.go index 47547d5..b7501f7 100644 --- a/internal/contract/customer_auth_contract.go +++ b/internal/contract/customer_auth_contract.go @@ -16,6 +16,9 @@ type RegisterStartRequest struct { PhoneNumber string `json:"phone_number" binding:"required"` Name string `json:"name" binding:"required"` BirthDate string `json:"birth_date" binding:"required"` + // The organization (brand) the customer registers with. A customer belongs to one + // organization; the app sends the one it is built for. + OrganizationID string `json:"organization_id" binding:"required"` } type RegisterVerifyOtpRequest struct { diff --git a/internal/processor/customer_auth_processor.go b/internal/processor/customer_auth_processor.go index b2c15b6..57356b0 100644 --- a/internal/processor/customer_auth_processor.go +++ b/internal/processor/customer_auth_processor.go @@ -3,6 +3,7 @@ package processor import ( "context" "fmt" + "strings" "time" "apskel-pos-be/internal/contract" @@ -142,6 +143,20 @@ func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *cont return nil, fmt.Errorf("phone number already registered") } + // The customer joins the organization the app is built for. Check it exists now, + // before an OTP is sent, rather than failing on a foreign key at the last step. + organizationID, err := uuid.Parse(strings.TrimSpace(req.OrganizationID)) + if err != nil { + return nil, fmt.Errorf("organization_id must be a valid UUID") + } + orgExists, err := p.customerAuthRepo.OrganizationExists(ctx, organizationID) + if err != nil { + return nil, err + } + if !orgExists { + return nil, fmt.Errorf("organization not found") + } + // Generate registration token and create OTP session registrationToken := uuid.New().String() @@ -156,6 +171,7 @@ func (p *customerAuthProcessor) StartRegistration(ctx context.Context, req *cont "registration_token": registrationToken, "name": req.Name, "birth_date": req.BirthDate, + "organization_id": organizationID.String(), "step": "otp_sent", } @@ -294,10 +310,14 @@ func (p *customerAuthProcessor) SetPassword(ctx context.Context, req *contract.R return nil, fmt.Errorf("invalid birth date format: %w", err) } - defaultOrgID := uuid.MustParse("87bec7c1-e274-4f66-bac5-84e632208470") // This should be configurable + orgIDStr, _ := otpSession.Metadata["organization_id"].(string) + organizationID, err := uuid.Parse(orgIDStr) + if err != nil { + return nil, fmt.Errorf("invalid registration data: organization not found, start the registration again") + } customer := &entities.Customer{ - OrganizationID: defaultOrgID, + OrganizationID: organizationID, Name: name, PhoneNumber: &otpSession.PhoneNumber, BirthDate: &birthDate, diff --git a/internal/repository/customer_auth_repository.go b/internal/repository/customer_auth_repository.go index c2132cd..58fd1f6 100644 --- a/internal/repository/customer_auth_repository.go +++ b/internal/repository/customer_auth_repository.go @@ -6,6 +6,7 @@ import ( "apskel-pos-be/internal/entities" + "github.com/google/uuid" "gorm.io/gorm" ) @@ -16,6 +17,8 @@ type CustomerAuthRepository interface { UpdateCustomer(ctx context.Context, customer *entities.Customer) error CheckPhoneNumberExists(ctx context.Context, phoneNumber string) (bool, error) SetCustomerPassword(ctx context.Context, customerID string, passwordHash string) error + // OrganizationExists reports whether an organization with this id exists. + OrganizationExists(ctx context.Context, organizationID uuid.UUID) (bool, error) } type customerAuthRepository struct { @@ -78,3 +81,12 @@ func (r *customerAuthRepository) SetCustomerPassword(ctx context.Context, custom } return nil } + +func (r *customerAuthRepository) OrganizationExists(ctx context.Context, organizationID uuid.UUID) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Table("organizations").Where("id = ?", organizationID).Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to check organization: %w", err) + } + return count > 0, nil +} diff --git a/internal/validator/customer_auth_validator.go b/internal/validator/customer_auth_validator.go index 7497eab..51df18f 100644 --- a/internal/validator/customer_auth_validator.go +++ b/internal/validator/customer_auth_validator.go @@ -5,6 +5,8 @@ import ( "regexp" "strings" + "github.com/google/uuid" + "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" ) @@ -68,6 +70,11 @@ func (v *CustomerAuthValidatorImpl) ValidateRegisterStartRequest(req *contract.R return errors.New("name cannot exceed 100 characters"), constants.ValidationErrorCode } + // Validate organization + if _, err := uuid.Parse(strings.TrimSpace(req.OrganizationID)); err != nil { + return errors.New("organization_id must be a valid UUID"), constants.ValidationErrorCode + } + // Validate birth date if strings.TrimSpace(req.BirthDate) == "" { return errors.New("birth date is required"), constants.ValidationErrorCode