mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +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