Flask App Architecture
The Flask app keeps HTTP concerns at the edge and integration/business logic in plain Python modules.
Request Boundary
app/blueprints/owns Flask route functions, request parsing, decorators, and conversion toflask.Response.app/http.pycontains response adapters. Useresponse_from_result()to turn anEndpointResultinto a Flask response.app/error_handlers.pyassigns a request id to every request, echoes it inX-Request-ID, and converts server-side failures into JSON error responses.app/route_support.pyandapp/helpers/inbound_auth.pyremain Flask-aware because they read request headers and can reject requests before a view runs.
Route Groups
Routes are registered in app/routes.py through Flask blueprints.
core_bpis always registered and owns lightweight operational routes such as health checks.public_root_bpis registered only when internal routes are disabled, so the public app still has a root response.report_exports_bp,crm_reports_bp, anddelivery_model_reports_bpown CSV and report-like endpoints used by Power BI or operators. Report endpoints use Basic auth, with the Delivery Model alert email preview as the current unauthenticated preview exception.project_webhooks_bpowns external webhook entrypoints from Zoho CRM/Flow. These routes are token-protected and remain available even when internal routes are disabled.delivery_model_automations_bp,projects_proxy_bp, andinternal_debug_bpare internal-only groups. They are registered only whenEXPOSE_INTERNAL_ROUTESis truthy.
Do not add new routes directly in server.py or app/__init__.py; put them in
the nearest blueprint and let register_routes() wire the group into the app.
Config And Environment
Runtime configuration is centralized in app/settings.py.
Settings.from_env()loads.envwhen no explicit environment mapping is provided, trims string values, parses comma-separated email recipient lists, and parses boolean-styleEXPOSE_INTERNAL_ROUTES.get_settings()caches the settings object for normal runtime use.reset_settings_cache()exists for tests or tools that intentionally change environment variables mid-process.- Settings are validated by feature-specific methods at first use, not at app startup. This lets tests and narrow scripts exercise one feature without configuring every integration.
Required environment groups:
- Zoho OAuth:
CLIENT_ID,CLIENT_SECRET,REFRESH_TOKEN,ACCOUNTS_BASE. - Zoho CRM:
API_DOMAIN. - Zoho Projects:
PROJECTS_API_DOMAIN,PROJECTS_PORTAL_ID. - Report Basic auth:
EXCEL_BASIC_USER,EXCEL_BASIC_PASS. - Inbound token auth:
INBOUND_API_TOKEN.
Optional or defaulted values:
ACCOUNTS_BASEdefaults tohttps://accounts.zoho.com.au.PROJECTS_PORTAL_LINK_BASEdefaults to the Peppermint Projects portal link.ALERT_RECIPIENT_EMAILSis optional and parsed as a comma-separated list.DEBUG_RECIPIENT_EMAILSis optional and parsed as a comma-separated list. It is for standalone debug notifications sent through Zoho Mail, not CRM record emails.EXPOSE_INTERNAL_ROUTESdefaults to false; accepted truthy values are1,true,yes, andon.
Auth Model
- Report routes use
basic_auth_required()fromapp/route_support.py. Credentials come fromEXCEL_BASIC_USERandEXCEL_BASIC_PASS, and failed requests receive a Basic auth challenge before report builders run. - Webhooks and automation/proxy routes use
token_protected()fromapp/helpers/inbound_auth.py. Callers can send eitherAuthorization: Bearer <token>orX-Pepperback-Token. - Missing auth configuration is treated as a server error. Missing or invalid
caller credentials are rejected with
401. - Internal-only routes are not merely auth-protected; they are not registered at
all unless
EXPOSE_INTERNAL_ROUTESis enabled.
Error And Logging Contract
- Every response receives
X-Request-ID. If the caller sendsX-Request-ID, the app preserves that value; otherwise the app generates one. - Server-side error responses include
error,code, andrequest_id. Zoho upstream failures also includedetailswith method, URL, status, and body. - Use
app.logging.log_event()for application logs. It emits one JSON object per log line and includesrequest_idautomatically when a request context exists. - Use
infofor lifecycle summaries,warningfor recoverable partial data, retries, skipped records, and cache/recipient lookup issues, anderrorfor failed writes or failed upstream calls. - A broad
except Exceptionmust either re-raise or calllog_event(..., exc_info=True, ...)before returning an explicit error payload. Background jobs must log stack traces because no HTTP caller may see their failures. - Do not use
print()orSystemExitinapp/; tests enforce this so errors are visible in structured logs and request error responses.
Service Results
- Service modules return
app.results.EndpointResultfor endpoint-like outputs. - Use
json_result,csv_result,html_result, orbytes_resultinstead of constructingflask.Responsein service code. - Service functions should use
*_resultnames when their output is intended for a route adapter.
Naming Contract
- A leading underscore means module-local only.
- Helpers shared across modules must use public names and be imported explicitly from their owning module.
- Do not import or alias a leading-underscore name from another
appmodule. Tests enforce this for application code.
Integration Clients
app/zoho/clients own Zoho URLs, auth headers, request execution, and raw upstream response handling.ZohoBaseClientowns OAuth header construction, request timeouts, URL normalization, and the split between rawrequest_response()calls and parsedrequest_json()calls.- Parsed client methods raise
ZohoAPIErrorfor upstream 4xx/5xx responses. Methods ending in_responsereturn the raw upstream response for proxy routes and services that must preserve status-code/body behavior. ZohoCRMClientowns CRM module pagination, record fetch/update/create methods, and Delivery Model mail actions.ZohoMailClientowns standalone debug email sends that must not be attached to CRM records.ZohoProjectsClientowns Projects portal and REST API URL shapes, project task/tasklist/phase operations, timelog collection, team lookups, and selected fallback payload encodings required by Zoho Projects.- Blueprints may still proxy selected upstream Zoho responses directly with
proxy_json_response()when the endpoint is intentionally a pass-through. - Business modules should depend on client methods or injected callables, not on Flask request/response objects.
Report Flow
Report endpoints should follow this shape:
- Blueprint route handles HTTP method/path and applies Basic auth.
- Route calls a builder/service function, injecting collectors where tests need isolation.
- Service code fetches CRM or Projects data through Zoho clients or collection helpers.
- Service returns an
EndpointResultviacsv_result,json_result,html_result, orbytes_result. - Route converts the result with
response_from_result().
Keep report builders deterministic where possible: normalize records into explicit schemas, keep CSV column order stable, and cover schema changes with characterization tests.
Webhook Flow
Webhook and automation endpoints should follow this shape:
- Blueprint route applies inbound token auth.
- Route reads a JSON object with
json_object_payload()and returns400for non-object payloads or missing required identifiers. - Route delegates to an automation or service module.
- Service code performs Zoho reads/writes through client methods or injected callables.
- Route returns either
json_response()for direct payload/status tuples orresponse_from_result()forEndpointResultoutputs.
Webhook services should return explicit partial-failure payloads when a workflow can continue after a failed sub-step, and should log unexpected failures through the structured logging contract.
Service Module Map
app/reporting_portal.pyowns Zoho Projects portal collection, report pagination, short-lived cache state, and task metadata mapping.app/reporting_metadata.pyowns shared CRM/Projects metadata joins used by report exports and Delivery Model reports.app/crm_reports/owns CRM report fields, normalizers, MSP billing calculations, and result builders.app/crm_reporting.pyis a compatibility facade.app/delivery_models/alert_email.pyowns Delivery Model alert email rendering and test email payloads.app/delivery_models/alert_recipients.pyowns alert recipient lookup and email deduplication.app/delivery_models/timelog_mapping.pyowns Delivery Model timelog scope matching and CSV column shape;timelog_monthly.pyowns recurring monthly aggregation.timelogs.pyis a compatibility facade.app/automations/delivery_model_completion_calculations.pyowns completion percentages, threshold crossing, and recurring current-month calculations.delivery_model_completion.pyowns state-sync orchestration.app/delivery_models/archived_backfill_cache.pyowns archived timelog backfill cache, status, lock, and background generation behavior.archived_backfill_restapi.pyowns archived Projects REST API collection and payload normalization.archived_backfill.pyis the endpoint facade and report orchestration layer.
Facade modules preserve older imports for routes and tests. New code should prefer importing from the focused modules directly unless it is intentionally working at an endpoint orchestration boundary.
Test Strategy
- Route-map tests preserve the public and internal route contracts, including
EXPOSE_INTERNAL_ROUTESbehavior. - Auth boundary tests verify Basic auth and inbound token rejection before builders or automations run.
- Settings tests cover defaults, feature-scoped validation, list parsing, and cache reset behavior.
- Zoho client tests use fake transports/responses to validate URL construction,
headers, parsed response handling, and
ZohoAPIErrordetails without making network calls. - Report characterization tests assert CSV schemas, lookup flattening, billing calculations, and route registration/auth for report endpoints.
- Webhook and automation tests exercise payload validation, delegation, idempotent setup behavior, and partial failure payloads.
- Static enforcement tests keep app code from hiding failures with
print()orSystemExit, and prevent cross-module imports or aliases of private underscore-prefixed names.