refactor: Remove generated GraphQL code and resolver files from api-server

- Delete generated.go containing GraphQL type definitions, resolvers, and schema
- Delete resolvers.go containing root Resolver struct and helper functions
- Remove GraphQL enum constants (SubscriberStatus, ProfileStatus, UsageType, etc.)
- Remove GraphQL input types (SubscriberFilter, CreateSubscriberInput, etc.)
- Remove GraphQL output types (PageInfo, SubscriberConnection, UsageStats, etc.)
- Remove resolver interfaces
This commit is contained in:
nutcas3
2026-04-30 04:57:58 +03:00
parent 9b64366e25
commit f8504de4ef
11 changed files with 0 additions and 1660 deletions
@@ -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{}
}
@@ -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:<n>" 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")))
}
}
@@ -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)
}
@@ -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,
})
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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 ""
}
@@ -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)
}
@@ -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
}