CreateOrUpdateChargingPileCommandHandler.cs 10.8 KB
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Rcs.Application.Common;
using Rcs.Application.MessageBus.Commands.ChargingPile;
using Rcs.Domain.Entities;
using Rcs.Domain.Repositories;

namespace Rcs.Infrastructure.MessageBus.Handlers.Commands.ChargingPile
{
    /// <summary>
    /// Create or update charging pile command handler.
    /// </summary>
    public class CreateOrUpdateChargingPileCommandHandler : IConsumer<CreateOrUpdateChargingPileCommand>
    {
        private readonly ILogger<CreateOrUpdateChargingPileCommandHandler> _logger;
        private readonly IChargingPileRepository _repository;
        private readonly IMapNodeRepository _mapNodeRepository;
        private readonly IRobotRepository _robotRepository;

        public CreateOrUpdateChargingPileCommandHandler(
            ILogger<CreateOrUpdateChargingPileCommandHandler> logger,
            IChargingPileRepository repository,
            IMapNodeRepository mapNodeRepository,
            IRobotRepository robotRepository)
        {
            _logger = logger;
            _repository = repository;
            _mapNodeRepository = mapNodeRepository;
            _robotRepository = robotRepository;
        }

        public async Task Consume(ConsumeContext<CreateOrUpdateChargingPileCommand> context)
        {
            var command = context.Message;

            try
            {
                ValidateCommand(command);

                var supportedRobotModels = command.SupportedRobotModels
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Select(x => x.Trim())
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

                var boundRobotIds = command.BoundRobotIds
                    .Where(x => Guid.TryParse(x, out _))
                    .Select(Guid.Parse)
                    .Distinct()
                    .ToList();

                Guid? currentChargingRobotId = null;
                if (!string.IsNullOrWhiteSpace(command.CurrentChargingRobotId))
                {
                    if (!Guid.TryParse(command.CurrentChargingRobotId, out var parsedCurrentChargingRobotId))
                    {
                        throw new InvalidOperationException("CurrentChargingRobotId format is invalid");
                    }

                    var currentChargingRobot = await _robotRepository.GetByIdAsync(parsedCurrentChargingRobotId, context.CancellationToken);
                    if (currentChargingRobot == null)
                    {
                        throw new InvalidOperationException($"Current charging robot not found: {parsedCurrentChargingRobotId}");
                    }

                    currentChargingRobotId = parsedCurrentChargingRobotId;
                }

                Guid? mapNodeId = null;
                if (!string.IsNullOrWhiteSpace(command.MapNodeId))
                {
                    if (!Guid.TryParse(command.MapNodeId, out var parsedMapNodeId))
                    {
                        throw new InvalidOperationException("MapNodeId format is invalid");
                    }

                    var mapNode = await _mapNodeRepository.GetByIdAsync(parsedMapNodeId, context.CancellationToken);
                    if (mapNode == null)
                    {
                        throw new InvalidOperationException($"Map node not found: {parsedMapNodeId}");
                    }

                    if (mapNode.Type != MapNodeTYPE.charger)
                    {
                        throw new InvalidOperationException($"Map node must be charger type: {parsedMapNodeId}");
                    }

                    mapNodeId = parsedMapNodeId;
                }

                if (!Guid.TryParse(command.PileId, out var pileId))
                {
                    await EnsureUniqueFields(command, null, mapNodeId, context.CancellationToken);

                    var entity = new Domain.Entities.ChargingPile
                    {
                        PileId = Guid.NewGuid(),
                        PileCode = command.PileCode.Trim(),
                        PileName = command.PileName.Trim(),
                        IpAddress = command.IpAddress.Trim(),
                        Port = command.Port,
                        MinChargingMinutes = command.MinChargingMinutes,
                        FullChargeThreshold = command.FullChargeThreshold,
                        SupportedRobotModels = supportedRobotModels,
                        BoundRobotIds = boundRobotIds,
                        CurrentChargingRobotId = currentChargingRobotId,
                        MapNodeId = mapNodeId,
                        AutoStartThreshold = command.AutoStartThreshold,
                        ResumeThreshold = command.ResumeThreshold,
                        MaxChargingMinutes = command.MaxChargingMinutes,
                        QueueTimeoutMinutes = command.QueueTimeoutMinutes,
                        Priority = command.Priority,
                        AllowTaskInterrupt = command.AllowTaskInterrupt,
                        IsActive = command.IsActive,
                        CreatedAt = DateTime.Now,
                        UpdatedAt = DateTime.Now,
                    };

                    await _repository.AddAsync(entity, context.CancellationToken);
                    await _repository.SaveChangesAsync(context.CancellationToken);
                }
                else
                {
                    var entity = await _repository.GetByIdAsync(pileId, context.CancellationToken);
                    if (entity == null)
                    {
                        throw new InvalidOperationException($"Charging pile not found: {pileId}");
                    }

                    await EnsureUniqueFields(command, pileId, mapNodeId, context.CancellationToken);

                    entity.PileCode = command.PileCode.Trim();
                    entity.PileName = command.PileName.Trim();
                    entity.IpAddress = command.IpAddress.Trim();
                    entity.Port = command.Port;
                    entity.MinChargingMinutes = command.MinChargingMinutes;
                    entity.FullChargeThreshold = command.FullChargeThreshold;
                    entity.SupportedRobotModels = supportedRobotModels;
                    entity.BoundRobotIds = boundRobotIds;
                    entity.CurrentChargingRobotId = currentChargingRobotId;
                    entity.MapNodeId = mapNodeId;
                    entity.AutoStartThreshold = command.AutoStartThreshold;
                    entity.ResumeThreshold = command.ResumeThreshold;
                    entity.MaxChargingMinutes = command.MaxChargingMinutes;
                    entity.QueueTimeoutMinutes = command.QueueTimeoutMinutes;
                    entity.Priority = command.Priority;
                    entity.AllowTaskInterrupt = command.AllowTaskInterrupt;
                    entity.IsActive = command.IsActive;
                    entity.UpdatedAt = DateTime.Now;

                    await _repository.UpdateAsync(entity, context.CancellationToken);
                    await _repository.SaveChangesAsync(context.CancellationToken);
                }

                await context.RespondAsync(ApiResponse.Successful());
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Create or update charging pile failed");
                await context.RespondAsync(ApiResponse.Failed(ex.Message));
            }
        }

        private static void ValidateCommand(CreateOrUpdateChargingPileCommand command)
        {
            if (string.IsNullOrWhiteSpace(command.PileCode))
            {
                throw new InvalidOperationException("PileCode is required");
            }

            if (string.IsNullOrWhiteSpace(command.PileName))
            {
                throw new InvalidOperationException("PileName is required");
            }

            if (string.IsNullOrWhiteSpace(command.IpAddress))
            {
                throw new InvalidOperationException("IpAddress is required");
            }

            if (command.Port <= 0 || command.Port > 65535)
            {
                throw new InvalidOperationException("Port must be between 1 and 65535");
            }

            if (command.MinChargingMinutes < 0)
            {
                throw new InvalidOperationException("MinChargingMinutes cannot be negative");
            }

            if (command.MaxChargingMinutes < command.MinChargingMinutes)
            {
                throw new InvalidOperationException("MaxChargingMinutes cannot be less than MinChargingMinutes");
            }

            if (command.FullChargeThreshold is < 0 or > 100)
            {
                throw new InvalidOperationException("FullChargeThreshold must be between 0 and 100");
            }

            if (command.AutoStartThreshold is < 0 or > 100)
            {
                throw new InvalidOperationException("AutoStartThreshold must be between 0 and 100");
            }

            if (command.ResumeThreshold is < 0 or > 100)
            {
                throw new InvalidOperationException("ResumeThreshold must be between 0 and 100");
            }

            if (command.AutoStartThreshold > command.ResumeThreshold)
            {
                throw new InvalidOperationException("AutoStartThreshold cannot be greater than ResumeThreshold");
            }

            if (command.QueueTimeoutMinutes < 0)
            {
                throw new InvalidOperationException("QueueTimeoutMinutes cannot be negative");
            }
        }

        private async Task EnsureUniqueFields(
            CreateOrUpdateChargingPileCommand command,
            Guid? currentId,
            Guid? mapNodeId,
            CancellationToken cancellationToken)
        {
            var byCode = await _repository.GetByPileCodeAsync(command.PileCode.Trim(), cancellationToken);
            if (byCode != null && byCode.PileId != currentId)
            {
                throw new InvalidOperationException($"PileCode already exists: {command.PileCode}");
            }

            var byIpPort = await _repository.GetByIpAndPortAsync(command.IpAddress.Trim(), command.Port, cancellationToken);
            if (byIpPort != null && byIpPort.PileId != currentId)
            {
                throw new InvalidOperationException($"IP and Port already exists: {command.IpAddress}:{command.Port}");
            }

            if (mapNodeId.HasValue)
            {
                var byMapNode = await _repository.GetQueryable()
                    .FirstOrDefaultAsync(p => p.MapNodeId == mapNodeId.Value, cancellationToken);
                if (byMapNode != null && byMapNode.PileId != currentId)
                {
                    throw new InvalidOperationException($"MapNodeId already bound by another charging pile: {mapNodeId.Value}");
                }
            }
        }
    }
}