mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-12 06:55:15 +00:00
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:
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user