add voice service call service and more
This commit is contained in:
56
src/GreenHome.Api/Controllers/AuthController.cs
Normal file
56
src/GreenHome.Api/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using GreenHome.Application;
|
||||||
|
|
||||||
|
namespace GreenHome.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IAuthService authService;
|
||||||
|
|
||||||
|
public AuthController(IAuthService authService)
|
||||||
|
{
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("send-code")]
|
||||||
|
public async Task<ActionResult<SendCodeResponse>> SendCode(
|
||||||
|
[FromBody] SendCodeRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await authService.SendVerificationCodeAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return BadRequest(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("verify-code")]
|
||||||
|
public async Task<ActionResult<VerifyCodeResponse>> VerifyCode(
|
||||||
|
[FromBody] VerifyCodeRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await authService.VerifyCodeAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return BadRequest(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("can-resend")]
|
||||||
|
public async Task<ActionResult<bool>> CanResend(
|
||||||
|
[FromQuery] string mobile,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var canResend = await authService.CanResendCodeAsync(mobile, cancellationToken);
|
||||||
|
return Ok(new { canResend });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -35,4 +35,31 @@ public class DevicesController : ControllerBase
|
|||||||
var id = await deviceService.AddDeviceAsync(dto, cancellationToken);
|
var id = await deviceService.AddDeviceAsync(dto, cancellationToken);
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("user/{userId}")]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<DeviceDto>>> GetUserDevices(int userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await deviceService.GetUserDevicesAsync(userId, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("filtered")]
|
||||||
|
public async Task<ActionResult<PagedResult<DeviceDto>>> GetDevices(
|
||||||
|
[FromQuery] int userId,
|
||||||
|
[FromQuery] string? search = null,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 10,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var filter = new DeviceFilter
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Search = search,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await deviceService.GetDevicesAsync(filter, cancellationToken);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
src/GreenHome.Api/Controllers/SmsTestController.cs
Normal file
138
src/GreenHome.Api/Controllers/SmsTestController.cs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
using GreenHome.Sms.Ippanel;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace GreenHome.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for testing SMS service
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class SmsTestController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ISmsService smsService;
|
||||||
|
private readonly ILogger<SmsTestController> logger;
|
||||||
|
|
||||||
|
public SmsTestController(ISmsService smsService, ILogger<SmsTestController> logger)
|
||||||
|
{
|
||||||
|
this.smsService = smsService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test sending Webservice SMS
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">SMS request details</param>
|
||||||
|
/// <returns>SMS response</returns>
|
||||||
|
[HttpPost("webservice")]
|
||||||
|
public async Task<IActionResult> TestWebserviceSms([FromBody] WebserviceSmsRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing Webservice SMS to {Recipient}", request.Recipient);
|
||||||
|
|
||||||
|
var result = await smsService.SendWebserviceSmsAsync(request);
|
||||||
|
|
||||||
|
logger.LogInformation("Webservice SMS sent successfully.");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "پیامک با موفقیت ارسال شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending Webservice SMS");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال پیامک",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test sending Pattern SMS
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Pattern SMS request details</param>
|
||||||
|
/// <returns>SMS response</returns>
|
||||||
|
[HttpPost("pattern")]
|
||||||
|
public async Task<IActionResult> TestPatternSms([FromBody] PatternSmsRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing Pattern SMS to {Recipient} with pattern {PatternCode}",
|
||||||
|
request.Recipients, request.PatternCode);
|
||||||
|
|
||||||
|
var result = await smsService.SendPatternSmsAsync(request);
|
||||||
|
|
||||||
|
logger.LogInformation("Pattern SMS sent successfully");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "پیامک با موفقیت ارسال شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending Pattern SMS");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال پیامک",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick test with default values (for easy testing)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phoneNumber">Phone number to send SMS to</param>
|
||||||
|
/// <returns>SMS response</returns>
|
||||||
|
[HttpPost("quick-test/{phoneNumber}")]
|
||||||
|
public async Task<IActionResult> QuickTest(string phoneNumber)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Quick test SMS to {PhoneNumber}", phoneNumber);
|
||||||
|
|
||||||
|
var request = new WebserviceSmsRequest
|
||||||
|
{
|
||||||
|
Sender = "10001001", // شما میتوانید این را از تنظیمات بخوانید
|
||||||
|
Recipient = phoneNumber,
|
||||||
|
Message = "این یک پیامک تست از سرویس GreenHome است. " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await smsService.SendWebserviceSmsAsync(request);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "پیامک تست با موفقیت ارسال شد",
|
||||||
|
data = result,
|
||||||
|
testInfo = new
|
||||||
|
{
|
||||||
|
phoneNumber,
|
||||||
|
sentAt = DateTime.Now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error in quick test SMS");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال پیامک تست",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,10 +8,12 @@ namespace GreenHome.Api.Controllers;
|
|||||||
public class TelemetryController : ControllerBase
|
public class TelemetryController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ITelemetryService telemetryService;
|
private readonly ITelemetryService telemetryService;
|
||||||
|
private readonly IAlertService alertService;
|
||||||
|
|
||||||
public TelemetryController(ITelemetryService telemetryService)
|
public TelemetryController(ITelemetryService telemetryService, IAlertService alertService)
|
||||||
{
|
{
|
||||||
this.telemetryService = telemetryService;
|
this.telemetryService = telemetryService;
|
||||||
|
this.alertService = alertService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -37,6 +39,26 @@ public class TelemetryController : ControllerBase
|
|||||||
TimestampUtc = DateTime.UtcNow
|
TimestampUtc = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
var id = await telemetryService.AddAsync(dto, cancellationToken);
|
var id = await telemetryService.AddAsync(dto, cancellationToken);
|
||||||
|
|
||||||
|
// Check and send alerts if needed (fire and forget)
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get deviceId from the saved telemetry record
|
||||||
|
var deviceId = dto.DeviceId;
|
||||||
|
if (deviceId > 0)
|
||||||
|
{
|
||||||
|
await alertService.CheckAndSendAlertsAsync(deviceId, dto, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Log error but don't fail the request
|
||||||
|
// Errors are logged in AlertService
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
270
src/GreenHome.Api/Controllers/VoiceCallTestController.cs
Normal file
270
src/GreenHome.Api/Controllers/VoiceCallTestController.cs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
using GreenHome.VoiceCall.Avanak;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace GreenHome.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller for testing Voice Call service (Avanak)
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class VoiceCallTestController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVoiceCallService voiceCallService;
|
||||||
|
private readonly ILogger<VoiceCallTestController> logger;
|
||||||
|
|
||||||
|
public VoiceCallTestController(IVoiceCallService voiceCallService, ILogger<VoiceCallTestController> logger)
|
||||||
|
{
|
||||||
|
this.voiceCallService = voiceCallService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test getting account status
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Account status response</returns>
|
||||||
|
[HttpGet("account-status")]
|
||||||
|
public async Task<IActionResult> GetAccountStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing AccountStatus");
|
||||||
|
|
||||||
|
var result = await voiceCallService.GetAccountStatusAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("AccountStatus retrieved successfully");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "وضعیت حساب با موفقیت دریافت شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error getting account status");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در دریافت وضعیت حساب",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test generating TTS (Text-to-Speech)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">TTS generation request</param>
|
||||||
|
/// <returns>TTS generation response</returns>
|
||||||
|
[HttpPost("generate-tts")]
|
||||||
|
public async Task<IActionResult> GenerateTTS([FromBody] GenerateTTSRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing GenerateTTS for text: {Text}", request.Text);
|
||||||
|
|
||||||
|
var result = await voiceCallService.GenerateTTSAsync(request);
|
||||||
|
|
||||||
|
logger.LogInformation("TTS generated successfully");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "تولید صوت با موفقیت انجام شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error generating TTS");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در تولید صوت",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test QuickSend (sending voice call with uploaded audio file)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">QuickSend request</param>
|
||||||
|
/// <returns>QuickSend response</returns>
|
||||||
|
[HttpPost("quick-send")]
|
||||||
|
public async Task<IActionResult> QuickSend([FromBody] QuickSendRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing QuickSend to {Recipient} with MessageId {MessageId}",
|
||||||
|
request.Recipient, request.MessageId);
|
||||||
|
|
||||||
|
var result = await voiceCallService.QuickSendAsync(request);
|
||||||
|
|
||||||
|
logger.LogInformation("QuickSend completed successfully");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "تماس صوتی با موفقیت ارسال شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending quick voice call");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال تماس صوتی",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test QuickSendWithTTS (sending voice call with TTS)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">QuickSendWithTTS request</param>
|
||||||
|
/// <returns>QuickSendWithTTS response</returns>
|
||||||
|
[HttpPost("quick-send-with-tts")]
|
||||||
|
public async Task<IActionResult> QuickSendWithTTS([FromBody] QuickSendWithTTSRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing QuickSendWithTTS to {Number} with text: {Text}",
|
||||||
|
request.Number, request.Text);
|
||||||
|
|
||||||
|
var result = await voiceCallService.QuickSendWithTTSAsync(request);
|
||||||
|
|
||||||
|
if (result != null && result.IsSuccess)
|
||||||
|
{
|
||||||
|
logger.LogInformation("QuickSendWithTTS completed successfully. QuickSendId: {QuickSendId}",
|
||||||
|
result.QuickSendId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogWarning("QuickSendWithTTS failed with ReturnValue: {ReturnValue}",
|
||||||
|
result?.ReturnValue ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = result?.IsSuccess ?? false,
|
||||||
|
message = result?.IsSuccess == true
|
||||||
|
? "تماس صوتی با موفقیت ارسال شد"
|
||||||
|
: $"خطا در ارسال تماس صوتی (کد خطا: {result?.ReturnValue})",
|
||||||
|
data = result,
|
||||||
|
quickSendId = result?.QuickSendId ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending quick voice call with TTS");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال تماس صوتی",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test getting QuickSend status
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">GetQuickSend request</param>
|
||||||
|
/// <returns>QuickSend status response</returns>
|
||||||
|
[HttpPost("quick-send-status")]
|
||||||
|
public async Task<IActionResult> GetQuickSendStatus([FromBody] GetQuickSendRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Testing GetQuickSendStatus for QuickSendId: {QuickSendId}",
|
||||||
|
request.QuickSendId);
|
||||||
|
|
||||||
|
var result = await voiceCallService.GetQuickSendStatusAsync(request);
|
||||||
|
|
||||||
|
logger.LogInformation("QuickSendStatus retrieved successfully");
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "وضعیت تماس صوتی با موفقیت دریافت شد",
|
||||||
|
data = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error getting quick send status");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در دریافت وضعیت تماس صوتی",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick test with default values (for easy testing)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phoneNumber">Phone number to send voice call to</param>
|
||||||
|
/// <param name="text">Text to convert to speech (optional)</param>
|
||||||
|
/// <returns>QuickSendWithTTS response</returns>
|
||||||
|
[HttpPost("quick-test/{phoneNumber}")]
|
||||||
|
public async Task<IActionResult> QuickTest(string phoneNumber, [FromQuery] string? text = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var testText = text ?? $"سلام. این یک تماس صوتی تست از سرویس GreenHome است. زمان تست: {DateTime.Now:yyyy/MM/dd HH:mm:ss}";
|
||||||
|
|
||||||
|
logger.LogInformation("Quick test voice call to {PhoneNumber} with text: {Text}",
|
||||||
|
phoneNumber, testText);
|
||||||
|
|
||||||
|
var request = new QuickSendWithTTSRequest
|
||||||
|
{
|
||||||
|
Number = phoneNumber,
|
||||||
|
Text = testText,
|
||||||
|
ServerID = 0 // Default
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await voiceCallService.QuickSendWithTTSAsync(request);
|
||||||
|
|
||||||
|
if (result != null && result.IsSuccess)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Quick test voice call sent successfully. QuickSendId: {QuickSendId}",
|
||||||
|
result.QuickSendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = result?.IsSuccess ?? false,
|
||||||
|
message = result?.IsSuccess == true
|
||||||
|
? "تماس صوتی تست با موفقیت ارسال شد"
|
||||||
|
: $"خطا در ارسال تماس صوتی تست (کد خطا: {result?.ReturnValue})",
|
||||||
|
data = result,
|
||||||
|
quickSendId = result?.QuickSendId ?? 0,
|
||||||
|
testInfo = new
|
||||||
|
{
|
||||||
|
phoneNumber,
|
||||||
|
text = testText,
|
||||||
|
sentAt = DateTime.Now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error in quick test voice call");
|
||||||
|
return StatusCode(500, new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "خطا در ارسال تماس صوتی تست",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||||
|
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using GreenHome.Application;
|
using GreenHome.Application;
|
||||||
using GreenHome.Infrastructure;
|
using GreenHome.Infrastructure;
|
||||||
|
using GreenHome.Sms.Ippanel;
|
||||||
|
using GreenHome.VoiceCall.Avanak;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -42,9 +44,33 @@ builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(optio
|
|||||||
builder.Services.AddScoped<GreenHome.Application.IDeviceService, GreenHome.Infrastructure.DeviceService>();
|
builder.Services.AddScoped<GreenHome.Application.IDeviceService, GreenHome.Infrastructure.DeviceService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.Infrastructure.TelemetryService>();
|
builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.Infrastructure.TelemetryService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
|
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
|
||||||
|
builder.Services.AddScoped<GreenHome.Application.IAlertService, GreenHome.Infrastructure.AlertService>();
|
||||||
|
|
||||||
|
// SMS Service Configuration
|
||||||
|
builder.Services.AddIppanelSms(builder.Configuration);
|
||||||
|
|
||||||
|
// Voice Call Service Configuration
|
||||||
|
builder.Services.AddAvanakVoiceCall(builder.Configuration);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Apply pending migrations automatically
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = services.GetRequiredService<GreenHome.Infrastructure.GreenHomeDbContext>();
|
||||||
|
context.Database.Migrate();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||||
|
logger.LogError(ex, "An error occurred while migrating the database.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
//if (app.Environment.IsDevelopment())
|
//if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Default": "Server=.;Database=GreenHomeDb;User Id=sa;Password=qwER12#$110;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
|
"Default": "Server=.;Database=GreenHomeDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||||
|
},
|
||||||
|
"IppanelSms": {
|
||||||
|
"BaseUrl": "https://edge.ippanel.com/v1",
|
||||||
|
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
|
||||||
|
"DefaultSender": "+983000505"
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,10 @@ public sealed class DeviceDto
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string DeviceName { get; set; } = string.Empty;
|
public string DeviceName { get; set; } = string.Empty;
|
||||||
public string Owner { get; set; } = string.Empty;
|
public int UserId { get; set; }
|
||||||
public string Mobile { get; set; } = string.Empty;
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string UserFamily { get; set; } = string.Empty;
|
||||||
|
public string UserMobile { get; set; } = string.Empty;
|
||||||
public string Location { get; set; } = string.Empty;
|
public string Location { get; set; } = string.Empty;
|
||||||
public string NeshanLocation { get; set; } = string.Empty;
|
public string NeshanLocation { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -66,6 +68,49 @@ public sealed class DayCount
|
|||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class SendCodeRequest
|
||||||
|
{
|
||||||
|
public required string Mobile { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SendCodeResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public int ResendAfterSeconds { get; set; } = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VerifyCodeRequest
|
||||||
|
{
|
||||||
|
public required string Mobile { get; set; }
|
||||||
|
public required string Code { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class VerifyCodeResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? Token { get; set; }
|
||||||
|
public UserDto? User { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Mobile { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Family { get; set; } = string.Empty;
|
||||||
|
public Domain.UserRole Role { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeviceFilter
|
||||||
|
{
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
public string? Search { get; set; } // جستجو در نام دستگاه، نام صاحب، نام خانوادگی صاحب، Location
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class DeviceSettingsDto
|
public sealed class DeviceSettingsDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|||||||
7
src/GreenHome.Application/IAlertService.cs
Normal file
7
src/GreenHome.Application/IAlertService.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IAlertService
|
||||||
|
{
|
||||||
|
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
9
src/GreenHome.Application/IAuthService.cs
Normal file
9
src/GreenHome.Application/IAuthService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GreenHome.Application;
|
||||||
|
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
Task<SendCodeResponse> SendVerificationCodeAsync(SendCodeRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<VerifyCodeResponse> VerifyCodeAsync(VerifyCodeRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,4 +6,6 @@ public interface IDeviceService
|
|||||||
Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken);
|
Task<int> AddDeviceAsync(DeviceDto dto, CancellationToken cancellationToken);
|
||||||
Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken);
|
Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken);
|
||||||
Task<DeviceDto> GetDeviceId(string deviceName, CancellationToken cancellationToken);
|
Task<DeviceDto> GetDeviceId(string deviceName, CancellationToken cancellationToken);
|
||||||
|
Task<IReadOnlyList<DeviceDto>> GetUserDevicesAsync(int userId, CancellationToken cancellationToken);
|
||||||
|
Task<PagedResult<DeviceDto>> GetDevicesAsync(DeviceFilter filter, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ public sealed class MappingProfile : Profile
|
|||||||
{
|
{
|
||||||
public MappingProfile()
|
public MappingProfile()
|
||||||
{
|
{
|
||||||
CreateMap<Domain.Device, DeviceDto>().ReverseMap();
|
CreateMap<Domain.Device, DeviceDto>()
|
||||||
|
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name))
|
||||||
|
.ForMember(dest => dest.UserFamily, opt => opt.MapFrom(src => src.User.Family))
|
||||||
|
.ForMember(dest => dest.UserMobile, opt => opt.MapFrom(src => src.User.Mobile))
|
||||||
|
.ReverseMap()
|
||||||
|
.ForMember(dest => dest.User, opt => opt.Ignore());
|
||||||
|
|
||||||
CreateMap<Domain.TelemetryRecord, TelemetryDto>().ReverseMap();
|
CreateMap<Domain.TelemetryRecord, TelemetryDto>().ReverseMap();
|
||||||
|
|
||||||
CreateMap<Domain.DeviceSettings, DeviceSettingsDto>()
|
CreateMap<Domain.DeviceSettings, DeviceSettingsDto>()
|
||||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||||
.ReverseMap()
|
.ReverseMap()
|
||||||
.ForMember(dest => dest.Device, opt => opt.Ignore());
|
.ForMember(dest => dest.Device, opt => opt.Ignore());
|
||||||
|
|
||||||
|
CreateMap<Domain.User, UserDto>().ReverseMap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/GreenHome.Domain/AlertNotification.cs
Normal file
17
src/GreenHome.Domain/AlertNotification.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
public sealed class AlertNotification
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
public string AlertType { get; set; } = string.Empty; // Temperature, Humidity, Soil, Gas, Lux
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs
|
||||||
|
public string? ErrorMessage { get; set; } // Error details if sending failed
|
||||||
|
public DateTime SentAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public bool IsSent { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,8 +4,9 @@ public sealed class Device
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string DeviceName { get; set; } = string.Empty; // varchar(10)
|
public string DeviceName { get; set; } = string.Empty; // varchar(10)
|
||||||
public string Owner { get; set; } = string.Empty; // نام صاحب
|
public int UserId { get; set; }
|
||||||
public string Mobile { get; set; } = string.Empty;
|
public User User { get; set; } = null!;
|
||||||
public string Location { get; set; } = string.Empty; // varchar(250)
|
public string Location { get; set; } = string.Empty; // varchar(250)
|
||||||
public string NeshanLocation { get; set; } = string.Empty; // varchar(80)
|
public string NeshanLocation { get; set; } = string.Empty; // varchar(80)
|
||||||
|
public ICollection<DeviceUser> DeviceUsers { get; set; } = new List<DeviceUser>();
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/GreenHome.Domain/DeviceUser.cs
Normal file
10
src/GreenHome.Domain/DeviceUser.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
public sealed class DeviceUser
|
||||||
|
{
|
||||||
|
public int DeviceId { get; set; }
|
||||||
|
public Device Device { get; set; } = null!;
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User User { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
14
src/GreenHome.Domain/User.cs
Normal file
14
src/GreenHome.Domain/User.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
public sealed class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Mobile { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Family { get; set; } = string.Empty;
|
||||||
|
public UserRole Role { get; set; } = UserRole.Normal;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? LastLoginAt { get; set; }
|
||||||
|
public ICollection<DeviceUser> DeviceUsers { get; set; } = new List<DeviceUser>();
|
||||||
|
}
|
||||||
|
|
||||||
9
src/GreenHome.Domain/UserRole.cs
Normal file
9
src/GreenHome.Domain/UserRole.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
public enum UserRole
|
||||||
|
{
|
||||||
|
Normal = 0, // کاربر عادی
|
||||||
|
Admin = 1, // ادمین
|
||||||
|
Supervisor = 2 // ناظر
|
||||||
|
}
|
||||||
|
|
||||||
13
src/GreenHome.Domain/VerificationCode.cs
Normal file
13
src/GreenHome.Domain/VerificationCode.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace GreenHome.Domain;
|
||||||
|
|
||||||
|
public sealed class VerificationCode
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Mobile { get; set; } = string.Empty;
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
public bool IsUsed { get; set; } = false;
|
||||||
|
public DateTime? UsedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
319
src/GreenHome.Infrastructure/AlertService.cs
Normal file
319
src/GreenHome.Infrastructure/AlertService.cs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
using GreenHome.Application;
|
||||||
|
using GreenHome.Sms.Ippanel;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text.Json;
|
||||||
|
using static GreenHome.Sms.Ippanel.IppanelSmsService;
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure;
|
||||||
|
|
||||||
|
public sealed class AlertService : IAlertService
|
||||||
|
{
|
||||||
|
private readonly GreenHomeDbContext dbContext;
|
||||||
|
private readonly IDeviceSettingsService deviceSettingsService;
|
||||||
|
private readonly ISmsService smsService;
|
||||||
|
private readonly ILogger<AlertService> logger;
|
||||||
|
private const int AlertCooldownMinutes = 10;
|
||||||
|
|
||||||
|
private sealed record AlertInfo(
|
||||||
|
string Type,
|
||||||
|
string Message,
|
||||||
|
string ParameterName,
|
||||||
|
decimal Value,
|
||||||
|
string Status
|
||||||
|
);
|
||||||
|
|
||||||
|
public AlertService(
|
||||||
|
GreenHomeDbContext dbContext,
|
||||||
|
IDeviceSettingsService deviceSettingsService,
|
||||||
|
ISmsService smsService,
|
||||||
|
ILogger<AlertService> logger)
|
||||||
|
{
|
||||||
|
this.dbContext = dbContext;
|
||||||
|
this.deviceSettingsService = deviceSettingsService;
|
||||||
|
this.smsService = smsService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
||||||
|
if (settings == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var device = await dbContext.Devices
|
||||||
|
.Include(d => d.User)
|
||||||
|
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||||
|
|
||||||
|
if (device == null || device.User == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alerts = CollectAlerts(telemetry, settings, device.DeviceName);
|
||||||
|
|
||||||
|
foreach (var alert in alerts)
|
||||||
|
{
|
||||||
|
await SendAlertIfNeededAsync(deviceId, device.User.Id, device.DeviceName, alert, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AlertInfo> CollectAlerts(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName)
|
||||||
|
{
|
||||||
|
var alerts = new List<AlertInfo>();
|
||||||
|
|
||||||
|
CheckTemperatureAlert(telemetry, settings, deviceName, alerts);
|
||||||
|
CheckHumidityAlert(telemetry, settings, deviceName, alerts);
|
||||||
|
CheckSoilAlert(telemetry, deviceName, alerts);
|
||||||
|
CheckGasAlert(telemetry, settings, deviceName, alerts);
|
||||||
|
CheckLuxAlert(telemetry, settings, deviceName, alerts);
|
||||||
|
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckTemperatureAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||||
|
{
|
||||||
|
if (telemetry.TemperatureC > settings.MaxTemperature)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Temperature",
|
||||||
|
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداکثر مجاز ({settings.MaxTemperature}) بیشتر است.",
|
||||||
|
ParameterName: "دما",
|
||||||
|
Value: telemetry.TemperatureC,
|
||||||
|
Status: "بالاتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (telemetry.TemperatureC < settings.MinTemperature)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Temperature",
|
||||||
|
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداقل مجاز ({settings.MinTemperature}) کمتر است.",
|
||||||
|
ParameterName: "دما",
|
||||||
|
Value: telemetry.TemperatureC,
|
||||||
|
Status: "پایینتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckHumidityAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||||
|
{
|
||||||
|
if (telemetry.HumidityPercent > settings.MaxHumidityPercent)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Humidity",
|
||||||
|
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداکثر مجاز ({settings.MaxHumidityPercent}%) بیشتر است.",
|
||||||
|
ParameterName: "رطوبت",
|
||||||
|
Value: telemetry.HumidityPercent,
|
||||||
|
Status: "بالاتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (telemetry.HumidityPercent < settings.MinHumidityPercent)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Humidity",
|
||||||
|
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداقل مجاز ({settings.MinHumidityPercent}%) کمتر است.",
|
||||||
|
ParameterName: "رطوبت",
|
||||||
|
Value: telemetry.HumidityPercent,
|
||||||
|
Status: "پایینتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckSoilAlert(TelemetryDto telemetry, string deviceName, List<AlertInfo> alerts)
|
||||||
|
{
|
||||||
|
if (telemetry.SoilPercent > 100)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Soil",
|
||||||
|
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
|
||||||
|
ParameterName: "رطوبت خاک",
|
||||||
|
Value: telemetry.SoilPercent,
|
||||||
|
Status: "بالاتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (telemetry.SoilPercent < 0)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Soil",
|
||||||
|
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
|
||||||
|
ParameterName: "رطوبت خاک",
|
||||||
|
Value: telemetry.SoilPercent,
|
||||||
|
Status: "پایینتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckGasAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||||
|
{
|
||||||
|
if (telemetry.GasPPM > settings.MaxGasPPM)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Gas",
|
||||||
|
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداکثر مجاز ({settings.MaxGasPPM}) بیشتر است.",
|
||||||
|
ParameterName: "گاز Co",
|
||||||
|
Value: telemetry.GasPPM,
|
||||||
|
Status: "بالاتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (telemetry.GasPPM < settings.MinGasPPM)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Gas",
|
||||||
|
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداقل مجاز ({settings.MinGasPPM}) کمتر است.",
|
||||||
|
ParameterName: "گاز Co",
|
||||||
|
Value: telemetry.GasPPM,
|
||||||
|
Status: "پایینتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckLuxAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||||
|
{
|
||||||
|
if (telemetry.Lux > settings.MaxLux)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Lux",
|
||||||
|
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداکثر مجاز ({settings.MaxLux}) بیشتر است.",
|
||||||
|
ParameterName: "نور",
|
||||||
|
Value: telemetry.Lux,
|
||||||
|
Status: "بالاتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (telemetry.Lux < settings.MinLux)
|
||||||
|
{
|
||||||
|
alerts.Add(new AlertInfo(
|
||||||
|
Type: "Lux",
|
||||||
|
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداقل مجاز ({settings.MinLux}) کمتر است.",
|
||||||
|
ParameterName: "نور",
|
||||||
|
Value: telemetry.Lux,
|
||||||
|
Status: "پایینتر"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendAlertIfNeededAsync(
|
||||||
|
int deviceId,
|
||||||
|
int userId,
|
||||||
|
string deviceName,
|
||||||
|
AlertInfo alert,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Check if alert was sent in the last 10 minutes
|
||||||
|
var cooldownTime = DateTime.UtcNow.AddMinutes(-AlertCooldownMinutes);
|
||||||
|
var recentAlert = await dbContext.AlertNotifications
|
||||||
|
.Where(a => a.DeviceId == deviceId &&
|
||||||
|
a.UserId == userId &&
|
||||||
|
a.AlertType == alert.Type &&
|
||||||
|
a.SentAt >= cooldownTime)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (recentAlert != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, AlertType={AlertType}", deviceId, alert.Type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user to send SMS
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null || string.IsNullOrWhiteSpace(user.Mobile))
|
||||||
|
{
|
||||||
|
logger.LogWarning("User not found or mobile is empty: UserId={UserId}", userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send SMS and collect response/errors
|
||||||
|
string? messageOutboxIdsJson = null;
|
||||||
|
string? errorMessage = null;
|
||||||
|
bool isSent = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||||
|
{
|
||||||
|
Recipients = [user.Mobile],
|
||||||
|
PatternCode = "64di3w9kb0fxvif",
|
||||||
|
Variables = new Dictionary<string, string> {
|
||||||
|
{ "name", deviceName },
|
||||||
|
{ "parameter", alert.ParameterName },
|
||||||
|
{ "value", alert.Value.ToString("F1") },
|
||||||
|
{ "status", alert.Status },
|
||||||
|
}
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
if (smsResponse != null)
|
||||||
|
{
|
||||||
|
// Check if SMS was sent successfully
|
||||||
|
if (smsResponse.Meta.Status && smsResponse.Data != null && smsResponse.Data.MessageOutboxIds != null && smsResponse.Data.MessageOutboxIds.Count > 0)
|
||||||
|
{
|
||||||
|
// Success - save message outbox IDs
|
||||||
|
messageOutboxIdsJson = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
|
||||||
|
isSent = true;
|
||||||
|
logger.LogInformation("Alert SMS sent: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, OutboxIds={OutboxIds}",
|
||||||
|
deviceId, userId, alert.Type, messageOutboxIdsJson);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Failed - save error from meta
|
||||||
|
var errors = new List<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(smsResponse.Meta.Message))
|
||||||
|
{
|
||||||
|
errors.Add(smsResponse.Meta.Message);
|
||||||
|
}
|
||||||
|
if (smsResponse.Meta.Errors != null && smsResponse.Meta.Errors.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var error in smsResponse.Meta.Errors)
|
||||||
|
{
|
||||||
|
errors.Add($"{error.Key}: {string.Join(", ", error.Value)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.Count == 0)
|
||||||
|
{
|
||||||
|
errors.Add("SMS sending failed with unknown error");
|
||||||
|
}
|
||||||
|
errorMessage = string.Join(" | ", errors);
|
||||||
|
isSent = false;
|
||||||
|
logger.LogWarning("Alert SMS failed: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, Error={Error}",
|
||||||
|
deviceId, userId, alert.Type, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "SMS service returned null response";
|
||||||
|
isSent = false;
|
||||||
|
logger.LogWarning("Alert SMS returned null: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}",
|
||||||
|
deviceId, userId, alert.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Exception: {ex.Message}";
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||||
|
}
|
||||||
|
isSent = false;
|
||||||
|
logger.LogError(ex, "Failed to send alert SMS: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}", deviceId, userId, alert.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save notification to database
|
||||||
|
var notification = new Domain.AlertNotification
|
||||||
|
{
|
||||||
|
DeviceId = deviceId,
|
||||||
|
UserId = userId,
|
||||||
|
AlertType = alert.Type,
|
||||||
|
Message = alert.Message,
|
||||||
|
MessageOutboxIds = messageOutboxIdsJson,
|
||||||
|
ErrorMessage = errorMessage,
|
||||||
|
SentAt = DateTime.UtcNow,
|
||||||
|
IsSent = isSent
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.AlertNotifications.Add(notification);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
242
src/GreenHome.Infrastructure/AuthService.cs
Normal file
242
src/GreenHome.Infrastructure/AuthService.cs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
using GreenHome.Application;
|
||||||
|
using GreenHome.Sms.Ippanel;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure;
|
||||||
|
|
||||||
|
public sealed class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private readonly GreenHomeDbContext dbContext;
|
||||||
|
private readonly ISmsService smsService;
|
||||||
|
private readonly ILogger<AuthService> logger;
|
||||||
|
private const int CodeExpirationMinutes = 5;
|
||||||
|
private const int ResendCooldownSeconds = 120;
|
||||||
|
|
||||||
|
public AuthService(
|
||||||
|
GreenHomeDbContext dbContext,
|
||||||
|
ISmsService smsService,
|
||||||
|
ILogger<AuthService> logger)
|
||||||
|
{
|
||||||
|
this.dbContext = dbContext;
|
||||||
|
this.smsService = smsService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SendCodeResponse> SendVerificationCodeAsync(
|
||||||
|
SendCodeRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Normalize mobile number (remove spaces, dashes, etc.)
|
||||||
|
var mobile = NormalizeMobile(request.Mobile);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length != 11 || !mobile.StartsWith("09"))
|
||||||
|
{
|
||||||
|
return new SendCodeResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "شماره موبایل معتبر نیست"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can resend (cooldown period)
|
||||||
|
var canResend = await CanResendCodeAsync(mobile, cancellationToken);
|
||||||
|
if (!canResend)
|
||||||
|
{
|
||||||
|
var lastCode = await dbContext.VerificationCodes
|
||||||
|
.Where(v => v.Mobile == mobile && !v.IsUsed)
|
||||||
|
.OrderByDescending(v => v.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (lastCode != null)
|
||||||
|
{
|
||||||
|
var secondsRemaining = (int)(ResendCooldownSeconds - (DateTime.UtcNow - lastCode.CreatedAt).TotalSeconds);
|
||||||
|
return new SendCodeResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"لطفاً {secondsRemaining} ثانیه دیگر دوباره تلاش کنید",
|
||||||
|
ResendAfterSeconds = secondsRemaining > 0 ? secondsRemaining : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 4-digit code
|
||||||
|
var code = GenerateCode();
|
||||||
|
|
||||||
|
// Invalidate previous unused codes for this mobile
|
||||||
|
var previousCodes = await dbContext.VerificationCodes
|
||||||
|
.Where(v => v.Mobile == mobile && !v.IsUsed)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var prevCode in previousCodes)
|
||||||
|
{
|
||||||
|
prevCode.IsUsed = true;
|
||||||
|
prevCode.UsedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new verification code
|
||||||
|
var verificationCode = new Domain.VerificationCode
|
||||||
|
{
|
||||||
|
Mobile = mobile,
|
||||||
|
Code = code,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ExpiresAt = DateTime.UtcNow.AddMinutes(CodeExpirationMinutes),
|
||||||
|
IsUsed = false
|
||||||
|
};
|
||||||
|
|
||||||
|
dbContext.VerificationCodes.Add(verificationCode);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Send SMS
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||||
|
{
|
||||||
|
Recipients = [mobile],
|
||||||
|
PatternCode = "ruvpjx7lajne1dx",
|
||||||
|
Variables = new Dictionary<string, string> { { "code", code } }
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Verification code sent to {Mobile}", mobile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to send SMS to {Mobile}", mobile);
|
||||||
|
// Continue even if SMS fails (for development/testing)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SendCodeResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "کد فعالسازی ارسال شد",
|
||||||
|
ResendAfterSeconds = ResendCooldownSeconds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VerifyCodeResponse> VerifyCodeAsync(
|
||||||
|
VerifyCodeRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var mobile = NormalizeMobile(request.Mobile);
|
||||||
|
var code = request.Code.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile) || string.IsNullOrWhiteSpace(code) || code.Length != 4)
|
||||||
|
{
|
||||||
|
return new VerifyCodeResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "اطلاعات وارد شده معتبر نیست"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find valid verification code
|
||||||
|
var verificationCode = await dbContext.VerificationCodes
|
||||||
|
.Where(v => v.Mobile == mobile && v.Code == code && !v.IsUsed && v.ExpiresAt > DateTime.UtcNow)
|
||||||
|
.OrderByDescending(v => v.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (verificationCode == null)
|
||||||
|
{
|
||||||
|
return new VerifyCodeResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "کد وارد شده معتبر نیست یا منقضی شده است"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark code as used
|
||||||
|
verificationCode.IsUsed = true;
|
||||||
|
verificationCode.UsedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
user = new Domain.User
|
||||||
|
{
|
||||||
|
Mobile = mobile,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastLoginAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
dbContext.Users.Add(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user.LastLoginAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Generate simple token (in production, use JWT)
|
||||||
|
var token = GenerateToken(user.Id, mobile);
|
||||||
|
|
||||||
|
return new VerifyCodeResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "ورود موفقیتآمیز بود",
|
||||||
|
Token = token,
|
||||||
|
User = new UserDto
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Mobile = user.Mobile,
|
||||||
|
Name = user.Name,
|
||||||
|
Family = user.Family,
|
||||||
|
Role = user.Role
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanResendCodeAsync(string mobile, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var normalizedMobile = NormalizeMobile(mobile);
|
||||||
|
var lastCode = await dbContext.VerificationCodes
|
||||||
|
.Where(v => v.Mobile == normalizedMobile && !v.IsUsed)
|
||||||
|
.OrderByDescending(v => v.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (lastCode == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var elapsed = (DateTime.UtcNow - lastCode.CreatedAt).TotalSeconds;
|
||||||
|
return elapsed >= ResendCooldownSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateCode()
|
||||||
|
{
|
||||||
|
var random = new Random();
|
||||||
|
return random.Next(1000, 9999).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateToken(int userId, string mobile)
|
||||||
|
{
|
||||||
|
// Simple token generation (in production, use JWT)
|
||||||
|
var data = $"{userId}:{mobile}:{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
var hash = SHA256.HashData(bytes);
|
||||||
|
return Convert.ToBase64String(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeMobile(string mobile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(mobile))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// Remove all non-digit characters
|
||||||
|
var normalized = new string(mobile.Where(char.IsDigit).ToArray());
|
||||||
|
|
||||||
|
// Convert to standard format (09xxxxxxxxx)
|
||||||
|
if (normalized.StartsWith("9") && normalized.Length == 10)
|
||||||
|
normalized = "0" + normalized;
|
||||||
|
else if (normalized.StartsWith("0098") && normalized.Length == 13)
|
||||||
|
normalized = "0" + normalized.Substring(3);
|
||||||
|
else if (normalized.StartsWith("98") && normalized.Length == 12)
|
||||||
|
normalized = "0" + normalized.Substring(2);
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -25,13 +25,102 @@ public sealed class DeviceService : IDeviceService
|
|||||||
|
|
||||||
public async Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<DeviceDto>> ListAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var items = await dbContext.Devices.AsNoTracking().OrderBy(d => d.DeviceName).ToListAsync(cancellationToken);
|
var items = await dbContext.Devices
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(d => d.User)
|
||||||
|
.OrderBy(d => d.DeviceName)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
return mapper.Map<IReadOnlyList<DeviceDto>>(items);
|
return mapper.Map<IReadOnlyList<DeviceDto>>(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeviceDto> GetDeviceId(string deviceName,CancellationToken cancellationToken)
|
public async Task<DeviceDto> GetDeviceId(string deviceName,CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var item = await dbContext.Devices.AsNoTracking().Where(d=>d.DeviceName==deviceName).FirstOrDefaultAsync(cancellationToken);
|
var item = await dbContext.Devices
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(d => d.User)
|
||||||
|
.Where(d=>d.DeviceName==deviceName)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
return mapper.Map<DeviceDto>(item);
|
return mapper.Map<DeviceDto>(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<DeviceDto>> GetUserDevicesAsync(int userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var items = await dbContext.Devices
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(d => d.User)
|
||||||
|
.Where(d => d.UserId == userId)
|
||||||
|
.OrderBy(d => d.DeviceName)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return mapper.Map<IReadOnlyList<DeviceDto>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<DeviceDto>> GetDevicesAsync(DeviceFilter filter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!filter.UserId.HasValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("UserId is required", nameof(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user and role
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == filter.UserId.Value, cancellationToken);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return new PagedResult<DeviceDto>
|
||||||
|
{
|
||||||
|
Items = Array.Empty<DeviceDto>(),
|
||||||
|
TotalCount = 0,
|
||||||
|
Page = filter.Page,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query based on role
|
||||||
|
IQueryable<Domain.Device> query = dbContext.Devices
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(d => d.User);
|
||||||
|
|
||||||
|
if (user.Role == Domain.UserRole.Normal)
|
||||||
|
{
|
||||||
|
// Normal user: only own devices
|
||||||
|
query = query.Where(d => d.UserId == user.Id);
|
||||||
|
}
|
||||||
|
else if (user.Role == Domain.UserRole.Supervisor)
|
||||||
|
{
|
||||||
|
// Supervisor: devices assigned to them
|
||||||
|
query = query.Where(d => d.DeviceUsers.Any(du => du.UserId == user.Id));
|
||||||
|
}
|
||||||
|
// Admin: all devices (no filter)
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Search))
|
||||||
|
{
|
||||||
|
var searchTerm = filter.Search.Trim().ToLower();
|
||||||
|
query = query.Where(d =>
|
||||||
|
d.DeviceName.ToLower().Contains(searchTerm) ||
|
||||||
|
d.User.Name.ToLower().Contains(searchTerm) ||
|
||||||
|
d.User.Family.ToLower().Contains(searchTerm) ||
|
||||||
|
d.Location.ToLower().Contains(searchTerm));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var items = await query
|
||||||
|
.OrderBy(d => d.DeviceName)
|
||||||
|
.Skip((filter.Page - 1) * filter.PageSize)
|
||||||
|
.Take(filter.PageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new PagedResult<DeviceDto>
|
||||||
|
{
|
||||||
|
Items = mapper.Map<IReadOnlyList<DeviceDto>>(items),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = filter.Page,
|
||||||
|
PageSize = filter.PageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
|
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
|
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
|
||||||
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
|
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
|
||||||
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
|
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
|
||||||
|
public DbSet<Domain.User> Users => Set<Domain.User>();
|
||||||
|
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||||
|
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||||
|
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -19,10 +23,13 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
b.ToTable("Devices");
|
b.ToTable("Devices");
|
||||||
b.HasKey(x => x.Id);
|
b.HasKey(x => x.Id);
|
||||||
b.Property(x => x.DeviceName).IsRequired().HasMaxLength(10);
|
b.Property(x => x.DeviceName).IsRequired().HasMaxLength(10);
|
||||||
b.Property(x => x.Owner).IsRequired();
|
b.Property(x => x.UserId).IsRequired();
|
||||||
b.Property(x => x.Mobile).IsRequired(false);
|
|
||||||
b.Property(x => x.Location).HasMaxLength(250);
|
b.Property(x => x.Location).HasMaxLength(250);
|
||||||
b.Property(x => x.NeshanLocation).HasMaxLength(80);
|
b.Property(x => x.NeshanLocation).HasMaxLength(80);
|
||||||
|
b.HasOne(x => x.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Domain.TelemetryRecord>(b =>
|
modelBuilder.Entity<Domain.TelemetryRecord>(b =>
|
||||||
@@ -56,5 +63,58 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
b.HasIndex(x => x.DeviceId).IsUnique();
|
b.HasIndex(x => x.DeviceId).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Domain.User>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("Users");
|
||||||
|
b.HasKey(x => x.Id);
|
||||||
|
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
|
||||||
|
b.Property(x => x.Name).IsRequired().HasMaxLength(100);
|
||||||
|
b.Property(x => x.Family).IsRequired().HasMaxLength(100);
|
||||||
|
b.Property(x => x.Role).IsRequired().HasConversion<int>();
|
||||||
|
b.HasIndex(x => x.Mobile).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Domain.DeviceUser>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("DeviceUsers");
|
||||||
|
b.HasKey(x => new { x.DeviceId, x.UserId });
|
||||||
|
b.HasOne(x => x.Device)
|
||||||
|
.WithMany(d => d.DeviceUsers)
|
||||||
|
.HasForeignKey(x => x.DeviceId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
b.HasOne(x => x.User)
|
||||||
|
.WithMany(u => u.DeviceUsers)
|
||||||
|
.HasForeignKey(x => x.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Domain.VerificationCode>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("VerificationCodes");
|
||||||
|
b.HasKey(x => x.Id);
|
||||||
|
b.Property(x => x.Mobile).IsRequired().HasMaxLength(11);
|
||||||
|
b.Property(x => x.Code).IsRequired().HasMaxLength(4);
|
||||||
|
b.HasIndex(x => new { x.Mobile, x.Code, x.IsUsed });
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<Domain.AlertNotification>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("AlertNotifications");
|
||||||
|
b.HasKey(x => x.Id);
|
||||||
|
b.Property(x => x.AlertType).IsRequired().HasMaxLength(50);
|
||||||
|
b.Property(x => x.Message).IsRequired().HasMaxLength(500);
|
||||||
|
b.Property(x => x.MessageOutboxIds).HasMaxLength(500);
|
||||||
|
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
||||||
|
b.HasOne(x => x.Device)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.DeviceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
b.HasOne(x => x.User)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertType, x.SentAt });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
263
src/GreenHome.Infrastructure/Migrations/20251118204845_UpdateDeviceUserRelationship.Designer.cs
generated
Normal file
263
src/GreenHome.Infrastructure/Migrations/20251118204845_UpdateDeviceUserRelationship.Designer.cs
generated
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using GreenHome.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GreenHomeDbContext))]
|
||||||
|
[Migration("20251118204845_UpdateDeviceUserRelationship")]
|
||||||
|
partial class UpdateDeviceUserRelationship
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DeviceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<string>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.Property<string>("NeshanLocation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("nvarchar(80)");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MaxGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("MinGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DeviceSettings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("GasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("HumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Lux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PersianDate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<int>("PersianMonth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("SoilPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TemperatureC")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimestampUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "TimestampUtc");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||||
|
|
||||||
|
b.ToTable("Telemetry", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Family")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||||
|
|
||||||
|
b.ToTable("VerificationCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateDeviceUserRelationship : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Mobile",
|
||||||
|
table: "Devices");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Owner",
|
||||||
|
table: "Devices");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "UserId",
|
||||||
|
table: "Devices",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Mobile = table.Column<string>(type: "nvarchar(11)", maxLength: 11, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Family = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
LastLoginAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "VerificationCodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Mobile = table.Column<string>(type: "nvarchar(11)", maxLength: 11, nullable: false),
|
||||||
|
Code = table.Column<string>(type: "nvarchar(4)", maxLength: 4, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
IsUsed = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
UsedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_VerificationCodes", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Devices_UserId",
|
||||||
|
table: "Devices",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_Mobile",
|
||||||
|
table: "Users",
|
||||||
|
column: "Mobile",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VerificationCodes_Mobile_Code_IsUsed",
|
||||||
|
table: "VerificationCodes",
|
||||||
|
columns: new[] { "Mobile", "Code", "IsUsed" });
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Devices_Users_UserId",
|
||||||
|
table: "Devices",
|
||||||
|
column: "UserId",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Devices_Users_UserId",
|
||||||
|
table: "Devices");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "VerificationCodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Devices_UserId",
|
||||||
|
table: "Devices");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UserId",
|
||||||
|
table: "Devices");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Mobile",
|
||||||
|
table: "Devices",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Owner",
|
||||||
|
table: "Devices",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
310
src/GreenHome.Infrastructure/Migrations/20251119133827_AddUserRolesAndDeviceUsers.Designer.cs
generated
Normal file
310
src/GreenHome.Infrastructure/Migrations/20251119133827_AddUserRolesAndDeviceUsers.Designer.cs
generated
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using GreenHome.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GreenHomeDbContext))]
|
||||||
|
[Migration("20251119133827_AddUserRolesAndDeviceUsers")]
|
||||||
|
partial class AddUserRolesAndDeviceUsers
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DeviceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<string>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.Property<string>("NeshanLocation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("nvarchar(80)");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MaxGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("MinGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DeviceSettings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("DeviceId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("DeviceUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("GasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("HumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Lux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PersianDate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<int>("PersianMonth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("SoilPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TemperatureC")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimestampUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "TimestampUtc");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||||
|
|
||||||
|
b.ToTable("Telemetry", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Family")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||||
|
|
||||||
|
b.ToTable("VerificationCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserRolesAndDeviceUsers : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Role",
|
||||||
|
table: "Users",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DeviceUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DeviceUsers", x => new { x.DeviceId, x.UserId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_DeviceUsers_Devices_DeviceId",
|
||||||
|
column: x => x.DeviceId,
|
||||||
|
principalTable: "Devices",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_DeviceUsers_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceUsers_UserId",
|
||||||
|
table: "DeviceUsers",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DeviceUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Role",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
368
src/GreenHome.Infrastructure/Migrations/20251119135914_AddAlertNotifications.Designer.cs
generated
Normal file
368
src/GreenHome.Infrastructure/Migrations/20251119135914_AddAlertNotifications.Designer.cs
generated
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using GreenHome.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GreenHomeDbContext))]
|
||||||
|
[Migration("20251119135914_AddAlertNotifications")]
|
||||||
|
partial class AddAlertNotifications
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlertType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSent")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
||||||
|
|
||||||
|
b.ToTable("AlertNotifications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DeviceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<string>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.Property<string>("NeshanLocation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("nvarchar(80)");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MaxGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("MinGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DeviceSettings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("DeviceId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("DeviceUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("GasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("HumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Lux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PersianDate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<int>("PersianMonth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("SoilPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TemperatureC")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimestampUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "TimestampUtc");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||||
|
|
||||||
|
b.ToTable("Telemetry", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Family")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||||
|
|
||||||
|
b.ToTable("VerificationCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAlertNotifications : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AlertNotifications",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
AlertType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||||
|
Message = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||||
|
SentAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
IsSent = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AlertNotifications", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AlertNotifications_Devices_DeviceId",
|
||||||
|
column: x => x.DeviceId,
|
||||||
|
principalTable: "Devices",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AlertNotifications_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AlertNotifications_DeviceId_UserId_AlertType_SentAt",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
columns: new[] { "DeviceId", "UserId", "AlertType", "SentAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AlertNotifications_UserId",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AlertNotifications");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
376
src/GreenHome.Infrastructure/Migrations/20251119142729_FixAlertNotifications.Designer.cs
generated
Normal file
376
src/GreenHome.Infrastructure/Migrations/20251119142729_FixAlertNotifications.Designer.cs
generated
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using GreenHome.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GreenHomeDbContext))]
|
||||||
|
[Migration("20251119142729_FixAlertNotifications")]
|
||||||
|
partial class FixAlertNotifications
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlertType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSent")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("MessageOutboxIds")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
||||||
|
|
||||||
|
b.ToTable("AlertNotifications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("DeviceName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<string>("Location")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(250)
|
||||||
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
|
b.Property<string>("NeshanLocation")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("nvarchar(80)");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Devices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("DangerMinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("MaxGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int>("MinGasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinHumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinLux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("MinTemperature")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DeviceSettings", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("DeviceId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("DeviceUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("GasPPM")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("HumidityPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Lux")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("PersianDate")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("nvarchar(10)");
|
||||||
|
|
||||||
|
b.Property<int>("PersianMonth")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("PersianYear")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("SoilPercent")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("TemperatureC")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("TimestampUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "TimestampUtc");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||||
|
|
||||||
|
b.ToTable("Telemetry", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Family")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||||
|
|
||||||
|
b.ToTable("VerificationCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GreenHome.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class FixAlertNotifications : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ErrorMessage",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
type: "nvarchar(1000)",
|
||||||
|
maxLength: 1000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "MessageOutboxIds",
|
||||||
|
table: "AlertNotifications",
|
||||||
|
type: "nvarchar(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ErrorMessage",
|
||||||
|
table: "AlertNotifications");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MessageOutboxIds",
|
||||||
|
table: "AlertNotifications");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,53 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AlertType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSent")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<string>("MessageOutboxIds")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
||||||
|
|
||||||
|
b.ToTable("AlertNotifications", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -40,20 +87,18 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
.HasMaxLength(250)
|
.HasMaxLength(250)
|
||||||
.HasColumnType("nvarchar(250)");
|
.HasColumnType("nvarchar(250)");
|
||||||
|
|
||||||
b.Property<string>("Mobile")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("NeshanLocation")
|
b.Property<string>("NeshanLocation")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(80)
|
.HasMaxLength(80)
|
||||||
.HasColumnType("nvarchar(80)");
|
.HasColumnType("nvarchar(80)");
|
||||||
|
|
||||||
b.Property<string>("Owner")
|
b.Property<int>("UserId")
|
||||||
.IsRequired()
|
.HasColumnType("int");
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("Devices", (string)null);
|
b.ToTable("Devices", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,6 +157,21 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("DeviceSettings", (string)null);
|
b.ToTable("DeviceSettings", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("DeviceId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("DeviceId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("DeviceUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -161,6 +221,113 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("Telemetry", (string)null);
|
b.ToTable("Telemetry", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Family")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUsed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Mobile")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(11)
|
||||||
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UsedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||||
|
|
||||||
|
b.ToTable("VerificationCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
@@ -171,6 +338,35 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Navigation("Device");
|
b.Navigation("Device");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("DeviceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
|
.WithMany("DeviceUsers")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Device");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("DeviceUsers");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ public sealed class TelemetryService : ITelemetryService
|
|||||||
var entity = mapper.Map<Domain.TelemetryRecord>(dto);
|
var entity = mapper.Map<Domain.TelemetryRecord>(dto);
|
||||||
if (!string.IsNullOrEmpty(dto.DeviceName))
|
if (!string.IsNullOrEmpty(dto.DeviceName))
|
||||||
{
|
{
|
||||||
entity.DeviceId = dbContext.Devices.First(d => d.DeviceName == dto.DeviceName).Id;
|
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.DeviceName == dto.DeviceName, cancellationToken);
|
||||||
|
if (device != null)
|
||||||
|
{
|
||||||
|
entity.DeviceId = device.Id;
|
||||||
|
dto.DeviceId = device.Id; // Update DTO for alert service
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var dt = dto.TimestampUtc;
|
var dt = dto.TimestampUtc;
|
||||||
var py = PersianCalendar.GetYear(dt);
|
var py = PersianCalendar.GetYear(dt);
|
||||||
|
|||||||
4
src/GreenHome.Infrastructure/VoiceCallService.cs
Normal file
4
src/GreenHome.Infrastructure/VoiceCallService.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
public class VoiceCallService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
16
src/GreenHome.Sms.Ippanel/GreenHome.Sms.Ippanel.csproj
Normal file
16
src/GreenHome.Sms.Ippanel/GreenHome.Sms.Ippanel.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
26
src/GreenHome.Sms.Ippanel/ISmsService.cs
Normal file
26
src/GreenHome.Sms.Ippanel/ISmsService.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using static GreenHome.Sms.Ippanel.IppanelSmsService;
|
||||||
|
|
||||||
|
namespace GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for SMS sending service
|
||||||
|
/// </summary>
|
||||||
|
public interface ISmsService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a webservice SMS message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">SMS request details</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>SMS response with bulk ID</returns>
|
||||||
|
Task<IppanelApiResponse<IppanelData>?> SendWebserviceSmsAsync(WebserviceSmsRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a pattern SMS message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Pattern SMS request details</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>SMS response with bulk ID</returns>
|
||||||
|
Task<IppanelApiResponse<IppanelData>?> SendPatternSmsAsync(PatternSmsRequest request, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
23
src/GreenHome.Sms.Ippanel/IppanelSmsOptions.cs
Normal file
23
src/GreenHome.Sms.Ippanel/IppanelSmsOptions.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for IPPanel SMS service
|
||||||
|
/// </summary>
|
||||||
|
public sealed class IppanelSmsOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IPPanel API base URL
|
||||||
|
/// </summary>
|
||||||
|
public string BaseUrl { get; set; } = "https://edge.ippanel.com/v1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPPanel authorization token
|
||||||
|
/// </summary>
|
||||||
|
public required string AuthorizationToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default sender number for webservice SMS
|
||||||
|
/// </summary>
|
||||||
|
public string? DefaultSender { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
160
src/GreenHome.Sms.Ippanel/IppanelSmsService.cs
Normal file
160
src/GreenHome.Sms.Ippanel/IppanelSmsService.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPPanel SMS service implementation
|
||||||
|
/// </summary>
|
||||||
|
public sealed class IppanelSmsService : ISmsService
|
||||||
|
{
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private readonly IppanelSmsOptions options;
|
||||||
|
private readonly ILogger<IppanelSmsService> logger;
|
||||||
|
|
||||||
|
public IppanelSmsService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
IppanelSmsOptions options,
|
||||||
|
ILogger<IppanelSmsService> logger)
|
||||||
|
{
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
this.options = options;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IppanelApiResponse<IppanelData>?> SendWebserviceSmsAsync(
|
||||||
|
WebserviceSmsRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Split recipients by comma and create array
|
||||||
|
var recipients = request.Recipient
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
sending_type = "webservice",
|
||||||
|
from_number = request.Sender ?? options.DefaultSender,
|
||||||
|
@params = new
|
||||||
|
{
|
||||||
|
recipients
|
||||||
|
},
|
||||||
|
message = request.Message
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"api/send",
|
||||||
|
payload,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<IppanelApiResponse<IppanelData>>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending webservice SMS to {Recipient}", request.Recipient);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error sending webservice SMS");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IppanelApiResponse<IppanelData>?> SendPatternSmsAsync(
|
||||||
|
PatternSmsRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
sending_type = "pattern",
|
||||||
|
code = request.PatternCode,
|
||||||
|
from_number = request.Originator ?? options.DefaultSender ?? string.Empty,
|
||||||
|
recipients = request.Recipients,
|
||||||
|
@params = ToAnonymousObject(request.Variables)
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"api/send",
|
||||||
|
payload,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
//response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<IppanelApiResponse<IppanelData>>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending pattern SMS to {Recipient}", request.Recipients);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error sending pattern SMS");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ToAnonymousObject(Dictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
var type = dictionary.GetType();
|
||||||
|
var obj = new System.Dynamic.ExpandoObject();
|
||||||
|
var dict = obj as IDictionary<string, object>;
|
||||||
|
|
||||||
|
foreach (var item in dictionary)
|
||||||
|
{
|
||||||
|
dict.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
public class IppanelApiResponse<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("meta")]
|
||||||
|
public IppanelMeta Meta { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IppanelMeta
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message_parameters")]
|
||||||
|
public List<object> MessageParameters { get; set; } = new List<object>();
|
||||||
|
|
||||||
|
[JsonPropertyName("message_code")]
|
||||||
|
public string? MessageCode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
public Dictionary<string, List<string>> Errors { get; set; } = new Dictionary<string, List<string>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific data models for different response types
|
||||||
|
public class IppanelData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message_outbox_ids")]
|
||||||
|
public List<long> MessageOutboxIds { get; set; } = new List<long>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
63
src/GreenHome.Sms.Ippanel/Models.cs
Normal file
63
src/GreenHome.Sms.Ippanel/Models.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
namespace GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
// SMS DTOs
|
||||||
|
public sealed class WebserviceSmsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sender number (must be registered in IPPanel)
|
||||||
|
/// </summary>
|
||||||
|
public string? Sender { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recipient phone number(s) - comma separated for multiple recipients
|
||||||
|
/// </summary>
|
||||||
|
public required string Recipient { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SMS message content
|
||||||
|
/// </summary>
|
||||||
|
public required string Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PatternSmsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pattern code (must be registered in IPPanel)
|
||||||
|
/// </summary>
|
||||||
|
public required string PatternCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recipient phone number(s) - comma separated for multiple recipients
|
||||||
|
/// Note: Pattern SMS typically supports single recipient per request
|
||||||
|
/// </summary>
|
||||||
|
public required List<string> Recipients { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pattern variables as key-value pairs
|
||||||
|
/// </summary>
|
||||||
|
public required Dictionary<string, string> Variables { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional sender/originator number (if not provided, uses default from configuration)
|
||||||
|
/// </summary>
|
||||||
|
public string? Originator { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SmsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bulk ID for tracking the SMS
|
||||||
|
/// </summary>
|
||||||
|
public required List<long> BulkId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response status
|
||||||
|
/// </summary>
|
||||||
|
public required string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response message
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
130
src/GreenHome.Sms.Ippanel/README.md
Normal file
130
src/GreenHome.Sms.Ippanel/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# GreenHome.Sms.Ippanel
|
||||||
|
|
||||||
|
سرویس ارسال پیامک IPPanel برای پروژه GreenHome
|
||||||
|
|
||||||
|
## نصب و راهاندازی
|
||||||
|
|
||||||
|
### 1. اضافه کردن Reference به پروژه
|
||||||
|
|
||||||
|
در فایل `.csproj` پروژه خود:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ثبت سرویس در Program.cs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
// روش 1: استفاده از Configuration (پیشنهادی)
|
||||||
|
builder.Services.AddIppanelSms(builder.Configuration);
|
||||||
|
|
||||||
|
// روش 2: استفاده از Configuration Section
|
||||||
|
builder.Services.AddIppanelSms(builder.Configuration.GetSection("IppanelSms"));
|
||||||
|
|
||||||
|
// روش 3: تنظیم دستی
|
||||||
|
builder.Services.AddIppanelSms(options =>
|
||||||
|
{
|
||||||
|
options.BaseUrl = "https://edge.ippanel.com/v1";
|
||||||
|
options.AuthorizationToken = "YOUR_TOKEN_HERE";
|
||||||
|
options.DefaultSender = "10001001"; // اختیاری
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. تنظیمات در appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"IppanelSms": {
|
||||||
|
"BaseUrl": "https://edge.ippanel.com/v1",
|
||||||
|
"AuthorizationToken": "YOUR_IPPANEL_TOKEN_HERE",
|
||||||
|
"DefaultSender": "10001001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## استفاده
|
||||||
|
|
||||||
|
### تزریق سرویس
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ISmsService smsService;
|
||||||
|
|
||||||
|
public MyController(ISmsService smsService)
|
||||||
|
{
|
||||||
|
this.smsService = smsService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ارسال Webservice SMS
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var request = new WebserviceSmsRequest
|
||||||
|
{
|
||||||
|
Sender = "10001001",
|
||||||
|
Recipient = "09123456789", // یا چند شماره با کاما: "09123456789,09187654321"
|
||||||
|
Message = "سلام، این یک پیامک تست است"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await smsService.SendWebserviceSmsAsync(request);
|
||||||
|
// result.BulkId برای ردیابی پیامک استفاده میشود
|
||||||
|
```
|
||||||
|
|
||||||
|
### ارسال Pattern SMS
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var request = new PatternSmsRequest
|
||||||
|
{
|
||||||
|
PatternCode = "your-pattern-code",
|
||||||
|
Recipient = "09123456789",
|
||||||
|
Variables = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "code", "12345" },
|
||||||
|
{ "name", "کاربر" }
|
||||||
|
},
|
||||||
|
Originator = "10001001" // اختیاری، اگر نباشد از DefaultSender استفاده میشود
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await smsService.SendPatternSmsAsync(request);
|
||||||
|
```
|
||||||
|
|
||||||
|
## ساختار پروژه
|
||||||
|
|
||||||
|
```
|
||||||
|
GreenHome.Sms.Ippanel/
|
||||||
|
├── IppanelSmsService.cs # پیادهسازی سرویس
|
||||||
|
├── IppanelSmsOptions.cs # تنظیمات
|
||||||
|
└── ServiceCollectionExtensions.cs # Extension Methods برای ثبت سرویس
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
1. **توکن IPPanel**: حتماً توکن معتبر خود را از پنل IPPanel دریافت و در تنظیمات قرار دهید
|
||||||
|
2. **Webservice SMS**: میتوانید چند گیرنده را با کاما جدا کنید
|
||||||
|
3. **Pattern SMS**: معمولاً یک گیرنده در هر درخواست پشتیبانی میشود
|
||||||
|
4. **خطاها**: تمام خطاها لاگ میشوند و به صورت Exception پرتاب میشوند
|
||||||
|
|
||||||
|
## استفاده در پروژههای دیگر
|
||||||
|
|
||||||
|
این پروژه مستقل است و میتوانید آن را در هر پروژه .NET دیگری استفاده کنید:
|
||||||
|
|
||||||
|
1. پروژه را به Solution خود اضافه کنید
|
||||||
|
2. Reference را اضافه کنید
|
||||||
|
3. `GreenHome.Application` را نیز Reference کنید (برای Interface و DTOs)
|
||||||
|
4. سرویس را ثبت کنید
|
||||||
|
|
||||||
|
## تغییر سرویس SMS
|
||||||
|
|
||||||
|
اگر بخواهید از سرویس SMS دیگری استفاده کنید:
|
||||||
|
|
||||||
|
1. یک پیادهسازی جدید از `ISmsService` ایجاد کنید
|
||||||
|
2. در `Program.cs` به جای `AddIppanelSms` از متد ثبت سرویس جدید استفاده کنید
|
||||||
|
|
||||||
|
Interface و DTOs در `GreenHome.Application` باقی میمانند و نیازی به تغییر کدهای استفادهکننده نیست.
|
||||||
|
|
||||||
101
src/GreenHome.Sms.Ippanel/ServiceCollectionExtensions.cs
Normal file
101
src/GreenHome.Sms.Ippanel/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace GreenHome.Sms.Ippanel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering IPPanel SMS service
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds IPPanel SMS service to the service collection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configuration">Configuration section for IppanelSms</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddIppanelSms(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return services.AddIppanelSms(configuration.GetSection("IppanelSms"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds IPPanel SMS service to the service collection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configurationSection">Configuration section for IppanelSms</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddIppanelSms(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfigurationSection? configurationSection = null)
|
||||||
|
{
|
||||||
|
// Configure options
|
||||||
|
IppanelSmsOptions? options = null;
|
||||||
|
if (configurationSection != null)
|
||||||
|
{
|
||||||
|
options = configurationSection.Get<IppanelSmsOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"IppanelSms configuration section is missing. " +
|
||||||
|
"Please add 'IppanelSms' section to your appsettings.json with 'AuthorizationToken' property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register options as singleton
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// Register HttpClient and service
|
||||||
|
services.AddHttpClient<ISmsService, IppanelSmsService>(client =>
|
||||||
|
{
|
||||||
|
// Ensure BaseUrl ends with / for proper relative path handling
|
||||||
|
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.AuthorizationToken);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds IPPanel SMS service to the service collection with explicit options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configureOptions">Action to configure options</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddIppanelSms(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<IppanelSmsOptions> configureOptions)
|
||||||
|
{
|
||||||
|
var options = new IppanelSmsOptions
|
||||||
|
{
|
||||||
|
AuthorizationToken = string.Empty // Will be set by configureOptions
|
||||||
|
};
|
||||||
|
configureOptions(options);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.AuthorizationToken))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"IppanelSms AuthorizationToken is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register options as singleton
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// Register HttpClient and service
|
||||||
|
services.AddHttpClient<ISmsService, IppanelSmsService>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(options.BaseUrl);
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.AuthorizationToken);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
18
src/GreenHome.VoiceCall.Avanak/AvanakVoiceCallOptions.cs
Normal file
18
src/GreenHome.VoiceCall.Avanak/AvanakVoiceCallOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace GreenHome.VoiceCall.Avanak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for Avanak Voice Call service
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AvanakVoiceCallOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Avanak API base URL
|
||||||
|
/// </summary>
|
||||||
|
public string BaseUrl { get; set; } = "https://portal.avanak.ir/Rest";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avanak authorization token
|
||||||
|
/// </summary>
|
||||||
|
public required string Token { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
220
src/GreenHome.VoiceCall.Avanak/AvanakVoiceCallService.cs
Normal file
220
src/GreenHome.VoiceCall.Avanak/AvanakVoiceCallService.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace GreenHome.VoiceCall.Avanak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Avanak Voice Call service implementation
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AvanakVoiceCallService : IVoiceCallService
|
||||||
|
{
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private readonly AvanakVoiceCallOptions options;
|
||||||
|
private readonly ILogger<AvanakVoiceCallService> logger;
|
||||||
|
|
||||||
|
public AvanakVoiceCallService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
AvanakVoiceCallOptions options,
|
||||||
|
ILogger<AvanakVoiceCallService> logger)
|
||||||
|
{
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
this.options = options;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<AccountStatusResponse?> GetAccountStatusAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PostAsync(
|
||||||
|
"AccountStatus",
|
||||||
|
null,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<AccountStatusResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error getting account status");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error getting account status");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<GenerateTTSResponse?> GenerateTTSAsync(GenerateTTSRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"GenerateTTS",
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GenerateTTSResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error generating TTS for text: {Text}", request.Text);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error generating TTS");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<QuickSendResponse?> QuickSendAsync(QuickSendRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"QuickSend",
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<QuickSendResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending quick voice call to {Recipient}", request.Recipient);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error sending quick voice call");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<QuickSendWithTTSResponse?> QuickSendWithTTSAsync(QuickSendWithTTSRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Build form data according to Avanak API documentation
|
||||||
|
var formData = new List<KeyValuePair<string, string>>
|
||||||
|
{
|
||||||
|
new("Text", request.Text),
|
||||||
|
new("Number", request.Number)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.Vote.HasValue)
|
||||||
|
{
|
||||||
|
formData.Add(new KeyValuePair<string, string>("Vote", request.Vote.Value.ToString().ToLower()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ServerID.HasValue)
|
||||||
|
{
|
||||||
|
formData.Add(new KeyValuePair<string, string>("ServerID", request.ServerID.Value.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.CallFromMobile))
|
||||||
|
{
|
||||||
|
formData.Add(new KeyValuePair<string, string>("CallFromMobile", request.CallFromMobile));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.RecordVoice.HasValue)
|
||||||
|
{
|
||||||
|
formData.Add(new KeyValuePair<string, string>("RecordVoice", request.RecordVoice.Value.ToString().ToLower()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.RecordVoiceDuration.HasValue)
|
||||||
|
{
|
||||||
|
formData.Add(new KeyValuePair<string, string>("RecordVoiceDuration", request.RecordVoiceDuration.Value.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = new FormUrlEncodedContent(formData);
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsync(
|
||||||
|
"QuickSendWithTTS",
|
||||||
|
content,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var jsonString = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var result = JsonSerializer.Deserialize<QuickSendWithTTSResponse>(jsonString, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result != null && !result.IsSuccess)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"QuickSendWithTTS failed with ReturnValue={ReturnValue} for Number={Number}. " +
|
||||||
|
"Error codes: -25=QuickSend disabled, 0=No permission/demo user, -2=Wrong number, " +
|
||||||
|
"-3=Insufficient credit, -5=Text too long (>1000 chars), -6=Invalid send time, " +
|
||||||
|
"-7/-9=TTS generation error, -8=Text too short (<3 sec), -10=Empty text, " +
|
||||||
|
"-11=Duplicate send limit, -71=Invalid record duration, -72=No record permission, -111=Too Many Requests",
|
||||||
|
result.ReturnValue, request.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending quick voice call with TTS to {Number}", request.Number);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error sending quick voice call with TTS");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<GetQuickSendResponse?> GetQuickSendStatusAsync(GetQuickSendRequest request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.PostAsJsonAsync(
|
||||||
|
"GetQuickSend",
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<GetQuickSendResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error getting quick send status for {QuickSendId}", request.QuickSendId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error getting quick send status");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
||||||
47
src/GreenHome.VoiceCall.Avanak/IVoiceCallService.cs
Normal file
47
src/GreenHome.VoiceCall.Avanak/IVoiceCallService.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace GreenHome.VoiceCall.Avanak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for Avanak Voice Call service
|
||||||
|
/// </summary>
|
||||||
|
public interface IVoiceCallService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets account status
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Account status response</returns>
|
||||||
|
Task<AccountStatusResponse?> GetAccountStatusAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates TTS (Text-to-Speech) audio from text
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">TTS generation request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>TTS generation response</returns>
|
||||||
|
Task<GenerateTTSResponse?> GenerateTTSAsync(GenerateTTSRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends quick voice call with uploaded audio file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Quick send request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Quick send response</returns>
|
||||||
|
Task<QuickSendResponse?> QuickSendAsync(QuickSendRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends quick voice call with TTS (Text-to-Speech)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Quick send with TTS request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Quick send with TTS response</returns>
|
||||||
|
Task<QuickSendWithTTSResponse?> QuickSendWithTTSAsync(QuickSendWithTTSRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets quick send status
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Get quick send request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Quick send status response</returns>
|
||||||
|
Task<GetQuickSendResponse?> GetQuickSendStatusAsync(GetQuickSendRequest request, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
263
src/GreenHome.VoiceCall.Avanak/Models.cs
Normal file
263
src/GreenHome.VoiceCall.Avanak/Models.cs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace GreenHome.VoiceCall.Avanak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for AccountStatus method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AccountStatusRequest
|
||||||
|
{
|
||||||
|
// No parameters needed for AccountStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for AccountStatus method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AccountStatusResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public AccountStatusData? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account status data
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AccountStatusData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("balance")]
|
||||||
|
public decimal? Balance { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("credit")]
|
||||||
|
public decimal? Credit { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("expireDate")]
|
||||||
|
public string? ExpireDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for GenerateTTS method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GenerateTTSRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Text to convert to speech
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("text")]
|
||||||
|
public required string Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Voice type (optional)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("voiceType")]
|
||||||
|
public string? VoiceType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for GenerateTTS method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GenerateTTSResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public GenerateTTSData? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GenerateTTS data
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GenerateTTSData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("messageId")]
|
||||||
|
public string? MessageId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("audioUrl")]
|
||||||
|
public string? AudioUrl { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("duration")]
|
||||||
|
public int? Duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for QuickSend method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuickSendRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recipient phone number
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("recipient")]
|
||||||
|
public required string Recipient { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message ID (uploaded audio file ID)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("messageId")]
|
||||||
|
public required string MessageId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for QuickSend method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuickSendResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public QuickSendData? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// QuickSend data
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuickSendData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("quickSendId")]
|
||||||
|
public string? QuickSendId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("trackingId")]
|
||||||
|
public string? TrackingId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for QuickSendWithTTS method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuickSendWithTTSRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Text to convert to speech and send (required)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("Text")]
|
||||||
|
public required string Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phone number (required)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("Number")]
|
||||||
|
public required string Number { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable voting option (optional)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("Vote")]
|
||||||
|
public bool? Vote { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server ID (optional, default: 0)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("ServerID")]
|
||||||
|
public int? ServerID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mobile number to add at the end of voice (optional)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("CallFromMobile")]
|
||||||
|
public string? CallFromMobile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record voice (optional)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("RecordVoice")]
|
||||||
|
public bool? RecordVoice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Record voice duration (optional)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("RecordVoiceDuration")]
|
||||||
|
public short? RecordVoiceDuration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for QuickSendWithTTS method
|
||||||
|
/// ReturnValue > 0 means success and contains the quickSendId
|
||||||
|
/// </summary>
|
||||||
|
public sealed class QuickSendWithTTSResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Return value: > 0 means success (contains quickSendId), negative values indicate errors
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("ReturnValue")]
|
||||||
|
public int ReturnValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// QuickSendId (when ReturnValue > 0)
|
||||||
|
/// </summary>
|
||||||
|
public long QuickSendId => ReturnValue > 0 ? ReturnValue : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if the operation was successful
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess => ReturnValue > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for GetQuickSend method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetQuickSendRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// QuickSend ID or Tracking ID
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("quickSendId")]
|
||||||
|
public required string QuickSendId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for GetQuickSend method
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetQuickSendResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public bool Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public GetQuickSendData? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GetQuickSend data
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetQuickSendData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("quickSendId")]
|
||||||
|
public string? QuickSendId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("trackingId")]
|
||||||
|
public string? TrackingId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("recipient")]
|
||||||
|
public string? Recipient { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("deliveryStatus")]
|
||||||
|
public string? DeliveryStatus { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sentAt")]
|
||||||
|
public string? SentAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("deliveredAt")]
|
||||||
|
public string? DeliveredAt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("duration")]
|
||||||
|
public int? Duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
110
src/GreenHome.VoiceCall.Avanak/ServiceCollectionExtensions.cs
Normal file
110
src/GreenHome.VoiceCall.Avanak/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace GreenHome.VoiceCall.Avanak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering Avanak Voice Call service
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Avanak Voice Call service to the service collection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configuration">Configuration section for AvanakVoiceCall</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddAvanakVoiceCall(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return services.AddAvanakVoiceCall(configuration.GetSection("AvanakVoiceCall"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Avanak Voice Call service to the service collection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configurationSection">Configuration section for AvanakVoiceCall</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddAvanakVoiceCall(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfigurationSection? configurationSection = null)
|
||||||
|
{
|
||||||
|
// Configure options
|
||||||
|
AvanakVoiceCallOptions? options = null;
|
||||||
|
if (configurationSection != null)
|
||||||
|
{
|
||||||
|
options = configurationSection.Get<AvanakVoiceCallOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AvanakVoiceCall configuration section is missing. " +
|
||||||
|
"Please add 'AvanakVoiceCall' section to your appsettings.json with 'Token' property.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Token))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AvanakVoiceCall Token is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register options as singleton
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// Register HttpClient and service
|
||||||
|
services.AddHttpClient<IVoiceCallService, AvanakVoiceCallService>(client =>
|
||||||
|
{
|
||||||
|
// Ensure BaseUrl ends with / for proper relative path handling
|
||||||
|
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
// Note: Content-Type will be set automatically by HttpClient based on content type (JSON or FormUrlEncoded)
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.Token);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Avanak Voice Call service to the service collection with explicit options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection</param>
|
||||||
|
/// <param name="configureOptions">Action to configure options</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddAvanakVoiceCall(
|
||||||
|
this IServiceCollection services,
|
||||||
|
Action<AvanakVoiceCallOptions> configureOptions)
|
||||||
|
{
|
||||||
|
var options = new AvanakVoiceCallOptions
|
||||||
|
{
|
||||||
|
Token = string.Empty // Will be set by configureOptions
|
||||||
|
};
|
||||||
|
configureOptions(options);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Token))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AvanakVoiceCall Token is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register options as singleton
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// Register HttpClient and service
|
||||||
|
services.AddHttpClient<IVoiceCallService, AvanakVoiceCallService>(client =>
|
||||||
|
{
|
||||||
|
// Ensure BaseUrl ends with / for proper relative path handling
|
||||||
|
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
|
||||||
|
client.BaseAddress = new Uri(baseUrl);
|
||||||
|
// Note: Content-Type will be set automatically by HttpClient based on content type (JSON or FormUrlEncoded)
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.Token);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Infrastructure",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Api", "GreenHome.Api\GreenHome.Api.csproj", "{75E498D4-5D04-4F63-A1A5-5D851FA40C74}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Api", "GreenHome.Api\GreenHome.Api.csproj", "{75E498D4-5D04-4F63-A1A5-5D851FA40C74}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Sms.Ippanel", "GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj", "{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.VoiceCall.Avanak", "GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj", "{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -33,6 +37,14 @@ Global
|
|||||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.Build.0 = Release|Any CPU
|
{75E498D4-5D04-4F63-A1A5-5D851FA40C74}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B1FF8C56-E758-48C9-A6EB-E2C938D1BE6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user