PPact
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.

These suggestions are not stored and are not the battlecards themselves. The battlecards live on the Sales Engineering workspace's Battlecards page (/se/battlecards, API /v1/se/battlecards), and these win/loss themes are one of their inputs:

  • Generate a card for a named competitor. Pact assembles it from consent-cleared wins in the win vault and from these win/loss themes.
  • Version history. Every regeneration is a new version with a structured delta, so you can see what changed and why.
  • Publish to Slack. Before publishing, Pact re-checks the consent of every cited quote. If a citation's consent was revoked, the publish is blocked rather than quietly dropping the quote.

Generated, not written

Battlecards are assembled deterministically from your data. There is no LLM talk track and no free-text editor. To change a card, change the evidence behind it and regenerate.