""" Organization Code Generator - Creates unique, memorable codes for organizations Format: [Initials(3)][Year(2)][Sequence(3)] = e.g., "FWL25001" """ from datetime import datetime from sqlalchemy.orm import Session from app.models.client import Client from app.models.contractor import Contractor def extract_initials(org_name: str, max_length: int = 3) -> str: """ Extract initials from organization name. Rules: - Take first letter of each word (up to max_length) - If single word, take first max_length characters - Convert to uppercase - Remove special characters Examples: "FiberWorks Ltd" -> "FWL" "TechInstall" -> "TEC" "ABC Corp" -> "ABC" "M" -> "M" """ # Remove special characters and extra spaces cleaned = ''.join(c if c.isalnum() or c.isspace() else ' ' for c in org_name) words = cleaned.split() if not words: return "ORG" # Fallback # If multiple words, take first letter of each if len(words) > 1: initials = ''.join(word[0].upper() for word in words if word)[:max_length] else: # Single word: take first N characters initials = words[0][:max_length].upper() # Ensure minimum length of 1 return initials if initials else "ORG" def find_next_sequence(prefix: str, year_suffix: str, db: Session) -> int: """ Find the next available sequence number for the given prefix+year. Searches both clients and contractors tables for codes matching pattern. Returns next available sequence number (1-999). Args: prefix: The initials prefix (e.g., "FWL") year_suffix: Two-digit year (e.g., "25") db: Database session Returns: Next available sequence number (1-999) """ pattern = f"{prefix}{year_suffix}%" # Find all codes matching this pattern in both tables client_codes = db.query(Client.swiftops_code).filter( Client.swiftops_code.like(pattern) ).all() contractor_codes = db.query(Contractor.swiftops_code).filter( Contractor.swiftops_code.like(pattern) ).all() # Extract sequence numbers existing_sequences = set() all_codes = [c[0] for c in client_codes] + [c[0] for c in contractor_codes] for code in all_codes: if code and len(code) >= 8: # Expected format: XXX25NNN try: # Extract last 3 digits sequence = int(code[-3:]) existing_sequences.add(sequence) except ValueError: continue # Find next available sequence (1-999) for seq in range(1, 1000): if seq not in existing_sequences: return seq # If all 999 slots are taken, raise error raise ValueError(f"All sequence numbers exhausted for prefix {prefix}{year_suffix}") def generate_org_code(org_name: str, db: Session) -> str: """ Generate a unique SwiftOps code for an organization. Format: [Initials(3)][Year(2)][Sequence(3)] Example: "FWL25001" for FiberWorks Ltd in 2025 Args: org_name: The organization name db: Database session for checking uniqueness Returns: Unique 8-character organization code Raises: ValueError: If unable to generate unique code """ # Extract initials (up to 3 characters) initials = extract_initials(org_name, max_length=3) # Get current year suffix (last 2 digits) year_suffix = str(datetime.now().year)[-2:] # Find next available sequence number sequence = find_next_sequence(initials, year_suffix, db) # Format code code = f"{initials}{year_suffix}{sequence:03d}" return code def validate_org_code_format(code: str) -> bool: """ Validate that a code follows the expected format. Format: [Initials(1-3)][Year(2)][Sequence(3)] Length: 6-8 characters Args: code: The code to validate Returns: True if valid format, False otherwise """ if not code or not isinstance(code, str): return False # Length should be 6-8 characters if not (6 <= len(code) <= 8): return False # Should be alphanumeric if not code.isalnum(): return False # Should be uppercase if not code.isupper(): return False # Last 3 characters should be digits if not code[-3:].isdigit(): return False # Characters before last 5 should be letters (initials) if len(code) >= 5: initials_part = code[:-5] if initials_part and not initials_part.isalpha(): return False return True def is_code_available(code: str, db: Session, exclude_id: int = None, org_type: str = None) -> bool: """ Check if an organization code is available (not in use). Args: code: The code to check db: Database session exclude_id: Optional ID to exclude (for updates) org_type: Optional org type filter ('client' or 'contractor') Returns: True if code is available, False if already in use """ # Check clients if org_type != 'contractor': query = db.query(Client).filter(Client.swiftops_code == code) if exclude_id: query = query.filter(Client.id != exclude_id) if query.first(): return False # Check contractors if org_type != 'client': query = db.query(Contractor).filter(Contractor.swiftops_code == code) if exclude_id: query = query.filter(Contractor.id != exclude_id) if query.first(): return False return True