Skip to main content

Microsoft Teams Integration

The Suma Harvest plugin sends budget milestone alerts to Microsoft Teams channels via incoming webhooks, notifying team members when project or task budgets hit configurable thresholds.


Architecture

Milestone triggered


Report::check_milestone() returns true


Check harvest_milestone_history (dedup)


Teams::send_webhook($url, $card_data)


Microsoft Teams Channel
└── MessageCard displayed with alert details

Webhook URL Storage

Each Harvest user can have a personal Teams webhook URL stored in the database:

Table: teams_webhooks

ColumnTypeDescription
idintPrimary key
user_idintHarvest user ID
urltextTeams incoming webhook URL

Setting Up a Webhook URL

  1. In Microsoft Teams, go to the target channel
  2. Click ...Connectors (or Workflows in newer Teams)
  3. Find Incoming Webhook → Configure
  4. Name it (e.g., "Harvest Budget Alerts") and copy the URL
  5. Store the URL in the admin interface (mapped to the Harvest user)

MessageCard Format

Teams webhooks accept the legacy MessageCard format:

class Teams {
/**
* Sends a milestone alert to a Teams webhook.
*
* @param string $webhook_url The Teams incoming webhook URL.
* @param array $data Alert data (title, project, task, percentage, milestone).
* @return bool Whether the webhook was sent successfully.
*/
public function send_webhook(string $webhook_url, array $data): bool {
$card = [
'@type' => 'MessageCard',
'@context' => 'http://schema.org/extensions',
'themeColor' => '0076D7',
'summary' => $data['title'],
'sections' => [
[
'activityTitle' => $data['title'],
'activitySubtitle' => 'Harvest Budget Alert',
'activityImage' => 'https://manage.rhinogroup.com/wp-content/plugins/suma-harvest/assets/harvest-icon.png',
'facts' => $this->build_facts($data),
'markdown' => true,
],
],
'potentialAction' => [
[
'@type' => 'OpenUri',
'name' => 'View in Harvest',
'targets' => [
[
'os' => 'default',
'uri' => $data['harvest_url'],
],
],
],
],
];

$response = wp_remote_post($webhook_url, [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode($card),
'timeout' => 10,
]);

$status_code = wp_remote_retrieve_response_code($response);
return $status_code === 200;
}

/**
* Builds the facts array for the MessageCard.
*
* @param array $data Alert data.
* @return array Facts in Teams MessageCard format.
*/
private function build_facts(array $data): array {
$facts = [
['name' => 'Project', 'value' => $data['project']],
];

if (!empty($data['task'])) {
$facts[] = ['name' => 'Task', 'value' => $data['task']];
}

$facts[] = ['name' => 'Budget Used', 'value' => $data['percentage'] . '%'];
$facts[] = ['name' => 'Milestone', 'value' => $data['milestone'] . '% threshold'];
$facts[] = ['name' => 'Alert Type', 'value' => ucfirst($data['kind'])];

return $facts;
}
}

Alert Types

Project-Level Alerts

Triggered when total project budget usage crosses a milestone threshold.

Recipients: Project manager (via email) + team webhook.

Example Card:

🎯 Budget Milestone: 75% Reached
├── Project: Client Website Redesign
├── Budget Used: 76%
├── Milestone: 75% threshold
├── Alert Type: Project
└── [View in Harvest] button

Task-Level Alerts

Triggered when an individual task's budget within a project crosses a milestone.

Recipients: Assigned user's Teams webhook only.

Example Card:

🎯 Task Budget Milestone: 100% Reached
├── Project: Client Website Redesign
├── Task: Development
├── Budget Used: 102%
├── Milestone: 100% threshold
├── Alert Type: Task
└── [View in Harvest] button

Theme Color Coding

The themeColor property creates a colored stripe on the card:

ColorHexMeaning
Blue0076D7Standard alert (default)
OrangeFF8C00Warning (75%+)
RedFF0000Critical (100%+)
private function get_theme_color(int $percentage): string {
if ($percentage >= 100) {
return 'FF0000'; // Over budget - red
}
if ($percentage >= 75) {
return 'FF8C00'; // Warning - orange
}
return '0076D7'; // Standard - blue
}

Deduplication

The harvest_milestone_history table prevents duplicate alerts:

// Before sending alert, check if already sent
$exists = Data::milestone_history_exists(
$milestone->id,
$project->id,
$task_id ?: 0
);

if ($exists) {
return; // Already alerted for this milestone/project/task combo
}

// Send alert
$this->teams->send_webhook($webhook_url, $alert_data);

// Record that we sent it
Data::record_milestone_history(
$milestone->id,
$project->id,
$task_id ?: 0
);

This means each milestone triggers exactly once per project/task, even if the percentage continues rising.


Error Handling

$response = wp_remote_post($webhook_url, [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode($card),
'timeout' => 10,
]);

if (is_wp_error($response)) {
error_log('[Teams Webhook] Connection error: ' . $response->get_error_message());
return false;
}

$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
error_log("[Teams Webhook] HTTP {$status_code} for user {$user_id}");
return false;
}

Troubleshooting

Webhook Not Delivering

  1. Verify webhook URL hasn't expired (Teams connectors can be disabled by admins)
  2. Test the URL manually with curl:
    curl -X POST "WEBHOOK_URL" -H "Content-Type: application/json" -d '{"text":"Test message"}'
  3. Check wp-content/debug.log for HTTP errors
  4. Ensure the webhook connector is still enabled in the Teams channel

Duplicate Alerts

  1. Check harvest_milestone_history table for existing records
  2. Verify the milestone ID, project ID, and task ID match expectations
  3. Clear history if needed: DELETE FROM harvest_milestone_history WHERE milestone_id = X

Wrong User Getting Alerts

  1. Check teams_webhooks.user_id matches the correct Harvest user
  2. Verify task assignment user IDs are correct in harvest_task_assignment
  3. Ensure webhook URL is mapped to the right person