diff --git a/internal/contract/customer_auth_contract.go b/internal/contract/customer_auth_contract.go index b7501f7..56d28dc 100644 --- a/internal/contract/customer_auth_contract.go +++ b/internal/contract/customer_auth_contract.go @@ -17,8 +17,9 @@ type RegisterStartRequest struct { 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"` + // organization. Optional: when it is left out and the database has exactly one + // organization, the customer joins that one. + OrganizationID string `json:"organization_id,omitempty"` } type RegisterVerifyOtpRequest struct { diff --git a/internal/processor/customer_auth_processor.go b/internal/processor/customer_auth_processor.go index 57356b0..02b59f9 100644 --- a/internal/processor/customer_auth_processor.go +++ b/internal/processor/customer_auth_processor.go @@ -143,19 +143,12 @@ 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) + // Resolve the organization before an OTP is sent, rather than failing on a foreign + // key at the last step. + organizationID, err := p.registrationOrganization(ctx, req.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() @@ -458,3 +451,37 @@ func (p *customerAuthProcessor) ResendOtp(ctx context.Context, req *contract.Res } // Helper functions - OTP generation is now handled by OtpProcessor + +// registrationOrganization is the organization a new customer joins: the one the app +// sent, which must exist, or, when the app sent none, the only organization there is. +// With several organizations and none sent there is no way to choose, so it refuses. +func (p *customerAuthProcessor) registrationOrganization(ctx context.Context, requested string) (uuid.UUID, error) { + requested = strings.TrimSpace(requested) + if requested != "" { + id, err := uuid.Parse(requested) + if err != nil { + return uuid.Nil, fmt.Errorf("organization_id must be a valid UUID") + } + exists, err := p.customerAuthRepo.OrganizationExists(ctx, id) + if err != nil { + return uuid.Nil, err + } + if !exists { + return uuid.Nil, fmt.Errorf("organization not found") + } + return id, nil + } + + ids, err := p.customerAuthRepo.OrganizationIDs(ctx, 2) + if err != nil { + return uuid.Nil, err + } + switch len(ids) { + case 1: + return ids[0], nil + case 0: + return uuid.Nil, fmt.Errorf("no organization exists to register customers into") + default: + return uuid.Nil, fmt.Errorf("organization_id is required: there is more than one organization") + } +} diff --git a/internal/repository/customer_auth_repository.go b/internal/repository/customer_auth_repository.go index 58fd1f6..56319d9 100644 --- a/internal/repository/customer_auth_repository.go +++ b/internal/repository/customer_auth_repository.go @@ -19,6 +19,8 @@ type CustomerAuthRepository interface { 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) + // OrganizationIDs returns up to limit organization ids. + OrganizationIDs(ctx context.Context, limit int) ([]uuid.UUID, error) } type customerAuthRepository struct { @@ -90,3 +92,12 @@ func (r *customerAuthRepository) OrganizationExists(ctx context.Context, organiz } return count > 0, nil } + +func (r *customerAuthRepository) OrganizationIDs(ctx context.Context, limit int) ([]uuid.UUID, error) { + var ids []uuid.UUID + err := r.db.WithContext(ctx).Table("organizations").Order("created_at").Limit(limit).Pluck("id", &ids).Error + if err != nil { + return nil, fmt.Errorf("failed to list organizations: %w", err) + } + return ids, nil +} diff --git a/internal/validator/customer_auth_validator.go b/internal/validator/customer_auth_validator.go index 51df18f..a62f459 100644 --- a/internal/validator/customer_auth_validator.go +++ b/internal/validator/customer_auth_validator.go @@ -71,8 +71,10 @@ func (v *CustomerAuthValidatorImpl) ValidateRegisterStartRequest(req *contract.R } // Validate organization - if _, err := uuid.Parse(strings.TrimSpace(req.OrganizationID)); err != nil { - return errors.New("organization_id must be a valid UUID"), constants.ValidationErrorCode + if orgID := strings.TrimSpace(req.OrganizationID); orgID != "" { + if _, err := uuid.Parse(orgID); err != nil { + return errors.New("organization_id must be a valid UUID"), constants.ValidationErrorCode + } } // Validate birth date