Files
telecom-platform/apps/api-server/internal/services/es2_client.go
T
nutcas3 2e1300282d feat: Add ES2Service for GSMA ES2+ eSIM profile management operations
- Add ES2Service struct with HTTP client and ES2Config
- Add NewES2Service with 30s timeout and TLS configuration
- Add ProvisionProfile for eSIM profile provisioning with metadata support
- Add ActivateProfile, DeactivateProfile, and DeleteProfile for profile lifecycle management
- Add GetProfileStatus for profile state queries
- Add ListProfiles for eUICC profile enumeration
- Add ValidateEID and ValidateICCID for identifier
2026-04-14 16:00:15 +03:00

388 lines
11 KiB
Go

package services
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
)
// ES2Service handles GSMA ES2+ operations for eSIM provisioning
type ES2Service struct {
httpClient *http.Client
config *config.ES2Config
}
// NewES2Service creates a new ES2 service
func NewES2Service(cfg *config.ES2Config) *ES2Service {
return &ES2Service{
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: cfg.InsecureSkipVerify,
},
},
},
config: cfg,
}
}
// ES2 request/response types
type DownloadOrderRequest struct {
EID string `json:"eid"`
ICCID string `json:"iccid"`
ProfileType string `json:"profileType"`
Confirmation bool `json:"confirmation"`
Metadata map[string]string `json:"metadata"`
}
type DownloadOrderResponse struct {
ICCID string `json:"iccid"`
ProfileID string `json:"profileId"`
ActivationCode string `json:"activationCode"`
ConfirmationAddress string `json:"confirmationAddress"`
}
type ActivationRequest struct {
EID string `json:"eid"`
ProfileID string `json:"profileId"`
}
type DeactivationRequest struct {
EID string `json:"eid"`
ProfileID string `json:"profileId"`
}
type DeletionRequest struct {
EID string `json:"eid"`
ProfileID string `json:"profileId"`
}
type ProfileStatusResponse struct {
ICCID string `json:"iccid"`
ProfileID string `json:"profileId"`
ProfileName string `json:"profileName"`
State string `json:"state"`
Operator string `json:"operator"`
}
type ListProfilesResponse struct {
Profiles []ProfileStatusResponse `json:"profiles"`
}
// ProfileInfo represents eSIM profile information
type ProfileInfo struct {
ICCID string `json:"iccid"`
ProfileID string `json:"profileId"`
ProfileName string `json:"profileName"`
State string `json:"state"`
Operator string `json:"operator"`
Activation ProfileActivation `json:"activation"`
}
// ProfileActivation represents profile activation details
type ProfileActivation struct {
ActivationCode string `json:"activationCode"`
ConfAddress string `json:"confirmationAddress"`
}
// ProvisionProfile provisions an eSIM profile for a subscriber
func (e *ES2Service) ProvisionProfile(ctx context.Context, subscriber *models.Subscriber) (*ProfileInfo, error) {
// Check if subscriber has EUICCID
if subscriber.EUICCID == "" {
return nil, fmt.Errorf("subscriber must have EUICCID for eSIM provisioning")
}
// Create profile download request
req := DownloadOrderRequest{
EID: subscriber.EUICCID,
ICCID: "", // Will be assigned by SM-SR
ProfileType: "operational",
Confirmation: true,
Metadata: map[string]string{
"subscriber_id": fmt.Sprintf("%d", subscriber.ID),
"imsi": string(subscriber.IMSI),
"organization": subscriber.OrganizationID,
},
}
// Call SM-SR to download profile
resp, err := e.downloadProfile(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to download profile: %w", err)
}
// Create profile info
profileInfo := &ProfileInfo{
ICCID: resp.ICCID,
ProfileID: resp.ProfileID,
ProfileName: fmt.Sprintf("Profile-%s", subscriber.IMSI),
State: "downloaded",
Operator: "Telecom Platform",
Activation: ProfileActivation{
ActivationCode: resp.ActivationCode,
ConfAddress: resp.ConfirmationAddress,
},
}
return profileInfo, nil
}
// downloadProfile is an internal method to download a profile
func (e *ES2Service) downloadProfile(ctx context.Context, req DownloadOrderRequest) (*DownloadOrderResponse, error) {
jsonData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal download request: %w", err)
}
url := fmt.Sprintf("%s/es2/download", e.config.BaseURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send download request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
var response DownloadOrderResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode download response: %w", err)
}
return &response, nil
}
// ActivateProfile activates an eSIM profile
func (e *ES2Service) ActivateProfile(ctx context.Context, euiccID, profileID string) error {
req := ActivationRequest{
EID: euiccID,
ProfileID: profileID,
}
return e.sendActivationRequest(ctx, req, "activate")
}
// DeactivateProfile deactivates an eSIM profile
func (e *ES2Service) DeactivateProfile(ctx context.Context, euiccID, profileID string) error {
req := DeactivationRequest{
EID: euiccID,
ProfileID: profileID,
}
return e.sendDeactivationRequest(ctx, req)
}
// DeleteProfile deletes an eSIM profile
func (e *ES2Service) DeleteProfile(ctx context.Context, euiccID, profileID string) error {
req := DeletionRequest{
EID: euiccID,
ProfileID: profileID,
}
return e.sendDeletionRequest(ctx, req)
}
// GetProfileStatus retrieves the status of an eSIM profile
func (e *ES2Service) GetProfileStatus(ctx context.Context, euiccID, profileID string) (*ProfileInfo, error) {
url := fmt.Sprintf("%s/es2/eid/%s/profile/%s/status", e.config.BaseURL, euiccID, profileID)
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send status request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
var response ProfileStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode status response: %w", err)
}
profileInfo := &ProfileInfo{
ICCID: response.ICCID,
ProfileID: response.ProfileID,
ProfileName: response.ProfileName,
State: response.State,
Operator: response.Operator,
}
return profileInfo, nil
}
// ListProfiles lists all profiles on an eUICC
func (e *ES2Service) ListProfiles(ctx context.Context, euiccID string) ([]*ProfileInfo, error) {
url := fmt.Sprintf("%s/es2/eid/%s/profiles", e.config.BaseURL, euiccID)
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send list request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
var response ListProfilesResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode list response: %w", err)
}
var profiles []*ProfileInfo
for _, profile := range response.Profiles {
profileInfo := &ProfileInfo{
ICCID: profile.ICCID,
ProfileID: profile.ProfileID,
ProfileName: profile.ProfileName,
State: profile.State,
Operator: profile.Operator,
}
profiles = append(profiles, profileInfo)
}
return profiles, nil
}
// Helper methods for ES2 operations
func (e *ES2Service) sendActivationRequest(ctx context.Context, req ActivationRequest, action string) error {
jsonData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal activation request: %w", err)
}
url := fmt.Sprintf("%s/es2/activate", e.config.BaseURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send activation request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
return nil
}
func (e *ES2Service) sendDeactivationRequest(ctx context.Context, req interface{}) error {
jsonData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal deactivation request: %w", err)
}
url := fmt.Sprintf("%s/es2/deactivate", e.config.BaseURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send deactivation request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
return nil
}
func (e *ES2Service) sendDeletionRequest(ctx context.Context, req DeletionRequest) error {
jsonData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal deletion request: %w", err)
}
url := fmt.Sprintf("%s/es2/delete", e.config.BaseURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-Request-ID", generateRequestID())
httpReq.Header.Set("Authorization", "Bearer "+e.config.APIKey)
resp, err := e.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send deletion request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ES2 server returned status %d", resp.StatusCode)
}
return nil
}
// ValidateEID validates an EID format
func (e *ES2Service) ValidateEID(eid string) error {
if len(eid) != 32 {
return fmt.Errorf("invalid EID length: expected 32 characters, got %d", len(eid))
}
// Additional validation logic can be added here
return nil
}
// ValidateICCID validates an ICCID format
func (e *ES2Service) ValidateICCID(iccid string) error {
if len(iccid) < 18 || len(iccid) > 22 {
return fmt.Errorf("invalid ICCID length: expected 18-22 characters, got %d", len(iccid))
}
// Additional validation logic can be added here
return nil
}