mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
690 lines
23 KiB
Go
690 lines
23 KiB
Go
package organization
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/repository"
|
|
)
|
|
|
|
const (
|
|
// InvitationTTL is the default invitation expiration time
|
|
InvitationTTL = 7 * 24 * time.Hour // 7 days
|
|
|
|
// InvitationTokenLength is the length of invitation tokens
|
|
InvitationTokenLength = 32
|
|
)
|
|
|
|
// OrganizationService defines the interface for organization management
|
|
type OrganizationService interface {
|
|
// CRUD
|
|
Create(ctx context.Context, userID uuid.UUID, name string) (*models.Organization, *errx.Error)
|
|
Get(ctx context.Context, orgID uuid.UUID) (*models.Organization, *errx.Error)
|
|
GetBySlug(ctx context.Context, slug string) (*models.Organization, *errx.Error)
|
|
Update(ctx context.Context, orgID uuid.UUID, req *models.UpdateOrganizationRequest) (*models.Organization, *errx.Error)
|
|
Delete(ctx context.Context, orgID uuid.UUID) *errx.Error
|
|
|
|
// User's organizations
|
|
GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]models.OrganizationMember, *errx.Error)
|
|
GetUserDefaultOrganization(ctx context.Context, userID uuid.UUID) (*models.Organization, *errx.Error)
|
|
|
|
// Member management
|
|
GetMembers(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationMember, *errx.Error)
|
|
GetMembership(ctx context.Context, orgID, userID uuid.UUID) (*models.OrganizationMember, *errx.Error)
|
|
InviteMember(ctx context.Context, orgID uuid.UUID, inviterID uuid.UUID, req *models.InviteMemberRequest) (*models.OrganizationInvitation, *errx.Error)
|
|
AcceptInvitation(ctx context.Context, token string, userID uuid.UUID, email string) (*models.OrganizationMember, *errx.Error)
|
|
UpdateMemberRole(ctx context.Context, orgID, memberUserID uuid.UUID, req *models.UpdateMemberRequest) (*models.OrganizationMember, *errx.Error)
|
|
RemoveMember(ctx context.Context, orgID, memberUserID uuid.UUID) *errx.Error
|
|
|
|
// Invitations
|
|
GetPendingInvitations(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationInvitation, *errx.Error)
|
|
GetUserPendingInvitations(ctx context.Context, email string) ([]models.OrganizationInvitation, *errx.Error)
|
|
CancelInvitation(ctx context.Context, invitationID uuid.UUID) *errx.Error
|
|
|
|
// Ownership transfer
|
|
TransferOwnership(ctx context.Context, orgID, newOwnerUserID uuid.UUID) *errx.Error
|
|
|
|
// Permission checks
|
|
HasPermission(ctx context.Context, orgID, userID uuid.UUID, perm models.OrganizationPermission) (bool, *errx.Error)
|
|
RequirePermission(ctx context.Context, orgID, userID uuid.UUID, perm models.OrganizationPermission) *errx.Error
|
|
|
|
// Limit checks
|
|
CanAddMember(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
|
|
CanAddCampaign(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
|
|
CanAddEmailAccount(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
|
|
GetCampaignCounts(ctx context.Context, orgID uuid.UUID) (total int, active int, err *errx.Error)
|
|
GetOrganizationLimits(ctx context.Context, orgID uuid.UUID) (*models.OrganizationLimits, *errx.Error)
|
|
GetOrganizationCounts(ctx context.Context, orgID uuid.UUID) (*models.OrganizationCounts, *errx.Error)
|
|
|
|
// Enterprise inquiries
|
|
CreateEnterpriseInquiry(ctx context.Context, inquiry *models.EnterpriseInquiry) (*models.EnterpriseInquiry, *errx.Error)
|
|
|
|
// Admin permissions (for admin middleware)
|
|
GetUserAdminPermissions(ctx context.Context, userID uuid.UUID) (uint32, error)
|
|
}
|
|
|
|
type organizationService struct {
|
|
orgRepo repository.OrganizationRepository
|
|
subRepo repository.SubscriptionRepository
|
|
userRepo repository.UserRepository
|
|
}
|
|
|
|
// NewService creates a new organization service
|
|
func NewService(
|
|
orgRepo repository.OrganizationRepository,
|
|
subRepo repository.SubscriptionRepository,
|
|
userRepo repository.UserRepository,
|
|
) OrganizationService {
|
|
return &organizationService{
|
|
orgRepo: orgRepo,
|
|
subRepo: subRepo,
|
|
userRepo: userRepo,
|
|
}
|
|
}
|
|
|
|
// Create creates a new organization and adds the user as owner
|
|
func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name string) (*models.Organization, *errx.Error) {
|
|
// Check organization limit
|
|
user, userErr := s.userRepo.GetUser(ctx, userID)
|
|
if userErr != nil {
|
|
sentry.CaptureException(userErr)
|
|
return nil, errx.New(errx.Internal, "failed to get user")
|
|
}
|
|
if user == nil {
|
|
return nil, errx.New(errx.NotFound, "user not found")
|
|
}
|
|
|
|
ownedCount, countErr := s.orgRepo.GetUserOwnedOrganizationCount(ctx, userID)
|
|
if countErr != nil {
|
|
sentry.CaptureException(countErr)
|
|
return nil, errx.New(errx.Internal, "failed to get organization count")
|
|
}
|
|
if ownedCount >= user.MaxOrganizations {
|
|
return nil, errx.New(errx.Forbidden, "maximum organization limit reached")
|
|
}
|
|
|
|
org := &models.Organization{
|
|
ID: uuid.New(),
|
|
Name: name,
|
|
OwnerUserID: userID,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
// Generate slug from name
|
|
slug := generateSlug(name)
|
|
org.Slug = &slug
|
|
|
|
if err := s.orgRepo.Create(ctx, org); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to create organization")
|
|
}
|
|
|
|
// Add creator as owner member
|
|
now := time.Now()
|
|
member := &models.OrganizationMember{
|
|
ID: uuid.New(),
|
|
OrganizationID: org.ID,
|
|
UserID: userID,
|
|
Role: string(models.RoleOwner),
|
|
Permissions: models.RolePermissions[models.RoleOwner],
|
|
InvitedAt: now,
|
|
AcceptedAt: &now,
|
|
}
|
|
|
|
if err := s.orgRepo.AddMember(ctx, member); err != nil {
|
|
sentry.CaptureException(err)
|
|
// Rollback org creation
|
|
_ = s.orgRepo.Delete(ctx, org.ID)
|
|
return nil, errx.New(errx.Internal, "failed to add owner member")
|
|
}
|
|
|
|
return org, nil
|
|
}
|
|
|
|
// Get retrieves an organization by ID
|
|
func (s *organizationService) Get(ctx context.Context, orgID uuid.UUID) (*models.Organization, *errx.Error) {
|
|
org, err := s.orgRepo.GetByID(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get organization")
|
|
}
|
|
if org == nil {
|
|
return nil, errx.ErrNotFound
|
|
}
|
|
return org, nil
|
|
}
|
|
|
|
// GetBySlug retrieves an organization by slug
|
|
func (s *organizationService) GetBySlug(ctx context.Context, slug string) (*models.Organization, *errx.Error) {
|
|
org, err := s.orgRepo.GetBySlug(ctx, slug)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get organization")
|
|
}
|
|
if org == nil {
|
|
return nil, errx.ErrNotFound
|
|
}
|
|
return org, nil
|
|
}
|
|
|
|
// Update updates an organization
|
|
func (s *organizationService) Update(ctx context.Context, orgID uuid.UUID, req *models.UpdateOrganizationRequest) (*models.Organization, *errx.Error) {
|
|
org, err := s.orgRepo.GetByID(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get organization")
|
|
}
|
|
if org == nil {
|
|
return nil, errx.ErrNotFound
|
|
}
|
|
|
|
if req.Name != nil {
|
|
org.Name = *req.Name
|
|
}
|
|
if req.Slug != nil {
|
|
// Validate slug uniqueness
|
|
existing, _ := s.orgRepo.GetBySlug(ctx, *req.Slug)
|
|
if existing != nil && existing.ID != orgID {
|
|
return nil, errx.New(errx.Conflict, "slug already in use")
|
|
}
|
|
org.Slug = req.Slug
|
|
}
|
|
|
|
org.UpdatedAt = time.Now()
|
|
|
|
if err := s.orgRepo.Update(ctx, org); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to update organization")
|
|
}
|
|
|
|
return org, nil
|
|
}
|
|
|
|
// Delete deletes an organization
|
|
func (s *organizationService) Delete(ctx context.Context, orgID uuid.UUID) *errx.Error {
|
|
if err := s.orgRepo.Delete(ctx, orgID); err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to delete organization")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserOrganizations retrieves all organizations a user is a member of
|
|
func (s *organizationService) GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]models.OrganizationMember, *errx.Error) {
|
|
members, err := s.orgRepo.GetUserOrganizations(ctx, userID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get user organizations")
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// GetUserDefaultOrganization retrieves the user's default (first owned) organization
|
|
func (s *organizationService) GetUserDefaultOrganization(ctx context.Context, userID uuid.UUID) (*models.Organization, *errx.Error) {
|
|
org, err := s.orgRepo.GetUserDefaultOrganization(ctx, userID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get default organization")
|
|
}
|
|
return org, nil
|
|
}
|
|
|
|
// GetMembers retrieves all members of an organization
|
|
func (s *organizationService) GetMembers(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationMember, *errx.Error) {
|
|
members, err := s.orgRepo.GetMembers(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get members")
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// GetMembership retrieves a user's membership in an organization
|
|
func (s *organizationService) GetMembership(ctx context.Context, orgID, userID uuid.UUID) (*models.OrganizationMember, *errx.Error) {
|
|
member, err := s.orgRepo.GetMember(ctx, orgID, userID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get membership")
|
|
}
|
|
return member, nil
|
|
}
|
|
|
|
// InviteMember invites a new member to the organization
|
|
func (s *organizationService) InviteMember(ctx context.Context, orgID uuid.UUID, inviterID uuid.UUID, req *models.InviteMemberRequest) (*models.OrganizationInvitation, *errx.Error) {
|
|
// Check member limit
|
|
canAdd, err := s.CanAddMember(ctx, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !canAdd {
|
|
return nil, errx.New(errx.Forbidden, "team member limit reached")
|
|
}
|
|
|
|
// Check if user is already a member
|
|
// First, we need to check if there's already a user with this email
|
|
// For now, we'll just create the invitation
|
|
|
|
// Determine role and permissions
|
|
role := string(models.RoleViewer)
|
|
if req.Role != "" && models.IsValidRole(req.Role) {
|
|
role = req.Role
|
|
}
|
|
|
|
var permissions models.OrganizationPermission
|
|
if req.Permissions != nil {
|
|
permissions = models.OrganizationPermission(*req.Permissions)
|
|
} else {
|
|
permissions = models.GetRolePermissions(models.Role(role))
|
|
}
|
|
|
|
// Generate invitation token
|
|
token, xerr := generateInvitationToken()
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return nil, errx.New(errx.Internal, "failed to generate invitation token")
|
|
}
|
|
|
|
inv := &models.OrganizationInvitation{
|
|
ID: uuid.New(),
|
|
OrganizationID: orgID,
|
|
Email: strings.ToLower(req.Email),
|
|
Role: role,
|
|
Permissions: permissions,
|
|
InvitedBy: inviterID,
|
|
Token: token,
|
|
ExpiresAt: time.Now().Add(InvitationTTL),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if err := s.orgRepo.CreateInvitation(ctx, inv); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to create invitation")
|
|
}
|
|
|
|
return inv, nil
|
|
}
|
|
|
|
// AcceptInvitation accepts an invitation and adds the user as a member
|
|
func (s *organizationService) AcceptInvitation(ctx context.Context, token string, userID uuid.UUID, email string) (*models.OrganizationMember, *errx.Error) {
|
|
inv, err := s.orgRepo.GetInvitationByToken(ctx, token)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get invitation")
|
|
}
|
|
if inv == nil {
|
|
return nil, errx.New(errx.NotFound, "invitation not found")
|
|
}
|
|
|
|
// Verify email matches
|
|
if strings.ToLower(email) != strings.ToLower(inv.Email) {
|
|
return nil, errx.New(errx.Forbidden, "email does not match invitation")
|
|
}
|
|
|
|
// Check if invitation is expired
|
|
if inv.IsExpired() {
|
|
// Delete expired invitation
|
|
_ = s.orgRepo.DeleteInvitation(ctx, inv.ID)
|
|
return nil, errx.New(errx.BadRequest, "invitation has expired")
|
|
}
|
|
|
|
// Check if already a member
|
|
existing, _ := s.orgRepo.GetMember(ctx, inv.OrganizationID, userID)
|
|
if existing != nil {
|
|
_ = s.orgRepo.DeleteInvitation(ctx, inv.ID)
|
|
return existing, nil
|
|
}
|
|
|
|
// Add member
|
|
now := time.Now()
|
|
member := &models.OrganizationMember{
|
|
ID: uuid.New(),
|
|
OrganizationID: inv.OrganizationID,
|
|
UserID: userID,
|
|
Role: inv.Role,
|
|
Permissions: inv.Permissions,
|
|
InvitedBy: &inv.InvitedBy,
|
|
InvitedAt: inv.CreatedAt,
|
|
AcceptedAt: &now,
|
|
}
|
|
|
|
if err := s.orgRepo.AddMember(ctx, member); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to add member")
|
|
}
|
|
|
|
// Delete the invitation
|
|
_ = s.orgRepo.DeleteInvitation(ctx, inv.ID)
|
|
|
|
return member, nil
|
|
}
|
|
|
|
// UpdateMemberRole updates a member's role and permissions
|
|
func (s *organizationService) UpdateMemberRole(ctx context.Context, orgID, memberUserID uuid.UUID, req *models.UpdateMemberRequest) (*models.OrganizationMember, *errx.Error) {
|
|
member, err := s.orgRepo.GetMember(ctx, orgID, memberUserID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get member")
|
|
}
|
|
if member == nil {
|
|
return nil, errx.New(errx.NotFound, "member not found")
|
|
}
|
|
|
|
// Cannot change owner's role
|
|
if member.Role == string(models.RoleOwner) {
|
|
return nil, errx.New(errx.Forbidden, "cannot modify owner role")
|
|
}
|
|
|
|
if req.Role != nil {
|
|
if !models.IsValidRole(*req.Role) {
|
|
return nil, errx.New(errx.BadRequest, "invalid role")
|
|
}
|
|
// Cannot promote to owner
|
|
if *req.Role == string(models.RoleOwner) {
|
|
return nil, errx.New(errx.Forbidden, "cannot promote to owner, use transfer ownership")
|
|
}
|
|
member.Role = *req.Role
|
|
// Update permissions to match new role unless custom permissions provided
|
|
if req.Permissions == nil {
|
|
member.Permissions = models.GetRolePermissions(models.Role(*req.Role))
|
|
}
|
|
}
|
|
|
|
if req.Permissions != nil {
|
|
member.Permissions = models.OrganizationPermission(*req.Permissions)
|
|
}
|
|
|
|
if err := s.orgRepo.UpdateMember(ctx, member); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to update member")
|
|
}
|
|
|
|
return member, nil
|
|
}
|
|
|
|
// RemoveMember removes a member from the organization
|
|
func (s *organizationService) RemoveMember(ctx context.Context, orgID, memberUserID uuid.UUID) *errx.Error {
|
|
member, err := s.orgRepo.GetMember(ctx, orgID, memberUserID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to get member")
|
|
}
|
|
if member == nil {
|
|
return errx.New(errx.NotFound, "member not found")
|
|
}
|
|
|
|
// Cannot remove owner
|
|
if member.Role == string(models.RoleOwner) {
|
|
return errx.New(errx.Forbidden, "cannot remove organization owner")
|
|
}
|
|
|
|
if err := s.orgRepo.RemoveMember(ctx, orgID, memberUserID); err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to remove member")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPendingInvitations retrieves all pending invitations for an organization
|
|
func (s *organizationService) GetPendingInvitations(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationInvitation, *errx.Error) {
|
|
invitations, err := s.orgRepo.GetPendingInvitations(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get invitations")
|
|
}
|
|
return invitations, nil
|
|
}
|
|
|
|
// GetUserPendingInvitations retrieves all pending invitations for a user
|
|
func (s *organizationService) GetUserPendingInvitations(ctx context.Context, email string) ([]models.OrganizationInvitation, *errx.Error) {
|
|
invitations, err := s.orgRepo.GetUserPendingInvitations(ctx, email)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get invitations")
|
|
}
|
|
return invitations, nil
|
|
}
|
|
|
|
// CancelInvitation cancels a pending invitation
|
|
func (s *organizationService) CancelInvitation(ctx context.Context, invitationID uuid.UUID) *errx.Error {
|
|
if err := s.orgRepo.DeleteInvitation(ctx, invitationID); err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to cancel invitation")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TransferOwnership transfers organization ownership
|
|
func (s *organizationService) TransferOwnership(ctx context.Context, orgID, newOwnerUserID uuid.UUID) *errx.Error {
|
|
// Verify new owner is a member
|
|
member, err := s.orgRepo.GetMember(ctx, orgID, newOwnerUserID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to verify membership")
|
|
}
|
|
if member == nil {
|
|
return errx.New(errx.BadRequest, "new owner must be a member of the organization")
|
|
}
|
|
|
|
if err := s.orgRepo.TransferOwnership(ctx, orgID, newOwnerUserID); err != nil {
|
|
sentry.CaptureException(err)
|
|
return errx.New(errx.Internal, "failed to transfer ownership")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasPermission checks if a user has a specific permission
|
|
func (s *organizationService) HasPermission(ctx context.Context, orgID, userID uuid.UUID, perm models.OrganizationPermission) (bool, *errx.Error) {
|
|
member, err := s.orgRepo.GetMember(ctx, orgID, userID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return false, errx.New(errx.Internal, "failed to check permission")
|
|
}
|
|
if member == nil {
|
|
return false, nil
|
|
}
|
|
return member.HasPermission(perm), nil
|
|
}
|
|
|
|
// RequirePermission checks permission and returns error if not granted
|
|
func (s *organizationService) RequirePermission(ctx context.Context, orgID, userID uuid.UUID, perm models.OrganizationPermission) *errx.Error {
|
|
has, err := s.HasPermission(ctx, orgID, userID, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !has {
|
|
return errx.ErrForbidden
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CanAddMember checks if the organization can add more members based on plan limits
|
|
func (s *organizationService) CanAddMember(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
|
|
limits, err := s.GetOrganizationLimits(ctx, orgID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// No limit set = unlimited
|
|
if limits == nil || limits.MaxTeamMembers == nil {
|
|
return true, nil
|
|
}
|
|
|
|
count, xerr := s.orgRepo.GetMemberCount(ctx, orgID)
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return false, errx.New(errx.Internal, "failed to get member count")
|
|
}
|
|
|
|
return count < *limits.MaxTeamMembers, nil
|
|
}
|
|
|
|
// CanAddCampaign checks if the organization can add more campaigns based on plan limits
|
|
func (s *organizationService) CanAddCampaign(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
|
|
limits, err := s.GetOrganizationLimits(ctx, orgID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
total, active, xerr := s.GetCampaignCounts(ctx, orgID)
|
|
if xerr != nil {
|
|
return false, xerr
|
|
}
|
|
|
|
// Check total campaign limit
|
|
if limits != nil && limits.MaxCampaigns != nil && total >= *limits.MaxCampaigns {
|
|
return false, nil
|
|
}
|
|
|
|
// Check active campaign limit
|
|
if limits != nil && limits.MaxActiveCampaigns != nil && active >= *limits.MaxActiveCampaigns {
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CanAddEmailAccount checks if the organization can add more email accounts based on plan limits
|
|
func (s *organizationService) CanAddEmailAccount(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
|
|
limits, err := s.GetOrganizationLimits(ctx, orgID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// No limit set = unlimited
|
|
if limits == nil || limits.MaxEmailAccounts == nil {
|
|
return true, nil
|
|
}
|
|
|
|
count, xerr := s.orgRepo.GetEmailAccountCount(ctx, orgID)
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return false, errx.New(errx.Internal, "failed to get email account count")
|
|
}
|
|
|
|
return count < *limits.MaxEmailAccounts, nil
|
|
}
|
|
|
|
// GetCampaignCounts returns total and active campaign counts
|
|
func (s *organizationService) GetCampaignCounts(ctx context.Context, orgID uuid.UUID) (total int, active int, err *errx.Error) {
|
|
t, a, xerr := s.orgRepo.GetCampaignCounts(ctx, orgID)
|
|
if xerr != nil {
|
|
sentry.CaptureException(xerr)
|
|
return 0, 0, errx.New(errx.Internal, "failed to get campaign counts")
|
|
}
|
|
return t, a, nil
|
|
}
|
|
|
|
// GetOrganizationLimits retrieves the organization's plan limits
|
|
func (s *organizationService) GetOrganizationLimits(ctx context.Context, orgID uuid.UUID) (*models.OrganizationLimits, *errx.Error) {
|
|
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get subscription")
|
|
}
|
|
if sub == nil || sub.Plan == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &models.OrganizationLimits{
|
|
MaxCampaigns: sub.Plan.MaxCampaigns,
|
|
MaxActiveCampaigns: sub.Plan.MaxActiveCampaigns,
|
|
MaxTeamMembers: sub.Plan.MaxTeamMembers,
|
|
MaxEmailAccounts: sub.Plan.MaxEmailAccounts,
|
|
DailyCampaignLimit: sub.Plan.DailyCampaignLimit,
|
|
}, nil
|
|
}
|
|
|
|
// GetOrganizationCounts retrieves the organization's resource counts
|
|
func (s *organizationService) GetOrganizationCounts(ctx context.Context, orgID uuid.UUID) (*models.OrganizationCounts, *errx.Error) {
|
|
total, active, err := s.orgRepo.GetCampaignCounts(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get campaign counts")
|
|
}
|
|
|
|
members, err := s.orgRepo.GetMemberCount(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get member count")
|
|
}
|
|
|
|
emails, err := s.orgRepo.GetEmailAccountCount(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get email account count")
|
|
}
|
|
|
|
contacts, err := s.orgRepo.GetContactCount(ctx, orgID)
|
|
if err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to get contact count")
|
|
}
|
|
|
|
sentToday, _ := s.orgRepo.GetEmailsSentTodayCount(ctx, orgID)
|
|
|
|
return &models.OrganizationCounts{
|
|
TotalCampaigns: total,
|
|
ActiveCampaigns: active,
|
|
TotalMembers: members,
|
|
EmailAccounts: emails,
|
|
TotalContacts: contacts,
|
|
EmailsSentToday: sentToday,
|
|
}, nil
|
|
}
|
|
|
|
// CreateEnterpriseInquiry creates an enterprise inquiry
|
|
func (s *organizationService) CreateEnterpriseInquiry(ctx context.Context, inquiry *models.EnterpriseInquiry) (*models.EnterpriseInquiry, *errx.Error) {
|
|
inquiry.ID = uuid.New()
|
|
inquiry.Status = string(models.EnterpriseInquiryStatusPending)
|
|
inquiry.CreatedAt = time.Now()
|
|
|
|
if err := s.orgRepo.CreateEnterpriseInquiry(ctx, inquiry); err != nil {
|
|
sentry.CaptureException(err)
|
|
return nil, errx.New(errx.Internal, "failed to create enterprise inquiry")
|
|
}
|
|
|
|
return inquiry, nil
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func generateSlug(name string) string {
|
|
// Simple slug generation - lowercase, replace spaces with dashes
|
|
slug := strings.ToLower(name)
|
|
slug = strings.ReplaceAll(slug, " ", "-")
|
|
// Remove non-alphanumeric characters except dashes
|
|
var result strings.Builder
|
|
for _, r := range slug {
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
|
result.WriteRune(r)
|
|
}
|
|
}
|
|
// Add random suffix to ensure uniqueness
|
|
suffix := make([]byte, 4)
|
|
rand.Read(suffix)
|
|
return result.String() + "-" + hex.EncodeToString(suffix)
|
|
}
|
|
|
|
func generateInvitationToken() (string, error) {
|
|
bytes := make([]byte, InvitationTokenLength)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// GetUserAdminPermissions retrieves the admin permissions for a user
|
|
func (s *organizationService) GetUserAdminPermissions(ctx context.Context, userID uuid.UUID) (uint32, error) {
|
|
return s.orgRepo.GetUserAdminPermissions(ctx, userID)
|
|
}
|