cast-ghl-plugin/.claude/tasks/03-cast-client.md
Head of Product & Engineering a40a4aa626
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: initial implementation of Cast GHL Provider
Complete MVP implementation of the Cast GHL Conversation Provider bridge:
- Go module setup with chi router and mongo-driver dependencies
- Config loading with env var validation and defaults
- MongoDB token store with upsert, get, update, delete operations
- Cast.ph SMS client with 429 retry logic and typed errors
- Phone number normalization (E.164 ↔ Philippine local format)
- GHL OAuth 2.0 install/callback/refresh flow
- GHL webhook handler with ECDSA signature verification (async dispatch)
- GHL API client for message status updates and inbound message stubs
- Multi-stage Dockerfile, docker-compose with MongoDB, Woodpecker CI pipeline
- Unit tests for phone normalization, Cast client, GHL webhook, and OAuth handlers

Co-Authored-By: SideKx <sidekx.ai@sds.dev>
2026-04-04 17:27:05 +02:00

84 lines
2.3 KiB
Markdown

# Task 03: Cast API Client
## Objective
Build `internal/cast/client.go` — a typed HTTP client for the Cast.ph SMS API.
## Reference
Read `CAST_API_REFERENCE.md` for exact request/response shapes.
## Types (`internal/cast/types.go`)
```go
type SendRequest struct {
To string `json:"to"`
Message string `json:"message"`
SenderID string `json:"sender_id,omitempty"`
}
type SendResponse struct {
Success bool `json:"success"`
MessageID string `json:"message_id,omitempty"`
Parts int `json:"parts,omitempty"`
Error string `json:"error,omitempty"`
}
```
## Client (`internal/cast/client.go`)
### Client struct
```go
type Client struct {
baseURL string
apiKey string
senderID string // default sender ID, can be empty
httpClient *http.Client
}
func NewClient(baseURL, apiKey, senderID string) *Client
```
### Methods
```go
func (c *Client) SendSMS(ctx context.Context, to, message string) (*SendResponse, error)
```
- POST to `/api/sms/send`
- Set headers: `X-API-Key`, `Content-Type: application/json`
- Body: `{ "to": to, "message": message, "sender_id": c.senderID }` (omit sender_id if empty)
- On non-200: return `CastAPIError` with status code and error message from body
- On 200 with `success: false`: return `CastAPIError` with the error field
- On 200 with `success: true`: return the response
- Retry on 429: max 3 retries, read `Retry-After` header, backoff 1s/2s/4s
### Error type
```go
type CastAPIError struct {
StatusCode int
APIError string
}
func (e *CastAPIError) Error() string {
return fmt.Sprintf("cast api error (HTTP %d): %s", e.StatusCode, e.APIError)
}
```
### Key behaviors
- HTTP client timeout: 30 seconds
- `sender_id` omitted from JSON when Client.senderID is empty
- Response body always read and closed, even on errors
- Retry on 429 only (not on 5xx — Cast should handle that)
- Log retries with `slog.Warn`
## Acceptance Criteria
- [ ] `go build ./cmd/server/` succeeds
- [ ] `SendSMS` calls correct URL with correct headers
- [ ] `X-API-Key` header set on every request
- [ ] `sender_id` omitted from JSON when empty
- [ ] `CastAPIError` returned on non-200 with statusCode + apiError
- [ ] `CastAPIError` returned on 200 with `success: false`
- [ ] Retry on 429 with backoff (max 3)
- [ ] Context passed to HTTP request
- [ ] HTTP client has 30s timeout