File size: 5,749 Bytes
2b747fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""
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