Skip to content

Client

sonnys_backoffice.SonnysBackofficeClient

Programmatic access to Sonny's Backoffice user management.

Caches are populated lazily on first use and reused for subsequent calls. Pass refresh=True to discovery methods to force a re-fetch.

__init__(*, subdomain, username, password, timeout=30.0, user_agent=None)

Create a new client for a single Sonny's Backoffice tenant.

Login is deferred until the first request that needs the session (lazy). Use as a context manager to guarantee session cleanup:

with SonnysBackofficeClient(...) as client:
    ...

Parameters:

Name Type Description Default
subdomain str

Tenant subdomain, e.g. "washu" for https://washu.sonnyscontrols.com.

required
username str

Backoffice bot user username.

required
password str

Backoffice bot user password.

required
timeout float

Per-request HTTP timeout in seconds. Defaults to 30.

30.0
user_agent str | None

Optional custom User-Agent header.

None

__enter__()

__exit__(*exc)

close()

Close the underlying HTTP session.

Called automatically by __exit__ when used as a context manager.

create_employee(*, first_name, last_name, phone, email, pos_user_id, wage_rate, start_date, available_sites, permission, pos_pin=None, overtime_wage_rate=None, departments=None, adp_employee_id=None, emergency_contact_name=None, emergency_contact_phone=None, requires_backoffice=False, backoffice_username=None, backoffice_password=None)

Create a new POS employee, optionally with a linked Backoffice user.

The call goes through a two-step flow:

  1. POST /employee/insert with personal, wage, department, and site fields.
  2. POST /employee/permissions/update with the full permission matrix for the resolved template.

If requires_backoffice=True, a third step creates the linked BO user via POST /user/insert. Note that in Milestone 1 the BO user's permission template is not applied automatically and must be assigned manually via the Backoffice UI — see the Creating a Backoffice user guide.

Parameters:

Name Type Description Default
first_name str

Employee first name. Leading/trailing whitespace is stripped.

required
last_name str

Employee last name.

required
phone str

Phone number. 9 or 10 digits after non-digit characters are stripped.

required
email str

Email address. Must contain a valid @domain.tld.

required
pos_user_id int

Caller-assigned unique POS login ID. Must be unique per tenant. Pre-flight with is_pos_user_id_available.

required
wage_rate Decimal | float

Hourly wage in dollars. Prefer Decimal to avoid floating-point drift.

required
start_date datetime

Employment start date.

required
available_sites list[str] | Literal['all']

List of site names or the literal "all". Unknown names raise LookupError before any HTTP call.

required
permission str

POS template name. Matched case-insensitively; unknown names fall back to "General User" with a warning.

required
pos_pin int | None

5-digit POS PIN integer (10000-99999). If None, a random PIN is generated. The final value is always returned in the result.

None
overtime_wage_rate Decimal | float | None

Overtime hourly wage. Defaults to wage_rate * 1.5.

None
departments list[str] | None

Department names. "Greeter" is always auto-added if omitted — see the guide for why.

None
adp_employee_id str | None

ADP payroll employee ID, if applicable.

None
emergency_contact_name str | None

Optional emergency contact name.

None
emergency_contact_phone str | None

Optional emergency contact phone (same validation as phone).

None
requires_backoffice bool

If True, also creates a linked BO user.

False
backoffice_username str | None

BO username. Required when requires_backoffice=True.

None
backoffice_password str | None

BO password. If None, a 12-character random password is generated.

None

Returns:

Name Type Description
EmployeeCreated EmployeeCreated

The created record including the auto-generated

EmployeeCreated

POS PIN, resolved permission, wage attribution site, and any

EmployeeCreated

warnings (permission fallbacks, BO M1 deferral, etc.).

Raises:

Type Description
ValidationError

If any input fails validation or Backoffice rejects the form.

DuplicateError

If pos_user_id, email, or phone already exists on the tenant (pre-flight or server-side).

AuthenticationError

If login or re-authentication fails.

BackofficeServerError

If Backoffice returns an unexpected response (HTTP 5xx, unparseable HTML, etc.).

LookupError

If available_sites contains an unknown site name.

disable_employee(*, pos_user_id=None, email=None)

Disable an employee looked up by POS User ID or email.

Disable uses a full-form round-trip: fetch the employee list to resolve the internal employee_id, GET the edit form, parse every field, re-POST with employee[isActive] omitted (Symfony binds checkbox presence as true regardless of value), and re-GET to verify the change took effect.

Exactly one of pos_user_id or email is required.

Parameters:

Name Type Description Default
pos_user_id int | None

POS User ID to look up.

None
email str | None

Email to look up. Note: the employee list page does not usually contain an email column, so email lookup may fail with NotFoundError even when the employee exists. Prefer pos_user_id when possible.

None

Returns:

Name Type Description
EmployeeDisabled EmployeeDisabled

The internal employee_id, echoed lookup key,

EmployeeDisabled

and the UTC timestamp of the disable.

Raises:

Type Description
ValidationError

If neither or both lookup keys are provided.

NotFoundError

If no employee matches the lookup key.

BackofficeServerError

If the disable POST is accepted but the verification GET shows the employee is still active (the full-form round-trip didn't take effect — usually means the form structure has changed).

AuthenticationError

If login or re-authentication fails.

create_backoffice_user(*, username, email, permission, password=None, link_to_employee_pos_user_id=None, link_to_employee_email=None, first_name=None, last_name=None, available_sites='all')

Create a Backoffice user — standalone or linked to an employee.

In linked mode the user inherits site access from the employee (pass link_to_employee_pos_user_id or link_to_employee_email). The linked employee must currently be active.

In standalone mode the user gets its own profile (pass first_name and last_name, no link fields).

Milestone 1 limitation

The account is created successfully but the permission template is not assigned automatically. Click the shield icon next to the new user in the Backoffice /user list, pick a template, and save. A reminder is included in the returned warnings list.

Parameters:

Name Type Description Default
username str

New BO username. Must match the pattern [A-Za-z][\w]{2,63}.

required
email str

New user email.

required
permission str

BO template name for documentation and future use. Currently stored in the result but not applied to the server.

required
password str | None

BO password. If None, a 12-character random password is generated and returned in the result.

None
link_to_employee_pos_user_id str | None

Link mode — look up the employee by POS User ID.

None
link_to_employee_email str | None

Link mode — look up the employee by email.

None
first_name str | None

Standalone mode — first name.

None
last_name str | None

Standalone mode — last name.

None
available_sites list[str] | Literal['all']

Documented in the result but not applied in M1 BO-permission path. Defaults to "all".

'all'

Returns:

Name Type Description
BackofficeUserCreated BackofficeUserCreated

The new user record, including the

BackofficeUserCreated

auto-generated password and a warning about the M1 deferral.

Raises:

Type Description
ValidationError

If neither linked nor standalone mode fields are provided (or both are provided).

NotFoundError

In linked mode, if the linked employee cannot be resolved.

DuplicateError

If the username or email already exists.

BackofficeServerError

If Backoffice rejects the insert (e.g., linked employee is inactive) or returns an unexpected response.

AuthenticationError

If login or re-authentication fails.

list_sites(*, refresh=False)

List all sites the bot user can see on the tenant.

The result is cached on the client after the first call. Pass refresh=True to re-fetch.

Parameters:

Name Type Description Default
refresh bool

If True, bypass the cache and re-fetch.

False

Returns:

Type Description
list[Site]

list[Site]: Every site visible to the bot user. On a hierarchical

list[Site]

tenant the returned Site objects carry district_id and

list[Site]

region_id; on a flat tenant those fields are None.

list_departments(*, refresh=False)

List the department options configured on the tenant.

Parameters:

Name Type Description Default
refresh bool

If True, bypass the cache and re-fetch.

False

Returns:

Type Description
list[Department]

list[Department]: Every department option, e.g. Cashier, Greeter,

list[Department]

Line, Management.

list_permissions(*, scope, refresh=False)

List the role templates available on the tenant for a given scope.

POS templates come from /employee/permissions/<id> on an existing employee; Backoffice templates come from /user/permissions/<id> on an existing BO user. Both lookups are cached per scope.

Parameters:

Name Type Description Default
scope Literal['pos', 'backoffice']

"pos" for POS employee templates, "backoffice" for Backoffice user templates.

required
refresh bool

If True, bypass the cache and re-fetch.

False

Returns:

Type Description
list[Permission]

list[Permission]: Every template on the tenant for the requested

list[Permission]

scope. POS templates carry grants and overrides ID sets;

list[Permission]

Backoffice templates are name-only in Milestone 1.

Raises:

Type Description
NotFoundError

If the tenant has no existing records from which to extract templates (e.g., an empty tenant with no employees).

is_pos_user_id_available(pos_user_id, *, refresh=False)

Return True if no existing employee uses this POS User ID.

The check uses a cached per-tenant employee index built lazily from /employee?limit=10000&active=all and /user/create.

is_email_available(email, *, refresh=False)

Return True if no existing employee uses this email (case-insensitive).

is_phone_available(phone, *, refresh=False)

Return True if no existing employee uses this phone number.

The phone argument is normalized to digits-only before comparison.