Provider interfaces, OAuth flows, and data flow documentation for external integrations.
------
interface CalendarProviderInterface
{
// OAuth
public function getAuthorizationUrl(string $state): string;
public function handleCallback(string $code): TokenData;
public function refreshToken(Integration $integration): TokenData;
public function disconnect(Integration $integration): void;
// Calendar discovery (for multi-calendar)
public function getAvailableCalendars(Integration $integration): Collection;
// Events (calendar-aware)
public function getEvents(Integration $integration, Calendar $calendar, DateRange $range): Collection;
public function createEvent(Integration $integration, Calendar $calendar, EventData $event): EventData;
public function updateEvent(Integration $integration, Calendar $calendar, string $eventId, EventData $event): EventData;
public function deleteEvent(Integration $integration, Calendar $calendar, string $eventId): void;
// Provider info
public function getProviderName(): string;
public function getProviderKey(): string;
public function getRequiredScopes(): array;
}class IntegrationManager
{
public function __construct(
private readonly TenantContext $tenantContext,
private readonly array $calendarProviders = [],
private readonly array $invoicingProviders = []
) {}
// Get all active integrations for current organization
public function getActiveCalendarIntegrations(): Collection
{
return Integration::where('provider_type', 'calendar')
->where('is_active', true)
->get();
}
// Get enabled calendars across all integrations
public function getEnabledCalendars(): Collection
{
return Calendar::where('is_enabled', true)
->with('integration')
->get();
}
// Sync events from all enabled calendars
public function syncAllCalendars(DateRange $range): void
{
foreach ($this->getEnabledCalendars() as $calendar) {
$provider = $this->calendarProvider($calendar->integration->provider);
$events = $provider->getEvents($calendar->integration, $calendar, $range);
// Process events...
}
}
}---
The danwel database is the **source of truth** for time blocks. External services (Google Calendar, Moneybird) are mirrors that are kept in sync.
┌─────────────────────────────────────────────────────────────────┐
│ danwel Database │
│ (Source of Truth) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Google │ │ Moneybird │
│ Calendar │ │ │
│ (Mirror) │ │ (Mirror) │
└─────────────┘ └─────────────┘sync_status: pending, retry with backoffpending → Not yet synced to external service
syncing → Currently being synced
synced → Successfully mirrored to external services
error → Sync failed (will retry)
conflict → External change detected, needs resolution
disabled → User disabled sync for this block// Sync single time block to external services
SyncTimeBlockJob::dispatch($timeBlock);
// Periodic: Pull changes from all calendars
PullCalendarChangesJob::dispatch($integration);
// Retry failed syncs
RetryPendingSyncsJob::dispatch();---
https://www.googleapis.com/auth/calendar.eventsjson
{
"access_token": "ya29.xxx",
"expires_in": 3599,
"refresh_token": "1//xxx",
"scope": "https://www.googleapis.com/auth/calendar.events",
"token_type": "Bearer"
}
**Additional Data Fetched on Connect:**
GET https://www.googleapis.com/calendar/v3/users/me/settings/timezone
→ { "value": "Europe/Amsterdam" }
**Stored Token Data:**
json
{
"access_token": "ya29.xxx",
"expires_in": 3599,
"refresh_token": "1//xxx",
"expires_at": 1702300000,
"timezone": "Europe/Amsterdam"
}
Events API
**Fetch Events (Read):**
GET https://www.googleapis.com/calendar/v3/calendars/primary/events
?timeMin=2024-01-08T00:00:00+01:00
&timeMax=2024-01-14T00:00:00+01:00
&singleEvents=true
&orderBy=startTime
**Event Structure (from Google):**
json
{
"id": "abc123xyz",
"summary": "Meeting with Client",
"description": "Project: 123456\nClient: 789012\n— Created with danwel",
"colorId": "10",
"start": {
"dateTime": "2024-01-08T09:00:00+01:00",
"timeZone": "Europe/Amsterdam"
},
"end": {
"dateTime": "2024-01-08T10:00:00+01:00",
"timeZone": "Europe/Amsterdam"
},
"extendedProperties": {
"private": {
"project": "123456",
"client": "789012",
"weekplanner": "true",
"timeEntryId": "456789"
}
}
}
**Create Event (Write):**
POST https://www.googleapis.com/calendar/v3/calendars/primary/events
{
"summary": "Client Work",
"description": "Project: 123456\nClient: 789012\n— Created with danwel",
"colorId": "10",
"start": {
"dateTime": "2024-01-08T09:00:00",
"timeZone": "Europe/Amsterdam"
},
"end": {
"dateTime": "2024-01-08T10:00:00",
"timeZone": "Europe/Amsterdam"
},
"extendedProperties": {
"private": {
"project": "123456",
"client": "789012",
"weekplanner": "true",
"timeEntryId": "456789"
}
}
}
**Fields We Use from Google Calendar:**
| Field | Purpose |
|-------|---------|
| id | Unique event identifier |
| summary | Event title |
| start.dateTime | Start time with timezone |
| end.dateTime | End time with timezone |
| colorId | Google Calendar color (1-11) |
| extendedProperties.private.project | Moneybird project ID |
| extendedProperties.private.client | Moneybird contact ID |
| extendedProperties.private.weekplanner | Flag: "true" if created by us |
| extendedProperties.private.timeEntryId | Moneybird time entry ID |---
Moneybird Integration
OAuth Flow
**Scopes Requested:**
time_entries settings
**Token Exchange Response:**
json
{
"access_token": "xxx",
"token_type": "bearer",
"expires_in": 7200,
"refresh_token": "xxx",
"scope": "time_entries settings",
"created_at": 1702300000
}
**Additional Data Fetched on Connect:**
GET https://moneybird.com/api/v2/administrations.json
→ [
{
"id": "123456789",
"name": "My Company BV",
...
}
]
**Stored Token Data:**
json
{
"access_token": "xxx",
"refresh_token": "xxx",
"expires_at": 1702307200,
"administration_id": "123456789",
"administration_name": "My Company BV"
}
Contacts (Clients) API
**Fetch Contacts:**
GET https://moneybird.com/api/v2/{admin_id}/contacts.json
**Contact Structure (from Moneybird):**
json
{
"id": "789012345",
"company_name": "Acme Corp",
"firstname": "John",
"lastname": "Doe",
"email": "john@acme.com",
...
}
**Fields We Use:**
| Field | Purpose |
|-------|---------|
| id | Unique contact identifier |
| company_name | Company name (preferred) |
| firstname + lastname | Fallback if no company name |
| email | Contact email |
**Create Contact:**
POST https://moneybird.com/api/v2/{admin_id}/contacts.json
{
"contact": {
"company_name": "New Client Inc"
}
}
Projects API
**Fetch Projects:**
GET https://moneybird.com/api/v2/{admin_id}/projects.json
**Project Structure (from Moneybird):**
json
{
"id": "123456",
"name": "Website Redesign",
"state": "active",
...
}
**Fields We Use:**
| Field | Purpose |
|-------|---------|
| id | Unique project identifier |
| name | Project name |
| state | Filter for "active" only |
**Create Project:**
POST https://moneybird.com/api/v2/{admin_id}/projects.json
{
"project": {
"name": "New Project"
}
}
Time Entries API
**Fetch User ID (Required for Time Entries):**
GET https://moneybird.com/api/v2/{admin_id}/users.json
→ [{ "id": "user123", ... }]
**Create Time Entry:**
POST https://moneybird.com/api/v2/{admin_id}/time_entries.json
{
"time_entry": {
"user_id": "user123",
"started_at": "2024-01-08 09:00:00",
"ended_at": "2024-01-08 10:00:00",
"description": "Client Work",
"paused_duration": 0,
"project_id": "123456",
"contact_id": "789012"
}
}
**Response:**
json
{
"id": "456789",
"user_id": "user123",
"started_at": "2024-01-08T09:00:00.000+01:00",
"ended_at": "2024-01-08T10:00:00.000+01:00",
"description": "Client Work",
"project_id": "123456",
"contact_id": "789012",
...
}
**Update Time Entry:**
PATCH https://moneybird.com/api/v2/{admin_id}/time_entries/{id}.json
{
"time_entry": {
"started_at": "2024-01-08 09:30:00",
"ended_at": "2024-01-08 11:00:00",
"description": "Updated description",
"project_id": "123456",
"contact_id": "789012"
}
}
**Delete Time Entry:**
DELETE https://moneybird.com/api/v2/{admin_id}/time_entries/{id}.json
---
Data Flow Summary
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER CREATES TIME BLOCK │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Input: title, day, startTime, duration, projectId, clientId │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────┴─────────────────┐
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ MONEYBIRD API │ │ GOOGLE CALENDAR API │
│ │ │ │
│ POST /time_entries.json │ │ POST /events │
│ { │ │ { │
│ user_id, │ │ summary: title, │
│ started_at, │ │ start: { dateTime }, │
│ ended_at, │ │ end: { dateTime }, │
│ description, │ │ colorId: "10", │
│ project_id, │ │ extendedProperties: { │
│ contact_id │ │ project, client, │
│ } │ │ weekplanner: "true", │
│ │ │ timeEntryId │
│ Returns: { id: "456" } │ │ } │
└───────────────────────────┘ │ } │
│ │ │
│ │ Returns: { id: "abc" } │
│ └───────────────────────────┘
│ │
└─────────────────┬─────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Stored References: │
│ - Google Calendar Event ID: "abc" │
│ - Moneybird Time Entry ID: "456" │
│ - Both linked via extendedProperties.private.timeEntryId │
└─────────────────────────────────────────────────────────────────────────────┘
`
External IDs to Store in Database
| External System | ID Field | Purpose |
|-----------------|----------|---------|
| Google Calendar | event.id | Link to calendar event |
| Moneybird | administration_id | Which Moneybird account |
| Moneybird | time_entry.id | Link to time entry |
| Moneybird | project.id | Link to project |
| Moneybird | contact.id | Link to client/contact |
| Moneybird | user.id` | Required for creating time entries |---
// app/Services/Integrations/Calendar/Office365CalendarProvider.php
class Office365CalendarProvider implements CalendarProviderInterface
{
// ... implement all methods
}// config/integrations.php
'providers' => [
'calendar' => [
'google_calendar' => [
'class' => GoogleCalendarProvider::class,
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'scopes' => ['https://www.googleapis.com/auth/calendar'],
],
'office365_calendar' => [
'class' => Office365CalendarProvider::class,
'client_id' => env('OFFICE365_CLIENT_ID'),
'client_secret' => env('OFFICE365_CLIENT_SECRET'),
'scopes' => ['Calendars.ReadWrite'],
],
],
'invoicing' => [
'moneybird' => [...],
'harvest' => [...],
],
],// IntegrationServiceProvider boots all configured providers