Skip to content

Latest commit

 

History

History
809 lines (655 loc) · 22.5 KB

File metadata and controls

809 lines (655 loc) · 22.5 KB

Integration Guide

🎯 Approach: Mock Data First, Real Integration Later

For the 90-minute prototype sprint, use mock data to simulate real API responses. This lets you:

  • ✅ Build & test frontend components without API delays
  • ✅ Avoid rate limiting issues
  • ✅ Have complete control over test data
  • ✅ Demo with realistic, consistent data
  • ⏰ Save 30+ minutes vs. real integration

Real API integration is post-sprint work.


Mock Data Strategy

1. Create Mock Services (src/services/mock/)

// src/services/mock/jira-mock.ts
import { JiraItem } from '@/types/jira';

export const mockJiraItems: JiraItem[] = [
  {
    id: 'jira-1',
    issueKey: 'DAIS-123',
    summary: 'API authentication not working in staging',
    priority: 'BLOCKER',
    projectKey: 'DAIS',
    projectName: 'Data Integration Service',
    currentStatus: 'In Progress',
    previousStatus: 'Open',
    statusChangedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
    changeType: 'STATUS_CHANGE',
    changedBy: { id: 'user-1', name: 'Alex Chen', avatarUrl: '...' },
    relevanceReason: 'ASSIGNEE',
    url: 'https://jira.company.com/browse/DAIS-123',
    updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
    blocks: ['DAIS-456', 'DAIS-789']
  },
  {
    id: 'jira-2',
    issueKey: 'DAIS-987',
    summary: 'Database migration rollback needed',
    priority: 'P1',
    projectKey: 'DAIS',
    projectName: 'Data Integration Service',
    currentStatus: 'Resolved',
    previousStatus: 'In Review',
    statusChangedAt: new Date(Date.now() - 4 * 60 * 60 * 1000),
    changeType: 'RESOLVED',
    changedBy: { id: 'user-2', name: 'Charlie Wong', avatarUrl: '...' },
    relevanceReason: 'WATCHER',
    url: 'https://jira.company.com/browse/DAIS-987',
    updatedAt: new Date(Date.now() - 4 * 60 * 60 * 1000)
  },
  // ... more items
];

export const mockJiraService = {
  getIssuesForUser: async (userId: string) => {
    // Simulate network delay
    await new Promise(resolve => setTimeout(resolve, 200));
    return mockJiraItems;
  }
};

2. Create Mock Outlook Service

// src/services/mock/outlook-mock.ts
import { EmailItem, CalendarItem } from '@/types/outlook';

export const mockEmails: EmailItem[] = [
  {
    id: 'email-1',
    messageId: '<msg@company.com>',
    subject: 'ACTION REQUIRED: Urgent approval needed for Q3 roadmap',
    sender: {
      name: 'Jennifer Wong',
      email: 'jennifer.wong@company.com',
      avatarUrl: '...'
    },
    body: 'We need your approval on the Q3 roadmap update. Can you review and sign off by EOD today?',
    isRead: false,
    isFlagged: true,
    receivedAt: new Date(Date.now() - 1.5 * 60 * 60 * 1000),
    sentAt: new Date(Date.now() - 1.5 * 60 * 60 * 1000),
    category: 'CRITICAL',
    senderPriority: 'LEADERSHIP',
    responseStatus: 'AWAITING_RESPONSE',
    isAutoGenerated: false,
    hasAttachments: true,
    url: 'https://outlook.office.com/mail/inbox/id/123'
  },
  // ... more emails
];

export const mockCalendarItems: CalendarItem[] = [
  {
    id: 'event-1',
    title: 'Architecture Review - Q3 Planning',
    startTime: new Date(Date.now() + 1 * 60 * 60 * 1000), // 1 hour from now
    endTime: new Date(Date.now() + 2 * 60 * 60 * 1000),
    duration: 60,
    isAllDay: false,
    conflictType: 'UNACCEPTED',
    userResponseStatus: 'TENTATIVE',
    organizer: { name: 'Michael Johnson', email: 'michael.johnson@company.com' },
    attendeeCount: 12,
    requiredAttendees: 8,
    calendarName: 'Team Calendar',
    isTeamsEvent: true,
    teamsMeetingUrl: 'https://teams.microsoft.com/l/meetup-join/...',
    url: 'https://outlook.office.com/calendar/item/123',
    updatedAt: new Date()
  },
  // ... more events
];

export const mockOutlookService = {
  getEmails: async () => {
    await new Promise(resolve => setTimeout(resolve, 150));
    return mockEmails;
  },
  getCalendarEvents: async () => {
    await new Promise(resolve => setTimeout(resolve, 150));
    return mockCalendarItems;
  }
};

3. Use Mock Services in Backend

// src/services/aggregation-service.ts
import { mockJiraService } from '@/services/mock/jira-mock';
import { mockOutlookService } from '@/services/mock/outlook-mock';

class AggregationService {
  async generateOvernightSummary(userId: string, date?: Date): Promise<OvernightSummary> {
    const user = { id: userId, timezone: 'America/New_York', businessHoursStart: 8, businessHoursEnd: 18 };
    const { start, end } = this.getOvernightWindow(user, date);
    
    // Use mock services
    const [jiraItems, emailItems, calendarItems] = await Promise.all([
      mockJiraService.getIssuesForUser(userId),     // Mock data
      mockOutlookService.getEmails(),                // Mock data
      mockOutlookService.getCalendarEvents()         // Mock data
    ]);
    
    // Rest of aggregation logic works the same
    return {
      // ... build summary from mock data
    };
  }
}

4. Create Realistic Mock Data Variants

// src/services/mock/mock-utils.ts
export const createMockOverview = (scenario: 'busy' | 'normal' | 'quiet') => {
  switch (scenario) {
    case 'busy':
      // 5+ Jira items, 3+ emails, 2+ conflicts
      return { jiraItems: [...], emailItems: [...], calendarItems: [...] };
    case 'normal':
      // 2-3 Jira items, 1-2 emails, 0-1 conflicts
      return { jiraItems: [...], emailItems: [...], calendarItems: [...] };
    case 'quiet':
      // 0-1 Jira items, 0 emails, 0 conflicts
      return { jiraItems: [], emailItems: [], calendarItems: [] };
  }
};

// Allows frontend to test different states without changing code

Quick Start: Data Source Setup (Mock)

Prerequisites

  • Node.js 18+
  • PostgreSQL 13+
  • Git
  • No API credentials needed!

1. Jira Cloud Integration (MOCK)

Setup Steps (Optional — Save for Post-Sprint)

This is fully optional during sprint. Build with mock data first.

1.1 Create OAuth App in Jira (POST-SPRINT)

  1. Login to Jira Cloud (https://dev.atlassian.com/)

  2. Go to Apps → OAuth 2.0 integrations

  3. Create new app with:

    • App name: Morning Command Center
    • Redirect URL: http://localhost:3000/api/auth/callback/jira
    • Scopes:
      • read:jira-work
      • read:jira-user
  4. Save Client ID and Secret

1.2 Environment Variables

# .env.local
JIRA_CLIENT_ID=xxxxx
JIRA_CLIENT_SECRET=xxxxx
JIRA_INSTANCE_URL=https://company.atlassian.net

1.3 API Client Implementation

// src/services/jira-service.ts
import { JiraClient } from '@jira/cloud-sdk';

class JiraService {
  private client: JiraClient;
  
  constructor(accessToken: string) {
    this.client = new JiraClient({
      host: process.env.JIRA_INSTANCE_URL,
      authentication: {
        bearer: accessToken
      }
    });
  }
  
  async getIssuesForUser(userId: string, options?: { days: number }): Promise<JiraItem[]> {
    const jql = `
      assignee = ${userId} 
      OR reporter = ${userId} 
      OR watchers in (${userId})
      OR text ~ "${userId}"
    `;
    
    const results = await this.client.issueSearch.searchForIssuesUsingJQL({
      jql,
      maxResults: 50,
      fields: [
        'key', 'summary', 'priority', 'status', 'updated', 'created',
        'assignee', 'reporter', 'labels', 'issuelinks'
      ]
    });
    
    return results.issues.map(issue => ({
      id: issue.id,
      issueKey: issue.key,
      summary: issue.fields.summary,
      priority: this.mapPriority(issue.fields.priority),
      // ... map other fields
    }));
  }
  
  private mapPriority(priority: any): 'BLOCKER' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5' {
    const nameMap = {
      'Blocker': 'BLOCKER',
      'Highest': 'P1',
      'High': 'P2',
      'Medium': 'P3',
      'Low': 'P4',
      'Lowest': 'P5'
    };
    return nameMap[priority.name] || 'P3';
  }
}

export default JiraService;

1.4 Testing

npm test -- src/services/jira-service.test.ts

2. Outlook / Microsoft 365 Integration

Setup Steps

2.1 Register Application in Azure AD

  1. Go to Azure Portal (https://portal.azure.com/)

  2. Azure AD → App registrations → New registration

  3. Name: Morning Command Center

  4. Redirect URI: http://localhost:3000/api/auth/callback/outlook

  5. In Certificates & secrets → New client secret

  6. In API permissions, add:

    • Mail.Read (Delegated)
    • Calendars.Read (Delegated)
    • User.Read (Delegated)
  7. Save Application ID and Client Secret

2.2 Environment Variables

# .env.local
OUTLOOK_CLIENT_ID=xxxxx
OUTLOOK_CLIENT_SECRET=xxxxx
OUTLOOK_TENANT_ID=xxxxx
OUTLOOK_REDIRECT_URI=http://localhost:3000/api/auth/callback/outlook

2.3 API Client Implementation

// src/services/outlook-service.ts
import { Client } from '@microsoft/microsoft-graph-client';
import 'isomorphic-fetch';

class OutlookService {
  private graphClient: Client;
  
  constructor(accessToken: string) {
    this.graphClient = Client.init({
      authProvider: (done) => {
        done(null, accessToken);
      }
    });
  }
  
  async getEmails(options?: { days: number }): Promise<EmailItem[]> {
    const sinceDate = new Date();
    sinceDate.setDate(sinceDate.getDate() - (options?.days || 1));
    
    const messages = await this.graphClient
      .api('/me/messages')
      .filter(`receivedDateTime ge ${sinceDate.toISOString()}`)
      .orderby('receivedDateTime desc')
      .top(50)
      .get();
    
    return messages.value.map(msg => ({
      id: msg.id,
      subject: msg.subject,
      sender: {
        name: msg.sender.emailAddress.name,
        email: msg.sender.emailAddress.address
      },
      // ... map other fields
      body: msg.bodyPreview,
      isRead: msg.isRead,
      isFlagged: msg.flag?.flagStatus === 'flagged',
      receivedAt: new Date(msg.receivedDateTime)
    }));
  }
  
  async getCalendarEvents(options?: { days: number }): Promise<CalendarItem[]> {
    const now = new Date();
    const future = new Date();
    future.setDate(future.getDate() + (options?.days || 7));
    
    const events = await this.graphClient
      .api('/me/events')
      .filter(`start/dateTime ge '${now.toISOString()}'`)
      .orderby('start/dateTime')
      .top(100)
      .get();
    
    return events.value.map(event => ({
      id: event.id,
      title: event.subject,
      startTime: new Date(event.start.dateTime),
      endTime: new Date(event.end.dateTime),
      // ... map other fields
    }));
  }
}

export default OutlookService;

2.4 Testing

npm test -- src/services/outlook-service.test.ts

3. Authentication Flow

OAuth 2.0 Code Flow

// src/services/auth-service.ts
import jwt from 'jsonwebtoken';

class AuthService {
  async exchangeCodeForToken(provider: string, code: string) {
    let tokenEndpoint = '';
    let clientId = '';
    let clientSecret = '';
    
    if (provider === 'jira') {
      tokenEndpoint = 'https://auth.atlassian.com/oauth/token';
      clientId = process.env.JIRA_CLIENT_ID;
      clientSecret = process.env.JIRA_CLIENT_SECRET;
    } else if (provider === 'outlook') {
      tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
      clientId = process.env.OUTLOOK_CLIENT_ID;
      clientSecret = process.env.OUTLOOK_CLIENT_SECRET;
    }
    
    const response = await fetch(tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        client_id: clientId,
        client_secret: clientSecret,
        redirect_uri: process.env.REDIRECT_URI
      })
    });
    
    const data = await response.json();
    
    // Store token in database
    await this.storeToken(provider, data.access_token, data.refresh_token, data.expires_in);
    
    return data;
  }
  
  async storeToken(provider: string, accessToken: string, refreshToken: string, expiresIn: number) {
    const expiresAt = new Date(Date.now() + expiresIn * 1000);
    
    // INSERT or UPDATE integration_tokens table
    await db.query(
      `INSERT INTO integration_tokens (user_id, provider, access_token, refresh_token, expires_at)
       VALUES ($1, $2, $3, $4, $5)
       ON CONFLICT (user_id, provider) DO UPDATE SET
         access_token = $3,
         refresh_token = $4,
         expires_at = $5`,
      [userId, provider, accessToken, refreshToken, expiresAt]
    );
  }
}

export default AuthService;

4. Aggregation Service (Core Logic)

Overnight Summary Generation

// src/services/aggregation-service.ts
class AggregationService {
  async generateOvernightSummary(userId: string, date?: Date): Promise<OvernightSummary> {
    const user = await this.getUser(userId);
    const { start, end } = this.getOvernightWindow(user, date);
    
    // Fetch data from all sources in parallel
    const [jiraItems, emailItems, calendarItems] = await Promise.all([
      this.getJiraItems(userId, { start, end }),
      this.getEmailItems(userId, { start, end }),
      this.getCalendarItems(userId, { start, end })
    ]);
    
    // Synthesize into summary
    const summary = {
      id: uuid(),
      userId,
      generatedAt: new Date(),
      generatedFromData: end,
      
      metrics: {
        jiraItemsRequiringAction: jiraItems.filter(i => i.changeType !== 'CLOSED').length,
        escalationsAssigned: jiraItems.filter(i => i.changeType === 'ESCALATION').length,
        itemsResolved: jiraItems.filter(i => i.currentStatus === 'Resolved').length,
        itemsClosed: jiraItems.filter(i => i.currentStatus === 'Closed').length,
        criticalEmails: emailItems.filter(i => i.category === 'CRITICAL').length,
        agingEmails: emailItems.filter(i => i.category === 'AGING').length,
        meetingConflicts: calendarItems.filter(i => i.conflictType === 'OVERLAP').length,
        unacceptedMeetings: calendarItems.filter(i => i.userResponseStatus === 'TENTATIVE').length
      },
      
      biggestRisk: this.identifyBiggestRisk(jiraItems, emailItems, calendarItems),
      recommendedActions: this.generateRecommendedActions(jiraItems, emailItems, calendarItems),
      
      jiraItems,
      emailItems,
      calendarItems,
      
      attentionScore: this.calculateAttentionScore({ jiraItems, emailItems, calendarItems }),
      sourcesQueried: ['JIRA', 'OUTLOOK', 'CALENDAR'],
      generationTimeMs: Date.now() - startTime
    };
    
    // Cache result
    await this.cacheResult(summary);
    
    return summary;
  }
  
  private identifyBiggestRisk(jiraItems, emailItems, calendarItems): BiggestRisk {
    // Find most severe blockers or critical items
    const blockers = jiraItems.filter(i => i.changeType === 'BLOCKER');
    const critical = emailItems.filter(i => i.category === 'CRITICAL');
    
    if (blockers.length > 0) {
      return {
        title: blockers[0].summary,
        source: 'JIRA',
        sourceId: blockers[0].issueKey,
        severity: 'CRITICAL'
      };
    }
    // ... else check emails, etc.
  }
  
  private generateRecommendedActions(jiraItems, emailItems, calendarItems): RecommendedAction[] {
    const actions: RecommendedAction[] = [];
    
    // Action 1: Review critical Jira item
    const criticalJira = jiraItems.find(i => i.priority === 'BLOCKER');
    if (criticalJira) {
      actions.push({
        order: 1,
        description: `Review ${criticalJira.issueKey}`,
        sourceType: 'JIRA',
        sourceId: criticalJira.issueKey,
        actionType: 'REVIEW',
        estimatedTime: 15
      });
    }
    
    // Action 2: Respond to critical email
    const criticalEmail = emailItems.find(i => i.category === 'CRITICAL' && i.responseStatus === 'AWAITING_RESPONSE');
    if (criticalEmail) {
      actions.push({
        order: 2,
        description: `Respond to "${criticalEmail.subject}"`,
        sourceType: 'EMAIL',
        sourceId: criticalEmail.id,
        actionType: 'RESPOND',
        estimatedTime: 10
      });
    }
    
    // Action 3: Accept meeting
    const unaccepted = calendarItems.find(i => i.userResponseStatus === 'TENTATIVE');
    if (unaccepted) {
      actions.push({
        order: 3,
        description: `Accept "${unaccepted.title}" meeting`,
        sourceType: 'CALENDAR',
        sourceId: unaccepted.id,
        actionType: 'ACCEPT',
        estimatedTime: 2
      });
    }
    
    return actions.slice(0, 5); // Top 5 actions
  }
}

export default AggregationService;

5. API Route Implementation

GET /api/overnight-summary

// src/routes/overnight-summary.ts
import { Router } from 'express';
import { authMiddleware } from '../middleware/auth';
import AggregationService from '../services/aggregation-service';

const router = Router();
const aggregationService = new AggregationService();

router.get('/api/overnight-summary', authMiddleware, async (req, res) => {
  try {
    const userId = req.user.id;
    const date = req.query.date ? new Date(req.query.date as string) : new Date();
    
    const summary = await aggregationService.generateOvernightSummary(userId, date);
    
    return res.status(200).json({
      status: 'SUCCESS',
      data: summary,
      metadata: {
        queriedSources: summary.sourcesQueried,
        generationTime: summary.generationTimeMs,
        cacheHit: false
      }
    });
  } catch (error) {
    return res.status(500).json({
      status: 'ERROR',
      error: {
        code: 'AGGREGATION_FAILED',
        message: 'Failed to generate overnight summary',
        details: error.message
      }
    });
  }
});

export default router;

6. Database Migrations

Initial Schema

-- migrations/001_initial_schema.sql

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(255),
  timezone VARCHAR(50) DEFAULT 'UTC',
  business_hours_start SMALLINT DEFAULT 8,
  business_hours_end SMALLINT DEFAULT 18,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE integration_tokens (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  provider VARCHAR(50) NOT NULL,
  access_token TEXT NOT NULL,
  refresh_token TEXT,
  expires_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(user_id, provider)
);

CREATE TABLE overnight_summaries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  generated_from_date TIMESTAMP,
  attention_score SMALLINT,
  biggest_risk_json JSONB,
  metrics_json JSONB,
  jira_items_json JSONB,
  email_items_json JSONB,
  calendar_items_json JSONB,
  generation_time_ms INTEGER,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(user_id, DATE(generated_from_date))
);

CREATE INDEX idx_overnight_summaries_user_date 
  ON overnight_summaries(user_id, generated_from_date DESC);

7. Testing Integration

Unit Test Example

// src/services/aggregation-service.test.ts
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import AggregationService from './aggregation-service';

describe('AggregationService', () => {
  let service: AggregationService;
  
  beforeEach(() => {
    service = new AggregationService();
  });
  
  it('should generate overnight summary', async () => {
    const userId = 'test-user-123';
    const summary = await service.generateOvernightSummary(userId);
    
    expect(summary).toBeDefined();
    expect(summary.userId).toBe(userId);
    expect(summary.metrics).toBeDefined();
    expect(summary.recommendedActions.length).toBeGreaterThan(0);
  });
  
  it('should identify biggest risk', () => {
    const jiraItems = [
      { changeType: 'BLOCKER', summary: 'API is down', issueKey: 'TEST-1' }
    ];
    
    const risk = service.identifyBiggestRisk(jiraItems, [], []);
    
    expect(risk.severity).toBe('CRITICAL');
    expect(risk.source).toBe('JIRA');
  });
});

8. Troubleshooting

Common Issues

Issue Solution
Jira API 401 Unauthorized Check token expiration, refresh if needed
Outlook API 429 Rate Limited Implement exponential backoff, use cache
Database connection failing Verify PostgreSQL is running, check connection string
CORS errors Add frontend URL to CORS whitelist in API server
Slow aggregation (>5s) Check Jira/Outlook API response times, add caching

Next Steps for Team

  1. Choose tech stack: Node.js + Express, Next.js, or other backend framework
  2. Set up databases: PostgreSQL local + staging + production
  3. Implement auth: Use Passport.js or similar for OAuth flows
  4. Build core services: Start with Jira, then Outlook, then Calendar
  5. Connect API routes: Wire up aggregation pipeline
  6. Frontend components: Build React components consuming API
  7. Testing & deployment: Add CI/CD pipeline

Summary: Mock First, Real Integration Later

For the 90-Minute Sprint

✅ Use mock data (see Mock Data Strategy section at top)
✅ Build all components with realistic test data
✅ Test edge cases (empty state, errors, loading)
✅ Demo with consistent, predictable data

After Sprint (Post-Sprint Checklist)

  • Implement real Jira OAuth flow
  • Implement real Outlook OAuth flow
  • Test with live API data
  • Handle API errors gracefully
  • Implement caching strategy
  • Set up rate limiting protection
  • Load test with real API rates

Mock Data Benefits

Benefit Why It Matters
No API rate limits Build fast without throttling
Consistent test data Same output every test run
Complete control Test edge cases easily
No credentials needed Faster sprint setup
Swap later Drop-in replacement post-sprint
Demo safely No risk of API outages during demo

Swapping Mock → Real (Post-Sprint Guide)

When you're ready to wire real APIs (after sprint):

// src/services/jira-service-real.ts
// (Copy code from sections 1.3 above)

// In aggregation-service.ts, swap:
- import { mockJiraService } from '@/services/mock/jira-mock';
+ import { jiraService } from '@/services/jira-service-real';

// That's it! Everything else stays the same.

Resources


Questions?

For the sprint: Use mock data, focus on UI/UX.
Post-sprint: Refer back to sections 1-3 for real API implementation.