Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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>
91 lines
3.0 KiB
Markdown
91 lines
3.0 KiB
Markdown
# Task 04: Phone Number Normalization
|
|
|
|
## Objective
|
|
Build `internal/phone/normalize.go` — bidirectional conversion between E.164 and Philippine local format.
|
|
|
|
## Functions
|
|
|
|
### ToLocal — E.164 → Philippine local
|
|
```go
|
|
func ToLocal(e164 string) (string, error)
|
|
```
|
|
|
|
| Input | Output | Notes |
|
|
|-------|--------|-------|
|
|
| `+639171234567` | `09171234567` | Standard E.164 |
|
|
| `639171234567` | `09171234567` | Missing `+` prefix |
|
|
| `09171234567` | `09171234567` | Already local, pass through |
|
|
| `9171234567` | `09171234567` | Missing leading `0` |
|
|
| `+1234567890` | error | Non-PH country code |
|
|
| `` | error | Empty |
|
|
| `abc` | error | Non-numeric |
|
|
|
|
Rules:
|
|
1. Strip all non-digit characters (spaces, dashes, parens, `+`)
|
|
2. If starts with `63` and length is 12: replace `63` with `0`
|
|
3. If starts with `9` and length is 10: prepend `0`
|
|
4. If starts with `0` and length is 11: pass through
|
|
5. Otherwise: return error "invalid Philippine phone number"
|
|
6. Validate result is exactly 11 digits starting with `09`
|
|
|
|
### ToE164 — Philippine local → E.164
|
|
```go
|
|
func ToE164(local string) (string, error)
|
|
```
|
|
|
|
| Input | Output | Notes |
|
|
|-------|--------|-------|
|
|
| `09171234567` | `+639171234567` | Standard local |
|
|
| `9171234567` | `+639171234567` | Missing leading `0` |
|
|
| `+639171234567` | `+639171234567` | Already E.164, pass through |
|
|
| `` | error | Empty |
|
|
|
|
Rules:
|
|
1. Strip all non-digit characters except leading `+`
|
|
2. If starts with `+63`: pass through
|
|
3. If starts with `63` and length is 12: prepend `+`
|
|
4. If starts with `0` and length is 11: replace `0` with `+63`
|
|
5. If starts with `9` and length is 10: prepend `+63`
|
|
6. Otherwise: return error "invalid Philippine phone number"
|
|
7. Validate result matches `+63` + 10 digits
|
|
|
|
## Tests (`internal/phone/normalize_test.go`)
|
|
|
|
Test both functions with the table-driven test pattern:
|
|
|
|
```go
|
|
func TestToLocal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{"e164 with plus", "+639171234567", "09171234567", false},
|
|
{"e164 without plus", "639171234567", "09171234567", false},
|
|
{"already local", "09171234567", "09171234567", false},
|
|
{"missing leading zero", "9171234567", "09171234567", false},
|
|
{"non-PH number", "+1234567890", "", true},
|
|
{"empty", "", "", true},
|
|
{"with spaces", "+63 917 123 4567", "09171234567", false},
|
|
{"with dashes", "0917-123-4567", "09171234567", false},
|
|
{"too short", "0917", "", true},
|
|
{"too long", "091712345678", "", true},
|
|
}
|
|
// ... run tests
|
|
}
|
|
```
|
|
|
|
Same pattern for `TestToE164`.
|
|
|
|
## Acceptance Criteria
|
|
- [ ] `go build ./cmd/server/` succeeds
|
|
- [ ] `ToLocal` handles all input formats in the table
|
|
- [ ] `ToE164` handles all input formats in the table
|
|
- [ ] Non-Philippine numbers return error
|
|
- [ ] Empty strings return error
|
|
- [ ] Non-numeric input returns error
|
|
- [ ] Spaces, dashes, parens are stripped
|
|
- [ ] Results are validated (length + prefix check)
|
|
- [ ] All tests pass: `go test ./internal/phone/`
|