Fix/customer register organization #39
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -71,9 +71,11 @@ func (v *CustomerAuthValidatorImpl) ValidateRegisterStartRequest(req *contract.R
|
||||
}
|
||||
|
||||
// Validate organization
|
||||
if _, err := uuid.Parse(strings.TrimSpace(req.OrganizationID)); err != nil {
|
||||
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
|
||||
if strings.TrimSpace(req.BirthDate) == "" {
|
||||
|
||||
Reference in New Issue
Block a user