Integrations¶
Xero (Accounting & Billing)¶
Xero integration enables syncing approved time bills to Xero Projects as time entries for invoicing.
OAuth Flow¶
sequenceDiagram
participant User
participant Web as Web Portal
participant API as API Gateway
participant Lambda as xero-sync Lambda
participant Xero
User->>Web: Click "Connect Xero"
Web->>API: GET /api/xero/auth-url
API->>Lambda: Generate auth URL
Lambda-->>Web: Xero authorization URL
Web->>Xero: Redirect to Xero login
Xero-->>Web: Redirect back with auth code
Web->>API: POST /api/xero/connect {code}
API->>Lambda: Exchange code
Lambda->>Xero: POST /connect/token
Xero-->>Lambda: access_token + refresh_token
Lambda->>Xero: GET /connections
Xero-->>Lambda: tenant ID
Lambda-->>Web: Connected
Note over Lambda: Tokens stored in<br/>xero-tokens table<br/>(id='default')
Scopes: openid profile email projects offline_access
Token Refresh¶
Tokens auto-refresh before any Xero API call when expiresAt <= now. The refresh uses the refresh_token grant type and updates the stored tokens in DynamoDB.
Time Bill Sync¶
sequenceDiagram
participant Admin
participant API as API Gateway
participant Lambda as time-entry-sync
participant Cognito
participant Xero
participant DB as DynamoDB
Admin->>API: PATCH /api/time-bills/{id}<br/>{status: "approved"}
API->>Lambda: Update bill
Lambda->>DB: Update status to approved
Lambda->>DB: Get Xero tokens
Lambda->>Xero: Refresh token (if expired)
Lambda->>Cognito: Get user email
Lambda->>Xero: Match email to Xero user
Lambda->>Xero: Get/create task for pillar
Note over Lambda,Xero: Rate hierarchy:<br/>1. Pillar rate<br/>2. Customer rate<br/>3. Project rate<br/>4. NON_CHARGEABLE
Lambda->>Xero: Create time entry
Lambda->>DB: Update bill with xeroTimeEntryId<br/>status → sent_to_billing
Rate Hierarchy¶
When creating Xero tasks for a pillar, the billing rate is determined in priority order:
- Pillar
ratefield (if set) - Customer
ratefield (if set) - Xero project
minuteRate(from project settings) - If none found, task is created as
NON_CHARGEABLE
Slack (Task Creation)¶
Slack integration allows creating tasks directly from Slack channels via slash commands or message shortcuts.
Slash Command Flow¶
sequenceDiagram
participant User as Slack User
participant Slack
participant API as API Gateway
participant Lambda as slack-integration
participant Claude as Claude Haiku
participant DB as DynamoDB
User->>Slack: /task fix the login bug
Slack->>API: POST /api/slack/events
API->>Lambda: Verify signature + parse
Lambda->>DB: Find channel mapping by prefix
Lambda->>Claude: "Summarize into task title"
Claude-->>Lambda: "Fix login authentication bug"
Lambda->>Slack: Open modal (prepopulated)
User->>Slack: Fill out modal + submit
Slack->>API: POST /api/slack/interactivity
API->>Lambda: Process submission
Lambda->>Slack: Get user email
Lambda->>DB: Find Cognito user by email
Lambda->>DB: Create task (status: intake)
Lambda-->>Slack: Success
Message Action Flow¶
sequenceDiagram
participant User as Slack User
participant Slack
participant API as API Gateway
participant Lambda as slack-integration
participant Claude as Claude Haiku
User->>Slack: Select message → "Add Task"
Slack->>API: POST /api/slack/interactivity<br/>(type: message_action)
API->>Lambda: Verify signature + parse
Lambda->>Claude: Summarize message into title
Claude-->>Lambda: Generated title
Lambda->>Slack: Open modal with title,<br/>message as description,<br/>auto-selected customer/pillar
User->>Slack: Review and submit
Note over Lambda: Task created with Slack<br/>permalink in link field
Channel Mapping¶
Slack channels are mapped to customers (and optionally pillars) by channel name prefix. When a slash command or message action fires, the Lambda finds the longest matching prefix.
Example: Channel customer-acme-dev matches mapping with prefix customer-acme and auto-selects the associated customer and pillar in the modal.
CRUD for mappings is available via authenticated API endpoints:
GET /api/slack/channel-mappings— list allPOST /api/slack/channel-mappings— createPUT /api/slack/channel-mappings/{id}— updateDELETE /api/slack/channel-mappings/{id}— delete
Claude AI Title Generation¶
- Model:
claude-haiku-4-5-20251001 - Prompt: Summarize message into a short task title (max 80 characters)
- Fallback: Truncate original message text if API call fails
- Behavior: Non-blocking; errors are logged but don't prevent the modal from opening
Cognito (Authentication)¶
User Pool Configuration¶
| Setting | Value |
|---|---|
| Pool Name | task-time-users |
| Sign-in | |
| Auto-verify | |
| Auth Flows | USER_PASSWORD, USER_SRP |
| Password | 8+ chars, uppercase, lowercase, digits, symbols |
Custom Attributes¶
| Attribute | Type | Purpose |
|---|---|---|
custom:customerId |
String | Associates user with a customer |
custom:userRole |
String | Role for authorization |
custom:name |
String | Display name |
custom:phone |
String (max 20) | Phone number |
custom:linkedIn |
String (max 200) | LinkedIn profile URL |
Role Definitions¶
| Role | Access Level |
|---|---|
internal_admin (or unset) |
Full access to all resources and endpoints |
customer_admin |
Manage own customer's users and pillars; view own time data |
customer_user |
Read-only tasks; view/edit own time entries and bills |
JWT Claims¶
The web frontend decodes Cognito JWT tokens to extract:
custom:userRole— drives UI navigation and permissionscustom:customerId— scopes API calls to customer datasub— unique user identifier for API calls
SES (Email)¶
Email Triggers¶
Lead Contact Form (POST /api/leads)¶
- Recipients:
CONTACT_EMAILenvironment variable - Content: Company name, contact person, email, systems used, message
- Behavior: Popular email domains (gmail, hotmail, etc.) send notification only; business domains also create/link customer record
User Invitation (POST /api/customers/{id}/users/invite)¶
- Recipients: Invited user's email
- Mechanism: Cognito
AdminCreateUserwith default invite message action - Content: Temporary password and login instructions
Pillar Inactivation (PUT /api/customers/{id}/pillars/{pillarId})¶
- Recipients: All internal users (fetched from Cognito, filtered by role)
- Trigger: When
inactivechanges fromfalse/undefined totrue - Subject:
Pillar Inactivated: {pillarName} ({customerName}) - Content: Notification that a customer admin deactivated a pillar
Configuration¶
- Sender:
SES_SENDER_EMAILenv var (must be verified in SES) - Contact:
CONTACT_EMAILenv var (receives lead submissions) - Error handling: Errors logged to Sentry but don't block the main operation