feat: add USSD management feature#938
Conversation
- Add USSD entity with session management (types, directions, statuses) - Add USSD repository (interface + GORM implementation) - Add USSD events (received and sent) - Add USSD request/response models - Add USSD service for receiving and sending USSD messages - Add USSD handler with routes for receive, send, index, delete - Add USSD validator - Register USSD routes, listeners, and DB migration in DI container API endpoints: POST /v1/ussd/receive - Receive USSD request from phone POST /v1/ussd/send - Send USSD response to phone GET /v1/ussd - List USSD session history DELETE /v1/ussd/:ussdID - Delete a USSD session
|
MacBook Air seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
|
USSD management feature |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 119 |
| Duplication | 55 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Greptile SummaryThis PR introduces a full USSD session management feature: entity, repository, service, handler, validator, events, and DI wiring. The implementation follows the existing codebase patterns well in most areas, but several data-mapping bugs would result in every stored USSD record having an incorrect (zero)
Confidence Score: 2/5Not ready to merge — every USSD record written to the database will have the wrong phone_id and outbound records will have owner/contact inverted, requiring a data migration if merged as-is. Three independent data-correctness bugs compound each other: phone_id is always zero for both inbound and outbound sessions, and the owner/contact fields are swapped for every sent USSD. On top of that, the duplicate route registration makes the Phone API Key authentication path for USSD completely unreachable at runtime. These defects affect the core write path and would produce corrupted data from the moment the feature is enabled.
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Phone as Mobile Phone
participant API as API Server
participant Svc as USSDService
participant Repo as GormUSSDRepository
participant ED as EventDispatcher
Phone->>API: POST /v1/ussd/receive
API->>API: Validate (USSDHandlerValidator)
API->>Svc: Receive(params, uuid.Nil)
Note over Svc: PhoneID = uuid.Nil (bug)
Svc->>Repo: Store(ussd)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.phone.received)
ED-->>Svc: ok
Svc-->>API: "*USSD"
API-->>Phone: 200 OK
Phone->>API: POST /v1/ussd/send
API->>API: Validate (USSDHandlerValidator)
API->>Svc: Send(params)
Note over Svc: PhoneID not set (bug)
Note over Svc: Owner = To (subscriber) — bug
Svc->>Repo: Store(ussd)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.phone.sent)
ED-->>Svc: ok
Svc-->>API: "*USSD"
API-->>Phone: 200 OK
Phone->>API: "GET /v1/ussd?phone_id=X"
API->>Svc: Index(user, params, phoneID)
Svc->>Repo: IndexByPhoneID(user, phoneID, params)
Note over Repo: Returns empty — phone_id stored as nil UUID
Repo-->>Svc: []
Svc-->>API: []
API-->>Phone: 200 OK (empty)
Phone->>API: DELETE /v1/ussd/:id
API->>Svc: Delete(source, userID, ussdID)
Svc->>Repo: Load(userID, ussdID)
Repo-->>Svc: "*USSD"
Svc->>Repo: Delete(userID, ussdID)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.deleted)
ED-->>Svc: ok
Svc-->>API: nil
API-->>Phone: 200 OK (should be 204)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Phone as Mobile Phone
participant API as API Server
participant Svc as USSDService
participant Repo as GormUSSDRepository
participant ED as EventDispatcher
Phone->>API: POST /v1/ussd/receive
API->>API: Validate (USSDHandlerValidator)
API->>Svc: Receive(params, uuid.Nil)
Note over Svc: PhoneID = uuid.Nil (bug)
Svc->>Repo: Store(ussd)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.phone.received)
ED-->>Svc: ok
Svc-->>API: "*USSD"
API-->>Phone: 200 OK
Phone->>API: POST /v1/ussd/send
API->>API: Validate (USSDHandlerValidator)
API->>Svc: Send(params)
Note over Svc: PhoneID not set (bug)
Note over Svc: Owner = To (subscriber) — bug
Svc->>Repo: Store(ussd)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.phone.sent)
ED-->>Svc: ok
Svc-->>API: "*USSD"
API-->>Phone: 200 OK
Phone->>API: "GET /v1/ussd?phone_id=X"
API->>Svc: Index(user, params, phoneID)
Svc->>Repo: IndexByPhoneID(user, phoneID, params)
Note over Repo: Returns empty — phone_id stored as nil UUID
Repo-->>Svc: []
Svc-->>API: []
API-->>Phone: 200 OK (empty)
Phone->>API: DELETE /v1/ussd/:id
API->>Svc: Delete(source, userID, ussdID)
Svc->>Repo: Load(userID, ussdID)
Repo-->>Svc: "*USSD"
Svc->>Repo: Delete(userID, ussdID)
Repo-->>Svc: ok
Svc->>ED: Dispatch(ussd.deleted)
ED-->>Svc: ok
Svc-->>API: nil
API-->>Phone: 200 OK (should be 204)
|
| // ToUSSDSendParams converts USSDSend to services.USSDSendParams | ||
| func (input *USSDSend) ToUSSDSendParams(userID entities.UserID, source string) *services.USSDSendParams { | ||
| return &services.USSDSendParams{ | ||
| Source: source, | ||
| UserID: userID, | ||
| Owner: input.To, | ||
| Contact: input.From, | ||
| Content: input.Content, | ||
| SessionID: input.SessionID, | ||
| } | ||
| } |
There was a problem hiding this comment.
Owner and Contact are swapped for the send direction. In USSDSend, From is the app's phone number (the owner) and To is the mobile subscriber (the contact). Passing input.To as Owner stores the subscriber's number in the owner column and the app phone number in the unused contact field, producing wrong data for every outbound USSD record. Compare with USSDReceive.ToUSSDReceiveParams where Owner: input.To is correct because in the receive direction To carries the app phone number.
| // ToUSSDSendParams converts USSDSend to services.USSDSendParams | |
| func (input *USSDSend) ToUSSDSendParams(userID entities.UserID, source string) *services.USSDSendParams { | |
| return &services.USSDSendParams{ | |
| Source: source, | |
| UserID: userID, | |
| Owner: input.To, | |
| Contact: input.From, | |
| Content: input.Content, | |
| SessionID: input.SessionID, | |
| } | |
| } | |
| // ToUSSDSendParams converts USSDSend to services.USSDSendParams | |
| func (input *USSDSend) ToUSSDSendParams(userID entities.UserID, source string) *services.USSDSendParams { | |
| return &services.USSDSendParams{ | |
| Source: source, | |
| UserID: userID, | |
| Owner: input.From, | |
| Contact: input.To, | |
| Content: input.Content, | |
| SessionID: input.SessionID, | |
| } | |
| } |
| return h.responseOK(c, "USSD session deleted successfully", nil) | ||
| } No newline at end of file |
There was a problem hiding this comment.
The swagger annotation declares
@Success 204 but the handler calls h.responseOK which returns HTTP 200. This is inconsistent with both the documented contract and the convention used by every other delete handler in the codebase (e.g., MessageHandler.Delete uses h.responseNoContent).
| return h.responseOK(c, "USSD session deleted successfully", nil) | |
| } | |
| return h.responseNoContent(c, "USSD session deleted successfully") | |
| } |
| } | ||
|
|
||
| // RegisterUSSDListeners registers event listeners for USSD events | ||
| func (container *Container) RegisterUSSDListeners() { |
There was a problem hiding this comment.
Add listener to delete all USSD messages when the phone is deleted and when the user account is deleted.
|
|
||
| const ( | ||
| // USSDDirectionMoToApp means the USSD message comes from the mobile phone to the application | ||
| USSDDirectionMoToApp = USSDDirection("MO-to-App") |
There was a problem hiding this comment.
We can use the same MobileOriginated or MobileTerminated directions which we use for the Message entity.
| } | ||
|
|
||
| // MarkActive marks the USSD session as active | ||
| func (ussd *USSD) MarkActive() *USSD { |
There was a problem hiding this comment.
Instead of this you can use SetStatusActive() etc.
| } | ||
|
|
||
| // USSDSend is the payload for sending a USSD response to a phone | ||
| type USSDSend struct { |
There was a problem hiding this comment.
Put this in a separate file., we use 1 file per request struct.
| request | ||
| From string `json:"from" example:"+18005550199"` | ||
| To string `json:"to" example:"+18005550100"` | ||
| Content string `json:"content" example:"*123#"` |
There was a problem hiding this comment.
This should be the response from the phone so the example should be something like "Welcome to USSD Menu"
|
|
||
| // ValidateReceive validates a USSD receive request | ||
| func (v *USSDHandlerValidator) ValidateReceive(ctx context.Context, request requests.USSDReceive) url.Values { | ||
| _, span := v.tracer.Start(ctx) |
There was a problem hiding this comment.
Use govalidate library to validate this struct.
|
|
||
| errs := url.Values{} | ||
|
|
||
| if matched, _ := regexp.MatchString("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", request.USSDID); !matched { |
There was a problem hiding this comment.
We already have a UUID validator.
|
@Diallola Thanks for this work.
|
API endpoints:
POST /v1/ussd/receive - Receive USSD request from phone POST /v1/ussd/send - Send USSD response to phone GET /v1/ussd - List USSD session history DELETE /v1/ussd/:ussdID - Delete a USSD session