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:
nutcas3
2026-04-24 08:49:54 +03:00
parent 8acba8b1e5
commit 5f6d58a0b7
@@ -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