Users - Services
Location: api/nextango/apps/users/services.py, api/nextango/apps/users/analytics_service.py
Last Updated: 2026-07-08
Overview
Three service classes carry the Users domain's business logic. UserService covers user display data, dashboard aggregates, Sanity sync, and role-consistency validation. RoleService holds role-specific lookups but is currently broken and uncalled (see its section below). UserAuthenticationService is the single entry point external callers, notably the POS Gateway, use to authenticate a PIN login without depending on the HTTP view directly. Analytics aggregation lives in a separate file; see Analytics Service for CustomerAnalyticsService and EmployeeAnalyticsService.
UserService
get_users_for_display(filters=None)
Returns a list of user data dictionaries enriched with both user_roles_data-derived fields (primary_role, all_roles, roles_count) and role-model-derived fields (role_records_count, roles_in_sync). filters accepts status, role (one of employee/customer/manager/influencer/vip), and email_domain. Results are cached for five minutes, keyed by the filter set, and read from the replica database when USE_REPLICA_DATABASE is enabled.
roles_in_sync is True only if the count of role-model records matches, role for role, what user_roles_data says should exist. It flags drift between the JSON array and the derived rows, which can occur if a signal handler failed or a record was edited directly.
get_dashboard_summary()
Returns aggregate counts: total/active/inactive/suspended users, per-role-type counts from the role models, and a split between users with exactly one role and users with more than one. Cached for five minutes.
get_user_by_employee_id(employee_id) / get_user_by_social_handle(social_handle)
Each returns a (User, role_instance) tuple, or (None, None) if no match. Used where a caller has an employee ID or social handle rather than a user UUID or email.
sync_user_from_sanity(user_data)
Creates or updates a User from an inbound Sanity document, wrapped in a database transaction. Before writing, the document goes through the same identity-disposition check used by the sync handler: an emailless document is quarantined unless every role on it is non-customer (employee, manager, influencer, VIP); an emailless non-customer document is adopted under its own _id; a document whose email matches an existing user under a different sanity_id is linked to that user instead of creating a duplicate, and the mismatch is recorded to SyncAuditLog with status='quarantined'. On a clean create-or-update, the method calls sync_roles_to_model_records() so the role models reflect the new user_roles_data immediately. Returns (User | None, created: bool), with None on quarantine.
sync_from_sanity()
Fetches every user document from Sanity and calls sync_user_from_sanity() for each, isolating failures per user rather than aborting the batch. Returns a summary dict (success, synced_count, error_count, total_attempted). Intended for initial migration or periodic reconciliation, not for real-time sync; real-time inbound sync runs through webhooks.
validate_role_consistency(user_id=None)
Compares user_roles_data against the role-model record counts for one user or every user, and returns a report of any mismatches found (total_users_checked, users_with_inconsistencies, inconsistencies). Useful as a periodic health check; a mismatch means a resync via user.sync_roles_to_model_records() is needed.
RoleService
Currently broken and uncalled. Do not use as-is. Every method in this class filters on camelCase names that do not exist as Django model fields, so each raises FieldError on its first call: get_employees_by_store filters storeAssignments__contains (services.py:419) where the model field is store_assignments (models.py:484); get_managers_by_reporting_level filters reportingLevel (services.py:436); get_customers_by_loyalty_tier filters and orders on loyaltyPoints (services.py:456-461); get_influencers_by_follower_range filters and orders on followerCount (services.py:478-483); get_vips_by_tier filters vipTier (services.py:497). The class has no callers and no tests, so the breakage is latent rather than user-facing. This page records the intended behavior for when the field names are corrected:
get_employees_by_store(store_reference): employees whose store assignments contain the given store reference.get_managers_by_reporting_level(reporting_level): managers at a given reporting level (Store Manager,Regional Manager,Corporate).get_customers_by_loyalty_tier(min_points=None, max_points=None): customers within a loyalty points range, ordered by points descending.get_influencers_by_follower_range(min_followers=None, max_followers=None): influencers within a follower-count range, ordered descending.get_vips_by_tier(vip_tier): VIP customers at a given tier, ordered alphabetically by user name.
For a working store-scoped employee query, use the ?store= filter on UserViewSet (see Views), which queries the snake_case fields correctly.
UserAuthenticationService
authenticate_pin_login(employee_id, pin_code, store_id)
The service-layer equivalent of the PIN-login view: it runs PINLoginSerializer and, on success, returns the same response shape as PINTokenLoginView (user, store, and employee data, including employee_uuid). External callers, the POS Gateway domain being the primary consumer, use this to authenticate without going through HTTP. The method authenticates only; creating a session token is the caller's responsibility.
The response never includes PIN codes or password hashes.
Related Documentation
- Models:
sync_roles_to_model_records()and the role models it populates - Serializers:
PINLoginSerializervalidation logic - Views:
UserViewSet,PINTokenLoginView - Signals: automatic role creation, outbound Sanity sync
- Analytics Service: customer and employee aggregate metrics
- Integrations: identity reconciliation and quarantine handling shared with inbound webhook sync