PPactDocs
Sales

Win/loss analysis

Generate hierarchy-scoped win/loss reports from recorded deal outcomes, extract recurring themes and competitors, and surface battlecard suggestions.

Win/loss analysis

Win/loss turns per-deal outcome capture into narrative reports a sales leader actually reads — recurring themes, competitor mentions, buyer decision criteria, and the pipeline stage where lost deals dropped off. Reports are scoped to the caller's management hierarchy so a rep never sees a peer's deals through the report. The API is under /v1/win-loss and is gated by the analytics module.

Live

Backed by the win_loss_reports and win_loss_report_deals tables plus the per-deal win_loss_records capture. Generation, theme extraction, and hierarchy scoping are all real code (core.win_loss).

What a report is

POST /v1/win-loss/generate (admin/owner only) pulls a bounded set of deal outcomes and produces one win_loss_reports row. Inputs:

  • period_start / period_end — bounded against win_loss_records.recorded_at.
  • segment_filter — an optional dict supporting segment (substring match on notes), owner_emails, and min_value / max_value bounds on deal value.

Generation is idempotent: the unique index over (tenant_id, period_start, period_end, segment_filter) means re-running the same triple updates the existing row in place rather than spawning a duplicate. Each re-run recomputes the theme block from scratch, since the underlying deal set can grow between runs.

Theme extraction

core.win_loss.theme_extractor produces the four blocks stamped onto win_loss_reports.ai_themes_json:

  • themes — recurring short noun phrases across deals
  • competitors — competitor mentions with frequency and share
  • decision_criteria — buyer-stated criteria recurring in notes
  • drop_off_stages — for lost deals, the stage where the deal was lost

Extraction has two paths. When the tenant has an AI provider configured, it routes through the tenant-scoped AIClient (credential + ledger + budget + kill-switch + PII redaction) with a strict-JSON prompt. When no key is present — CI, dev, or a tenant that hasn't connected a provider — it falls through to a deterministic keyword-bucketing heuristic tagged heuristic-v1, so the detail page always renders something concrete.

Hierarchy scoping

Report contents are constrained by core.visibility.hierarchy — specifically owner_email_filter_clause. The report header is a tenant-scoped artifact, but the deals inside it are filtered to the viewer's subtree. Default- deny applies: a viewer with no subordinates and no admin role sees a report header only when they personally recorded at least one of its deals. The same filter is applied on both the list and the drill-down, so a rep can't reach peer deals through GET /v1/win-loss/reports/{id}.

Endpoints

Method & pathPurpose
GET /v1/win-lossPer-deal feed for the caller
POST /v1/win-loss/generateGenerate a report (admin/owner)
GET /v1/win-loss/reportsPaginated report list, hierarchy-filtered
GET /v1/win-loss/reports/{id}Full detail with scoped deal drill-down

Battlecards

When a competitor recurs across reports, core.win_loss.battlecards.suggest_battlecards aggregates the competitor and decision-criteria blocks and emits deterministic suggestion records — "Competitor X appears in 5 of the last 8 reports, price cited each time." This is the statistical signal to go build a battlecard, not a generated talk track; suggestions are sorted by descending total mentions and filtered by minimum-mention/minimum-report thresholds so a single stray mention doesn't create noise.

Battlecard authoring is a signal, not a full editor

The current surface emits suggestions and thresholds them. A full battlecard authoring tool with LLM-enriched talk tracks is planned; the suggestion shape on disk is stable so that enrichment can land without a migration.