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
| Column | Type | Description |
|---|---|---|
id | int | Primary key |
user_id | int | Harvest user ID |
url | text | Teams incoming webhook URL |
Setting Up a Webhook URL
- In Microsoft Teams, go to the target channel
- Click ... → Connectors (or Workflows in newer Teams)
- Find Incoming Webhook → Configure
- Name it (e.g., "Harvest Budget Alerts") and copy the URL
- 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:
| Color | Hex | Meaning |
|---|---|---|
| Blue | 0076D7 | Standard alert (default) |
| Orange | FF8C00 | Warning (75%+) |
| Red | FF0000 | Critical (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
- Verify webhook URL hasn't expired (Teams connectors can be disabled by admins)
- Test the URL manually with curl:
curl -X POST "WEBHOOK_URL" -H "Content-Type: application/json" -d '{"text":"Test message"}' - Check
wp-content/debug.logfor HTTP errors - Ensure the webhook connector is still enabled in the Teams channel
Duplicate Alerts
- Check
harvest_milestone_historytable for existing records - Verify the milestone ID, project ID, and task ID match expectations
- Clear history if needed:
DELETE FROM harvest_milestone_history WHERE milestone_id = X
Wrong User Getting Alerts
- Check
teams_webhooks.user_idmatches the correct Harvest user - Verify task assignment user IDs are correct in
harvest_task_assignment - Ensure webhook URL is mapped to the right person