TOM (Talent Accelerator) Backend — Engineering Documentation¶
Audience: Backend developers joining or maintaining this codebase. Goal: Enable a new backend engineer to understand the architecture, business logic, data flow, and codebase well enough to be productive without a live knowledge-transfer session.
Method: Everything below is inferred from the actual source in this repository. Where a fact cannot be derived from the code, it is explicitly marked "Unable to determine from the codebase." Assumptions are labelled (Assumption). Facts are stated plainly.
Table of Contents¶
- Backend Overview
- Project Structure
- Architecture
- Module Breakdown
- Request Lifecycle
- Business Logic
- API Layer
- Database Interaction
- Authentication & Authorization
- Background Processing
- Integrations
- Configuration
- Logging & Error Handling
- Performance
- Security
- Dependency Map
- Developer Guide
- Code Quality Review
- Improvement Opportunities
- System Overview & Architecture
- System Diagrams
- API Reference
- Version Control
- Deployment & Maintenance
1. Backend Overview¶
Purpose¶
The TOM backend is the core engine of a compensation-management SaaS platform operated by The Talent Accelerator. It is a multi-tenant system where each tenant is a Company. For each company it manages the full lifecycle of compensation data and decisioning:
- Compensation reference data — job grades, salary ranges, cash allowances, short-term incentives (STI/bonus), long-term incentives (LTI/equity), benefit plans, internal payroll, and market survey data.
- Offer modelling — building individual candidate offers that pull from the reference data, computing a full compensation breakdown, generating offer emails/PDFs, and versioning revisions.
- Job evaluation & grading — scoring job roles against a fixed rules matrix and mapping the score to a company grade.
- Market benchmarking — the "Live Pay Benchmarking Tool" (LBT) that aggregates market pay statistics across companies.
- AI insights — LLM-generated compensation insights and analysis alerts on offers.
- Bulk data ingestion — CSV upload/download of all reference datasets, plus AI-assisted job-function mapping.
Main Responsibilities¶
| Responsibility | Owning module(s) |
|---|---|
| Tenant & org configuration (companies, business units, regions, countries, legal entities, taxonomies) | tom_company_setup |
| Users, roles, permissions, JWT auth | authentication |
| Compensation reference data (grades, salary, STI, LTI, benefits, payroll, market data) | tom_compensation |
| Candidate offer modelling & compensation formula | tom_offer_modeler, services/offer_formula.py |
| Job evaluation scoring | job_evaluations, job_grades |
| Grade↔rank normalization | tom_grade |
| Market benchmarking (LBT) | benchmarking_tools |
| CSV ingestion + AI mapping | csv_uploader |
| AI insights & analysis alerts | ai_insights, ai_module |
| Dashboards & aggregation | dashboard |
| Company-specific custom job taxonomy | custom_jobs |
| File uploads to S3 | s3_bucket |
| Cross-cutting: response envelope, exceptions, shared services, admin | response, tom_exceptions, services, utils, tom_admin, tom_applications, tom_enums, swagger |
High-Level Architecture¶
flowchart TB
Client[Frontend SPA / API Clients]
subgraph Edge
WN[WhiteNoise static]
CORS[CORS Middleware]
PROM[Prometheus Middleware]
end
subgraph Django["Django + DRF (Gunicorn)"]
URLS[tombackend/urls.py]
AUTH[JWT Auth + Permissions]
VIEWS[App Views / Controllers]
SVC[Service Layer<br/>services/*, per-app services/]
SER[Serializers / Validation]
ORM[Django ORM]
end
DB[(PostgreSQL)]
REDIS[(Redis<br/>Celery broker + cache)]
CELERY[Celery Workers]
S3[(AWS S3)]
subgraph External
OPENAI[OpenAI]
GROQ[Groq]
FOREX[FastForex / Currency API]
STOCK[MarketStack Stock API]
SMTP[Office365 SMTP]
end
Client --> Edge --> URLS --> AUTH --> VIEWS
VIEWS --> SER --> SVC --> ORM --> DB
VIEWS --> SVC
SVC --> REDIS
VIEWS -. enqueue .-> REDIS --> CELERY
CELERY --> ORM
CELERY --> S3
CELERY --> SMTP
SVC --> OPENAI
SVC --> GROQ
SVC --> FOREX
SVC --> STOCK
VIEWS --> S3
Main Technologies¶
| Layer | Technology | Version (from requirements.txt / Readme.md) |
|---|---|---|
| Language | Python | 3.8 (Dockerfile) / 3.9 (Pipfile) — see note below |
| Web framework | Django | 3.2.5 |
| API framework | Django REST Framework | 3.12.4 |
| Auth | djangorestframework-simplejwt | 4.7.2 |
| Async tasks | Celery | 5.2.7 |
| Broker / cache | Redis | 4.3.4 |
| Database | PostgreSQL | 13+ (via psycopg2-binary 2.9.1) |
| DB driver wrapper | django-prometheus (django_prometheus.db.backends.postgresql) |
2.3.1 |
| Object storage | AWS S3 via boto3 1.19.12 + django-storages 1.12.3 |
— |
| Data processing | pandas 1.3.1, numpy 1.24.4, openpyxl 3.0.10 | — |
| LLM / AI | openai 1.109.1, groq 0.33.0, langchain 0.2.17, langchain-openai, langchain-groq | — |
| PDF / image rendering | pdfkit 1.0.0, imgkit 1.2.3 (wrap wkhtmltopdf / wkhtmltoimage) |
— |
| API docs | drf-yasg 1.20.0 (Swagger) | — |
| Static files | whitenoise 6.7.0 | — |
| History | django-simple-history 3.6.0 (installed, not actively used — see §18) | — |
| Testing | pytest 8.3.3, pytest-django, factory-boy, Faker | — |
| Lint/type | flake8, pylint, mypy, prospector, isort, pre-commit | — |
Note (Python version discrepancy):
Dockerfileusespython:3.8-slim,Pipfiledeclarespython_version = "3.9", andReadme.mdsays Python 3.8.5. Treat 3.8 as the runtime of record (that is what the container ships). (Assumption)
Libraries reference (grouped by purpose)¶
requirements.txt pins 162 packages (including transitive deps). The ones that matter for development, grouped by what they do:
| Category | Library (version) | Used for |
|---|---|---|
| Web framework | Django==3.2.5 |
Core framework, ORM, admin |
djangorestframework==3.12.4 |
REST API (views, serializers, permissions) | |
djangorestframework-simplejwt==4.7.2 |
JWT auth + token blacklist | |
django-cors-headers==3.7.0 |
CORS handling | |
drf-yasg==1.20.0 |
Swagger/OpenAPI docs at /swagger/ |
|
| Database | psycopg2-binary==2.9.1 |
PostgreSQL driver |
django-prometheus==2.3.1 |
DB backend wrapper + /metrics |
|
| Async / queue | celery==5.2.7 |
Background tasks (reports, AI export, email) |
redis==4.3.4 |
Celery broker + result backend + cache | |
django-celery-email==3.0.0 |
Email-via-Celery integration | |
| Storage / files | boto3==1.19.12 |
AWS S3 access |
django-storages==1.12.3 |
S3 as Django's default file storage | |
whitenoise==6.7.0 |
Static file serving | |
| Data processing | pandas==1.3.1 |
CSV parsing/validation |
numpy==1.24.4 |
Benchmarking statistics (percentiles) | |
openpyxl==3.0.10 |
Excel handling | |
| AI / LLM | openai==1.109.1 |
OpenAI (AI mapping, ai_module summaries) |
groq==0.33.0 |
Groq (ai_insights messages) |
|
langchain==0.2.17 + langchain-openai==0.1.25 + langchain-groq==0.1.10 + langchain-core + langchain-text-splitters |
LLM orchestration & prompts | |
| Documents | pdfkit==1.0.0 |
Offer PDF (wraps wkhtmltopdf) |
imgkit==1.2.3 |
Offer email image (wraps wkhtmltoimage) |
|
| Auth / crypto | PyJWT==2.9.0 |
JWT encode/decode |
| HTTP | requests==2.32.4 |
External API calls (currency, stock) |
| Model utilities | django-enumchoicefield==2.0.0 |
Enum-backed model fields |
django-multiselectfield==0.1.12 |
Multi-select fields (benchmarking) | |
django-simple-history==3.6.0 |
Audit history (installed but disabled — see §18) | |
django-extensions==3.2.3 |
Dev/management helpers | |
environs==9.5.0 |
Env-var settings loading | |
| Testing / quality | pytest==8.3.3, pytest-django, factory-boy, Faker, coverage |
Tests + fixtures + coverage |
flake8, pylint, mypy, prospector, isort, pre-commit |
Lint / type-check / format |
Server (not in requirements.txt but used in deployment):
gunicorn— the WSGI server (procfile). Full pinned list: seerequirements.txt(note: it is UTF‑16 encoded).
2. Project Structure¶
The repository is a modular Django project: one project package (tombackend) plus ~20 Django "apps," each a folder registered in INSTALLED_APPS (tombackend/settings/base.py:45-81).
text
backend-prod/
├── tombackend/ # Project root: settings, urls, wsgi/asgi, celery
│ └── settings/ # Split settings: base + per-env (local/dev/staging/prod/docker)
├── authentication/ # Users, roles, permissions, JWT login (v1 + v2)
├── tom_company_setup/ # Multi-tenant config: companies, BUs, regions, countries, taxonomies
├── tom_compensation/ # Compensation reference data (grades, salary, STI, LTI, benefits, ...)
├── tom_offer_modeler/ # Candidate offer modelling + compensation formula
├── job_evaluations/ # Job evaluation scoring workflow
├── job_grades/ # Grade-point-range ↔ company value mapping
├── tom_grade/ # Company ↔ TA-rank grade normalization
├── benchmarking_tools/ # Live Pay Benchmarking Tool (LBT)
├── csv_uploader/ # Bulk CSV ingestion + AI job-function mapping
├── custom_jobs/ # Company-specific custom job taxonomy
├── ai_insights/ # Groq-backed compensation insight messages
├── ai_module/ # OpenAI-backed offer analysis alerts
├── dashboard/ # JE & TOM/CI dashboard aggregation
├── tom_admin/ # TOM (internal staff) admin user management
├── tom_applications/ # "Application" entitlements (TOM / JE / LBT / ...)
├── tom_enums/ # Application-module enum endpoint
├── s3_bucket/ # S3 file-upload endpoints
├── emails/ # (empty — no Python modules)
├── services/ # SHARED cross-app service layer (biggest: services.py, offer_formula.py)
├── response/ # Uniform response envelope + pagination mixins
├── tom_exceptions/ # Exception hierarchy, error codes, DRF exception handler
├── utils/ # Enums (Roles, Permissions), constants, base enum, helpers
├── swagger/ # drf-yasg schema view + reusable swagger fields
├── benchmarking_tools/, ... # (see Module Breakdown)
├── codedeploy/ # AWS CodeDeploy lifecycle hooks (before/after install, start/stop)
├── docker/ # Docker env files
├── manage.py, conftest.py, pytest.ini, requirements.txt, Pipfile, Dockerfile, ...
Folder-by-folder¶
Only meaningful folders are described. Each app follows Django conventions (models.py, views.py, urls.py, serializers*.py, migrations/, sometimes services/, enums.py, validators.py).
tombackend/ — Project configuration root¶
- Purpose: Holds Django settings, root URL routing, WSGI/ASGI entrypoints, and the Celery app.
- Important files:
settings/base.py— all shared settings (installed apps, middleware, DRF config, DB, JWT, Celery, email, AWS, throttling, logging).settings/__init__.py— environment selector: reads theenvOS variable and dynamically importstombackend.settings.<env>(raises ifenvunset).settings/{local,dev,staging,prod,docker}.py— per-environment overrides (ALLOWED_HOSTS,FRONTEND_BASE_URL, DB URL, email backend,CELERY_ALWAYS_EAGERin local).celery.py— Celery app bootstrap;autodiscover_tasksacross all installed apps.model.py—BaseModelabstract base (idBigAutoField,created_at,updated_at). Every domain model inherits this.urls.py— includes every app'surls.pyunder/api/…and mounts admin, swagger, Prometheus.- Why it exists: Central wiring; nothing here contains business logic.
authentication/ — Identity & access¶
- Purpose: Custom user model, roles, permissions, JWT login (two API versions), password reset, account lockout, throttling.
- Responsibilities:
CustomUser(email-based),RoleModel/PermissionModel/PermissionRoleModel/UserRoleModel, per-application role assignment, allpermission_classesused across the platform (permissions.py, ~1000 lines). - Important files:
models.py,permissions.py,views.py(v1),v2/views.py(v2 login/verify),services/role_service.py,throttle.py,utils.py(lockout backend),serializers/. - Why it exists: Single source of truth for who a user is and what they may do; referenced by every other app.
tom_company_setup/ — Multi-tenant foundation¶
- Purpose: Defines the tenant (
CompanyModel) and all reference/org data hung off it. - Responsibilities: Global taxonomies (Sector→Industry→SubIndustry, JobFunction→JobSubFunction), geography/currency (Country, Currency), per-company org structure (BusinessUnit, Region, RegionCountry, RegionsBusinessUnit, LegalEntity), stock tracking, company-user provisioning, company-role listing.
- Dependencies: Imports
authentication,services,tom_applications; reaches intotom_offer_modeler.modelsandtom_compensation.modelsfor referential delete-guards. - Why it exists: It is the coupling hub — nearly every other app imports
CompanyModel/CountryModel/CurrencyModelfrom here.
tom_compensation/ — Compensation reference-data master¶
- Purpose: Stores structured, versioned compensation data per company.
- Responsibilities: ~55 models across job grades, salary ranges, cash allowances, STI, LTI, benefits, internal payroll, market data, company job-function taxonomy, and TA-mapping.
- Important files:
models.py(3k+ lines),views.py(3126 lines),serializers.py(3506 lines),services/company_function_delete_guard.py. - Why it exists: The read-source that offer modelling, benchmarking, dashboards, and AI all consume.
tom_offer_modeler/ — Offer modelling¶
- Purpose: Build individual candidate offers, compute compensation, produce email/PDF, version revisions.
- Important files:
models.py(offer aggregate + component models),views.py(1547 lines),serializers.py(1449 lines),email.py,services/{offer_formula via services/, versioning, copy_service, comparison_service, allowance_data}.py. - Why it exists: The primary user-facing "decision" workflow.
services/ — Shared service layer¶
- Purpose: Cross-app business logic and query helpers used everywhere.
- Important files:
services.py(1337 lines — ~60 role-scoped getters likeget_business_units_by_company_id_and_role),offer_formula.py(684 lines — the compensation formula engine),email_service.py(all transactional emails + the_send_emailCelery task),role_metafield_service.py,validate_metafield.py. - Why it exists: DRY home for logic that would otherwise be duplicated across apps (row-level scoping, offer math, email).
response/, tom_exceptions/, utils/, swagger/ — Cross-cutting infrastructure¶
response/—TomResponse.get_response(...)(the uniform JSON envelope),TomPagination,QueryParamSortMixin(sort/search), custom renderer.tom_exceptions/— exception classes (TomException,TomValidationException,TomResourceNotFoundException, …), theerrors.pyerror-code catalog, andcustom_exception_handler(wired as DRFEXCEPTION_HANDLER).utils/—RolesandPermissionsenums,constants.py(role groupings, URLs),base_enum.py, small helpers.swagger/— drf-yasg schema view + reusable manual parameters.
Other apps¶
ai_insights/,ai_module/,dashboard/,benchmarking_tools/,csv_uploader/,job_evaluations/,job_grades/,tom_grade/,custom_jobs/,tom_admin/,tom_applications/,tom_enums/,s3_bucket/— all detailed in §4 Module Breakdown.emails/— empty (no.pymodules). Transactional email lives inservices/email_service.py.
3. Architecture¶
Architectural patterns actually implemented¶
flowchart LR
subgraph "Per Request"
V[View<br/>GenericAPIView] --> S[Serializer<br/>validation + create/update]
V --> SL[Service functions<br/>services.py / per-app services]
S --> SL
SL --> ORM[Django ORM<br/>Active Record models]
end
-
Modular Monolith (Django apps). A single deployable Django project split into cohesive apps by domain. This is the dominant, explicit pattern (
INSTALLED_APPS). There are no microservices — Celery workers run the same codebase, not separate services. -
Layered architecture (loosely). Requests flow View → Serializer → Service → ORM. The layering is conventional, not enforced:
- Controllers = DRF
GenericAPIViewsubclasses (the codebase does not use ViewSets or routers). - Validation + write logic lives largely in serializers (
create()/update()methods do real work). - Business/query logic lives in a service layer: the shared
services/services.pyand per-appservices/packages. -
Data access = Django ORM (Active Record). There is no separate Repository layer — models and querysets are the data layer.
-
Service Pattern — yes. Explicit service classes/functions:
RoleService,TomApplicationService,RoleMetafieldService,CIDashboardService,LivePayBenchmarkingReportBuilder,OfferVersioningService,OfferCopyService,OfferComparisonService,JobEvaluationService,JobGradeEvaluatorService, CSV service classes, AI service classes, plus the ~60 functions inservices/services.py. -
Repository Pattern — no. Not implemented. Data access is done directly via
Model.objects/ querysets, often inside services or serializers. -
MVC / MVT. Standard Django MVT, but templates are used only for offer email/PDF rendering — the app is API-first (JSON).
-
CQRS — no. Reads and writes use the same models. (Read serializers vs write serializers is a light separation, not CQRS.)
-
Event-Driven — partial. Not a message/event bus. Asynchrony is task-queue based (Celery over Redis) for: LBT report generation, AI mapping export, and email sending. A minimal Django signal exists (
authentication/signals.py). -
Versioned reference data (domain pattern). A signature design choice in
tom_compensation: each dataset has a Version model + Data model + child scoping models. Exactly one version is active per company (UniqueConstraint(fields=["company","is_active"])), and "set-active" flips which version data rows are read through. See §8. -
Soft-delete convention. Most models use
is_activeas a string sentinel:"TRUE"= active,None= deleted (there is no"FALSE"). Partial unique constraints (condition=Q(is_active="TRUE")) enforce uniqueness only among active rows. A few models (CompanyModel,tom_grade,ai_module) use a realBooleanFieldinstead — this inconsistency is a known smell (§18).
Uniform response envelope¶
Every endpoint returns the same shape via TomResponse.get_response (response/resp.py):
json
{
"success": true,
"is_validation_error": false,
"message": "Human-readable message",
"code": 0,
"data": {},
"error": {},
"is_paginated": false,
"pagination": {}
}
4. Module Breakdown¶
Conventions used below: Entry points = URL prefix (all app URLs are included under
/api/intombackend/urls.py). Permissions are DRF permission classes fromauthentication/permissions.pyunless noted. Every model inheritsBaseModel(id,created_at,updated_at).
4.1 authentication¶
- Purpose / business objective: Identity and access management — who a user is and what they can do, across multiple "applications" (TOM, JE, LBT).
- Entry points:
/api/auth/(v1),/api/v2/auth/(v2). - Models (
models.py): PermissionModel(permissions) — a named permission.RoleModel(roles) — name,is_admin_role,is_active,companyFK (null for TOM/global roles),metafield(JSON — holds row-level scoping id lists), M2MpermissionsthroughPermissionRoleModel.PermissionRoleModel(permission_roles) — role↔permission link withis_active.CustomUser(users) — email is the username (USERNAME_FIELD="email"),roleFK (single "active" role),rolesM2M throughUserRoleModel,companyFK,countryFK,is_company_user,is_one_time_password,failed_login_attempts,locked_until, M2Mapplications. Rich helper methods:get_active_applications(),get_allowed_applications(),is_app_allowed(),register_failed_attempt(),reset_failed_attempts(),is_locked.UserRoleModel(unique_user_role) — per-application role assignment:user,role,application(enum),grant_access,metafield. This is how a company user gets different roles/permissions in TOM vs JE vs LBT.UserAuthModel(user_auths) — one-time-password / forgot-password tokens with expiry.PasswordResetAttempt(password_reset_attempts) — lockout counter for password reset.- Controllers:
views.py(v1 Login/Logout/RefreshToken/ForgotPassword/ChangePassword),v2/views.py(LoginViewwith claims tokens +VerifyUserViewfor per-application role switching). - Services:
services/role_service.py(RoleService— role create/update, per-app role provisioning,create_roles_on_company_create). - Serializers:
serializers/{serializers,user_serializers,role_serializers}.py. - Middleware/backends:
utils.py→LockoutAuthenticationBackend(the configuredAUTHENTICATION_BACKENDS), raisingAccountLockedwhenlocked_untilis in the future. - Validation:
validators.py(password + user field validators), DjangoAUTH_PASSWORD_VALIDATORS. - Throttling:
throttle.py—LoginThrottle(scopelogin, 10/min keyed by email) andForgotPasswordThrottle(3 per 2 hours via cache). - Dependencies:
tom_applications,tom_company_setup(Company/Country),services,response,tom_exceptions,utils. - Internal workflow: See §9.
4.2 tom_company_setup¶
- Purpose: The multi-tenant foundation — the
CompanyModeltenant plus all reference/org data and company-user provisioning. - Entry points:
/api/(e.g.sector/,company/,company/<id>/business-unit/). - Key models (
models.py):SectorModel→IndustryModel→SubIndustryModel;JobFunctionModel→JobSubFunctionModel;CountryModel,CurrencyModel;CompanyModel(tenant);StockTracking;BusinessUnitModel,RegionModel,RegionsBusinessUnitModel,RegionCountryModel,LegalEntityModel. CompanyModel(companies): name, address,country_headquarter,financial_year, contract dates, logo URLs,country/currencyFK,status(ACTIVE/IN_PROGRESS/EXPIRED enum), reporting quotas (max_grade_function_reports,max_individual_reports_percent,used_*counters), M2Mapplications,is_active(real Boolean here).- Controllers: one large
views.py(~2117 lines, ~35GenericAPIViewclasses) following the Create / GetList / GetAll / Retrieve(GET/PUT/DELETE) shape. - Services: relies on shared
services/services.py(role-scoped getters),authentication.RoleService,tom_applications.TomApplicationService. A module-level helper_update_role_metafield_with_resourceappends newly-created resource ids into the creating user'srole.metafield(row-level scoping maintenance). - Serializers:
serializers/serializers.py(~1224 lines) +serializers/company_user_serializers.py. Notable:CreateOrUpdateCompanySerializerprovisions company +COMPANY_SUPER_USER+ roles + applications + stock data in one transaction. - Permissions:
IsAuthenticated,IsAdmin,IsAdminOrOwnCompany, plus fine-grainedCanCreateSector,CanCreateCompany,CanCreateBusinessUnit, etc. - Dependencies:
authentication,services,tom_applications, and (delete-guard coupling)tom_offer_modeler,tom_compensation. - Known issues:
CountryView/CurrencyViewhave no permission classes (public); heavy N+1 in nested read serializers; magic currency id138. See §18.
4.3 tom_compensation¶
- Purpose: Versioned compensation reference-data master (grades, salary, allowances, STI, LTI, benefits, payroll, market data, company job-function taxonomy + TA mapping).
- Entry points:
/api/company/<company_id>/…(grades, salary-range, cash-allowance, short-term-incentive, long-term-incentive, benefit-plans, internal-payroll, market-data, company-job-function[-mapping]). - Models: ~55 models, mostly in Version + Data + child-scoping triads. Representative:
CompanyJobGradeVersionModel/CompanyJobGradeModel/CompanyJobGradeCountryModel;CompanySalaryRange*;CompanyCashAllowance*;CompanyShortTermIncentivePlanModel+...Plan{Grade,Country,Function,SubFunction}Model+ valueCompanyShortTermIncentiveModel; LTI mirror;CompanyBenefitPlanModel+ scoping;CompanyInternalPayrollModel(wide employee table);CompanyMarketDataModel(p25/p50/p75);CompanyJobFunctionModel/CompanySubJobFunctionModel;CompanyJobFunctionMappingModel/CompanySubJobFunctionMappingModel(company→TA);CompanyJobFunctionMappingAIExportModel(async AI export tracking). - Controllers:
views.py(3126 lines) — List (withTomPagination,QueryParamSortMixin), Create (@transaction.atomic), Retrieve, and ~9 identicalset-activeversion PATCH views.BulkReplaceCompanyJFMappingsViewdoes full replace-diff of mappings. - Serializers:
serializers.py(3506 lines, ~40 serializers) — nested writes for plan+scoping; grade↔country intersection validation (duplicated ~4×). - Services:
services/company_function_delete_guard.py(blocks delete if referenced by active sub-functions, offers, or job evaluations). - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, Can<Verb><Dataset>]. - Dependencies:
tom_company_setup(incl. its private_update_role_metafield_with_resource),authentication,services,tom_grade,csv_uploader.services, and (delete-guard)job_evaluations,tom_offer_modeler.
4.4 tom_offer_modeler¶
- Purpose: Model individual candidate offers and compute compensation.
- Entry points:
/api/company/<company_id>/offer[s]/…. - Models:
OfferPositionDetailModel,OfferCandidateDetailModel,CompanyEmailTemplate(O2O Company); component headers + child rows —OfferFixedCashModel/OfferFixedCashAllowanceModel,OfferSTIModel/OfferSTIBonusModel,OfferLTIModel/OfferOtherLTIModel,OfferBenefitModel/OfferBenefitOtherModel,OfferSignOnBonusModel/OfferSignOnOtherBonusModel; theOfferModelaggregate (status DRAFTED/PLACED/ACCEPTED/REJECTED/EDITED, stage POSITION_DETAILS/CANDIDATE_DETAILS/OFFER_MODELLER, self-FKoriginal_offerfor revisions,allowance_data_fingerprint);OfferVersionHistory. - Controllers:
views.py(1547 lines) — list (full + light), retrieve/patch/delete, staged create (position → candidate → modeller), currency conversion (v1/v2), comparator-data, auto-populate-data, allowance-data-status, email page/PDF, email template + S3 logo upload, revise, compare, range-type. - Services: the compensation formula in
services/offer_formula.py; plusservices/{versioning,copy_service,comparison_service,allowance_data}.py;email.py(email/PDF rendering). - Serializers:
serializers.py(1449 lines) — resolves company job/sub function → TA via mapping tables; the bigCreateOrUpdateOfferModellerSerializerbuilds all components. - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, Can…Offer]. - Dependencies:
tom_compensation,tom_company_setup,authentication,services,tom_grade; externalrequests,imgkit,pdfkit,boto3. - Business logic detail: see §6.2.
4.5 job_evaluations¶
- Purpose: Score a job role against a fixed rules matrix and map the score to a company grade.
- Entry points:
/api/company/<company_id>/job-evaluations/…and/api/job-evaluations/enums/. - Models:
JobEvaluation(single model) — company scoping, TA + company function FKs, five "division" enum fields (Knowledge & Skills, Problem Solving, Stakeholder Mgmt, Decision Impact, Financial/Non-financial Responsibility),evaluation_model(financial/non_financial),evaluation_result(resolved grade value),status(open/evaluated/close), documents. - Controllers:
views.py— list (role-scoped), sort, list-with-completion, retrieve/update/delete, evaluate, submit, document upload, search. - Services:
services/job_evaluation.py(JobEvaluationService),services/job_grade_evaluator.py(JobGradeEvaluatorService.evaluate_grade— sums points fromje_rules.pyand maps to aJobGradeModelband). - Serializers: create (all function-mapping validated), update,
EvaluateJobSerializer(all divisions required), retrieve, list (completion %). - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, CanCreate/View/Update/DeleteJobEvaluation]. - Dependencies:
tom_company_setup,tom_compensation(company functions),job_grades,authentication,services,tom_grade. - Business logic detail: see §6.3.
- Known bugs:
submit()writes non-existentsubmitted_by/submitted_at;JeDocsUploadViewhas no permission classes. See §18.
4.6 job_grades¶
- Purpose: Map a company to 25 fixed numeric "grade point ranges" (100–1000) and assign a company-specific value/label to each.
- Entry points:
/api/job-grade-mapping/…. - Models:
JobGradeModel(job_grades) —companyFK (CASCADE),grade_point_range(choices fromGradePointRangeenum),grade_point_value(label),created_by. Unique(company, grade_point_range). Propertiesstart_range/end_range. - Controllers:
views.py— list unassigned ranges, list all ranges w/ assignment, mapping list/create/update, all-companies, grade-points pivot, delete. - Permissions: uniformly
[IsAuthenticated, IsAdmin](admin-only; company users blocked). - Dependencies:
tom_company_setup,authentication,tom_grade(serializer reuse). - Known issues: delete by pk hard-deletes all companies' rows sharing that range; update conflict-validation is dead code. See §18.
4.7 tom_grade¶
- Purpose: Normalize each company's internal grade labels onto a canonical TA rank ladder.
- Entry points:
/api/grade/…. - Models:
TARankModel(ta_ranks, ordered rank ladder),GradeCompanyModel(grade_companies, links a company in),GradeCompanyRankModel(grade_company_ranks, the company's label per TA rank). - Controllers:
views.py— TA ranks, grade-company GET/POST, client-companies,company/all(auto-provisions mappings), update/delete. - Permissions:
[IsAuthenticated, Can…GradeCompanyMapping]. - Known issues:
GetGradeCompanyViewis a side-effecting GET (backfills data on read). See §18.
4.8 benchmarking_tools¶
- Purpose: The Live Pay Benchmarking Tool (LBT) — aggregate market pay statistics from offers + payroll and produce a CSV report.
- Entry points:
/api/company/<company_id>/benchmarking-report/live-pay/…,custom-filtered/,function/,benchmarking-report-title/. - Models:
LivePayBenchmarkingReportModel(config + result, M2M scoping, percentiles, aging,stage,report_url),LivePayBenchmarkingReportTrackModel(snapshot),LivePayBenchmarkingReportLogModel(by-title audit). - Controllers:
views.py— create+trigger async report, list, download (S3 proxy), synchronous by-title lookup, custom peer companies, functions-with-data. - Services:
services/calculations.py(the numpy stats engine — percentiles, suppression thresholds, org- vs incumbent-weighting, aging),services/report_creation.py(LivePayBenchmarkingReportBuilder, 1082 lines — build + CSV + S3 upload),services/live_pay/{grade_service,title_service,by_title_service}.py,services/report_utils/*. - Background:
tasks.py→generate_live_pay_report(Celery). - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, Can…LBTReport];custom-filtered/andfunction/are[IsAuthenticated]only (weak scoping — §18). - Business logic detail: see §6.4.
4.9 csv_uploader¶
- Purpose: Bulk CSV ingestion/export for all reference datasets, plus AI-assisted company→TA job-function mapping.
- Entry points:
/api/…/upload/and/…/download/per dataset;…/ai-suggestions/…. - Models: none (writes models owned by
tom_compensation/tom_company_setup). - Controllers:
views.py— one Uploader + one DownloadAPIViewper dataset;ReUploadView. - Services:
services/csv_uploader.py(base validation toolkit),services/admin_csv.py,services/client_csv.py(2896 lines — one service per dataset, versioned bulk persistence),services/csv_services/{internal_payroll,market_data}.py,services/mapping_ai_service.py(1248 lines — OpenAI embedding + LLM mapping pipeline),services/mapping_ai_export_service.py. - Background:
tasks.py→generate_company_jf_mapping_ai_export(Celery, S3, fingerprint idempotency). - Permissions: per-dataset
Can…classes;ReUploadViewis fully open (§15/§18). - Business logic detail: see §6.5 and §6.6.
4.10 ai_insights¶
- Purpose: Groq-LLM short compensation-insight messages for an offer (pay trend, one-time payment, retention risk, roles in demand), with hardcoded fallbacks.
- Entry points:
/api/company/<company_id>/offer/<offer_id>/{base_pay_insights,ttc_pay_insights,ai_insights,one_time_payment,retention_risk,roles_in_demand}/. - Models:
AIInsightModel(read-back log). Note:ai_insights.utils.store_insight_logactually writes toai_module.AIInsight— a cross-app coupling/naming hazard (§18). - Controllers:
views.py— validate → load offer → cache →insight_*inutils.py→ generate vialangchain_helper.AIInsightGenerator(Groqllama-3.3-70b-versatile) → store + cache. - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, CanCreateOffer].
4.11 ai_module¶
- Purpose: OpenAI-backed rule-based offer analysis alerts (compa ratio, conversion rate, critical pay, retention risk, payment recommendation) refined into NL summaries.
- Entry points:
/api/company/<company_id>/offer/<offer_id>/{compa-ratio-review,conversion-analysis,critical-pay-positioning,retention-risk,payment-recommendation,insights}/. - Models:
AIInsight(ai_insightstable) — alert records. - Controllers:
views.pyonAIViewMixin— validate → analyze → cache → generate OpenAI summary → dedupe+persist → respond. Cleaner/layered architecture thanai_insights. - Services:
utils.py(analyzers),rag_chain_utils.py(AISummaryGenerator, OpenAIgpt-4o-mini),rag_prompts.py,mixins.py,cache_utils.py,enums.py. - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, CanCreateOffer]. - Note: "RAG" naming is a misnomer — there is no retrieval, just prompt templating over analyzer output.
4.12 dashboard¶
- Purpose: Aggregate offer/payroll/salary/JE data into dashboard metrics + filter option lists for TOM/CI and JE dashboards.
- Entry points:
/api/company/<company_id>/{dashboard-ci,dashboard-ci-filters,dashboard-filters,dashboard-data,dashboardje-filters,dashboardJE-data}/. - Models: none.
- Services:
service.py(CIDashboardService, dataclass filters,Decimalmoney math),utils.py(midpoint maps, compa-ratio/pay-gap calculators, currency conversion). - Permissions:
[IsAuthenticated, IsAdminOrOwnCompany, CanViewOffer](+CanViewTomDashboard/CanViewJeDashboard).
4.13 custom_jobs¶
- Purpose: Company-specific custom job-function/sub-function taxonomy (independent of the TA catalog).
- Entry points:
/api/company/<company_id>/custom-jobs/…,/api/custom-sub-jobs/…. - Models:
CustomJobFunctionModel(custom_job_functions),CustomJobSubFunctionModel(custom_job_sub_functions, withfile_urlJSON). - Permissions: function endpoints
[IsAuthenticated, IsAdminOrOwnCompany, Can…]; sub-function endpoints omitIsAdminOrOwnCompanyand lack company scoping (§18). - Known bugs: function
deletesignature dropscompany_id(broken);getmisusesmany=True. See §18.
4.14 tom_admin, tom_applications, tom_enums, s3_bucket, swagger, emails¶
tom_admin—/api/admin/tom-user[s]/…. Internal TOM-staff (non-company) user CRUD. Views:TomAdminUserView,GetTomAdminUsersView,RetrieveTomAdminUserView. Permissions:Can…TomAdminUser.tom_applications—TomApplicationModel(tom_applicationstable) +TomApplicationsEnum(TOM/JE/LIVE_PAY_BENCHMARKING_TOOL/ADMIN_PANEL/SSO) +TomApplicationsMixin(givesCustomUser/CompanyModeltheirapplicationshelpers) +TomApplicationService. No URLs of its own; drives entitlement logic.tom_enums—/api/tom_enums/tom-application-modules/(returns application-module enum choices).s3_bucket—/api/upload-image/,/api/upload-docx/,/api/company/<id>/upload-policy/. Direct S3 uploads viaboto3/django-storages. Includes DOCX sanitization.swagger— drf-yasg schema view (login-protected at/swagger/) + reusable manual parameters.emails— empty package.
5. Request Lifecycle¶
Middleware order (tombackend/settings/base.py:85-98)¶
A request passes top-to-bottom through this chain on the way in, and bottom-to-top on the way out.
flowchart TB
REQ([Incoming request]) --> M1[corsheaders.CorsMiddleware<br/><i>CORS headers</i>]
M1 --> M2[django_prometheus.PrometheusBeforeMiddleware<br/><i>start metrics timer</i>]
M2 --> M3[SecurityMiddleware<br/><i>HTTPS / security headers</i>]
M3 --> M4[SessionMiddleware<br/><i>load session</i>]
M4 --> M5[CommonMiddleware]
M5 --> M6[CsrfViewMiddleware<br/><i>CSRF protection</i>]
M6 --> M7[AuthenticationMiddleware<br/><i>attach request.user</i>]
M7 --> M8[MessageMiddleware]
M8 --> M9[XFrameOptionsMiddleware<br/><i>clickjacking protection</i>]
M9 --> M10[WhiteNoiseMiddleware<br/><i>serve static files</i>]
M10 --> M11[PrometheusAfterMiddleware<br/><i>record metrics</i>]
M11 --> VIEW([DRF view / URL routing])
End-to-end flow¶
sequenceDiagram
participant C as Client
participant MW as Middleware chain
participant U as URLconf (tombackend/urls.py → app urls)
participant A as JWTAuthentication
participant P as Permission classes
participant T as Throttle classes
participant V as View (GenericAPIView)
participant Se as Serializer
participant Sv as Service layer
participant O as ORM
participant DB as PostgreSQL
participant EH as custom_exception_handler
C->>MW: HTTPS request (Authorization: Bearer <jwt>)
MW->>U: routed request
U->>A: resolve view, authenticate
A->>A: decode JWT → request.user (or AnonymousUser)
A->>P: has_permission(request, view)
P-->>V: allowed? (else PermissionDenied)
V->>T: allow_request?
T-->>V: ok (else Throttled → 429)
V->>Se: validate request data
Se-->>V: validated_data (or ValidationError)
V->>Sv: business logic (@transaction.atomic on writes)
Sv->>O: queryset / save
O->>DB: SQL
DB-->>O: rows
Sv-->>V: result
V->>Se: serialize result
V-->>C: TomResponse.get_response(...) JSON
Note over EH: Any raised exception is caught by<br/>custom_exception_handler and mapped<br/>to the TomResponse envelope + status code
Key points:
- Authentication is DRF-level (DEFAULT_AUTHENTICATION_CLASSES = JWT, Basic, Session) — the first that succeeds sets request.user.
- Default permission is IsAuthenticated (DEFAULT_PERMISSION_CLASSES). Views add fine-grained classes on top.
- Authorization happens in permission classes before the view body (see §9).
- Write endpoints wrap logic in @transaction.atomic at the view or serializer level.
- All exceptions funnel through tom_exceptions.exception_handler.custom_exception_handler (registered as DRF EXCEPTION_HANDLER), which normalizes them into the TomResponse envelope.
6. Business Logic¶
6.1 Multi-tenant company setup & provisioning¶
- What it does: Creates a company tenant together with its first
COMPANY_SUPER_USER, its application entitlements, its default roles, and initial stock data. - Where it starts:
POST /api/company/→CreateCompanyView.post(@transaction.atomic). - Where the logic lives:
tom_company_setup/serializers/serializers.py::CreateOrUpdateCompanySerializer→ usesauthentication.RoleService.create_roles_on_company_create,tom_applications.TomApplicationService, and MarketStack for international stock values. On success, a one-time password email is sent to the super user (services/email_service.py::send_one_time_password_email). - Related models:
CompanyModel,CustomUser,RoleModel/UserRoleModel,TomApplicationModel,StockTracking. - Row-level scoping: as an admin creates BUs/regions/grades/functions,
_update_role_metafield_with_resourceappends the new ids into the creating user'srole.metafield. Theservices/services.pygetters later read these lists to constrain what each user can see (the platform's row-level authorization mechanism).
6.2 Offer modelling & the compensation formula¶
- What it does: Builds a candidate offer across three stages and computes the full compensation breakdown.
- Where it starts:
POST /api/company/<id>/offer/(position) →…/candidate-detail/→…/offer-modeller/. - Where the logic lives: validation/writes in
tom_offer_modeler/serializers.py; the compensation math inservices/offer_formula.py(get_offer_context_data_serviceorchestrates five component builders).
flowchart TB
P[POST offer/ position-detail] -->|stage=POSITION/CANDIDATE| CD[POST candidate-detail]
CD -->|stage=OFFER_MODELLER| AP[GET auto-populate-data<br/>pull reference comp + fingerprint]
AP --> CM[GET comparator-data<br/>internal + external benchmarks]
CM --> OM[POST offer-modeller<br/>save all components]
OM -->|is_draft=false| PLACED[status=PLACED<br/>allowance_data_fingerprint stored]
PLACED --> EMAIL[email-page / pdf]
PLACED --> STATUS[PATCH status ACCEPTED/REJECTED]
PLACED --> REV[POST revise → OfferCopyService deep-copy<br/>original=EDITED, new revision]
REV --> CMP[GET compare versions]
Compensation formula (the heart of the platform), from services/offer_formula.py:
- Fixed cash: monthly = annual/12; compa ratio = proposed monthly base ÷ internal salary-range mid; market ratio = proposed monthly base ÷ internal-payroll average base; allowances paired current vs proposed.
- STI (bonus): bonus target amount = annual base × (bonus target % / 100); proposed sales incentive = (proposed_annual_base / proposed_base_pay_percentage) × (100 − percentage); Target Total Cash (TTC) = fixed total + total bonus (adjusted when a sales incentive is proposed).
- LTI (equity): unvested + new-hire equity + annual grant; other-LTI split into unit vs non-unit bonuses.
- Sign-on: one-time payment current/proposed totals.
- Benefits: each computed by calculation_basis (percent_basic → % of base; percent_guaranteed/percent_actual → % of guaranteed cash; flat → value); respects include_in_total_remuneration / include_in_offer_detail.
- Totals: total comp and total remuneration current/proposed with % differences.
- Data-freshness: services/allowance_data.py::compute_allowance_data_fingerprint SHA-256-hashes the (id, updated_at) of the reference querysets an offer depends on; the retrieve endpoint compares the stored fingerprint to a fresh one to flag stale offers.
- Revisions: OfferVersioningService generates version codes; OfferCopyService.copy_offer deep-copies an offer into a new revision (original → EDITED); OfferComparisonService diffs versions.
⚠️ There are three implementations of the compensation math —
services/offer_formula.py,tom_offer_modeler/email.py, andcomparison_service.py— which can drift. See §18.
6.3 Job evaluation scoring¶
- What it does: Sums points across five "divisions" from a static rules matrix and maps the total to a company grade band.
- Where it starts:
POST /api/company/<id>/job-evaluations/<pk>/evaluate/. - Where the logic lives:
job_evaluations/services/job_grade_evaluator.py::JobGradeEvaluatorService.evaluate_gradeusing the ~575-line matrix inje_rules.py; then finds theJobGradeModelwhosestart_range ≤ points ≤ end_rangeand returns itsgrade_point_value(stored toevaluation_result). Lifecycle:open→evaluated→close(submit). - Risk: matrix lookups use direct
dict[key]indexing without fallback — a missing/invalid division value raisesKeyError→ 500 (mitigated by the evaluate serializer requiring all fields).
6.4 Live Pay Benchmarking (LBT)¶
- What it does: Aggregates market pay from offers + payroll into percentile/mean statistics, applies data-aging and small-sample suppression, generates a CSV report to S3, and emails the user.
- Where it starts:
POST /api/company/<id>/benchmarking-report/live-pay/→ validates, checks data sufficiency, persists the report, and enqueuesgenerate_live_pay_report(Celery). - Where the logic lives:
benchmarking_tools/services/calculations.py(numpy stats, suppression thresholds, org- vs incumbent-weighting, aging),services/report_creation.py::LivePayBenchmarkingReportBuilder(build + CSV + S3),services/live_pay/*(sufficiency + synchronous by-title lookup with quota enforcement). - Statistical model: incumbent-weighted = stats over all raw records; organization-weighted = stats over each company's mean. Small-sample suppression thresholds (e.g. p50 needs ≥3 orgs/≥3 obs; p10/p90 need ≥5 orgs/≥10 obs) except for the requesting company's own data.
6.5 CSV ingestion¶
- What it does: Validates and bulk-loads reference datasets from CSV; produces downloadable templates/exports.
- Where it starts:
POST /api/…/upload/(multipart, fieldattachment,?active=flag). - Where the logic lives:
csv_uploader/services/client_csv.py(per-dataset services). Flow: parse CSV to DataFrame (read_csv_inclusively, chardet encoding detection) → validate (nulls, duplicates, enums, numeric/date columns, countries/currencies/grades against DB and role metafields) → on error, email the errors and return(False, message); on success, create a new version, deactivate the old,bulk_createrows, and email success.
6.6 AI job-function mapping¶
- What it does: Suggests a mapping from a company's job functions/sub-functions to the TA catalog using OpenAI embeddings + LLM.
- Where it starts: synchronous
GET …/ai-suggestions/download/or asyncPOST …/ai-suggestions/exports/(Celery + S3, polled via…/exports/latest/). - Where the logic lives:
csv_uploader/services/mapping_ai_service.py— 4 phases: (1) embed TA + company parents/pairs (cached 7 days), (2) cosine-similarity scoring + greedy sub assignment, (3) LLM picks one TA parent per company function, (4) LLM maps subs within the locked parent; combined match % = harmonic mean; low-confidence rows blanked. Async export is idempotent via a SHA-256source_fingerprint.
7. API Layer¶
Controller organization¶
- All controllers are DRF
GenericAPIViewsubclasses (no ViewSets, no routers). One class per operation-group; HTTP verbs implemented asget/post/put/patch/deletemethods. - URL composition:
tombackend/urls.pyincludes each app'surls.py. Most are mounted at/api/; exceptions:/api/auth/(v1 auth),/api/v2/auth/(v2 auth),/api/admin/(tom_admin),/api/grade/(tom_grade),/api/tom_enums/.
API versioning¶
- Only
authenticationis versioned —/api/auth/(v1) and/api/v2/auth/(v2, adds embedded claims tokens + per-application role verification). Everything else is unversioned. Two ad-hoc "v2" endpoints exist insidetom_offer_modeler(currency…/v2) but that is a per-route suffix, not a version namespace.
Request validation¶
- Primarily via DRF serializers; write serializers implement
validate(),create(),update(). Some validation lives in view bodies (readingrequest.data/query_paramsdirectly). CSV validation is custom pandas logic in the CSV service classes.
Authentication & Authorization¶
- JWT bearer tokens (see §9). Authorization via stacked permission classes.
Error handling & response format¶
- Uniform envelope from
TomResponse.get_response. All errors normalized bycustom_exception_handler(see §13).
Major endpoints by module¶
All paths below are relative to the host; prefixes as noted.
<cid>=company_id.
Auth (/api/auth/, /api/v2/auth/)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/login/ | v1 login (?is_company=) |
| POST | /api/auth/logout/ | Blacklist refresh token |
| POST | /api/auth/refresh-token/ | Refresh access token |
| POST | /api/auth/forgot-password/ | Send reset link |
| POST | /api/auth/change-password/ | Reset via token |
| POST | /api/v2/auth/login/ | v2 login (claims + application) |
| GET | /api/v2/auth/verify-user/ | Switch/verify per-application role |
TOM Admin (/api/admin/): tom-user/ (POST), tom-users/ (GET), tom-user/<pk>/ (GET/PUT/PATCH).
Company setup (/api/): permissions/, sector[s]/, industry/industries/, sub-industry/…, job-function[s]/, job-sub-function[s]/, countries/, currencies/, company/, companies[/all|/list]/, company/<cid>/, company/<cid>/business-unit[s]/…, …/region[s]/…, …/legal-entity/…, …/user[s]/…, …/roles/…, …/stocks/update/.
Compensation (/api/company/<cid>/): job-grade[s]/…, salary-range[s]/…, cash-allowance[s]/…, short-term-incentive[-plan][s]/…, long-term-incentive[-plan][s]/…, benefit-plans/…, internal-payroll-versions/…, market-data-versions/…, company-job-function[s]/…, company-sub-job-function[s]/…, company-job-function-mapping[s]/…. Each dataset: …-versions/ (GET), …-version/<pk>/set-active/ (PATCH), list (GET), create (POST), <pk>/ (GET/PUT/DELETE).
Offer modeler (/api/company/<cid>/): offers[/list]/, offer/<pk>/, offer/ (POST), offer/<pk>/{position-detail,candidate-detail,offer-modeller,comparator-data,auto-populate-data,allowance-data-status,email-page[/pdf],revise,compare}/, offer/<pk>/currency/<cur>[/v2], add-template/, range_type/.
Job evaluations (/api/): job-evaluations/enums/, company/<cid>/job-evaluations[/list|-sort]/, …/<pk>/, …/<pk>/{evaluate,submit,documents}/, …/search.
Job grades (/api/): job-grade-mapping/{,<pk>/delete/,grade-point-ranges[-assigned]/,grade-points/,company/all}.
TOM grade (/api/grade/): ta-ranks/, company/, client-companies/, company/all, company/<pk>/.
Benchmarking (/api/company/<cid>/): benchmarking-report/live-pay[/list]/, benchmarking-report-get/<pk>/, benchmarking-report-title/, custom-filtered/, function/.
CSV (/api/): {sector,job-function,custom-jobs,grade-table}/{upload,download}/, company/<cid>/{job-grade,salary-range,cash-allowance,short-term-incentive,long-term-incentive,benefits-plan,internal-payroll,market-data,company-job-function,company-job-function-mapping}/{upload,download}/, …/company-job-function-mapping/ai-suggestions/{download,exports[/latest]}/, upload/re-upload.
AI insights (/api/company/<cid>/offer/<offer_id>/): base_pay_insights/, ttc_pay_insights/, ai_insights/, one_time_payment/, retention_risk/, roles_in_demand/.
AI module (/api/company/<cid>/offer/<offer_id>/): compa-ratio-review/, conversion-analysis/, critical-pay-positioning/, retention-risk/, payment-recommendation/, insights/.
Dashboard (/api/company/<cid>/): dashboard-ci/, dashboard-ci-filters/, dashboard-filters/, dashboard-data/, dashboardje-filters/, dashboardJE-data/.
Custom jobs (/api/): company/<cid>/custom-jobs/{create,,all,<pk>}/, custom-sub-jobs/{create,,<pk>}/.
S3 (/api/): upload-image/, upload-docx/, company/<cid>/upload-policy/.
Enums (/api/tom_enums/): tom-application-modules/.
Ops: /metrics (django-prometheus), /swagger/ (login-protected), /admin/ (Django admin).
8. Database Interaction¶
ORM¶
- Django ORM exclusively (Active Record). The DB backend is
django_prometheus.db.backends.postgresql(wraps PostgreSQL to expose query metrics). - No raw SQL of significance and no Repository layer — queries are built inline in services/serializers/views.
Models & relationships¶
- Every model inherits
tombackend.model.BaseModel(idBigAutoField,created_at,updated_at). AUTH_USER_MODEL = "authentication.CustomUser".- The dominant relational shape in
tom_compensationis the Version → Data → child-scoping triad:
erDiagram
CompanyModel ||--o{ CompanyJobGradeVersionModel : has
CompanyJobGradeVersionModel ||--o{ CompanyJobGradeModel : contains
CompanyJobGradeModel ||--o{ CompanyJobGradeCountryModel : scoped_to
CompanyModel ||--o{ CompanySalaryRangeVersionModel : has
CompanySalaryRangeVersionModel ||--o{ CompanySalaryRangeModel : contains
CompanyModel ||--o{ CompanyShortTermIncentivePlanModel : has
CompanyShortTermIncentivePlanModel ||--o{ CompanyShortTermIncentiveModel : values
OfferModel }o--|| CompanyModel : belongs_to
OfferModel }o--|| OfferPositionDetailModel : has
OfferModel }o--o| OfferCandidateDetailModel : has
OfferModel }o--o| OfferFixedCashModel : has
OfferModel }o--o| OfferSTIModel : has
OfferModel }o--o| OfferLTIModel : has
OfferModel }o--o| OfferBenefitModel : has
OfferModel }o--o| OfferSignOnBonusModel : has
CustomUser }o--|| RoleModel : active_role
CustomUser }o--o{ RoleModel : roles_via_UserRoleModel
RoleModel }o--o{ PermissionModel : via_PermissionRoleModel
CompanyModel ||--o{ CustomUser : employs
Schema reference — data types & character limits¶
Types below are Django field types and their PostgreSQL equivalents. Every table also has the inherited
BaseModelcolumns:id(BigAutoField→bigint, PK),created_at(DateTimeField→timestamptz),updated_at(DateTimeField→timestamptz). "Limit" =max_length(chars),max_digits/decimal_places(numeric precision), or the value range.
users (authentication.CustomUser)¶
| Column | Django type | PostgreSQL type | Limit / precision | Null? | Notes |
|---|---|---|---|---|---|
email |
EmailField |
varchar |
254 (Django default) | No | unique; is the login (USERNAME_FIELD) |
password |
CharField |
varchar |
255, min 8 | No | hashed |
first_name / last_name |
CharField |
varchar |
150 (Django default) | — | from AbstractUser |
phone_number |
CharField |
varchar |
20 | Yes | unique with phone_code |
phone_code |
IntegerField |
integer |
−2.1B…2.1B | Yes | |
role_id |
ForeignKey → roles |
bigint |
— | Yes | PROTECT |
country_id |
ForeignKey → countries |
bigint |
— | Yes | PROTECT |
company_id |
ForeignKey → companies |
bigint |
— | Yes | PROTECT |
is_active, is_superuser, is_staff, is_company_user, is_one_time_password |
BooleanField |
boolean |
— | No | default False (except is_active=True) |
failed_login_attempts |
PositiveIntegerField |
integer |
≥ 0 | No | default 0 |
locked_until |
DateTimeField |
timestamptz |
— | Yes | lockout expiry |
date_joined |
DateTimeField |
timestamptz |
— | No | default now |
roles (RoleModel) & permissions (PermissionModel)¶
| Table.Column | Type | PostgreSQL | Limit | Null? |
|---|---|---|---|---|
roles.name |
CharField |
varchar |
150 | No |
roles.description |
CharField |
varchar |
255 | Yes |
roles.is_admin_role / is_active |
BooleanField |
boolean |
— | No |
roles.metafield |
JSONField |
jsonb |
— | No |
roles.company_id |
FK → companies | bigint |
— | Yes |
permissions.name |
CharField |
varchar |
150 | No |
permissions.description |
CharField |
varchar |
255 | Yes |
user_auths (UserAuthModel) — password reset / OTP tokens¶
| Column | Type | PostgreSQL | Limit | Null? |
|---|---|---|---|---|
token |
CharField |
varchar |
255 | Yes |
otp |
IntegerField |
integer |
— | Yes |
auth_type |
CharField (choices) |
varchar |
255 | No |
expiry |
DateTimeField |
timestamptz |
— | Yes |
is_active |
BooleanField |
boolean |
— | No |
companies (CompanyModel)¶
| Column | Django type | PostgreSQL | Limit | Null? |
|---|---|---|---|---|
name |
CharField |
varchar |
255, unique | No |
address |
TextField |
text |
unbounded | No |
postal_code |
CharField |
varchar |
12 | Yes |
country_headquarter |
CharField |
varchar |
255 | No |
financial_year |
PositiveIntegerField |
integer |
≥ 0 | No |
contract_start_date / contract_end_date |
DateTimeField |
timestamptz |
— | No |
large_logo_url / small_logo_url |
TextField |
text |
unbounded | No |
stock_tracking_ids |
ArrayField(ArrayField(CharField)) |
varchar[][] |
inner 255 | No |
country_id / currency_id |
FK | bigint |
— | No (PROTECT) |
status |
EnumChoiceField |
varchar |
ACTIVE / IN_PROGRESS / EXPIRED | No |
max_grade_function_reports |
IntegerField |
integer |
— | Yes (default 2) |
max_individual_reports_percent |
FloatField |
double precision |
— | Yes (default 10.0) |
is_active |
BooleanField |
boolean |
— | No |
countries / currencies / soft-delete taxonomies¶
| Table.Column | Type | PostgreSQL | Limit | Null? |
|---|---|---|---|---|
countries.name |
CharField |
varchar |
255, unique | No |
countries.is_system_field |
BooleanField |
boolean |
— | No |
currencies.name |
CharField |
varchar |
255, unique | No |
currencies.symbol |
CharField |
varchar |
255 | No |
currencies.code |
CharField |
varchar |
255, unique | No |
sectors/industries/…name |
CharField |
varchar |
255 | No |
sectors/…is_active |
CharField (choices) |
varchar |
4 ("TRUE"/null) |
Yes |
stock_data.stock_value |
DecimalField |
numeric |
12,2 (max ±9,999,999,999.99) | — |
stock_data.tracking_id |
CharField |
varchar |
255, unique | — |
stock_data.stock_type |
CharField (choices) |
varchar |
50 | — |
Compensation money fields (representative)¶
| Table.Column | Type | PostgreSQL | Limit / precision | Notes |
|---|---|---|---|---|
company_salary_ranges.salary_min/mid/max |
FloatField |
double precision |
IEEE-754 float | ⚠️ float, not decimal — see note |
company_long_term_incentives.equity_min/mid/max |
FloatField |
double precision |
float | |
company_cash_allowances.value / STI value |
FloatField |
double precision |
float | |
company_benefits_plans.employer_cost_value |
DecimalField |
numeric |
(max_digits/decimal_places set on model) | decimal (exact) |
company_benefits_plans.benefit_limit |
DecimalField |
numeric |
(as defined) | decimal (exact) |
company_internal_payrolls.annual_base_pay |
FloatField |
double precision |
float, non-null | |
company_internal_payrolls.annual_* (bonus, LTI, TR, …) |
FloatField |
double precision |
float, nullable | ~15 numeric columns |
company_market_data.*_p25/p50/p75 |
FloatField |
double precision |
float | annual_base_pay_p50 non-null |
company_job_grades.grade |
CharField |
varchar |
(see model) |
⚠️ Data-type caution for developers: most monetary values use
FloatField(double precision), notDecimalField. Floats can introduce rounding error in money math (benefits useDecimalField, which is exact). This inconsistency is flagged in §18; when adding new money fields, preferDecimalField(max_digits, decimal_places).
Offers (offers + component tables) — representative¶
| Table.Column | Type | PostgreSQL | Limit | Null? |
|---|---|---|---|---|
offers.status |
CharField (choices) |
varchar |
40 (DRAFTED/PLACED/ACCEPTED/REJECTED/EDITED) | No |
offers.stage |
CharField (choices) |
varchar |
40 | No |
offers.version_code |
CharField |
varchar |
50 | Yes |
offers.rejection_reason |
TextField |
text |
unbounded | Yes |
offers.allowance_data_fingerprint |
CharField |
varchar |
64 (SHA-256 hex) | Yes |
offer_position_details.grade / reporting_grade |
CharField |
varchar |
30 | No / Yes |
offer_position_details.job_title / city / range_type |
CharField |
varchar |
255 | mixed |
offer_candidate_details.candidate_name |
CharField |
varchar |
255 | No |
offer_candidate_details.gender |
CharField (choices) |
varchar |
20 | Yes |
offer_fixed_cash.*_annual_base, *_compa_ratio |
FloatField |
double precision |
float | Yes |
Full column-level schema for every table is the model files themselves (
<app>/models.py) — this reference covers the high-traffic tables. To regenerate a complete schema, runpython manage.py inspectdborpython manage.py sqlmigrate <app> <migration>.
The versioning lifecycle¶
stateDiagram-v2
[*] --> NoVersion
NoVersion --> ActiveV1: create version (auto-active if first)
ActiveV1 --> ActiveV2: upload new CSV creates V2, deactivates V1
ActiveV2 --> ActiveV1: set-active PATCH on V1
note right of ActiveV2
Exactly one version has is_active=TRUE
per company (UniqueConstraint).
Data rows are read "through" the active version.
end note
Transactions¶
- Write endpoints use
@transaction.atomicwidely (view- or serializer-level; present acrosstom_compensation,tom_offer_modeler,csv_uploader,tom_company_setup,benchmarking_tools,job_evaluations,s3_bucket, etc.). OfferVersioningServiceusesselect_for_update()to lock the root original offer when generating a revision code (prevents duplicate version codes under concurrency).- Gap: several cascade-nulling soft-delete methods are not wrapped in
atomic, risking partial deletes (§18).
Query flow & data lifecycle¶
- Reads go through role-scoped getters in
services/services.pythat filter bycompany_idand by the id-lists in the user'srole.metafield(row-level scoping), then by the active version. - Writes (create/update) go through serializer
create()/update(), which resolve FKs, run cross-entity validation, andbulk_createchild rows. - Soft-delete flips
is_activetoNone(cascading to children).
Performance concerns (DB)¶
- N+1 queries are pervasive — nested
SerializerMethodFields re-query per parent row across nearly every app; list views generally lackselect_related/prefetch_related. The LBT calculation path is the highest risk: querysets OR-combined in per-grade/per-company loops, then deep related access (offer.offer_fixed_cash,offer.offer_sti, …) with no prefetching. - Counter updates use read-modify-write (
(x or 0)+1) instead ofF()expressions (race-prone). - See §14 for the full list.
9. Authentication & Authorization¶
Login flow (v2 — the current path)¶
sequenceDiagram
participant C as Client
participant LV as v2 LoginView
participant BE as LockoutAuthenticationBackend
participant U as CustomUser
C->>LV: POST /api/v2/auth/login/ {email, password, application?}
LV->>LV: LoginThrottle (10/min per email)
LV->>BE: auth.authenticate(email, password)
alt account locked
BE-->>LV: raise AccountLocked
LV-->>C: 400 ACCOUNT_LOCKED (minutes remaining)
else invalid
BE->>U: register_failed_attempt() (lock after 5)
LV-->>C: 400 INVALID_CREDENTIALS
else valid
BE->>U: reset_failed_attempts()
alt is_one_time_password
LV->>U: create ONE_TIME_PASSWORD UserAuthModel
LV-->>C: {is_one_time_password:true, token}
else normal
LV->>LV: resolve per-application role (UserRoleModel)
LV->>LV: RefreshToken.for_user + ClaimsToken(role, permissions)
LV-->>C: {user, token{access,refresh,claims}, applications}
end
end
Tokens¶
- JWT via
djangorestframework-simplejwt. Access token lifetime 1 day, refresh 5 days (SIMPLE_JWTin base settings). - v2 additionally issues a
ClaimsToken(authentication/tokens.py) embeddingroleandpermissions. - Logout blacklists the refresh token (
rest_framework_simplejwt.token_blacklistis installed). - Sessions are also enabled (Basic + Session auth are in the default classes), used for Django admin and Swagger.
Password reset & one-time password¶
forgot-password/issues aUserAuthModeltoken (5-min expiry), emails a reset link, and is throttled (3 per 2h per email). New company users get a one-time password; on first login they receive a reset token and must set a new password (change-password/).
Account lockout¶
LockoutAuthenticationBackend(the soleAUTHENTICATION_BACKENDSentry) locks a user for 15 minutes after 5 failed attempts (CustomUser.register_failed_attempt).
Permission system¶
flowchart TB
subgraph Identity
U[CustomUser] -->|active role| R[RoleModel]
U -->|per-application| UR[UserRoleModel: app → role + metafield]
R -->|M2M via PermissionRoleModel| PM[PermissionModel]
R -->|metafield JSON| MF[Row-level scope:<br/>business_units, regions,<br/>countries, grades, job_functions]
end
subgraph Enforcement
V[View permission_classes] --> HP["_has_permission(request, perm, methods, is_company)"]
HP -->|TOM_MASTER_USER| ALLOW[allow]
HP -->|is_company & COMPANY_SUPER_USER| ALLOW
HP -->|method not in class methods| ALLOW
HP -->|role has permission| ALLOW
HP -->|else| DENY[deny 403]
end
subgraph RowLevel
SVC[services.services getters] -->|read role.metafield| MF
SVC --> QS[filtered querysets]
end
Roles (utils/enums.py::Roles): TOM_MASTER_USER, TOM_SUPER_USER, TOM_ADMIN, TOM_SALES (internal staff); COMPANY_SUPER_USER, COMPANY_ADMIN, COMPANY_USER, COMPANY_BUSINESS_ACCESS (tenant users).
Two enforcement layers:
1. Object/verb permissions — DRF permission classes (authentication/permissions.py). The central helper _has_permission(request, permission, methods, is_company=False) returns True if the user is TOM_MASTER_USER, or (is_company and COMPANY_SUPER_USER), or the request method is not in the class's guarded method list, or the role holds the named permission.
2. Row-level scoping — the id-lists in role.metafield (business_units/regions/countries/grades/job_functions), consumed by services/services.py getters to filter querysets to what the user is allowed to see.
⚠️ Critical gotcha for reviewers: because
_has_permissionreturnsTruewhen the request method isn't in a class's method list, stackingCanView + CanEdit + CanDeleteon aRetrieveview means each verb is effectively gated only by its matching class. Do not assume "all listed permission classes must pass for every request." This also means a misconfigured method list silently allows access. See §15/§18.
Coarse permissions: IsAuthenticated (default), IsAdmin (rejects company users), IsAdminOrOwnCompany (admins, or company users whose company.id == view.kwargs["company_id"]).
10. Background Processing¶
Infrastructure¶
- Celery 5.2.7 over Redis (
CELERY_BROKER_URL/CELERY_RESULT_BACKEND, defaultredis://localhost:6379/0). JSON serialization; timezoneAmerica/New_York. - App defined in
tombackend/celery.py;autodiscover_tasksscans all installed apps. - In local settings,
CELERY_ALWAYS_EAGER = True— tasks run synchronously (no worker needed for dev). - In production, Celery runs under Supervisor alongside Gunicorn (see
codedeploy/after_install.sh).
Tasks (there are exactly three producers)¶
flowchart LR
subgraph Producers
LBTV[LBT create view] -->|.delay| T1
CSVV[AI export trigger view] -->|enqueue_export| T2
ANY[Any email caller] -->|shared_task| T3
end
subgraph "Celery Worker"
T1[generate_live_pay_report<br/>benchmarking_tools/tasks.py]
T2[generate_company_jf_mapping_ai_export<br/>csv_uploader/tasks.py]
T3[_send_email<br/>services/email_service.py]
end
T1 --> S3[(S3)]
T1 --> MAIL[SMTP success email]
T2 --> S3
T3 --> SMTP[Office365 SMTP]
| Task | Trigger | What it does | Retry / status |
|---|---|---|---|
generate-live-pay-report (benchmarking_tools/tasks.py) |
POST benchmarking-report/live-pay/ → .delay() |
Marks report processing, runs LivePayBenchmarkingReportBuilder.build() + CSV, uploads to S3, marks completed, sends success email; on error marks error and re-raises. |
Status via stage enum; no explicit max_retries. |
generate-company-jf-mapping-ai-export (csv_uploader/tasks.py) |
POST …/ai-suggestions/exports/ → enqueue_export |
Marks export processing, generates AI mapping CSV, uploads to S3, stores file_url/row_count, marks completed. |
bind=True, max_retries=3, default_retry_delay=60; MappingAIError is non-retryable, other exceptions self.retry. Idempotent via SHA-256 source_fingerprint. |
_send_email (services/email_service.py) |
Any transactional email helper | Sends via Django send_mail. |
Decorated @shared_task (so can run async) — but most callers invoke the wrapper functions synchronously within the request (see §18). |
Scheduled jobs / beat¶
- None active.
tombackend/celery.pyimportscrontabbut defines nobeat_schedule. There is a manual management commandauthentication/management/commands/resend_otp_emails.py(run on demand, not scheduled). No Celery Beat schedule is configured in the codebase.
Event handlers¶
- One Django signal file (
authentication/signals.py).simple_history'sHistoryRequestMiddlewareis commented out — no automatic history capture.
11. Integrations¶
flowchart LR
APP[TOM Backend]
APP -->|boto3 / django-storages| S3[(AWS S3<br/>files, reports, logos)]
APP -->|langchain-openai: ChatOpenAI gpt-4o-mini + embeddings| OPENAI[OpenAI]
APP -->|langchain-groq: llama-3.3-70b| GROQ[Groq]
APP -->|requests GET| FOREX[FastForex / exconvert currency API]
APP -->|requests GET| STOCK[MarketStack stock API]
APP -->|SMTP TLS 587| O365[Office365 smtp.office365.com]
APP -->|django-prometheus| PROM[Prometheus /metrics]
| Integration | Purpose | Method | Where | Failure handling |
|---|---|---|---|---|
| AWS S3 | Store uploaded files, LBT reports, offer logos, AI export CSVs | boto3 sessions + django-storages (DEFAULT_FILE_STORAGE = s3boto3) |
s3_bucket/views.py, benchmarking_tools, csv_uploader, tom_offer_modeler |
Exceptions propagate; report task marks error. Some inline credential use. |
| OpenAI | AI job-function mapping (embeddings + chat) and ai_module offer-analysis summaries |
langchain_openai (ChatOpenAI gpt-4o-mini, OpenAIEmbeddings text-embedding-3-small) |
csv_uploader/services/mapping_ai_service.py, ai_module/rag_chain_utils.py |
Structured Pydantic output + heuristic fallback; ai_module returns None (graceful) if unavailable. Key env TOM_AI_PRIMARY_TOKEN. |
| Groq | ai_insights compensation-insight messages |
langchain_groq.ChatGroq (llama-3.3-70b-versatile) |
ai_insights/langchain_helper.py |
On any exception → hardcoded backup messages (management/backup_message.py). Key env GROQ_API. |
| Currency API (FastForex / exconvert) | FX conversion for offers, dashboards, benchmarking | requests.get |
dashboard/utils.py, services/offer_formula.py, services/services.py |
Returns 1.0 on any failure (silent — risk of wrong numbers). Key CURRENCY_FAST_FOREX_API_KEY. No timeout/retry. |
| MarketStack (stock API) | Fetch stock prices for international stock tracking | requests.get |
tom_company_setup/utils.py, model StockTracking.set_stock_value |
Raises ValueError on failure (can surface inside a transaction). Key STOCK_API_KEY. |
| Office365 SMTP | All transactional email | Django send_mail (TLS, port 587) |
services/email_service.py |
_send_email re-raises; offer email send swallows exceptions (except: pass). |
| Prometheus | Metrics | django-prometheus middleware + DB backend |
settings + /metrics |
N/A |
Note:
requirements.txtalso liststiktoken,langsmith,sqlalchemy,httpx(transitive/optional AI-stack deps). No evidence of direct use beyond the LangChain integrations above.
12. Configuration¶
Environment selection¶
tombackend/settings/__init__.pyreads theenvOS variable (local|dev|staging|prod|docker) and dynamically imports the matching settings module (raisesValueErrorifenvis unset).- Settings are loaded via
environs.Env(.envfile supported).
Environment variables (.env.example)¶
| Variable | Purpose |
|---|---|
SECRET_KEY |
Django secret |
env |
Environment selector (local/dev/staging/prod) |
DEBUG |
Debug flag |
ALL_EXC_TO_SENTRY |
Route common exceptions to Sentry (flag present; Sentry wiring not found in settings — see note) |
DB_NAME, DB_USER, DB_PASSWORD, DB_HOST |
PostgreSQL connection (port hardcoded 5432) |
AWS_REGION_NAME, AWS_REGION_NAME_2, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET_NAME |
AWS S3 |
CURRENCY_FAST_FOREX_API_KEY, CURRENCY_API_KEY |
Currency APIs |
STOCK_API_KEY |
MarketStack |
GROQ_API_KEY (and code reads GROQ_API) |
Groq LLM |
OPENAI_API_KEY (and code reads TOM_AI_PRIMARY_TOKEN) |
OpenAI |
AI_SUMMARY_ENABLED |
AI summary feature flag |
EMAIL_HOST_USER, EMAIL_HOST_PASSWORD |
Office365 SMTP |
CELERY_BROKER_URL, CELERY_RESULT_BACKEND |
Redis (default localhost) |
IN_DOCKER |
Switches DB host in local/docker settings |
⚠️ Env-var naming mismatch:
.env.exampledocumentsGROQ_API_KEYandOPENAI_API_KEY, but the code readsGROQ_API(ai_insights/langchain_helper.py) andTOM_AI_PRIMARY_TOKEN(ai_module,csv_uploadermapping). Both the documented and the actually-read names must be set. (Fact — verify env on deploy.)
Config files¶
settings/base.py+ per-env modules;.env(from.env.example);docker/env/local/*.env;pytest.ini,conftest.py,.coveragerc,mypy.ini,.flake8,.isort.cfg,prospector_profile.yaml,.pre-commit-config.yaml;Dockerfile,docker-compose*.yml,appspec.yml+codedeploy/*(AWS CodeDeploy),procfile(Gunicorn).
Feature flags¶
AI_SUMMARY_ENABLED(env).MAPPING_AI_PARENT_HEURISTIC_ONLYand many tunable weights/thresholds in the AI mapping service (env-driven). No general-purpose feature-flag framework.
Secrets¶
- All secrets come from environment variables (no committed secrets in settings). However
docker-compose.ymlcontains hardcoded credentials for the Dozzle log viewer and DB env files — acceptable for local only. Do not reuse those in any shared environment.
Sentry note:
ALL_EXC_TO_SENTRYexists in.env.example, but no Sentry SDK initialization is present in the settings modules examined. Unable to determine from the codebase whether Sentry is wired elsewhere (e.g. an untracked settings module).
13. Logging & Error Handling¶
Logging strategy¶
- Django
LOGGINGconfig (settings/base.py) defines a singleconsolehandler (StreamHandler) with asimpleformatter and one named logger:csv_uploader.services.mapping_ai_serviceatINFO. - Individual modules use
logging.getLogger(__name__)(Celery tasks, AI services). - Heavy use of
print()across many modules (auth, AI, benchmarking, dashboards, CSV) instead of the logging framework — a consistent smell (§18). These print to stdout and are captured by container logging / Dozzle.
Exception handling¶
- Custom exception hierarchy (
tom_exceptions/exceptions.py):TomBaseException(APIException)→TomException(400),TomValidationException(400),TomResourceNotFoundException(404),TomResourceAlreadyExistException(409),TomUnauthorizedException(403). Each carries anerror(a code/message fromtom_exceptions/errors.py) and an optionalvalueinterpolated into the message. - Central handler
custom_exception_handler(registered as DRFEXCEPTION_HANDLER) maps exceptions to theTomResponseenvelope:
flowchart TB
EX[Exception raised] --> H{custom_exception_handler}
H -->|Throttled| R429[429 code=1001]
H -->|TomBaseException| RT["TomResponse: exc.error.code + message"]
H -->|ValidationError| RV["400 VALIDATION_ERROR, is_validation_error=true"]
H -->|Http404 / DoesNotExist| R404[404 RESOURCE_NOT_FOUND]
H -->|PermissionDenied / NotAuthenticated| R401[UNAUTHORIZED_ERROR]
H -->|InvalidToken / TokenError| RTK[INVALID_TOKEN]
H -->|KeyError| RK[400 field invalid]
H -->|ValueError| RVE[400 VALIDATION_ERROR]
H -->|anything else| R500["500 SOMETHING_WENT_WRONG (traceback printed)"]
Error responses¶
- Always the
TomResponseenvelope withsuccess:false, a numericcode, amessage, and (for validation) anerrorobject +is_validation_error:true. Error codes/messages are catalogued intom_exceptions/errors.py(TomError,TomValidationError,TomResourceNotFoundError,TomAuthorizedError,JobGradeError, …).
Monitoring¶
django-prometheusexposes/metrics(request counts/latencies, DB query metrics via the prometheus DB backend).- Container logs viewable via Dozzle (
docker-compose.yml). - Application-level error tracking (Sentry) — see §12 note.
14. Performance¶
Caching¶
- Django cache used by
ai_module/ai_insights(analysis results + AI summaries; TTLs viaCacheTimeout, e.g. 600s) and by AI-mapping embeddings (7-day cache with versioned invalidation key). Theforgot_passwordthrottle also uses the cache. - Assumption: cache backend is Redis in production (Redis is present as broker), but the
CACHESsetting is not explicitly defined in the settings examined → defaults to Django's local-memory cache unless configured elsewhere. Unable to fully determine from the codebase — verify the productionCACHESconfig.
Pagination¶
- DRF
PageNumberPagination,PAGE_SIZE = 10(global).response/tom_pagination.py::TomPaginationwraps paginated results in the envelope and honors a?page_size=override. Most list endpoints paginate;…/allvariants deliberately return unpaginated data.
Database optimization¶
bulk_createis used for child-row persistence in CSV ingestion, offer components, and mapping replace.select_for_updateguards offer version-code generation.- Weaknesses (the dominant performance issue):
- N+1 everywhere — nested
SerializerMethodFields re-query per row; list views generally lackselect_related/prefetch_related. - LBT OR-combines querysets in nested per-grade/per-company loops then deep-accesses related objects without prefetch — the highest-risk hotspot.
get_user_filtersandget_target_company_ids_by_grade_filtercomputed repeatedly per request.- Counter columns updated via read-modify-write instead of
F()(race-prone). - Some
get()methods build a queryset then discard and re-query.
Lazy loading & batch processing¶
- Django querysets are lazy by default; the issue is not enough eager loading where serializers traverse relations (opposite problem). Batch processing appears in CSV ingestion (
bulk_create) and LBT report building (numpy vectorized stats).
Possible bottlenecks¶
- Synchronous external HTTP inside request/transaction — currency conversion (per-record in dashboard/offer math, no timeout/cache), MarketStack stock fetch (inside company create transaction), LLM calls in
ai_insights/ai_module(mitigated only by caching). - LBT report N+1 + giant god-methods (
build()~590 lines,generate_csv_and_upload()~440 lines) — CPU + query heavy; correctly moved to Celery. - In-memory pivots (
job_grades.GradePointsViewloads all grades+companies and loops in Python). - Zip decompression in
ReUploadView(compute_payload_size) — unbounded (zip-bomb risk, §15).
15. Security¶
| Area | Status in this codebase |
|---|---|
| Authentication | JWT (simplejwt), 1-day access / 5-day refresh, refresh blacklisting on logout, account lockout after 5 failed logins, login throttle (10/min/email), forgot-password throttle (3/2h). |
| Authorization | Role/permission classes + row-level role.metafield scoping. ⚠️ The _has_permission "method not in list → allow" behavior makes stacked permissions per-verb and easy to misconfigure. Several endpoints under-scope company access (see below). |
| Input validation | DRF serializers on most endpoints; custom pandas validation for CSV. Some views read request.data/query_params directly (KeyError/ValueError risk, handled by the exception handler). |
| SQL injection | Low risk — Django ORM parameterizes queries; no significant raw SQL. |
| XSS | API is JSON-first. Offer email/PDF render HTML via templates + token replacement — user-supplied fields (candidate names, email body) are interpolated; ensure template escaping (Django templates auto-escape by default). Offer logo upload validates magic bytes (PNG/JPEG, blocks SVG). |
| CSRF | CsrfViewMiddleware enabled; JWT (stateless) endpoints are not CSRF-bound. One view (UploadCompanyBenefitPlanPolicyManualView) is explicitly csrf_exempt. |
| Rate limiting | DRF throttling: anon 100/day, user 1000/day, login 10/min, forgot_password 3/min (plus the custom 3/2h). Throttle exceed → 429 with code 1001. |
| Sensitive data | Secrets via env vars. Passwords hashed by Django. Forgot-password responses do not reveal account existence. But hardcoded creds exist in docker-compose.yml (local Dozzle/DB) — local-only. |
Concrete security findings (from code review — see §18 for the full list)¶
- Unauthenticated endpoints:
ReUploadView(permission_classes = []) decompresses attacker-supplied (optionally AES) zips with no size/depth limit → zip-bomb / decompression DoS.CountryViewandCurrencyViewexpose reference data with no auth.JeDocsUploadViewhas no permission classes (any authenticated user can upload to any JE by pk). - Weak company scoping / horizontal access: LBT
custom-filtered/andfunction/, allcustom_jobssub-function endpoints, and severalget_queryset-by-pk views (LivePayBenchMarkingDownload, offer/JE retrieve) fetch by pk without confirming the object belongs to the URL's company — potential cross-tenant data access. - Prompt-injection surface: user-uploaded job-function names/descriptions are interpolated into LLM prompts. The mapping pipeline mitigates via structured output + ID allowlists; the free-text summary prompts in
ai_insights/ai_moduleare more exposed. - Silent failure of FX conversion returns
1.0, which can silently produce wrong compensation numbers. CORS_ORIGIN_ALLOW_ALL = True— all origins allowed. Acceptable only if the API is strictly token-authenticated and not cookie-authenticated for sensitive actions; review for production.
16. Dependency Map¶
Module dependency graph (internal)¶
flowchart TB
subgraph Foundation
UTILS[utils]
RESP[response]
EXC[tom_exceptions]
BASE[tombackend.model / settings / celery]
SWAG[swagger]
end
subgraph Identity
AUTH[authentication]
APPS[tom_applications]
end
subgraph Shared
SVC[services<br/>services.py / offer_formula.py / email_service.py]
end
subgraph Setup
CS[tom_company_setup]
GRADE[tom_grade]
JG[job_grades]
CJ[custom_jobs]
end
subgraph Comp
COMP[tom_compensation]
OFFER[tom_offer_modeler]
end
subgraph Features
JE[job_evaluations]
LBT[benchmarking_tools]
CSV[csv_uploader]
AII[ai_insights]
AIM[ai_module]
DASH[dashboard]
S3[s3_bucket]
end
AUTH --> APPS
AUTH --> UTILS
CS --> AUTH
CS --> APPS
CS --> SVC
CS -. delete-guard .-> OFFER
CS -. delete-guard .-> COMP
COMP --> CS
COMP --> SVC
COMP --> GRADE
COMP --> AUTH
COMP -. delete-guard .-> JE
COMP -. delete-guard .-> OFFER
OFFER --> COMP
OFFER --> CS
OFFER --> SVC
OFFER --> GRADE
JE --> CS
JE --> COMP
JE --> JG
JE --> GRADE
LBT --> COMP
LBT --> OFFER
LBT --> CS
LBT --> GRADE
LBT --> SVC
CSV --> COMP
CSV --> CS
AII --> OFFER
AII --> AIM
AIM --> OFFER
AIM --> COMP
DASH --> OFFER
DASH --> COMP
DASH --> CS
JG --> CS
JG --> GRADE
CJ --> CS
GRADE --> CS
SVC --> OFFER
SVC --> COMP
ALL[All apps] --> RESP
ALL --> EXC
ALL --> UTILS
Key observations:
- tom_company_setup is the coupling hub (imported by nearly everyone), yet it also imports back into tom_compensation and tom_offer_modeler (via delete-guards) → circular dependency risk between "setup" and downstream domains. These back-references are done with function-local imports in places to avoid import cycles.
- services/services.py is a second hub — it imports tom_offer_modeler and tom_compensation while being imported by almost every app.
- The AI apps form a small cluster (ai_insights → ai_module), sharing the AIInsight table and cache utilities.
17. Developer Guide¶
Where to add things (the mental model)¶
| I want to… | Do it here |
|---|---|
| Add a new API endpoint | Add a GenericAPIView subclass in the app's views.py, wire it in the app's urls.py (path is auto-prefixed by tombackend/urls.py). Follow the existing Create/GetList/GetAll/Retrieve pattern. |
| Add request validation | In a DRF serializer (serializers.py) — validate() for cross-field rules, field-level validators, or an entry in validators.py. Keep view bodies thin. |
| Add business logic | Prefer a service — a function in services/services.py (if cross-app/reusable) or a class in the app's services/ package. Serializers may host create/update logic (existing convention), but new non-trivial logic belongs in a service. |
| Add a model | The app's models.py; inherit BaseModel; set an explicit db_table; follow the is_active="TRUE"/None soft-delete convention (or a Boolean if you must — but be consistent within the domain). For versioned reference data, follow the Version + Data + child-scoping triad. Then python manage.py makemigrations && migrate. |
| Add a permission | A new Permissions enum value (utils/enums.py), a Can… class in authentication/permissions.py, and add it to the relevant role grouping in utils/constants.py. Attach to views via permission_classes. |
| Add row-level scoping | Read/write ids in role.metafield and filter through services/services.py getters (use RoleMetafieldService). |
| Add a background job | A @app.task/@shared_task in the app's tasks.py; enqueue with .delay()/.apply_async(); it is auto-discovered. Update stage/status fields for progress. |
| Add an integration | Wrap the client in a service; read keys from settings (env); add timeouts/retries (many existing calls lack them). |
| Add an email | A helper in services/email_service.py that calls _send_email. |
| Add a file upload | Use s3_bucket patterns (boto3 / default_storage); validate type/size. |
How to run & debug locally¶
```bash
1. Environment¶
cp .env.example .env # fill in DB, AWS, API keys; set env=local, DEBUG=true export env=local # settings/init.py requires this OS var
2. Dependencies¶
python -m venv venv && source venv/bin/activate # (Windows: .\venv\Scripts\activate) pip install -r requirements.txt
3. Database (PostgreSQL running locally per .env)¶
python manage.py migrate python manage.py createsuperuser
4. Run¶
python manage.py runserver 0.0.0.0:8080
In local settings, CELERY_ALWAYS_EAGER=True → tasks run inline (no worker needed).¶
For async behavior, run a worker: celery -A tombackend worker -l info¶
```
- Docker:
docker-compose up(brings uptom_dbPostgres, pgAdmin, Dozzle logs,tom_api). SetIN_DOCKER=1. - API docs:
/swagger/(login-protected). Metrics:/metrics. Django admin:/admin/. - Debugging:
DEBUG=truefor tracebacks; the exception handler prints tracebacks for unexpected errors; watch stdout (many modulesprint()).docker-compose.debug.ymlexists for debugger attach.
Testing¶
bash
pytest # all tests (pytest-django, factory_boy fixtures)
pytest --cov=. # with coverage
pytest path::TestClass::test # a single test
mypy tombackend # type check
prospector --profile prospector_profile.yaml # static analysis
Test config: pytest.ini, conftest.py, .coveragerc. Coverage is currently light (~19 test files/dirs) — heaviest in services/tests/ and authentication/v2/tests/.
Common workflow¶
- Branch from the default branch.
- Add/modify model →
makemigrations→ review the migration →migrate. - Add serializer validation + service logic + view + URL.
- Add/attach permission classes; consider row-level scoping.
- Write tests; run
pytest,mypy,flake8/prospector(pre-commit hooks configured). - Verify via
/swagger/or curl; confirm theTomResponseenvelope shape.
18. Code Quality Review¶
Findings below are drawn directly from the source. Severity is the author's engineering judgment.
Correctness bugs (high priority)¶
| # | Location | Issue |
|---|---|---|
| 1 | csv_uploader/views.py (CompanyJobGradeUploaderView.post, CompanySalaryRangeUploaderView.post) |
upload_data(...) is called twice, duplicating validation, emails, and potentially writes. |
| 2 | csv_uploader/views.py (CompanyCashAllowanceUploaderView) |
Checks if status is None against the imported DRF status module, not status_param — the required-param guard never fires. |
| 3 | custom_jobs/views.py (RetrieveCustomJobFunctionView.delete) |
Method signature omits the company_id URL kwarg → TypeError at call time (delete is broken). |
| 4 | custom_jobs/views.py (RetrieveCustomJobFunctionView.get) |
Uses many=True over a single instance returned by the getter → error/misbehavior. |
| 5 | job_evaluations/services/job_evaluation.py::submit |
Sets submitted_by/submitted_at which do not exist on the model or migrations → silently dropped. |
| 6 | job_grades/views.py (DeleteJobGradeMappingView.delete) |
Deleting one pk hard-deletes all companies' rows sharing that grade range. |
| 7 | job_grades/serializers.py (UpdateJobGradeSerializer) |
Conflict validation guarded by if self.instance, which the view never sets → dead validation. |
| 8 | tom_compensation/views.py |
CreateDefaultCompanyShortTermIncentiveVersionsView / …LongTermIncentiveVersionsView declare the wrong serializer (CompanyCashAllowanceVersionSerializer). |
| 9 | tom_compensation/serializers.py (CreateCompanyBenefitPlanView.post) |
Possible UnboundLocalError on an unnamed IntegrityError. |
| 10 | benchmarking_tools/services/report_creation.py::build (aggregate title block) |
References loop-leaked variables (combined_offer, etc.) holding the last grade's querysets; guarded elsewhere with if 'x' in locals() (a smell) → aggregate stats can use the wrong grade's data. |
| 11 | ai_insights/utils.py::store_insight_log |
Computes an "existing" dedupe check but then unconditionally creates a new row; also writes to ai_module.AIInsight, not ai_insights.AIInsightModel. |
Duplicate logic¶
- Compensation math implemented three times (
services/offer_formula.py,tom_offer_modeler/email.py,comparison_service.py) — drift risk between offer list, email/PDF, and comparison. - ~9 identical
set-activePATCH bodies intom_compensation/views.py. - Grade↔country intersection validation duplicated ~4× in
tom_compensation/serializers.py;get_stock_datacopied 4×. - CSV service classes (
client_csv.py, ~10 classes) copy the sameupload_data/_save_data/_send_error_email/download_csvshape. dashboard/service.pyduplicatesdashboard/utils.pyCI computations.ai_insightsrepeats boilerplate thatai_modulealready factored intoAIViewMixin.- Near-duplicate views: JE list/sort/search (~90% identical); offer full-list vs light-list; two currency-conversion methods.
Tight coupling¶
tom_compensationimports a private helper (_update_role_metafield_with_resource) fromtom_company_setup.views.tom_company_setup↔tom_compensation/tom_offer_modelercircular dependency (delete-guards), papered over with function-local imports.services/services.pyimports downstream apps while being imported by nearly all of them.ai_insightswrites toai_module's table.
Large classes / functions¶
LivePayBenchmarkingReportBuilder.build()(~590 lines) andgenerate_csv_and_upload()(~440 lines).tom_compensation/views.py(3126),serializers.py(3506);tom_company_setup/views.py(2117);csv_uploader/services/client_csv.py(2896),mapping_ai_service.py(1248).je_rules.py— 575-line hardcoded matrix.
Missing abstractions¶
- No shared base for CSV services, for the set-active version flip, for the "cannot delete because referenced" guard (copy-pasted across taxonomy views), or for the AI-view flow (only
ai_modulehas the mixin). - Three coexisting grading concepts (
tom_graderanks,job_gradespoint ranges,role.metafieldgrade ids) with no unifying model.
Performance issues¶
- Pervasive N+1 (nested
SerializerMethodFields, missingselect_related/prefetch_related), worst in LBT. - Synchronous external HTTP inside requests/transactions (FX, stock, LLM) without timeout/retry/caching.
- Read-modify-write counters instead of
F(). - In-memory pivots (
job_grades.GradePointsView).
Security concerns¶
- Unauthenticated
ReUploadView(zip-bomb),CountryView/CurrencyView,JeDocsUploadView. - Weak company scoping on several by-pk /
[IsAuthenticated]-only endpoints (cross-tenant risk). - The
_has_permissionper-method allow behavior (easy to misconfigure). - Prompt-injection surface in AI prompts.
CORS_ORIGIN_ALLOW_ALL = True.- Silent
1.0FX fallback → wrong money.
Miscellaneous smells¶
print()used for logging across auth, AI, benchmarking, dashboard, CSV; left-in debug prints and[TIMING]/hardcoded test values (current_bonus_target = 9,acceptance_deadline = "July 15, 2023").- Inconsistent soft-delete (
"TRUE"/Nonestring vs Boolean). - Magic numbers (currency id
138/197, sub-function threshold≥6). - Dead code / vestigial fields (
stock_tracking_idsArrayField,IntegerListField, emptyservice.py,validators.validate_business_unitno-op,*Oldenums, misspelledvalidtors.py). safe_floatrounds to int, silently dropping cents in offer math.- Hard deletes of STI/LTI child rows lose history despite the soft-delete convention elsewhere.
- Env-var name mismatches (
GROQ_APIvsGROQ_API_KEY;TOM_AI_PRIMARY_TOKENvsOPENAI_API_KEY). simple_historyinstalled but disabled (middleware commented out; noHistoricalRecordsfields) → the offer app hand-rollsOfferVersionHistoryinstead.
19. Improvement Opportunities¶
Refactoring¶
- Unify the compensation formula. Collapse
offer_formula.py,email.py, andcomparison_service.pyinto one authoritative module; useDecimalend-to-end and stop rounding to int insafe_float. - Extract shared bases: a
VersionedDatasetService(create version / set-active / soft-delete), aCSVDatasetServicebase, a "referential delete-guard" helper, and adoptAIViewMixininai_insights. - Split god files/methods: decompose
LivePayBenchmarkingReportBuilder, and breaktom_compensation/tom_company_setupviews and serializers into per-resource modules. - Introduce a thin repository/query layer (or at least
Manager/QuerySetmethods) so N+1-safe querysets (select_related/prefetch_related) are defined once and reused. - Standardize soft-delete on one mechanism (recommend a nullable
deleted_at+ a custom manager) and migrate the string-"TRUE"/Boolean split. - Move validation and write logic out of serializers into services for new code; keep serializers to (de)serialization + declarative validation.
Scalability¶
- Kill N+1 systematically (add prefetching to all list serializers; the single biggest win, especially LBT).
- Make external calls resilient: add timeouts + retries (tenacity is already a dependency) + caching for FX/stock; never return a silent
1.0for FX — surface an error or use a cached rate. - Use
F()expressions for the report/usage counters to remove races. - Push more work to Celery (currency prefetch, dashboard aggregation for large tenants) and add a proper
CACHES(Redis) definition to settings. - Add DB indexes for the hot filter columns (company_id + is_active + version) if not already covered by the unique constraints.
Maintainability¶
- Replace all
print()with the logging framework; add structured logging and (re-)enable Sentry (ALL_EXC_TO_SENTRYimplies it was intended). - Remove dead code (
*Oldenums, unused fields/services, empty modules) and fix misspellings. - Externalize
je_rules.pyinto data (DB or config) so the scoring matrix can change without a deploy; guard matrix lookups with.get()+ clear errors. - Fix the env-var name mismatches and document the full required env set.
- Raise test coverage, especially around the compensation formula, JE scoring, LBT statistics, and permission edge cases.
- Fix the correctness bugs in §18 (double-upload, broken custom-job delete, JE submit fields, over-broad job-grade delete, wrong serializers, LBT variable leakage).
Architecture¶
- Break the setup↔compensation/offer circular dependency — move the shared reference-checks behind an interface or a dedicated
catalog/domainapp, sotom_company_setupno longer imports downstream models. - Consolidate the three grading concepts behind one grading domain model.
- Formalize the row-level authorization (
role.metafieldscoping) into a documented, tested policy layer rather than ad-hoc id-list reads scattered throughservices.py. - Separate AI concerns — merge
ai_insightsandai_module(or clearly split responsibilities) and stop cross-writing theAIInsighttable; add a provider abstraction so OpenAI/Groq are swappable. - Introduce API versioning platform-wide (only auth is versioned today) before breaking changes are needed.
- Add a feature-flag mechanism rather than scattered env booleans.
20. System Overview & Architecture¶
What the system is¶
A modular-monolith Django REST backend serving five frontend apps (SSO, TOM, JE, LBT, Dashboard) over one HTTP API, backed by PostgreSQL, with Celery workers for background jobs and S3 for files. (Frontend detail: Frontend Documentation; product view: Product Handbook.)
Runtime topology (production)¶
flowchart TB
subgraph Clients["5 Frontend SPAs"]
SSO[SSO] & TOM[TOM] & JE[JE] & LBT[LBT] & DASH[Dashboard]
end
LB[HTTPS / reverse proxy]
subgraph Server["Application server (EC2, Ubuntu)"]
SUP[Supervisor]
GUN[Gunicorn<br/>WSGI, Django]
CEL[Celery worker]
SUP --> GUN
SUP --> CEL
end
DB[(PostgreSQL)]
RED[(Redis<br/>broker + cache)]
S3[(AWS S3)]
EXT[OpenAI · Groq · Currency ·<br/>Stock · Office365 SMTP]
Clients --> LB --> GUN
GUN --> DB
GUN --> RED
GUN --> S3
GUN --> EXT
RED <--> CEL
CEL --> DB
CEL --> S3
CEL --> EXT
GUN -. /metrics .-> PROM[Prometheus]
Architectural summary¶
| Dimension | Choice |
|---|---|
| Style | Modular monolith (one Django project, ~20 apps) |
| Layering | View → Serializer → Service → ORM |
| Sync work | Gunicorn (WSGI) |
| Async work | Celery + Redis |
| Data | PostgreSQL (versioned reference data pattern) |
| Files | AWS S3 (django-storages) |
| Process mgmt | Supervisor |
| Auth | JWT (shared cookie SSO across frontends) |
Detailed patterns are in §3 Architecture. This section is the one-glance overview; the diagrams are consolidated in §21.
21. System Diagrams¶
A consolidated index of the key diagrams in this document, plus a component map. (Individual diagrams live in their sections; this is the map.)
Component map¶
flowchart LR
subgraph "API layer"
URLS[urls.py] --> AUTHV[Auth views]
URLS --> BIZ[Business app views]
end
subgraph "Cross-cutting"
PERM[permissions.py]
EXC[exception_handler]
RESP[TomResponse]
SVC[services/*]
end
subgraph "Domains"
COMP[tom_compensation]
OFFER[tom_offer_modeler]
JE[job_evaluations]
LBT[benchmarking_tools]
AI[ai_insights / ai_module]
CSV[csv_uploader]
SETUP[tom_company_setup]
end
BIZ --> PERM
BIZ --> SVC
BIZ --> RESP
BIZ -.errors.-> EXC
SVC --> COMP & OFFER & JE & LBT & AI & CSV & SETUP
Diagram index¶
| Diagram | Section |
|---|---|
| High-level architecture | §1 |
| Middleware chain | §5 |
| Request lifecycle (sequence) | §5 |
| Entity relationships (ER) | §8 |
| Versioning state machine | §8 |
| Login flow (sequence) | §9 |
| Permission model | §9 |
| Background tasks | §10 |
| Integrations | §11 |
| Exception routing | §13 |
| Dependency map | §16 |
| Runtime topology | §20 |
| CI/CD pipeline | §24 |
22. API Reference¶
This is the reference contract for the API. The full endpoint catalogue by module is in §7 API Layer; this section documents the conventions a client/integrator needs.
Base & versioning¶
- Base path: all endpoints are under
/api/…. - Versioning: only auth is versioned —
/api/auth/(v1) and/api/v2/auth/(v2, current). Everything else is unversioned. - Company scoping: most business endpoints follow
/api/company/<company_id>/…. - Interactive docs:
/swagger/(login-protected,drf-yasg).
Authentication¶
| Aspect | Value |
|---|---|
| Scheme | Bearer JWT — Authorization: Bearer <access_token> |
| Obtain | POST /api/v2/auth/login/ |
| Refresh | POST /api/auth/refresh-token/ |
| Access token lifetime | 1 day |
| Refresh token lifetime | 5 days |
| Logout | POST /api/auth/logout/ (blacklists refresh) |
Standard response envelope¶
Every response uses this shape (TomResponse.get_response):
json
{
"success": true,
"is_validation_error": false,
"message": "Human-readable message",
"code": 0,
"data": {},
"error": {},
"is_paginated": false,
"pagination": { "count": 0, "next": null, "previous": null, "page_size": 10 }
}
Pagination¶
| Param | Meaning | Default |
|---|---|---|
page |
Page number | 1 |
page_size |
Items per page | 10 (global PAGE_SIZE) |
Paginated responses set is_paginated: true and fill pagination. List endpoints ending in /all are intentionally unpaginated.
Common query parameters (list endpoints)¶
| Param | Meaning |
|---|---|
search |
Case-insensitive partial match across whitelisted fields |
sort_by |
Field to sort by (per-view whitelist) |
order |
asc | desc (default desc) |
statuses |
Filter (e.g. offers by status) |
HTTP status & error codes¶
| Situation | HTTP | code |
|---|---|---|
| Success | 200/201 | 0 |
| Validation error | 400 | VALIDATION_ERROR (with is_validation_error: true) |
| Invalid/expired token | 400/401 | INVALID_TOKEN (1002) |
| Unauthorized | 403 | UNAUTHORIZED_ERROR |
| Not found | 404 | RESOURCE_NOT_FOUND |
| Throttled | 429 | 1001 |
| Server error | 500 | SOMETHING_WENT_WRONG |
Error codes/messages are catalogued in tom_exceptions/errors.py. Full handler behavior: §13.
Rate limits (throttling)¶
| Scope | Limit |
|---|---|
anon |
100/day |
user |
1000/day |
login |
10/min (per email) |
forgot_password |
3/min (plus a custom 3 / 2h) |
Content types¶
- Requests/responses:
application/json(default). - File upload (CSV, logos, docs):
multipart/form-data. - CSV download endpoints return
text/csv(the frontend auto-downloads these).
Representative endpoints¶
A short, high-value slice (full catalogue in §7):
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v2/auth/login/ |
Login → tokens + claims |
| GET | /api/v2/auth/verify-user/?application=&company_id= |
Confirm per-app access |
| GET | /api/company/<cid>/offers/list/ |
Paginated offers |
| POST | /api/company/<cid>/offer/ |
Start an offer |
| POST | /api/company/<cid>/offer/<pk>/offer-modeller/ |
Save compensation model |
| GET | /api/company/<cid>/job-grades/ |
List job grades |
| POST | /api/company/<cid>/job-grade/upload/ |
Upload job-grade CSV |
| POST | /api/company/<cid>/benchmarking-report/live-pay/ |
Trigger LBT report (async) |
| POST | /api/company/<cid>/job-evaluations/<pk>/evaluate/ |
Run job evaluation |
23. Version Control¶
Note: the analyzed snapshot is not a git repository (
gitreports no repo at the analysis path), so branch/commit history could not be inspected directly. The following is inferred from CI/CD config files present in the repo (.github/workflows/,buildspec.yml,appspec.yml).
Branching (inferred from workflows)¶
| Branch | Role (from CI triggers) |
|---|---|
main |
Production line — github_actions.yml (CI) and prod.yml (deploy) trigger on push to main |
dev |
Development — github_actions.yml CI also runs on dev |
The docs-automation workflows added for this project use
prod(backend) andcicd(frontend) branches per the team's repo layout — see the automation setup guide. The canonical product branch names should be confirmed with the team (Unable to fully determine from the codebase).
CI on every push / PR (github_actions.yml)¶
flowchart LR
PR[Push / Pull request<br/>main or dev] --> SETUP[Set up Python 3.9]
SETUP --> DEPS[pip install -r requirements.txt]
DEPS --> TEST[pytest]
TEST --> LINT[prospector<br/>static analysis]
Tooling that guards commits¶
| Tool | Config | Purpose |
|---|---|---|
pre-commit |
.pre-commit-config.yaml |
Runs hooks before commit |
flake8 |
.flake8 |
Style/lint |
isort |
.isort.cfg |
Import ordering |
mypy |
mypy.ini |
Type checking |
prospector |
prospector_profile.yaml |
Aggregated static analysis (runs in CI) |
pytest |
pytest.ini, conftest.py |
Tests (runs in CI) |
| Dependabot | .github/workflows/dependabot.yml |
Dependency updates |
Commit / PR conventions¶
- CI runs on every pull request and on pushes to
main/dev, so PRs must passpytest+prospector. - Specific commit-message or PR-template conventions: Unable to determine from the available code.
24. Deployment & Maintenance¶
Two deployment mechanisms exist in the repo¶
The codebase contains config for both of these (confirm with the team which is live):
- Self-hosted GitHub Actions runner (
.github/workflows/prod.yml) — deploys via docker-compose on push tomain. - AWS CodeDeploy (
appspec.yml+codedeploy/*.sh) — lifecycle-hook based deploy to EC2.
Path A — GitHub Actions self-hosted deploy (prod.yml)¶
flowchart TB
PUSH[Push to main] --> RUN[Self-hosted runner<br/>labels: prod, backend]
RUN --> ENV[Write .env from<br/>secrets.BACKEND_ENV]
ENV --> VENV[Activate venv<br/>pip install -r requirements.txt]
VENV --> MIG[python manage.py migrate]
MIG --> DC[docker compose -f docker-compose.prod.yml<br/>down && up -d]
DC --> SUP[supervisorctl restart gunicorn]
Path B — AWS CodeDeploy (appspec.yml + codedeploy/)¶
Deploys to /home/ubuntu/tombackend, running hook scripts in order:
flowchart TB
D[CodeDeploy] --> BI[BeforeInstall<br/>before_install.sh]
BI -->|stop gunicorn+celery,<br/>backup app keep last 3,<br/>ensure venv, apt deps| AI[AfterInstall<br/>after_install.sh]
AI -->|pip install, migrate,<br/>collectstatic, start services| ST[ApplicationStart<br/>start_server.sh]
ST --> SP[ApplicationStop<br/>stop_server.sh]
| Hook | Script | Does |
|---|---|---|
| BeforeInstall | before_install.sh |
Stops gunicorn/celery, backs up current app (keeps last 3), ensures venv, installs libpq-dev/build-essential |
| AfterInstall | after_install.sh |
pip install, manage.py migrate, collectstatic, starts gunicorn + celery via Supervisor, verifies they're running |
| ApplicationStart | start_server.sh |
Start marker |
| ApplicationStop | stop_server.sh |
Stop marker |
Build (buildspec.yml — AWS CodeBuild)¶
- Pulls the
.envfrom Secrets Manager keyprod/tom/backend. - Produces the deployable artifact (all files).
Runtime processes¶
| Process | Command | Managed by |
|---|---|---|
| Web (WSGI) | gunicorn tombackend.wsgi --timeout 15 --keep-alive 5 (procfile) |
Supervisor |
| Worker | celery -A tombackend worker |
Supervisor |
| Static | WhiteNoise (in-process) | — |
⚠️ Note: the
procfilereferencestomebackend.wsgi(typo — should betombackend.wsgi). Verify the live process command.
Environment / secrets¶
- Settings module chosen by the
envOS var (local/dev/staging/prod) — see §12. - Production secrets: AWS Secrets Manager (
prod/tom/backend) or the GH ActionsBACKEND_ENVsecret, written to.envat deploy. - Full env-var list: §12.
Database migrations¶
- Applied automatically on deploy (
python manage.py migrate --noinputin both paths). - Create new migrations locally with
makemigrations, review them, commit. Never edit applied migrations.
Maintenance runbook¶
| Task | How |
|---|---|
| Deploy | Push to main (Path A) or trigger CodeDeploy (Path B). |
| Roll back | Path B keeps the last 3 app backups (*_backup_<timestamp>) — restore one and restart Supervisor. |
| Restart services | sudo supervisorctl restart gunicorn / celery. |
| Check health | sudo supervisorctl status; hit /metrics; check /swagger/. |
| View logs | Container logs (Dozzle in compose) / Supervisor logs / stdout. |
| Run a management command | Activate the venv, python manage.py <command> (e.g. resend_otp_emails). |
| Rotate a secret | Update Secrets Manager / GH secret, redeploy. |
| Scheduled jobs | None configured (no Celery beat) — see §10. |
Monitoring¶
- Prometheus metrics at
/metrics(django-prometheus). - Application error tracking (Sentry): flag exists (
ALL_EXC_TO_SENTRY) but SDK wiring Unable to determine from the available code — see §12.
Appendix — Assumptions & "Unable to determine" items¶
- Python runtime: 3.8 (Dockerfile) is treated as authoritative despite Pipfile/Readme divergence. (Assumption)
- Cache backend:
CACHESis not defined in the settings modules read; production cache backend (likely Redis) is unable to be determined from the codebase. - Sentry:
ALL_EXC_TO_SENTRYenv flag exists but no Sentry init found — unable to determine whether it is wired elsewhere. - Celery Beat: no
beat_schedulepresent — no scheduled jobs exist in the codebase (only an on-demand management command). - AI mapping version set-active views (
GetCompanyJFMappingVersionsView,SetActiveCompanyJFMappingVersionView) exist but are not routed inurls.py. - Authoritative source among the three compensation-math implementations is not documented in code.
Document generated from static analysis of the repository at backend-prod/. Line-number references reflect the state of the code at analysis time and may drift as the code evolves.