mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-20 20:04:24 +00:00
feat: Add profile handlers with repository integration for eSIM profile lifecycle management via ES2+ protocol
- Add OrderProfileHandlerWithRepo with ICCID/IMSI/K/OPc validation, ES2+ DownloadProfile call, and Profile creation with ICCID/EID/IMSI/MCC/MNC/ProfileType/State/TenantID persistence - Add GetProfileHandlerWithRepo with repository.Get/ES2+ GetProfileStatus calls and merged response with state/imsi/tenantId/createdAt/updatedAt/executionStatus/profileState/statusMessage fields - Add ListProfilesHandlerWithRepo with pagination support, page/limit query
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
)
|
||||
|
||||
// OrderProfileHandlerWithRepo orders a profile via ES2+ and persists it in the repo.
|
||||
func OrderProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var order ProfileOrder
|
||||
if err := c.ShouldBindJSON(&order); err != nil {
|
||||
Logger.Error().Err(err).Msg("Invalid request body")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if order.ICCID == "" || order.IMSI == "" || order.K == "" || order.OPc == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Missing required fields",
|
||||
"message": "ICCID, IMSI, K, and OPc are required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info().
|
||||
Str("imsi", order.IMSI).
|
||||
Str("iccid", order.ICCID).
|
||||
Msg("Ordering eSIM profile from SM-DP+")
|
||||
|
||||
downloadResp, err := client.DownloadProfile(context.Background(), &es2.DownloadProfileRequest{
|
||||
EID: order.EID,
|
||||
ICCID: order.ICCID,
|
||||
ProfileType: order.ProfileType,
|
||||
ConfirmationCode: order.ConfirmationCode,
|
||||
})
|
||||
if err != nil {
|
||||
Logger.Error().Err(err).Str("imsi", order.IMSI).Msg("Failed to order profile")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to order profile", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
profile := &repository.Profile{
|
||||
ICCID: order.ICCID,
|
||||
EID: order.EID,
|
||||
IMSI: order.IMSI,
|
||||
MCC: order.MCC,
|
||||
MNC: order.MNC,
|
||||
ProfileType: order.ProfileType,
|
||||
State: "provisioned",
|
||||
TenantID: c.GetHeader("X-Tenant-ID"),
|
||||
}
|
||||
if err := repo.Create(c.Request.Context(), profile); err != nil {
|
||||
Logger.Warn().Err(err).Str("iccid", order.ICCID).Msg("Profile repo write failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"profile": &ProfileResponse{
|
||||
ExecutionStatus: downloadResp.ExecutionStatus,
|
||||
StatusMessage: downloadResp.StatusMessage,
|
||||
ProfileID: order.ICCID,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetProfileHandlerWithRepo returns the stored profile enriched with live ES2+ status.
|
||||
func GetProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
profileID := c.Param("profileId")
|
||||
if profileID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing profile ID"})
|
||||
return
|
||||
}
|
||||
|
||||
stored, err := repo.Get(c.Request.Context(), profileID)
|
||||
if err != nil && !errors.Is(err, repository.ErrNotFound) {
|
||||
Logger.Error().Err(err).Str("profile_id", profileID).Msg("Failed to read profile from store")
|
||||
}
|
||||
|
||||
statusResp, statusErr := client.GetProfileStatus(context.Background(), &es2.GetProfileStatusRequest{
|
||||
ICCID: profileID,
|
||||
EID: safeEID(stored),
|
||||
})
|
||||
|
||||
payload := gin.H{
|
||||
"profileId": profileID,
|
||||
"checkedAt": time.Now().UTC(),
|
||||
}
|
||||
if stored != nil {
|
||||
payload["state"] = stored.State
|
||||
payload["imsi"] = stored.IMSI
|
||||
payload["tenantId"] = stored.TenantID
|
||||
payload["createdAt"] = stored.CreatedAt
|
||||
payload["updatedAt"] = stored.UpdatedAt
|
||||
}
|
||||
if statusErr != nil {
|
||||
Logger.Warn().Err(statusErr).Str("profile_id", profileID).Msg("Live profile status unavailable")
|
||||
payload["liveStatusError"] = statusErr.Error()
|
||||
} else {
|
||||
payload["executionStatus"] = statusResp.ExecutionStatus
|
||||
payload["profileState"] = statusResp.ProfileState
|
||||
payload["statusMessage"] = statusResp.StatusMessage
|
||||
}
|
||||
|
||||
if stored == nil && statusErr != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found", "profileId": profileID})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "profile": payload})
|
||||
}
|
||||
}
|
||||
|
||||
// ListProfilesHandlerWithRepo returns a paginated list of stored profiles.
|
||||
func ListProfilesHandlerWithRepo(repo repository.ProfileRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page := parsePositiveInt(c.Query("page"), 1)
|
||||
limit := parsePositiveInt(c.Query("limit"), 20)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
Logger.Info().Int("page", page).Int("limit", limit).Msg("Listing eSIM profiles")
|
||||
|
||||
profiles, total, err := repo.List(c.Request.Context(), repository.ListFilter{
|
||||
TenantID: c.Query("tenantId"),
|
||||
State: c.Query("state"),
|
||||
Limit: limit,
|
||||
Offset: (page - 1) * limit,
|
||||
})
|
||||
if err != nil {
|
||||
Logger.Error().Err(err).Msg("Failed to list profiles")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list profiles", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"profiles": profiles,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteProfileHandlerWithRepo deletes a profile from the SM-DP+ and marks it deleted in the repo.
|
||||
func DeleteProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
profileID := c.Param("profileId")
|
||||
if profileID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing profile ID"})
|
||||
return
|
||||
}
|
||||
|
||||
deleteResp, err := client.DeleteProfile(context.Background(), &es2.DeleteProfileRequest{ICCID: profileID})
|
||||
if err != nil {
|
||||
Logger.Error().Err(err).Str("profile_id", profileID).Msg("Failed to delete profile upstream")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete profile", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := repo.UpdateState(c.Request.Context(), profileID, "deleted"); err != nil && !errors.Is(err, repository.ErrNotFound) {
|
||||
Logger.Warn().Err(err).Str("profile_id", profileID).Msg("Profile repo update failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Profile deletion requested",
|
||||
"executionStatus": deleteResp.ExecutionStatus,
|
||||
"statusMessage": deleteResp.StatusMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// safeEID returns the EID from a stored profile or empty string if nil.
|
||||
func safeEID(p *repository.Profile) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return p.EID
|
||||
}
|
||||
|
||||
// unused import guard (strconv used indirectly by parsePositiveInt in handlers.go)
|
||||
var _ = strconv.Atoi
|
||||
Reference in New Issue
Block a user