expense-approval-bot/README.md
2026-04-20 18:01:48 -04:00

2.6 KiB

Expense Approval Bot

Slack reaction-driven expense approval router. Replaces the Pipedream expenses_pipeline workflow.

Flow

A user reacts ✅ to a message in an expense channel. The bot:

  1. Maps the source channel to the next stage's channel.
  2. Copies the message text (stripped of any prior "react to advance" hint) into the next channel, with a permalink back to the original on the first advance.
  3. Replies in-thread on the original message with ➡️ Advanced to {Stage}.

Channel chain: Submitted → Processed → Authorized → Matched.

Architecture

  • Receiver Lambda (src/receiver/) — fronted by API Gateway HTTP API. Verifies the Slack request signature, handles the URL-verification handshake, and async-invokes the processor. Returns 200 within Slack's 3-second window to prevent retries.
  • Processor Lambda (src/processor/) — does the 5-step sequence of Slack API calls. No inbound HTTP; invoked only by the receiver.
  • Secrets — Slack bot token and signing secret each stored as separate AWS Secrets Manager secrets. Each Lambda only has IAM access to the secret it needs.

Runtime: Python 3.12 on arm64, no third-party dependencies (stdlib urllib for HTTP, boto3 from the Lambda runtime).

Setup

  1. Store the two Slack credentials in Secrets Manager:

    aws secretsmanager create-secret \
      --name expense-bot-slack-token \
      --secret-string "xoxb-..."
    
    aws secretsmanager create-secret \
      --name expense-bot-slack-signing-secret \
      --secret-string "..."
    
  2. Copy the sample SAM config and fill in real values:

    cp samconfig.toml.example samconfig.toml
    
    • SlackBotTokenSecretArn — ARN of the bot-token secret
    • SlackSigningSecretArn — ARN of the signing-secret secret
  3. Build and deploy:

    sam build && sam deploy
    
  4. After the first deploy, take the SlackEventsUrl output and paste it into the Slack app's Event Subscriptions → Request URL. Subscribe the bot to the reaction_added event.

Configuration

The channel → stage mapping lives in src/processor/app.py (STAGES). Update there if channels change. The "on first advance, include a permalink" check is keyed on the Processed-stage channel ID — update that too if the first-stage channel changes.

Manual testing

Invoke the processor directly with a synthetic Slack reaction event:

aws lambda invoke \
  --function-name expense-approval-processor \
  --payload '{"reaction":"white_check_mark","item":{"channel":"C0AQ2AWLNEN","ts":"1700000000.000000"}}' \
  /dev/stdout