diff --git a/apps/api-server/internal/graphql/generated.go b/apps/api-server/internal/graphql/generated.go deleted file mode 100644 index 18ca3af..0000000 --- a/apps/api-server/internal/graphql/generated.go +++ /dev/null @@ -1,375 +0,0 @@ -package graphql - -// This file will be generated by gqlgen -// Run: go run github.com/99designs/gqlgen generate - -import ( - "context" - "time" - - "github.com/99designs/gqlgen/graphql" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" - "github.com/vektah/gqlparser/v2/ast" -) - -// GraphQL type definitions -type SubscriberStatus string -type ProfileStatus string -type UsageType string -type SessionStatus string -type PaymentMethodType string -type InvoiceStatus string -type AlertType string -type AlertSeverity string -type SubscriberSort string -type UsagePeriod string -type TrendDirection string - -const ( - SubscriberStatusActive SubscriberStatus = "ACTIVE" - SubscriberStatusInactive SubscriberStatus = "INACTIVE" - SubscriberStatusSuspended SubscriberStatus = "SUSPENDED" - SubscriberStatusTerminated SubscriberStatus = "TERMINATED" - SubscriberStatusProvisioning SubscriberStatus = "PROVISIONING" -) - -const ( - ProfileStatusActive ProfileStatus = "ACTIVE" - ProfileStatusInactive ProfileStatus = "INACTIVE" - ProfileStatusDownloading ProfileStatus = "DOWNLOADING" - ProfileStatusFailed ProfileStatus = "FAILED" -) - -const ( - UsageTypeData UsageType = "DATA" - UsageTypeVoice UsageType = "VOICE" - UsageTypeSMS UsageType = "SMS" -) - -const ( - SessionStatusActive SessionStatus = "ACTIVE" - SessionStatusCompleted SessionStatus = "COMPLETED" - SessionStatusTerminated SessionStatus = "TERMINATED" -) - -const ( - PaymentMethodTypeCreditCard PaymentMethodType = "CREDIT_CARD" - PaymentMethodTypeBankAccount PaymentMethodType = "BANK_ACCOUNT" -) - -const ( - InvoiceStatusDraft InvoiceStatus = "DRAFT" - InvoiceStatusPending InvoiceStatus = "PENDING" - InvoiceStatusPaid InvoiceStatus = "PAID" - InvoiceStatusOverdue InvoiceStatus = "OVERDUE" - InvoiceStatusCancelled InvoiceStatus = "CANCELLED" -) - -const ( - AlertTypeLowBalance AlertType = "LOW_BALANCE" - AlertTypeHighUsage AlertType = "HIGH_USAGE" - AlertTypePaymentFailed AlertType = "PAYMENT_FAILED" - AlertTypeSystemError AlertType = "SYSTEM_ERROR" -) - -const ( - AlertSeverityLow AlertSeverity = "LOW" - AlertSeverityMedium AlertSeverity = "MEDIUM" - AlertSeverityHigh AlertSeverity = "HIGH" - AlertSeverityCritical AlertSeverity = "CRITICAL" -) - -const ( - SubscriberSortCreatedAtAsc SubscriberSort = "CREATED_AT_ASC" - SubscriberSortCreatedAtDesc SubscriberSort = "CREATED_AT_DESC" - SubscriberSortUpdatedAtAsc SubscriberSort = "UPDATED_AT_ASC" - SubscriberSortUpdatedAtDesc SubscriberSort = "UPDATED_AT_DESC" - SubscriberSortFirstNameAsc SubscriberSort = "FIRST_NAME_ASC" - SubscriberSortFirstNameDesc SubscriberSort = "FIRST_NAME_DESC" - SubscriberSortLastNameAsc SubscriberSort = "LAST_NAME_ASC" - SubscriberSortLastNameDesc SubscriberSort = "LAST_NAME_DESC" - SubscriberSortEmailAsc SubscriberSort = "EMAIL_ASC" - SubscriberSortEmailDesc SubscriberSort = "EMAIL_DESC" - SubscriberSortBalanceAsc SubscriberSort = "BALANCE_ASC" - SubscriberSortBalanceDesc SubscriberSort = "BALANCE_DESC" - SubscriberSortDataUsedAsc SubscriberSort = "DATA_USED_ASC" - SubscriberSortDataUsedDesc SubscriberSort = "DATA_USED_DESC" -) - -const ( - UsagePeriodDaily UsagePeriod = "DAILY" - UsagePeriodWeekly UsagePeriod = "WEEKLY" - UsagePeriodMonthly UsagePeriod = "MONTHLY" - UsagePeriodYearly UsagePeriod = "YEARLY" -) - -const ( - TrendDirectionUp TrendDirection = "UP" - TrendDirectionDown TrendDirection = "DOWN" - TrendDirectionStable TrendDirection = "STABLE" -) - -// Input types -type SubscriberFilter struct { - Status *SubscriberStatus `json:"status"` - OrganizationID *string `json:"organizationId"` - PlanID *int `json:"planId"` - Search *string `json:"search"` - BalanceMin *float64 `json:"balanceMin"` - BalanceMax *float64 `json:"balanceMax"` - DataUsageMin *float64 `json:"dataUsageMin"` - DataUsageMax *float64 `json:"dataUsageMax"` -} - -type UsageEventFilter struct { - UsageType *UsageType `json:"usageType"` - TimestampGte *time.Time `json:"timestampGte"` - TimestampLte *time.Time `json:"timestampLte"` - CostMin *float64 `json:"costMin"` - CostMax *float64 `json:"costMax"` -} - -type CreateSubscriberInput struct { - MSISDN string `json:"msisdn"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Email string `json:"email"` - OrganizationID *string `json:"organizationId"` - PlanID int `json:"planId"` - EUICCID *string `json:"euiccId"` -} - -type UpdateSubscriberInput struct { - FirstName *string `json:"firstName"` - LastName *string `json:"lastName"` - Email *string `json:"email"` - OrganizationID *string `json:"organizationId"` - PlanID *int `json:"planId"` -} - -type TopUpInput struct { - Amount float64 `json:"amount"` - PaymentMethodID *string `json:"paymentMethodId"` -} - -type UsageStatsInput struct { - IMSI string `json:"imsi"` - Period UsagePeriod `json:"period"` -} - -type PaymentMethodInput struct { - Type PaymentMethodType `json:"type"` - Token string `json:"token"` - IsDefault *bool `json:"isDefault"` -} - -type CreateAlertInput struct { - Type AlertType `json:"type"` - Severity AlertSeverity `json:"severity"` - Message string `json:"message"` - SubscriberID *int `json:"subscriberId"` -} - -// Output types -type PageInfo struct { - HasNextPage bool `json:"hasNextPage"` - HasPreviousPage bool `json:"hasPreviousPage"` - StartCursor *string `json:"startCursor"` - EndCursor *string `json:"endCursor"` -} - -type SubscriberEdge struct { - Node *models.Subscriber `json:"node"` - Cursor string `json:"cursor"` -} - -type SubscriberConnection struct { - Edges []*SubscriberEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` - TotalCount int `json:"totalCount"` -} - -type UsageEventEdge struct { - Node *models.UsageEvent `json:"node"` - Cursor string `json:"cursor"` -} - -type UsageEventConnection struct { - Edges []*UsageEventEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` - TotalCount int `json:"totalCount"` -} - -type InvoiceEdge struct { - Node *models.Invoice `json:"node"` - Cursor string `json:"cursor"` -} - -type InvoiceConnection struct { - Edges []*InvoiceEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` - TotalCount int `json:"totalCount"` -} - -type AlertEdge struct { - Node *models.Alert `json:"node"` - Cursor string `json:"cursor"` -} - -type AlertConnection struct { - Edges []*AlertEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` - TotalCount int `json:"totalCount"` -} - -type ChargingSessionEdge struct { - Node *ChargingSession `json:"node"` - Cursor string `json:"cursor"` -} - -type ChargingSessionConnection struct { - Edges []*ChargingSessionEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` - TotalCount int `json:"totalCount"` -} - -type UsageTrend struct { - Direction TrendDirection `json:"direction"` - Percentage float64 `json:"percentage"` - PeriodOverPeriod float64 `json:"periodOverPeriod"` -} - -type UsageStats struct { - DataUsage float64 `json:"dataUsage"` - VoiceUsage float64 `json:"voiceUsage"` - SmsUsage float64 `json:"smsUsage"` - Cost float64 `json:"cost"` - Period string `json:"period"` - Trend *UsageTrend `json:"trend"` -} - -type CurrentSession struct { - SessionID string `json:"sessionId"` - StartTime time.Time `json:"startTime"` - DataUsed float64 `json:"dataUsed"` - VoiceUsed float64 `json:"voiceUsed"` - SmsUsed float64 `json:"smsUsed"` - Cost float64 `json:"cost"` -} - -type TodayUsage struct { - DataUsed float64 `json:"dataUsed"` - VoiceUsed float64 `json:"voiceUsed"` - SmsUsed float64 `json:"smsUsed"` - Cost float64 `json:"cost"` -} - -type RealTimeUsage struct { - CurrentSession *CurrentSession `json:"currentSession"` - TodayUsage *TodayUsage `json:"todayUsage"` -} - -type ESIMProvisionResult struct { - ProfileID string `json:"profileId"` - ActivationCode string `json:"activationCode"` - QRCode string `json:"qrCode"` -} - -type ChargingSession struct { - SessionID string `json:"sessionId"` - IMSI string `json:"imsi"` - StartTime time.Time `json:"startTime"` - EndTime *time.Time `json:"endTime"` - DataBytes float64 `json:"dataBytes"` - VoiceSeconds float64 `json:"voiceSeconds"` - SmsCount float64 `json:"smsCount"` - TotalCost float64 `json:"totalCost"` - Status SessionStatus `json:"status"` -} - -// GraphQL configuration -type Config struct { - Resolvers ResolverRoot -} - -type ResolverRoot interface { - Query() QueryResolver - Mutation() MutationResolver - Subscription() SubscriptionResolver -} - -type QueryResolver interface { - Subscriber(ctx context.Context, id string) (*models.Subscriber, error) - SubscriberByImsi(ctx context.Context, imsi string) (*models.Subscriber, error) - Subscribers(ctx context.Context, first *int, after *string, filter *SubscriberFilter, sort *SubscriberSort) (*SubscriberConnection, error) - SubscriberAccount(ctx context.Context, imsi string) (*models.SubscriberAccount, error) - UsageEvents(ctx context.Context, first *int, after *string, imsi *string, filter *UsageEventFilter) (*UsageEventConnection, error) - UsageStats(ctx context.Context, input UsageStatsInput) (*UsageStats, error) - RealTimeUsage(ctx context.Context, imsi string) (*RealTimeUsage, error) - Invoices(ctx context.Context, first *int, after *string, imsi *string, status *models.InvoiceStatus) (*InvoiceConnection, error) - Invoice(ctx context.Context, id string) (*models.Invoice, error) - RatingPlans(ctx context.Context) ([]*models.RatingPlan, error) - RatingPlan(ctx context.Context, planId string) (*models.RatingPlan, error) - ChargingSessions(ctx context.Context, first *int, after *string, imsi *string, status *SessionStatus) (*ChargingSessionConnection, error) - ChargingSession(ctx context.Context, sessionId string) (*ChargingSession, error) - Alerts(ctx context.Context, first *int, after *string, subscriberId *int, severity *models.AlertSeverity, resolved *bool) (*AlertConnection, error) - SystemStats(ctx context.Context) (*models.SystemStats, error) - HealthStatus(ctx context.Context) (*models.HealthStatus, error) - SearchSubscribers(ctx context.Context, query string, limit int) ([]*models.Subscriber, error) - SearchUsageEvents(ctx context.Context, query string, limit int) ([]*models.UsageEvent, error) -} - -type MutationResolver interface { - CreateSubscriber(ctx context.Context, input CreateSubscriberInput) (*models.Subscriber, error) - UpdateSubscriber(ctx context.Context, id string, input UpdateSubscriberInput) (*models.Subscriber, error) - DeleteSubscriber(ctx context.Context, id string) (bool, error) - TopUpBalance(ctx context.Context, imsi string, input TopUpInput) (*models.SubscriberAccount, error) - SuspendSubscriber(ctx context.Context, id string) (*models.Subscriber, error) - ActivateSubscriber(ctx context.Context, id string) (*models.Subscriber, error) - TerminateSubscriber(ctx context.Context, id string) (*models.Subscriber, error) - ProvisionESIM(ctx context.Context, imsi string) (*ESIMProvisionResult, error) - ActivateESIM(ctx context.Context, imsi string, profileId string) (bool, error) - DeactivateESIM(ctx context.Context, imsi string, profileId string) (bool, error) - AddPaymentMethod(ctx context.Context, subscriberId int, input PaymentMethodInput) (*models.PaymentMethod, error) - RemovePaymentMethod(ctx context.Context, paymentMethodId string) (bool, error) - SetDefaultPaymentMethod(ctx context.Context, paymentMethodId string) (*models.PaymentMethod, error) - ResolveAlert(ctx context.Context, alertId string) (*models.Alert, error) - CreateAlert(ctx context.Context, input CreateAlertInput) (*models.Alert, error) - TriggerSystemMaintenance(ctx context.Context) (bool, error) -} - -type SubscriptionResolver interface { - SubscriberUpdates(ctx context.Context, subscriberId string) (<-chan *models.Subscriber, error) - UsageUpdates(ctx context.Context, imsi string) (<-chan *models.UsageEvent, error) - AlertUpdates(ctx context.Context, severity models.AlertSeverity) (<-chan *models.Alert, error) - SystemStatsUpdates(ctx context.Context) (<-chan *models.SystemStats, error) -} - -func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { - return &executableSchema{ - resolvers: cfg.Resolvers, - } -} - -type executableSchema struct { - resolvers ResolverRoot -} - -func (e *executableSchema) Schema() *ast.Schema { - // This would be generated by gqlgen - return nil -} - -func (e *executableSchema) Complexity(ctx context.Context, typeName, fieldName string, childComplexity int, args map[string]any) (int, bool) { - // Default complexity calculation - return childComplexity, true -} - -func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { - // This would be generated by gqlgen - return nil -} - -func (e *executableSchema) Extensions() map[string]any { - return map[string]any{} -} diff --git a/apps/api-server/internal/graphql/resolvers.go b/apps/api-server/internal/graphql/resolvers.go deleted file mode 100644 index 46fc698..0000000 --- a/apps/api-server/internal/graphql/resolvers.go +++ /dev/null @@ -1,135 +0,0 @@ -// Package graphql hosts the GraphQL resolver, split across multiple files: -// -// resolvers_subscriber.go - Subscriber queries -// resolvers_usage.go - Usage events & stats -// resolvers_billing.go - Invoices, rating plans, top-up -// resolvers_mutations.go - Subscriber CRUD mutations -// resolvers_esim.go - eSIM + payment-method mutations -// resolvers_alerts.go - Alerts, system stats, maintenance -// resolvers_subscriptions.go - Streaming subscriptions + charging -// -// This file holds only the root Resolver struct, constructor, helpers, and -// the HTTP handler wiring. -package graphql - -import ( - "context" - "encoding/base64" - "fmt" - "strings" - "time" - - "github.com/99designs/gqlgen/graphql/handler" - "github.com/99designs/gqlgen/graphql/playground" - "github.com/gin-gonic/gin" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/config" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/database" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" -) - -// ensure used imports remain referenced when helpers below are the only consumers -var _ = context.Background -var _ = base64.StdEncoding - -// Resolver is the root GraphQL resolver. -type Resolver struct { - db *database.Database - subscriber *services.SubscriberService - charging *services.ChargingService - es2Service *services.ES2Service -} - -// NewResolver creates a new GraphQL resolver with wired-up services. -func NewResolver(db *database.Database, cfg *config.Config) *Resolver { - return &Resolver{ - db: db, - subscriber: services.NewSubscriberService(db, cfg), - charging: services.NewChargingService(db, cfg), - es2Service: services.NewES2Service(&cfg.ES2), - } -} - -// parseID decodes a GraphQL "Subscriber:" opaque ID to a uint. -func parseID(id string) (uint, error) { - var n int - _, err := fmt.Sscanf(id, "Subscriber:%d", &n) - if err != nil { - return 0, fmt.Errorf("invalid subscriber ID format") - } - return uint(n), nil -} - -// parseCursorOffset reads the numeric offset from a decoded cursor string. -func parseCursorOffset(cursor string) int { - var offset int - fmt.Sscanf(cursor, "%d", &offset) - return offset -} - -// strPtr returns a pointer to the given string. -func strPtr(s string) *string { return new(s) } - -// buildConnectionPageInfo builds a Relay-style PageInfo for a page of size count. -func buildConnectionPageInfo(offset, count int, total int64) *PageInfo { - var startCursor, endCursor *string - if count > 0 { - startCursor = strPtr(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+1))) - endCursor = strPtr(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+count))) - } - return &PageInfo{ - HasNextPage: int64(offset+count) < total, - HasPreviousPage: offset > 0, - StartCursor: startCursor, - EndCursor: endCursor, - } -} - -// buildUsageEventFilter builds a SQL WHERE clause from optional filters. -func buildUsageEventFilter(imsi *string, filter *UsageEventFilter) string { - conditions := []string{} - if imsi != nil { - conditions = append(conditions, fmt.Sprintf("imsi = '%s'", *imsi)) - } - if filter != nil { - if filter.UsageType != nil { - conditions = append(conditions, fmt.Sprintf("usage_type = '%s'", *filter.UsageType)) - } - if filter.TimestampGte != nil { - conditions = append(conditions, fmt.Sprintf("timestamp >= '%s'", filter.TimestampGte.Format(time.RFC3339))) - } - if filter.TimestampLte != nil { - conditions = append(conditions, fmt.Sprintf("timestamp <= '%s'", filter.TimestampLte.Format(time.RFC3339))) - } - if filter.CostMin != nil { - conditions = append(conditions, fmt.Sprintf("cost >= %f", *filter.CostMin)) - } - if filter.CostMax != nil { - conditions = append(conditions, fmt.Sprintf("cost <= %f", *filter.CostMax)) - } - } - if len(conditions) > 0 { - return "WHERE " + strings.Join(conditions, " AND ") - } - return "" -} - -// Query returns the QueryResolver (itself). -func (r *Resolver) Query() QueryResolver { return r } - -// Mutation returns the MutationResolver (itself). -func (r *Resolver) Mutation() MutationResolver { return r } - -// Subscription returns the SubscriptionResolver (itself). -func (r *Resolver) Subscription() SubscriptionResolver { return r } - -// SetupGraphQLHandler wires the GraphQL HTTP endpoint and playground. -func SetupGraphQLHandler(router *gin.Engine, resolver *Resolver) { - graphqlHandler := handler.NewDefaultServer( - NewExecutableSchema(Config{Resolvers: resolver}), - ) - router.POST("/graphql", gin.WrapH(graphqlHandler)) - if gin.Mode() == gin.DebugMode { - router.GET("/graphql", gin.WrapH(playground.Handler("GraphQL Playground", "/graphql"))) - } -} diff --git a/apps/api-server/internal/graphql/resolvers_alerts.go b/apps/api-server/internal/graphql/resolvers_alerts.go deleted file mode 100644 index 37dd359..0000000 --- a/apps/api-server/internal/graphql/resolvers_alerts.go +++ /dev/null @@ -1,74 +0,0 @@ -package graphql - -import ( - "context" - "encoding/base64" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" -) - -// Alerts resolves a paginated connection of alerts. -func (r *Resolver) Alerts(ctx context.Context, first *int, after *string, subscriberId *int, severity *models.AlertSeverity, resolved *bool) (*AlertConnection, error) { - limit := 50 - if first != nil { - limit = *first - } - offset := 0 - if after != nil { - cursor, err := base64.StdEncoding.DecodeString(*after) - if err != nil { - return nil, fmt.Errorf("invalid cursor") - } - offset = parseCursorOffset(string(cursor)) - } - - alerts, total, err := r.subscriber.ListAlerts(ctx, limit, offset, subscriberId, severity, resolved) - if err != nil { - return nil, err - } - - edges := make([]*AlertEdge, len(alerts)) - for i, alert := range alerts { - edges[i] = &AlertEdge{ - Node: alert, - Cursor: base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+i+1)), - } - } - - return &AlertConnection{ - Edges: edges, - PageInfo: buildConnectionPageInfo(offset, len(alerts), total), - TotalCount: int(total), - }, nil -} - -// SystemStats resolves system-wide stats. -func (r *Resolver) SystemStats(ctx context.Context) (*models.SystemStats, error) { - return r.charging.GetSystemStats(ctx) -} - -// HealthStatus resolves the platform health status. -func (r *Resolver) HealthStatus(ctx context.Context) (*models.HealthStatus, error) { - return r.charging.GetHealthStatus(ctx) -} - -// ResolveAlert marks an alert as resolved. -func (r *Resolver) ResolveAlert(ctx context.Context, alertId string) (*models.Alert, error) { - return r.subscriber.ResolveAlert(ctx, alertId) -} - -// CreateAlert creates a new alert from GraphQL input. -func (r *Resolver) CreateAlert(ctx context.Context, input CreateAlertInput) (*models.Alert, error) { - return r.subscriber.CreateAlert(ctx, &models.CreateAlertRequest{ - Type: models.AlertType(input.Type), - Severity: models.AlertSeverity(input.Severity), - Message: input.Message, - SubscriberID: input.SubscriberID, - }) -} - -// TriggerSystemMaintenance triggers a maintenance cycle. -func (r *Resolver) TriggerSystemMaintenance(ctx context.Context) (bool, error) { - return r.charging.TriggerMaintenance(ctx) -} diff --git a/apps/api-server/internal/graphql/resolvers_billing.go b/apps/api-server/internal/graphql/resolvers_billing.go deleted file mode 100644 index bae289f..0000000 --- a/apps/api-server/internal/graphql/resolvers_billing.go +++ /dev/null @@ -1,67 +0,0 @@ -package graphql - -import ( - "context" - "encoding/base64" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" -) - -// Invoices resolves a paginated connection of invoices. -func (r *Resolver) Invoices(ctx context.Context, first *int, after *string, imsi *string, status *models.InvoiceStatus) (*InvoiceConnection, error) { - limit := 20 - if first != nil { - limit = *first - } - offset := 0 - if after != nil { - cursor, err := base64.StdEncoding.DecodeString(*after) - if err != nil { - return nil, fmt.Errorf("invalid cursor") - } - offset = parseCursorOffset(string(cursor)) - } - - invoices, total, err := r.subscriber.ListInvoices(ctx, limit, offset, imsi, status) - if err != nil { - return nil, err - } - - edges := make([]*InvoiceEdge, len(invoices)) - for i, inv := range invoices { - edges[i] = &InvoiceEdge{ - Node: inv, - Cursor: base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+i+1)), - } - } - - return &InvoiceConnection{ - Edges: edges, - PageInfo: buildConnectionPageInfo(offset, len(invoices), total), - TotalCount: int(total), - }, nil -} - -// Invoice resolves a single invoice by ID. -func (r *Resolver) Invoice(ctx context.Context, id string) (*models.Invoice, error) { - return r.subscriber.GetInvoice(ctx, id) -} - -// RatingPlans resolves all available rating plans. -func (r *Resolver) RatingPlans(ctx context.Context) ([]*models.RatingPlan, error) { - return r.subscriber.ListRatingPlans(ctx) -} - -// RatingPlan resolves a single rating plan by plan_id. -func (r *Resolver) RatingPlan(ctx context.Context, planId string) (*models.RatingPlan, error) { - return r.subscriber.GetRatingPlan(ctx, planId) -} - -// TopUpBalance tops up a subscriber balance (mutation). -func (r *Resolver) TopUpBalance(ctx context.Context, imsi string, input TopUpInput) (*models.SubscriberAccount, error) { - return r.subscriber.TopUpBalance(ctx, imsi, &models.TopUpRequest{ - Amount: input.Amount, - PaymentMethodID: input.PaymentMethodID, - }) -} diff --git a/apps/api-server/internal/graphql/resolvers_esim.go b/apps/api-server/internal/graphql/resolvers_esim.go deleted file mode 100644 index 3e6e7a0..0000000 --- a/apps/api-server/internal/graphql/resolvers_esim.go +++ /dev/null @@ -1,59 +0,0 @@ -package graphql - -import ( - "context" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" -) - -// ProvisionESIM provisions an eSIM profile for the given IMSI. -func (r *Resolver) ProvisionESIM(ctx context.Context, imsi string) (*ESIMProvisionResult, error) { - sub, err := r.subscriber.GetSubscriberByIMSI(ctx, models.IMSI(imsi)) - if err != nil { - return nil, fmt.Errorf("subscriber not found for IMSI %s: %w", imsi, err) - } - result, err := r.es2Service.ProvisionProfile(ctx, sub) - if err != nil { - return nil, err - } - return &ESIMProvisionResult{ - ProfileID: result.ProfileID, - ActivationCode: result.Activation.ActivationCode, - }, nil -} - -// ActivateESIM activates the given eSIM profile. -func (r *Resolver) ActivateESIM(ctx context.Context, imsi string, profileId string) (bool, error) { - err := r.es2Service.ActivateProfile(ctx, imsi, profileId) - return err == nil, err -} - -// DeactivateESIM deactivates the given eSIM profile. -func (r *Resolver) DeactivateESIM(ctx context.Context, imsi string, profileId string) (bool, error) { - err := r.es2Service.DeactivateProfile(ctx, imsi, profileId) - return err == nil, err -} - -// AddPaymentMethod adds a payment method for the given subscriber. -func (r *Resolver) AddPaymentMethod(ctx context.Context, subscriberId int, input PaymentMethodInput) (*models.PaymentMethod, error) { - isDefault := false - if input.IsDefault != nil { - isDefault = *input.IsDefault - } - return r.subscriber.AddPaymentMethod(ctx, subscriberId, &models.AddPaymentMethodRequest{ - Type: models.PaymentMethodType(input.Type), - Token: input.Token, - IsDefault: isDefault, - }) -} - -// RemovePaymentMethod removes a payment method. -func (r *Resolver) RemovePaymentMethod(ctx context.Context, paymentMethodId string) (bool, error) { - return r.subscriber.RemovePaymentMethod(ctx, paymentMethodId) -} - -// SetDefaultPaymentMethod marks a payment method as default. -func (r *Resolver) SetDefaultPaymentMethod(ctx context.Context, paymentMethodId string) (*models.PaymentMethod, error) { - return r.subscriber.SetDefaultPaymentMethod(ctx, paymentMethodId) -} diff --git a/apps/api-server/internal/graphql/resolvers_mutations.go b/apps/api-server/internal/graphql/resolvers_mutations.go deleted file mode 100644 index f098096..0000000 --- a/apps/api-server/internal/graphql/resolvers_mutations.go +++ /dev/null @@ -1,87 +0,0 @@ -package graphql - -import ( - "context" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" -) - -// CreateSubscriber creates a new subscriber from GraphQL input. -func (r *Resolver) CreateSubscriber(ctx context.Context, input CreateSubscriberInput) (*models.Subscriber, error) { - req := &services.CreateSubscriberRequest{ - MSISDN: input.MSISDN, - FirstName: input.FirstName, - LastName: input.LastName, - Email: input.Email, - PlanID: uint(input.PlanID), - } - if input.OrganizationID != nil { - req.OrganizationID = *input.OrganizationID - } - if input.EUICCID != nil { - req.EUICCID = *input.EUICCID - } - return r.subscriber.CreateSubscriber(ctx, req) -} - -// UpdateSubscriber updates subscriber fields. -func (r *Resolver) UpdateSubscriber(ctx context.Context, id string, input UpdateSubscriberInput) (*models.Subscriber, error) { - sid, err := parseID(id) - if err != nil { - return nil, err - } - var planID *uint - if input.PlanID != nil { - p := uint(*input.PlanID) - planID = &p - } - return r.subscriber.UpdateSubscriber(ctx, sid, &services.UpdateSubscriberRequest{ - FirstName: input.FirstName, - LastName: input.LastName, - Email: input.Email, - PlanID: planID, - }) -} - -// DeleteSubscriber deletes a subscriber by GraphQL ID. -func (r *Resolver) DeleteSubscriber(ctx context.Context, id string) (bool, error) { - sid, err := parseID(id) - if err != nil { - return false, err - } - return r.subscriber.DeleteSubscriber(ctx, sid) -} - -// SuspendSubscriber suspends and returns the refreshed subscriber. -func (r *Resolver) SuspendSubscriber(ctx context.Context, id string) (*models.Subscriber, error) { - sid, err := parseID(id) - if err != nil { - return nil, err - } - if err := r.subscriber.SuspendSubscriber(ctx, sid); err != nil { - return nil, err - } - return r.subscriber.GetSubscriber(ctx, sid) -} - -// ActivateSubscriber activates and returns the subscriber. -func (r *Resolver) ActivateSubscriber(ctx context.Context, id string) (*models.Subscriber, error) { - sid, err := parseID(id) - if err != nil { - return nil, err - } - return r.subscriber.ActivateSubscriber(ctx, sid) -} - -// TerminateSubscriber terminates and returns the refreshed subscriber. -func (r *Resolver) TerminateSubscriber(ctx context.Context, id string) (*models.Subscriber, error) { - sid, err := parseID(id) - if err != nil { - return nil, err - } - if err := r.subscriber.TerminateSubscriber(ctx, sid); err != nil { - return nil, err - } - return r.subscriber.GetSubscriber(ctx, sid) -} diff --git a/apps/api-server/internal/graphql/resolvers_subscriber.go b/apps/api-server/internal/graphql/resolvers_subscriber.go deleted file mode 100644 index 3e85a36..0000000 --- a/apps/api-server/internal/graphql/resolvers_subscriber.go +++ /dev/null @@ -1,73 +0,0 @@ -package graphql - -import ( - "context" - "encoding/base64" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" -) - -// Subscriber resolves a single subscriber by GraphQL ID. -func (r *Resolver) Subscriber(ctx context.Context, id string) (*models.Subscriber, error) { - sid, err := parseID(id) - if err != nil { - return nil, err - } - return r.subscriber.GetSubscriber(ctx, sid) -} - -// SubscriberByImsi resolves a subscriber by IMSI. -func (r *Resolver) SubscriberByImsi(ctx context.Context, imsi string) (*models.Subscriber, error) { - return r.subscriber.GetSubscriberByIMSI(ctx, models.IMSI(imsi)) -} - -// Subscribers resolves a paginated connection of subscribers. -func (r *Resolver) Subscribers(ctx context.Context, first *int, after *string, filter *SubscriberFilter, sort *SubscriberSort) (*SubscriberConnection, error) { - limit := 20 - if first != nil { - limit = *first - } - offset := 0 - if after != nil { - cursor, err := base64.StdEncoding.DecodeString(*after) - if err != nil { - return nil, fmt.Errorf("invalid cursor") - } - offset = parseCursorOffset(string(cursor)) - } - - resp, err := r.subscriber.ListSubscribers(ctx, &services.ListSubscribersRequest{ - Page: offset/limit + 1, - PageSize: limit, - }) - if err != nil { - return nil, err - } - - edges := make([]*SubscriberEdge, len(resp.Subscribers)) - for i, sub := range resp.Subscribers { - s := sub - edges[i] = &SubscriberEdge{ - Node: &s, - Cursor: base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+i+1)), - } - } - - return &SubscriberConnection{ - Edges: edges, - PageInfo: buildConnectionPageInfo(offset, len(resp.Subscribers), resp.Total), - TotalCount: int(resp.Total), - }, nil -} - -// SubscriberAccount resolves a subscriber's account by IMSI. -func (r *Resolver) SubscriberAccount(ctx context.Context, imsi string) (*models.SubscriberAccount, error) { - return r.subscriber.GetAccount(ctx, imsi) -} - -// SearchSubscribers performs a subscriber search, capped at the given limit. -func (r *Resolver) SearchSubscribers(ctx context.Context, query string, limit int) ([]*models.Subscriber, error) { - return r.subscriber.SearchSubscribers(ctx, query, limit) -} diff --git a/apps/api-server/internal/graphql/resolvers_subscriptions.go b/apps/api-server/internal/graphql/resolvers_subscriptions.go deleted file mode 100644 index cada9f2..0000000 --- a/apps/api-server/internal/graphql/resolvers_subscriptions.go +++ /dev/null @@ -1,45 +0,0 @@ -package graphql - -import ( - "context" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" -) - -// SubscriberUpdates streams subscriber updates via a channel. -func (r *Resolver) SubscriberUpdates(ctx context.Context, subscriberId string) (<-chan *models.Subscriber, error) { - return r.subscriber.SubscribeToSubscriberUpdates(ctx, subscriberId) -} - -// UsageUpdates streams usage-event updates for an IMSI. -func (r *Resolver) UsageUpdates(ctx context.Context, imsi string) (<-chan *models.UsageEvent, error) { - return r.charging.SubscribeToUsageUpdates(ctx, imsi) -} - -// AlertUpdates streams alert updates filtered by severity. -func (r *Resolver) AlertUpdates(ctx context.Context, severity models.AlertSeverity) (<-chan *models.Alert, error) { - severityStr := string(severity) - return r.subscriber.SubscribeToAlertUpdates(ctx, &severityStr) -} - -// SystemStatsUpdates streams system stats updates. -func (r *Resolver) SystemStatsUpdates(ctx context.Context) (<-chan *models.SystemStats, error) { - return r.charging.SubscribeToSystemStatsUpdates(ctx) -} - -// ChargingSessions resolves a paginated connection of charging sessions. -// Placeholder implementation until the charging query surface is finalized. -func (r *Resolver) ChargingSessions(ctx context.Context, first *int, after *string, imsi *string, status *SessionStatus) (*ChargingSessionConnection, error) { - return &ChargingSessionConnection{ - Edges: []*ChargingSessionEdge{}, - PageInfo: &PageInfo{}, - TotalCount: 0, - }, nil -} - -// ChargingSession resolves a single charging session by ID. -// Placeholder implementation until the charging query surface is finalized. -func (r *Resolver) ChargingSession(ctx context.Context, sessionId string) (*ChargingSession, error) { - return nil, fmt.Errorf("charging session %s not found", sessionId) -} diff --git a/apps/api-server/internal/graphql/resolvers_test.go b/apps/api-server/internal/graphql/resolvers_test.go deleted file mode 100644 index 5bacf5b..0000000 --- a/apps/api-server/internal/graphql/resolvers_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package graphql - -import ( - "encoding/base64" - "fmt" - "strconv" - "strings" - "testing" -) - -func TestParseID_Valid(t *testing.T) { - id, err := parseID("Subscriber:42") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if id != 42 { - t.Errorf("expected 42, got %d", id) - } -} - -func TestParseID_Invalid(t *testing.T) { - _, err := parseID("invalid") - if err == nil { - t.Fatal("expected error for invalid ID format") - } -} - -func TestParseID_Zero(t *testing.T) { - id, err := parseID("Subscriber:0") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if id != 0 { - t.Errorf("expected 0, got %d", id) - } -} - -func TestEncodeCursor(t *testing.T) { - cursor := encodeCursor(42) - decoded, err := base64.StdEncoding.DecodeString(cursor) - if err != nil { - t.Fatalf("unexpected error decoding cursor: %v", err) - } - if string(decoded) != "cursor:42" { - t.Errorf("expected 'cursor:42', got '%s'", string(decoded)) - } -} - -func TestDecodeCursor_Valid(t *testing.T) { - encoded := base64.StdEncoding.EncodeToString([]byte("cursor:100")) - offset, err := decodeCursor(encoded) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if offset != 100 { - t.Errorf("expected 100, got %d", offset) - } -} - -func TestDecodeCursor_Invalid(t *testing.T) { - _, err := decodeCursor("not-base64!!!") - if err == nil { - t.Fatal("expected error for invalid cursor") - } -} - -func TestDecodeCursor_BadFormat(t *testing.T) { - encoded := base64.StdEncoding.EncodeToString([]byte("bad:format")) - _, err := decodeCursor(encoded) - if err == nil { - t.Fatal("expected error for bad cursor format") - } -} - -func TestBuildUsageFilterWhere_Empty(t *testing.T) { - result := buildUsageFilterWhere(nil) - if result != "" { - t.Errorf("expected empty string for nil filter, got '%s'", result) - } -} - -func TestBuildUsageFilterWhere_WithType(t *testing.T) { - usageType := UsageTypeData - filter := &UsageEventFilter{UsageType: &usageType} - result := buildUsageFilterWhere(filter) - if result == "" { - t.Fatal("expected non-empty WHERE clause") - } - expected := "WHERE usage_type = 'DATA'" - if result != expected { - t.Errorf("expected '%s', got '%s'", expected, result) - } -} - -func TestBuildUsageFilterWhere_WithCostRange(t *testing.T) { - min := 1.0 - max := 10.0 - filter := &UsageEventFilter{CostMin: &min, CostMax: &max} - result := buildUsageFilterWhere(filter) - if result == "" { - t.Fatal("expected non-empty WHERE clause") - } -} - -func TestResolverRoot_Methods(t *testing.T) { - r := &Resolver{} - if r.Query() != r { - t.Error("Query() should return receiver") - } - if r.Mutation() != r { - t.Error("Mutation() should return receiver") - } - if r.Subscription() != r { - t.Error("Subscription() should return receiver") - } -} - -// Helper functions for testing (these should match the implementations in resolvers.go) - -func encodeCursor(offset int) string { - return base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "cursor:%d", offset)) -} - -func decodeCursor(cursor string) (int, error) { - decoded, err := base64.StdEncoding.DecodeString(cursor) - if err != nil { - return 0, fmt.Errorf("invalid cursor encoding: %w", err) - } - - parts := strings.Split(string(decoded), ":") - if len(parts) != 2 || parts[0] != "cursor" { - return 0, fmt.Errorf("invalid cursor format") - } - - return strconv.Atoi(parts[1]) -} - -func buildUsageFilterWhere(filter *UsageEventFilter) string { - if filter == nil { - return "" - } - - var conditions []string - - if filter.UsageType != nil { - conditions = append(conditions, fmt.Sprintf("usage_type = '%s'", *filter.UsageType)) - } - - if filter.CostMin != nil { - conditions = append(conditions, fmt.Sprintf("cost >= %f", *filter.CostMin)) - } - - if filter.CostMax != nil { - conditions = append(conditions, fmt.Sprintf("cost <= %f", *filter.CostMax)) - } - - if len(conditions) > 0 { - return "WHERE " + strings.Join(conditions, " AND ") - } - - return "" -} diff --git a/apps/api-server/internal/graphql/resolvers_usage.go b/apps/api-server/internal/graphql/resolvers_usage.go deleted file mode 100644 index 9095fb3..0000000 --- a/apps/api-server/internal/graphql/resolvers_usage.go +++ /dev/null @@ -1,100 +0,0 @@ -package graphql - -import ( - "context" - "encoding/base64" - "fmt" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" -) - -// UsageEvents resolves a paginated connection of usage events. -func (r *Resolver) UsageEvents(ctx context.Context, first *int, after *string, imsi *string, filter *UsageEventFilter) (*UsageEventConnection, error) { - limit := 50 - if first != nil { - limit = *first - } - offset := 0 - if after != nil { - cursor, err := base64.StdEncoding.DecodeString(*after) - if err != nil { - return nil, fmt.Errorf("invalid cursor") - } - offset = parseCursorOffset(string(cursor)) - } - - where := buildUsageEventFilter(imsi, filter) - events, total, err := r.charging.ListUsageEvents(ctx, limit, offset, where) - if err != nil { - return nil, err - } - - edges := make([]*UsageEventEdge, len(events)) - for i, event := range events { - edges[i] = &UsageEventEdge{ - Node: event, - Cursor: base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%d", offset+i+1)), - } - } - - return &UsageEventConnection{ - Edges: edges, - PageInfo: buildConnectionPageInfo(offset, len(events), total), - TotalCount: int(total), - }, nil -} - -// UsageStats resolves aggregate usage stats for a subscriber and period. -func (r *Resolver) UsageStats(ctx context.Context, input UsageStatsInput) (*UsageStats, error) { - stats, err := r.charging.GetUsageStats(ctx, input.IMSI, string(input.Period)) - if err != nil { - return nil, err - } - return &UsageStats{ - DataUsage: stats.DataUsage, - VoiceUsage: stats.VoiceUsage, - SmsUsage: stats.SmsUsage, - Cost: stats.Cost, - Period: stats.Period, - Trend: &UsageTrend{ - Direction: TrendDirection(stats.Trend.Direction), - Percentage: stats.Trend.Percentage, - PeriodOverPeriod: stats.Trend.PeriodOverPeriod, - }, - }, nil -} - -// RealTimeUsage resolves real-time usage for a subscriber by IMSI. -func (r *Resolver) RealTimeUsage(ctx context.Context, imsi string) (*RealTimeUsage, error) { - usage, err := r.charging.GetRealTimeUsage(ctx, imsi) - if err != nil { - return nil, err - } - - var cs *CurrentSession - if usage.CurrentSession != nil { - cs = &CurrentSession{ - SessionID: usage.CurrentSession.SessionID, - StartTime: usage.CurrentSession.StartTime, - DataUsed: usage.CurrentSession.DataUsed, - VoiceUsed: usage.CurrentSession.VoiceUsed, - SmsUsed: usage.CurrentSession.SmsUsed, - Cost: usage.CurrentSession.Cost, - } - } - - return &RealTimeUsage{ - CurrentSession: cs, - TodayUsage: &TodayUsage{ - DataUsed: usage.TodayUsage.DataUsed, - VoiceUsed: usage.TodayUsage.VoiceUsed, - SmsUsed: usage.TodayUsage.SmsUsed, - Cost: usage.TodayUsage.Cost, - }, - }, nil -} - -// SearchUsageEvents searches usage events, capped at limit. -func (r *Resolver) SearchUsageEvents(ctx context.Context, query string, limit int) ([]*models.UsageEvent, error) { - return r.charging.SearchUsageEvents(ctx, query, limit) -} diff --git a/apps/api-server/internal/graphql/schema.graphql b/apps/api-server/internal/graphql/schema.graphql deleted file mode 100644 index 9ab3da3..0000000 --- a/apps/api-server/internal/graphql/schema.graphql +++ /dev/null @@ -1,483 +0,0 @@ -scalar DateTime -scalar JSON - -enum SubscriberStatus { - ACTIVE - INACTIVE - SUSPENDED - TERMINATED - PROVISIONING -} - -enum ProfileStatus { - ACTIVE - INACTIVE - DOWNLOADING - FAILED -} - -enum UsageType { - DATA - VOICE - SMS -} - -enum SessionStatus { - ACTIVE - COMPLETED - TERMINATED -} - -enum PaymentMethodType { - CREDIT_CARD - BANK_ACCOUNT -} - -enum InvoiceStatus { - DRAFT - PENDING - PAID - OVERDUE - CANCELLED -} - -enum AlertType { - LOW_BALANCE - HIGH_USAGE - PAYMENT_FAILED - SYSTEM_ERROR -} - -enum AlertSeverity { - LOW - MEDIUM - HIGH - CRITICAL -} - -type Subscriber { - id: ID! - imsi: String! - msisdn: String! - firstName: String! - lastName: String! - email: String! - organizationId: String - planId: Int! - euiccId: String - profileId: String - profileStatus: ProfileStatus! - status: SubscriberStatus! - balance: Float! - dataLimit: Float! - dataUsed: Float! - voiceLimit: Float! - voiceUsed: Float! - smsLimit: Float! - smsUsed: Float! - createdAt: DateTime! - updatedAt: DateTime! - - account: SubscriberAccount! - usageEvents(first: Int, after: String, filter: UsageEventFilter): UsageEventConnection! - invoices(first: Int, after: String, status: InvoiceStatus): InvoiceConnection! - alerts(first: Int, after: String, severity: AlertSeverity): AlertConnection! - ratingPlan: RatingPlan! -} - -type SubscriberAccount { - imsi: String! - balance: Float! - dataLimit: Float! - dataUsed: Float! - voiceLimit: Float! - voiceUsed: Float! - smsLimit: Float! - smsUsed: Float! - status: SubscriberStatus! - lastUpdated: DateTime! -} - -type UsageEvent { - id: ID! - imsi: String! - sessionId: String! - usageType: UsageType! - volume: Float! - timestamp: DateTime! - rate: Float! - cost: Float! - - subscriber: Subscriber! -} - -type RatingPlan { - planId: String! - name: String! - dataRate: Float! - voiceRate: Float! - smsRate: Float! - monthlyFee: Float! - dataLimit: Float! - voiceLimit: Float! - smsLimit: Float! - - subscribers(first: Int, after: String, status: SubscriberStatus): SubscriberConnection! -} - -type ChargingSession { - sessionId: String! - imsi: String! - startTime: DateTime! - endTime: DateTime - dataBytes: Float! - voiceSeconds: Float! - smsCount: Float! - totalCost: Float! - status: SessionStatus! - - subscriber: Subscriber! - usageEvents(first: Int, after: String): UsageEventConnection! -} - -type PaymentMethod { - id: String! - type: PaymentMethodType! - last4: String! - expiryMonth: Int! - expiryYear: Int! - brand: String! - isDefault: Boolean! -} - -type Invoice { - id: String! - subscriberId: Int! - amount: Float! - currency: String! - status: InvoiceStatus! - dueDate: DateTime! - createdAt: DateTime! - lineItems: [InvoiceLineItem!]! - - subscriber: Subscriber! -} - -type InvoiceLineItem { - description: String! - quantity: Int! - unitPrice: Float! - amount: Float! -} - -type Alert { - id: String! - type: AlertType! - severity: AlertSeverity! - message: String! - subscriberId: Int - timestamp: DateTime! - resolved: Boolean! - - subscriber: Subscriber -} - -type SystemStats { - activeSessions: Int! - totalAccounts: Int! - blockedUsers: Int! - lowBalanceAlerts: Int! - uptime: Float! -} - -type HealthStatus { - redisConnected: Boolean! - activeSync: Boolean! - lastSync: DateTime! - memoryUsage: Float! -} - -type SubscriberConnection { - edges: [SubscriberEdge!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type SubscriberEdge { - node: Subscriber! - cursor: String! -} - -type UsageEventConnection { - edges: [UsageEventEdge!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type UsageEventEdge { - node: UsageEvent! - cursor: String! -} - -type InvoiceConnection { - edges: [InvoiceEdge!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type InvoiceEdge { - node: Invoice! - cursor: String! -} - -type AlertConnection { - edges: [AlertEdge!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type AlertEdge { - node: Alert! - cursor: String! -} - -type PageInfo { - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - endCursor: String -} - -type SubscriberFilter { - status: SubscriberStatus - organizationId: String - planId: Int - search: String - balanceMin: Float - balanceMax: Float - dataUsageMin: Float - dataUsageMax: Float -} - -type UsageEventFilter { - usageType: UsageType - timestampGte: DateTime - timestampLte: DateTime - costMin: Float - costMax: Float -} - -type CreateSubscriberInput { - msisdn: String! - firstName: String! - lastName: String! - email: String! - organizationId: String - planId: Int! - euiccId: String -} - -type UpdateSubscriberInput { - firstName: String - lastName: String - email: String - organizationId: String - planId: Int -} - -type TopUpInput { - amount: Float! - paymentMethodId: String -} - -type UsageStatsInput { - imsi: String! - period: UsagePeriod! -} - -enum UsagePeriod { - DAILY - WEEKLY - MONTHLY - YEARLY -} - -type Query { - subscriber(id: ID!): Subscriber - subscriberByImsi(imsi: String!): Subscriber - subscribers( - first: Int - after: String - filter: SubscriberFilter - sort: SubscriberSort - ): SubscriberConnection! - - subscriberAccount(imsi: String!): SubscriberAccount - - usageEvents( - first: Int - after: String - imsi: String - filter: UsageEventFilter - ): UsageEventConnection! - usageStats(input: UsageStatsInput!): UsageStats! - realtimeUsage(imsi: String!): RealTimeUsage! - - invoices( - first: Int - after: String - imsi: String - status: InvoiceStatus - ): InvoiceConnection! - invoice(id: String!): Invoice - - ratingPlans: [RatingPlan!]! - ratingPlan(planId: String!): RatingPlan - - chargingSessions( - first: Int - after: String - imsi: String - status: SessionStatus - ): ChargingSessionConnection! - chargingSession(sessionId: String!): ChargingSession - - alerts( - first: Int - after: String - subscriberId: Int - severity: AlertSeverity - resolved: Boolean - ): AlertConnection! - - systemStats: SystemStats! - healthStatus: HealthStatus! - - searchSubscribers(query: String!, limit: Int): [Subscriber!]! - searchUsageEvents(query: String!, limit: Int): [UsageEvent!]! -} - -type Mutation { - createSubscriber(input: CreateSubscriberInput!): Subscriber! - updateSubscriber(id: ID!, input: UpdateSubscriberInput!): Subscriber! - deleteSubscriber(id: ID!): Boolean! - - topUpBalance(imsi: String!, input: TopUpInput!): SubscriberAccount! - - suspendSubscriber(id: ID!): Subscriber! - activateSubscriber(id: ID!): Subscriber! - terminateSubscriber(id: ID!): Subscriber! - - provisionESIM(imsi: String!): ESIMProvisionResult! - activateESIM(imsi: String!, profileId: String!): Boolean! - deactivateESIM(imsi: String!, profileId: String!): Boolean! - - addPaymentMethod(subscriberId: Int!, input: PaymentMethodInput!): PaymentMethod! - removePaymentMethod(paymentMethodId: String!): Boolean! - setDefaultPaymentMethod(paymentMethodId: String!): PaymentMethod! - addPaymentMethod(subscriberId: Int!, input: PaymentMethodInput!): PaymentMethod! - removePaymentMethod(paymentMethodId: String!): Boolean! - setDefaultPaymentMethod(paymentMethodId: String!): PaymentMethod! - - resolveAlert(alertId: String!): Alert! - createAlert(input: CreateAlertInput!): Alert! - - triggerSystemMaintenance: Boolean! -} - -type Subscription { - # Real-time subscriptions - subscriberUpdates(subscriberId: ID!): Subscriber! - usageUpdates(imsi: String!): UsageEvent! - alertUpdates(severity: AlertSeverity): Alert! - systemStatsUpdates: SystemStats! -} - -type UsageStats { - dataUsage: Float! - voiceUsage: Float! - smsUsage: Float! - cost: Float! - period: String! - trend: UsageTrend! -} - -type UsageTrend { - direction: TrendDirection! - percentage: Float! - periodOverPeriod: Float! -} - -enum TrendDirection { - UP - DOWN - STABLE -} - -type RealTimeUsage { - currentSession: CurrentSession - todayUsage: TodayUsage! -} - -type CurrentSession { - sessionId: String! - startTime: DateTime! - dataUsed: Float! - voiceUsed: Float! - smsUsed: Float! - cost: Float! -} - -type TodayUsage { - dataUsed: Float! - voiceUsed: Float! - smsUsed: Float! - cost: Float! -} - -type ESIMProvisionResult { - profileId: String! - activationCode: String! - qrCode: String! -} - -type PaymentMethodInput { - type: PaymentMethodType! - token: String! - isDefault: Boolean -} - -type CreateAlertInput { - type: AlertType! - severity: AlertSeverity! - message: String! - subscriberId: Int -} - -type ChargingSessionConnection { - edges: [ChargingSessionEdge!]! - pageInfo: PageInfo! - totalCount: Int! -} - -type ChargingSessionEdge { - node: ChargingSession! - cursor: String! -} - -enum SubscriberSort { - CREATED_AT_ASC - CREATED_AT_DESC - UPDATED_AT_ASC - UPDATED_AT_DESC - FIRST_NAME_ASC - FIRST_NAME_DESC - LAST_NAME_ASC - LAST_NAME_DESC - EMAIL_ASC - EMAIL_DESC - BALANCE_ASC - BALANCE_DESC - DATA_USED_ASC - DATA_USED_DESC -}