Skip to content

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:

  1. Pillar rate field (if set)
  2. Customer rate field (if set)
  3. Xero project minuteRate (from project settings)
  4. 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 all
  • POST /api/slack/channel-mappings — create
  • PUT /api/slack/channel-mappings/{id} — update
  • DELETE /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 Email
Auto-verify Email
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 permissions
  • custom:customerId — scopes API calls to customer data
  • sub — unique user identifier for API calls

SES (Email)

Email Triggers

Lead Contact Form (POST /api/leads)

  • Recipients: CONTACT_EMAIL environment 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 AdminCreateUser with 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 inactive changes from false/undefined to true
  • Subject: Pillar Inactivated: {pillarName} ({customerName})
  • Content: Notification that a customer admin deactivated a pillar

Configuration

  • Sender: SES_SENDER_EMAIL env var (must be verified in SES)
  • Contact: CONTACT_EMAIL env var (receives lead submissions)
  • Error handling: Errors logged to Sentry but don't block the main operation