Skip to content

Deal Management

Complete sales pipeline management with deal lifecycle tracking, stage-based probability, and revenue analytics for B2B and B2C sales processes.

Overview

Deal Management is an aggregate within Customer Management context that handles all sales opportunity tracking functionality. This is where sales pipeline, deal stages, and revenue forecasting are managed.

Status: Production-ready (December 2025)
Aggregate: Deal (within Customer Management context)
Endpoints: 12 HTTP routes
Database: 1 table (customer_deals) with soft delete
Tests: 24 tests (100% passing)


Key Features

Deal Lifecycle Management

Track deals through sales pipeline stages with automatic probability calculation.

Stages:

  • Lead: Initial opportunity, early contact (10% probability)
  • Qualified: Qualified prospect with budget/authority (25%)
  • Proposal: Proposal submitted to customer (50%)
  • Negotiation: Active negotiation with customer (75%)
  • Closed Won: Deal successfully closed (100%)
  • Closed Lost: Deal lost to competitor or abandoned (0%)

Stage Transitions:

                           
 Lead  >  Qualified  >  Proposal  >  Negotiation 
                           
                                                                   
                                                                   
   >
                              Skip stages allowed                  
                                                                   
   v                                                                v
                                          
 Closed Lost                                             Closed Won

Money Handling

Deal values stored as cents in database for precision, displayed as dollars in API.

Example:

json
{
  "value": 50000000,     // $500,000.00 (stored as cents)
  "currency": "USD"
}

Pipeline Analytics

Real-time statistics for sales pipeline management.

Available Statistics:

  1. Pipeline Stats: Count of deals per stage
  2. Won Deals: Total count and revenue of won deals
  3. Stage Filtering: List deals by specific stage

Domain Model

Deal Aggregate

go
type Deal struct {
    aggregate.BaseAggregate

    ID           uuidv7.UUID         // Deal ID
    CustomerID   uuidv7.UUID         // Foreign key to Customer
    AssignedTo   uuidv7.UUID         // Sales rep (foreign key to User)
    
    // Deal Info
    Name         string              // Deal name/description
    Value        valueobject.Money   // Deal value (cents)
    Stage        DealStage           // Current pipeline stage
    Probability  int                 // Win probability (0-100%)
    
    // Dates
    ExpectedCloseDate *time.Time     // Forecast close date
    ActualCloseDate   *time.Time     // Actual close date (if closed)
    
    // Metadata
    Source       DealSource          // inbound, outbound, referral, partner
    LossReason   string              // Reason for loss (if closed_lost)
    Tags         []string            // JSONB array
    
    // Audit
    CreatedAt    time.Time
    UpdatedAt    time.Time
    DeletedAt    *time.Time          // Soft delete
}

Deal Stage Enum

go
type DealStage string

const (
    DealStageLead        DealStage = "lead"         // 10% probability
    DealStageQualified   DealStage = "qualified"    // 25% probability
    DealStageProposal    DealStage = "proposal"     // 50% probability
    DealStageNegotiation DealStage = "negotiation"  // 75% probability
    DealStageClosedWon   DealStage = "closed_won"   // 100% probability
    DealStageClosedLost  DealStage = "closed_lost"  // 0% probability
)

Deal Source Enum

go
type DealSource string

const (
    DealSourceInbound  DealSource = "inbound"   // Customer initiated
    DealSourceOutbound DealSource = "outbound"  // Sales outreach
    DealSourceReferral DealSource = "referral"  // Referral partner
    DealSourcePartner  DealSource = "partner"   // Channel partner
)

Business Rules

1. Deal Creation Rules

Required Fields:

  • customer_id: Must reference existing customer
  • assigned_to: Must reference existing user (sales rep)
  • name: Deal name (max 200 chars)
  • value: Deal amount in cents (must be positive)
  • currency: ISO 4217 currency code
  • expected_close_date: Forecast close date (must be future)

Default Values:

  • stage: Always starts as lead
  • probability: Automatically set based on stage (10% for lead)
  • source: Defaults to inbound if not specified

Validation:

go
func (d *Deal) Validate() error {
    if d.CustomerID == uuid.Nil {
        return errors.New("customer_id is required")
    }
    if d.AssignedTo == uuid.Nil {
        return errors.New("assigned_to is required")
    }
    if d.Name == "" || len(d.Name) > 200 {
        return errors.New("name must be 1-200 characters")
    }
    if d.Value.Amount() <= 0 {
        return errors.New("value must be positive")
    }
    return nil
}

2. Stage Transition Rules

Valid Transitions:

  • Can move forward through pipeline: lead → qualified → proposal → negotiation
  • Can skip stages (e.g., lead → proposal directly)
  • Can move to closed_won or closed_lost from any stage
  • Cannot move from closed stages to other stages (terminal states)

Probability Auto-Update:

go
func (d *Deal) MoveToStage(newStage DealStage) error {
    // Validate not moving from terminal stage
    if d.IsTerminalStage() {
        return fmt.Errorf("cannot move from terminal stage %s", d.Stage)
    }
    
    // Update stage and probability
    d.Stage = newStage
    d.Probability = d.StageProbability()
    
    return nil
}

func (d *Deal) StageProbability() int {
    switch d.Stage {
    case DealStageLead:
        return 10
    case DealStageQualified:
        return 25
    case DealStageProposal:
        return 50
    case DealStageNegotiation:
        return 75
    case DealStageClosedWon:
        return 100
    case DealStageClosedLost:
        return 0
    default:
        return 0
    }
}

3. Win/Loss Rules

Mark as Won:

  • Sets stage to closed_won
  • Sets probability to 100%
  • Sets actual_close_date to provided date
  • Cannot win already lost deal

Mark as Lost:

  • Sets stage to closed_lost
  • Sets probability to 0%
  • Sets actual_close_date to provided date
  • Optionally records loss_reason
  • Cannot lose already won deal

API Reference

Base URL

/api/v1/customer-mgmt/deals

Endpoints

1. Create Deal

http
POST /api/v1/customer-mgmt/deals
Content-Type: application/json

{
  "customer_id": "019b72b7-0636-79b5-b8fe-28b439ec801d",
  "assigned_to": "019b72b6-e679-7635-a86d-ac29a7923f3d",
  "name": "Enterprise Software License",
  "value": 50000000,
  "currency": "USD",
  "expected_close_date": "2026-03-31"
}

Response (201 Created):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "customer_id": "019b72b7-0636-79b5-b8fe-28b439ec801d",
    "assigned_to": "019b72b6-e679-7635-a86d-ac29a7923f3d",
    "name": "Enterprise Software License",
    "value": 50000000,
    "currency": "USD",
    "stage": "lead",
    "probability": 10,
    "source": "inbound",
    "expected_close_date": "2026-03-31",
    "created_at": "2025-12-31T06:42:47+02:00",
    "updated_at": "2025-12-31T06:42:47+02:00"
  }
}

2. Get Deal by ID

http
GET /api/v1/customer-mgmt/deals/:id

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "customer_id": "019b72b7-0636-79b5-b8fe-28b439ec801d",
    "assigned_to": "019b72b6-e679-7635-a86d-ac29a7923f3d",
    "name": "Enterprise Software License",
    "value": 50000000,
    "currency": "USD",
    "stage": "lead",
    "probability": 10,
    "source": "inbound",
    "expected_close_date": "2026-03-31",
    "actual_close_date": null,
    "loss_reason": null,
    "tags": [],
    "created_at": "2025-12-31T06:42:47+02:00",
    "updated_at": "2025-12-31T06:42:47+02:00"
  }
}

3. List Deals (with Pagination)

http
GET /api/v1/customer-mgmt/deals?page=1&page_size=20

Query Parameters:

  • page: Page number (default: 1)
  • page_size: Items per page (default: 20, max: 100)

Response (200 OK):

json
{
  "status": "success",
  "data": [
    {
      "id": "019b72b7-339c-73ca-850c-8459913f2cde",
      "name": "Enterprise Software License",
      "stage": "lead",
      "value": 50000000
    }
  ],
  "pagination": {
    "total": 45,
    "page": 1,
    "page_size": 20,
    "total_pages": 3
  }
}

4. Update Deal Basic Info

http
PUT /api/v1/customer-mgmt/deals/:id/basic-info
Content-Type: application/json

{
  "name": "Enterprise License + Support",
  "expected_close_date": "2026-04-30"
}

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "name": "Enterprise License + Support",
    "expected_close_date": "2026-04-30",
    "updated_at": "2025-12-31T06:45:00+02:00"
  }
}

5. Update Deal Value

http
PUT /api/v1/customer-mgmt/deals/:id/value
Content-Type: application/json

{
  "value": 75000000,
  "currency": "USD"
}

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "value": 75000000,
    "currency": "USD",
    "updated_at": "2025-12-31T06:46:00+02:00"
  }
}

6. Move Deal to Stage

http
PUT /api/v1/customer-mgmt/deals/:id/stage
Content-Type: application/json

{
  "stage": "qualified"
}

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "stage": "qualified",
    "probability": 25,
    "updated_at": "2025-12-31T06:47:00+02:00"
  }
}

Error Response (400 Bad Request - terminal stage):

json
{
  "status": "error",
  "error": {
    "code": "MOVE_STAGE_FAILED",
    "message": "cannot move from terminal stage closed_won"
  }
}

7. Mark Deal as Won

http
POST /api/v1/customer-mgmt/deals/:id/win
Content-Type: application/json

{
  "close_date": "2025-12-31"
}

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "stage": "closed_won",
    "probability": 100,
    "actual_close_date": "2025-12-31",
    "updated_at": "2025-12-31T06:48:00+02:00"
  }
}

8. Mark Deal as Lost

http
POST /api/v1/customer-mgmt/deals/:id/lose
Content-Type: application/json

{
  "close_date": "2025-12-31",
  "reason": "Lost to competitor - pricing"
}

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "id": "019b72b7-339c-73ca-850c-8459913f2cde",
    "stage": "closed_lost",
    "probability": 0,
    "actual_close_date": "2025-12-31",
    "loss_reason": "Lost to competitor - pricing",
    "updated_at": "2025-12-31T06:49:00+02:00"
  }
}

9. Delete Deal (Soft Delete)

http
DELETE /api/v1/customer-mgmt/deals/:id

Response (204 No Content)

10. List Deals by Stage

http
GET /api/v1/customer-mgmt/deals/stage/:stage

Example:

http
GET /api/v1/customer-mgmt/deals/stage/lead

Response (200 OK):

json
{
  "status": "success",
  "data": [
    {
      "id": "019b72b7-339c-73ca-850c-8459913f2cde",
      "name": "Enterprise Software License",
      "value": 50000000,
      "stage": "lead"
    }
  ]
}

11. Get Pipeline Statistics

http
GET /api/v1/customer-mgmt/deals/stats/pipeline

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "stats": {
      "lead": 12,
      "qualified": 8,
      "proposal": 5,
      "negotiation": 3,
      "closed_won": 45,
      "closed_lost": 23
    }
  }
}

12. Get Won Deals Statistics

http
GET /api/v1/customer-mgmt/deals/stats/won

Response (200 OK):

json
{
  "status": "success",
  "data": {
    "count": 45,
    "total_value": 2250000000
  }
}

Note: total_value is in cents ($22,500,000.00 in this example)


Usage Examples

Example 1: Create and Close Deal

bash
# 1. Create deal
curl -X POST http://localhost:8081/api/v1/customer-mgmt/deals \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "019b72b7-0636-79b5-b8fe-28b439ec801d",
    "assigned_to": "019b72b6-e679-7635-a86d-ac29a7923f3d",
    "name": "Enterprise License",
    "value": 50000000,
    "currency": "USD",
    "expected_close_date": "2026-03-31"
  }'

# Response: deal_id = 019b72b7-339c-73ca-850c-8459913f2cde

# 2. Move through stages
curl -X PUT http://localhost:8081/api/v1/customer-mgmt/deals/019b72b7-339c-73ca-850c-8459913f2cde/stage \
  -H "Content-Type: application/json" \
  -d '{"stage": "qualified"}'

curl -X PUT http://localhost:8081/api/v1/customer-mgmt/deals/019b72b7-339c-73ca-850c-8459913f2cde/stage \
  -H "Content-Type: application/json" \
  -d '{"stage": "proposal"}'

curl -X PUT http://localhost:8081/api/v1/customer-mgmt/deals/019b72b7-339c-73ca-850c-8459913f2cde/stage \
  -H "Content-Type: application/json" \
  -d '{"stage": "negotiation"}'

# 3. Close as won
curl -X POST http://localhost:8081/api/v1/customer-mgmt/deals/019b72b7-339c-73ca-850c-8459913f2cde/win \
  -H "Content-Type: application/json" \
  -d '{"close_date": "2025-12-31"}'

Result: Deal progressed through pipeline and closed as won with 100% probability.

Example 2: Update Deal Value During Negotiation

bash
# Deal currently at negotiation stage
DEAL_ID="019b72b7-339c-73ca-850c-8459913f2cde"

# Customer negotiated higher price
curl -X PUT http://localhost:8081/api/v1/customer-mgmt/deals/$DEAL_ID/value \
  -H "Content-Type: application/json" \
  -d '{
    "value": 60000000,
    "currency": "USD"
  }'

# Update expected close date
curl -X PUT http://localhost:8081/api/v1/customer-mgmt/deals/$DEAL_ID/basic-info \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise License + Premium Support",
    "expected_close_date": "2026-02-28"
  }'

Example 3: Track Lost Deal

bash
DEAL_ID="019b72b8-1234-5678-abcd-123456789abc"

# Mark as lost with reason
curl -X POST http://localhost:8081/api/v1/customer-mgmt/deals/$DEAL_ID/lose \
  -H "Content-Type: application/json" \
  -d '{
    "close_date": "2025-12-31",
    "reason": "Customer chose competitor due to pricing"
  }'

# Get pipeline stats to see impact
curl http://localhost:8081/api/v1/customer-mgmt/deals/stats/pipeline

Example 4: Sales Pipeline Dashboard

bash
# Get all pipeline statistics
curl http://localhost:8081/api/v1/customer-mgmt/deals/stats/pipeline | jq .

# Get deals in proposal stage
curl http://localhost:8081/api/v1/customer-mgmt/deals/stage/proposal | jq .

# Get won deals revenue
curl http://localhost:8081/api/v1/customer-mgmt/deals/stats/won | jq .

Example Output:

json
{
  "status": "success",
  "data": {
    "stats": {
      "lead": 12,
      "qualified": 8,
      "proposal": 5,
      "negotiation": 3,
      "closed_won": 45,
      "closed_lost": 23
    }
  }
}

Database Schema

Table: customer_deals

sql
CREATE TABLE IF NOT EXISTS customer_deals (
    -- Primary Key
    id UUID PRIMARY KEY DEFAULT uuid_v7(),
    
    -- Foreign Keys
    customer_id UUID NOT NULL REFERENCES customer_mgmt_customers(id),
    assigned_to UUID NOT NULL REFERENCES identity_users(id),
    
    -- Deal Information
    name VARCHAR(200) NOT NULL,
    value BIGINT NOT NULL CHECK (value >= 0),  -- Cents
    currency VARCHAR(3) NOT NULL DEFAULT 'USD',
    
    -- Pipeline Stage
    stage VARCHAR(20) NOT NULL DEFAULT 'lead' 
        CHECK (stage IN ('lead', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost')),
    probability INTEGER NOT NULL DEFAULT 10 CHECK (probability >= 0 AND probability <= 100),
    
    -- Deal Source
    source VARCHAR(20) NOT NULL DEFAULT 'inbound'
        CHECK (source IN ('inbound', 'outbound', 'referral', 'partner')),
    
    -- Dates
    expected_close_date DATE,
    actual_close_date DATE,
    
    -- Metadata
    loss_reason TEXT,
    tags JSONB DEFAULT '[]'::jsonb,
    
    -- Audit Fields
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP,
    
    -- Indexes
    CONSTRAINT fk_deal_customer FOREIGN KEY (customer_id) REFERENCES customer_mgmt_customers(id),
    CONSTRAINT fk_deal_assigned_to FOREIGN KEY (assigned_to) REFERENCES identity_users(id)
);

-- Indexes for performance
CREATE INDEX idx_deals_customer_id ON customer_deals(customer_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_deals_assigned_to ON customer_deals(assigned_to) WHERE deleted_at IS NULL;
CREATE INDEX idx_deals_stage ON customer_deals(stage) WHERE deleted_at IS NULL;
CREATE INDEX idx_deals_expected_close ON customer_deals(expected_close_date) WHERE deleted_at IS NULL;
CREATE INDEX idx_deals_actual_close ON customer_deals(actual_close_date) WHERE deleted_at IS NULL;
CREATE INDEX idx_deals_tags ON customer_deals USING GIN (tags) WHERE deleted_at IS NULL;

-- Trigger for updated_at
CREATE TRIGGER trg_deals_updated_at
    BEFORE UPDATE ON customer_deals
    FOR EACH ROW
    EXECUTE FUNCTION tfn_entity_updated_at();

Indexes Strategy

Performance Optimizations:

  1. Foreign Keys: Fast lookup by customer and sales rep
  2. Stage Index: Quick filtering by pipeline stage
  3. Date Indexes: Fast date range queries for forecasting
  4. GIN Index: Efficient JSONB tag searches
  5. Partial Indexes: Exclude soft-deleted records for better performance

Testing

Test Coverage

24 Tests (100% passing)

Entity Tests (entity_test.go):

  • Create deal with valid data
  • Validate required fields (customer_id, assigned_to, name, value)
  • Stage transition validation
  • Probability auto-calculation per stage
  • Win/Loss state transitions
  • Terminal stage protection

Use Case Tests (usecase_test.go):

  • Create deal business logic
  • Update basic info
  • Update value
  • Move to stage (with validation)
  • Mark as won/lost
  • List with pagination
  • Filter by stage
  • Pipeline statistics
  • Won deals statistics

Repository Tests (repository_test.go):

  • Create and retrieve
  • Update operations
  • Soft delete
  • List with filters
  • Statistics queries

Running Tests

bash
# All Deal tests
go test ./internal/contexts/customer-mgmt/deal/... -v

# Entity tests only
go test ./internal/contexts/customer-mgmt/deal -run TestDeal -v

# Use case tests
go test ./internal/contexts/customer-mgmt/deal -run TestUseCase -v

# Integration tests
go test ./test/integration/contexts/customer-mgmt/deal/... -v

Test Examples

Entity Test - Stage Transition:

go
func TestDeal_MoveToStage(t *testing.T) {
    customerID := uuidv7.New()
    assignedTo := uuidv7.New()
    deal, _ := NewDeal(customerID, assignedTo, "Test Deal", 1000000, "USD", time.Now().AddDate(0, 1, 0))

    // Move from lead to qualified
    err := deal.MoveToStage(DealStageQualified)
    assert.NoError(t, err)
    assert.Equal(t, DealStageQualified, deal.Stage)
    assert.Equal(t, 25, deal.Probability)

    // Close as won
    err = deal.MarkWon(time.Now())
    assert.NoError(t, err)
    assert.Equal(t, DealStageClosedWon, deal.Stage)
    assert.Equal(t, 100, deal.Probability)

    // Cannot move from closed stage
    err = deal.MoveToStage(DealStageNegotiation)
    assert.Error(t, err)
}

Integration with Other Contexts

Customer Management Context

Deal is part of Customer Management context, integrated with Customer aggregate.

Relationships:

  • Deal → Customer: Many-to-one (one customer can have multiple deals)
  • Deal → User (Sales Rep): Many-to-one (one sales rep manages multiple deals)

Event Publishing:

go
// On deal creation
eventBus.Publish(ctx, bus.TopicDealCreated, bus.NewEvent(
    "customer.deal.created",
    deal.ID,
    map[string]interface{}{
        "customer_id": deal.CustomerID,
        "assigned_to": deal.AssignedTo,
        "value":       deal.Value.Amount(),
    },
))

// On deal won
eventBus.Publish(ctx, bus.TopicDealWon, bus.NewEvent(
    "customer.deal.won",
    deal.ID,
    map[string]interface{}{
        "customer_id": deal.CustomerID,
        "value":       deal.Value.Amount(),
        "close_date":  deal.ActualCloseDate,
    },
))

Identity Context

Deal references User aggregate via assigned_to for sales rep assignment.

Order Management Context (Future)

When deal is won, can trigger order creation:

go
// Subscribe to deal won events
eventBus.Subscribe(bus.TopicDealWon, func(ctx context.Context, event bus.Event) error {
    // Create order from deal
    order := NewOrderFromDeal(deal)
    return orderRepo.Create(ctx, order)
})

Best Practices

1. Deal Naming Convention

Use descriptive names that identify the opportunity:

 Good: "Enterprise License + Support", "Cloud Migration Project"
 Bad: "Deal 1", "Customer ABC"

2. Stage Progression

Progress deals through stages sequentially when possible:

 Recommended: lead → qualified → proposal → negotiation → closed_won
  Allowed: lead → proposal (skip stages)
 Avoid: closed_won → negotiation (cannot move from terminal)

3. Value Updates

Update deal value as negotiation progresses:

go
// Initial deal
value: 50000000 // $500k

// After negotiation
value: 60000000 // $600k

4. Loss Tracking

Always provide reason when marking deal as lost:

json
{
  "close_date": "2025-12-31",
  "reason": "Lost to competitor due to pricing"
}

5. Expected Close Date

Keep expected_close_date current:

go
// Update as deal progresses
PUT /deals/:id/basic-info
{
  "expected_close_date": "2026-02-28"  // Moved out 1 month
}

Common Patterns

Pattern 1: Quick Win

Deal closed immediately without going through all stages:

bash
# Create deal
curl -X POST /api/v1/customer-mgmt/deals -d '{...}'

# Close as won immediately
curl -X POST /api/v1/customer-mgmt/deals/:id/win -d '{"close_date": "2025-12-31"}'

Pattern 2: Full Pipeline

Deal progresses through all stages:

bash
# lead → qualified → proposal → negotiation → closed_won
for stage in qualified proposal negotiation; do
  curl -X PUT /api/v1/customer-mgmt/deals/:id/stage -d "{\"stage\": \"$stage\"}"
done
curl -X POST /api/v1/customer-mgmt/deals/:id/win -d '{"close_date": "2025-12-31"}'

Pattern 3: Price Negotiation

Update value during negotiation:

bash
# Increase value after negotiation
curl -X PUT /api/v1/customer-mgmt/deals/:id/value \
  -d '{"value": 75000000, "currency": "USD"}'

Roadmap

Current Features ( Implemented)

  • Deal CRUD operations
  • Stage-based pipeline management
  • Automatic probability calculation
  • Win/Loss tracking
  • Pipeline statistics
  • Money handling (cents precision)
  • Soft delete support
  • 24 tests (100% passing)

Planned Features ( Q1 2026)

  • [ ] Deal activities timeline (calls, emails, meetings)
  • [ ] Deal notes and attachments
  • [ ] Deal collaborators (multiple sales reps)
  • [ ] Custom deal stages per organization
  • [ ] Revenue forecasting by period
  • [ ] Deal conversion analytics
  • [ ] Integration with email/calendar

Future Enhancements ( Q2 2026)

  • [ ] Deal templates for common scenarios
  • [ ] Automated lead scoring
  • [ ] AI-powered close date prediction
  • [ ] Deal health indicators
  • [ ] Competitor tracking


Version: 1.0.0
Status: Production-ready
Last Updated: December 31, 2025
Maintainer: Promenade Team

Built with Domain-Driven Design and Go