"""Badge resolution for the printedparts plugin. Same input contract as the USB plugin (deliberately copied, not imported - cross-plugin imports break the shopdb.api-only contract): - all digits -> an SSO typed or scanned directly - 0BZ -> a physical badge wrapping a PayNo (keyboard-wedge scanners emit this shape) - anything else -> unresolvable Name lookup honors the site's employee directory mode (the same setting the employees/usb plugins use): - selfhosted: the employees plugin's directoryemployees table, by SSO. The table has no PayNo column, so a PayNo badge only resolves when the wrapped digits are themselves the SSO. - external: the HR directory via employee_connection() - SSO badges by SSO, PayNo badges by PayNo (which also recovers the real SSO to record). Policy (Setting printedparts_unknown_badge): 'deny' (default) rejects a badge with no directory match; 'allow' records the SSO with an empty name. """ import logging import re from shopdb.api import Setting, employee_connection logger = logging.getLogger(__name__) _PAYNO_BADGE = re.compile(r'^0(\d+)BZ$', re.IGNORECASE) class BadgeError(ValueError): """Raised when a badge cannot be accepted under the site policy.""" def _parse_badge(badge): """Return ('sso'|'payno', digits) or raise BadgeError on unknown shape.""" badge = (badge or '').strip() if not badge: raise BadgeError('Scan or enter a badge') if badge.isdigit(): return 'sso', badge match = _PAYNO_BADGE.match(badge) if match: return 'payno', match.group(1) raise BadgeError('Unrecognized badge format') def _selfhosted_lookup(digits): """(sso, name) from the employees plugin directory, or None.""" try: from plugins.employees.models import DirectoryEmployee from shopdb.api import db employee = db.session.get(DirectoryEmployee, int(digits)) if employee: return digits, f'{employee.firstname} {employee.lastname}'.strip() except Exception: logger.exception('Self-hosted directory lookup failed for %s', digits) return None def _external_lookup(kind, digits): """(sso, name) from the HR directory, or None. PayNo badges resolve to the employee's real SSO.""" try: conn = employee_connection() except Exception: logger.exception('HR directory connection failed') return None try: with conn.cursor() as cursor: column = 'SSO' if kind == 'sso' else 'PayNo' cursor.execute( f'SELECT SSO, First_Name, Last_Name FROM employees ' f'WHERE {column} = %s', (digits,)) row = cursor.fetchone() if row: sso = str(row[0] if not isinstance(row, dict) else row['SSO']) first = row[1] if not isinstance(row, dict) else row['First_Name'] last = row[2] if not isinstance(row, dict) else row['Last_Name'] return sso, f'{(first or "").strip()} {(last or "").strip()}'.strip() except Exception: logger.exception('HR directory lookup failed for %s %s', kind, digits) finally: try: conn.close() except Exception: pass return None def resolve_badge(badge): """Return (sso, name) for a scanned badge, enforcing the site policy. Raises BadgeError with a kiosk-displayable message when the badge shape is unrecognized or the policy denies an unmatched badge. """ kind, digits = _parse_badge(badge) mode = (Setting.get('employee_directory_mode') or 'selfhosted').lower() if mode == 'external': resolved = _external_lookup(kind, digits) else: resolved = _selfhosted_lookup(digits) if resolved is None: policy = (Setting.get('printedparts_unknown_badge') or 'deny').lower() if policy != 'allow': raise BadgeError('Badge not recognized - see the parts team') return digits, '' return resolved