package simstatus import ( "context" "fmt" "strings" zsetconsumer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consumer/zset" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset" simstatusrepo "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/simstatus" ) const ( telecomRetryableCode = "S00004" telecomStatusTemplate = "sim_dx_v1" chanGatewayInternalError = 9999 // tokenExpiredCode is the carrier code for an expired/invalid upstream // token. The gateway already invalidates and refreshes the token once on // this code; when it still reaches the worker, retrying in a later round // lets a refreshed token succeed instead of parking the card as a terminal // business failure. tokenExpiredCode = 12021 ) type StatusClient interface { QuerySimCardStatus(context.Context, *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) } type StatusRepository interface { ApplyBatch(context.Context, []simstatusrepo.Result) error } type Handler struct { client StatusClient repo StatusRepository networkRetries int } func NewHandler(client StatusClient, repo StatusRepository, networkRetries ...int) (*Handler, error) { if client == nil || repo == nil { return nil, fmt.Errorf("sim status handler dependencies are incomplete") } retries := 0 if len(networkRetries) > 0 && networkRetries[0] > 0 { retries = networkRetries[0] } return &Handler{client: client, repo: repo, networkRetries: retries}, nil } func (h *Handler) Query(ctx context.Context, task queue.Task) zsetconsumer.Result { result := zsetconsumer.Result{Task: task} input := &model.SimCardStatusInput{SimKeys: []*model.SimKey{{ ChanKey: task.ChanKey, ICCID: task.ICCID, IMSI: task.IMSI, MSISDN: task.MSISDN, }}} var ( output []model.SimBaseRes[model.SimCardStatusItem] err error ) for attempt := 0; attempt <= h.networkRetries; attempt++ { output, err = h.client.QuerySimCardStatus(ctx, input) if err == nil { break } if ctx.Err() != nil { break } } if err != nil { result.ErrorClass = "network" result.Retryable = true result.Message = err.Error() return result } if len(output) == 0 { result.ErrorClass = "protocol" result.Retryable = true result.Message = "chan returned empty status result" return result } item := output[0] if item.SimKey == nil { item.SimKey = &model.SimKey{ChanKey: task.ChanKey, ICCID: task.ICCID, IMSI: task.IMSI, MSISDN: task.MSISDN} } if task.ICCID != "" && item.SimKey.ICCID != task.ICCID { result.ErrorClass = "protocol" result.Retryable = true result.Message = "chan returned mismatched card identifier" return result } result.Code = item.Code result.Message = item.Message result.ErrCode = item.ErrCode if item.Code != 0 { result.ErrorClass = "business" if model.IsTelecomTemplate(task.TempKey) && strings.EqualFold(strings.TrimSpace(item.ErrCode), telecomRetryableCode) { result.ErrorClass = "carrier_retry" result.Retryable = true } else if item.Code == chanGatewayInternalError && strings.TrimSpace(item.ErrCode) == "" { result.ErrorClass = "gateway" result.Retryable = true } else if item.Code == tokenExpiredCode { result.ErrorClass = "token_expired" result.Retryable = true } return result } result.Success = true status := item.ResData.CardStatus result.CardStatus = &status return result } func (h *Handler) Apply(ctx context.Context, results []zsetconsumer.Result) error { updates := make([]simstatusrepo.Result, 0, len(results)) for _, result := range results { if result.Success && result.CardStatus != nil { updates = append(updates, simstatusrepo.Result{CardID: result.Task.CardID, CardStatus: *result.CardStatus}) } } return h.repo.ApplyBatch(ctx, updates) } // ShouldRetryNextRound is the full-status-sync retry policy. It is passed to // the Run-aware ZSet consumer only; single-card tasks retain lease recovery. func ShouldRetryNextRound(result zsetconsumer.Result) bool { return !result.Success && result.Retryable }