Skip to content

Error Handling

All library errors inherit from a single base class, so you can catch everything the wrapper raises with one except:

from sonnys_backoffice import SonnysBackofficeError

try:
    client.create_employee(...)
except SonnysBackofficeError as e:
    log.exception(e)

Most of the time you'll want to catch specific subclasses to apply different recovery strategies.

Exception hierarchy

SonnysBackofficeError
├── AuthenticationError
├── DuplicateError
├── NotFoundError
├── ValidationError
├── PermissionDeniedError
└── BackofficeServerError

AuthenticationError

Login failed, or the session expired and re-authentication also failed.

Common causes: - Wrong bot credentials. - Bot user was disabled in Backoffice. - Subdomain misspelled.

Recovery: inspect credentials, fix, retry. Do not loop on this.

try:
    client.create_employee(...)
except AuthenticationError:
    alert_ops("Sonny's bot credentials broken")
    raise

DuplicateError

POS User ID, email, or phone already exists on the tenant.

Raised at two points:

  1. Pre-flight (before any HTTP call to /employee/insert) — the cached employee index already contains the value. This is the common case.
  2. Post-insert — the server rejected the create with "already exists" in the response body. This usually means the cached index was stale and someone created a colliding record between cache load and your call.

Recovery: log the collision and either skip the row or generate a new value. For auto-generated IDs:

for attempt in range(5):
    try:
        result = client.create_employee(pos_user_id=random_id(), **rest)
        break
    except DuplicateError:
        continue

NotFoundError

A lookup didn't match. Raised by: - disable_employee(pos_user_id=...) when no employee has that POS User ID. - disable_employee(email=...) when the email isn't in the employee list's visible columns (see Disabling an Employee). - create_backoffice_user(link_to_employee_pos_user_id=...) when the employee doesn't exist.

Recovery: inspect the key, double-check the tenant has the record, retry with a corrected lookup.

ValidationError

Caller input violated a constraint or Backoffice rejected the submitted payload. Common causes:

  • Phone that isn't 9 or 10 digits after stripping symbols.
  • Email without a valid @domain.tld.
  • Missing required field (e.g., permission omitted).
  • available_sites is empty and the tenant is not auto-resolvable.
  • requires_backoffice=True but backoffice_username is missing.

Pydantic's error messages are included in the exception text.

Recovery: fix the input, don't retry blindly.

PermissionDeniedError

The bot user lacks sufficient rights for the requested operation. Reserved for future use — Milestone 1 raises BackofficeServerError when the server rejects an operation on permission grounds, because the server response is indistinguishable from generic 403s without form-specific parsing.

BackofficeServerError

Unexpected server response. Covers:

  • HTTP 5xx from Backoffice.
  • HTML that the parser couldn't understand (e.g., Backoffice shipped a new UI version).
  • Disable round-trip accepted by the server but the employee is still active (bindings changed).
  • Linked BO user creation failed because the linked employee is inactive.

Recovery: look at the exception message, check Sonny's status, retry with exponential backoff if transient. If the HTML parser is broken against a new Backoffice release, file a bug.

Warnings vs exceptions

Things that are non-fatal go into result.warnings as strings, not exceptions. Examples:

  • Permission name fallback to General User.
  • Department name that didn't match any known department (silently dropped).
  • Milestone 1 BO permission template deferral notice.

Always log result.warnings after a successful create — they're how the library tells you "something unexpected happened but I made a reasonable choice."

result = client.create_employee(...)
for w in result.warnings:
    log.warning(f"create_employee: {w}")