diff --git a/src/DEVICE_TOKEN_API.md b/src/DEVICE_TOKEN_API.md
new file mode 100644
index 0000000..3e0b012
--- /dev/null
+++ b/src/DEVICE_TOKEN_API.md
@@ -0,0 +1,243 @@
+# API مدیریت توکن و تنظیمات دستگاه
+
+این سند توضیح دهنده API های جدید اضافه شده برای مدیریت توکن و تنظیمات دستگاه است.
+
+## تغییرات در مدل داده
+
+### فیلدهای جدید در `DeviceSettings`
+
+1. **UploadIntervalMin** (int): فاصله زمانی آپلود داده به دقیقه (پیشفرض: 5)
+2. **DevicePhoneNumber** (string): شماره تلفن دستگاه
+3. **SimCardType** (enum, nullable): نوع سیمکارت (همراه اول/ایرانسل/رایتل)
+4. **TokenCode** (string, nullable): کد توکن 5 رقمی
+5. **VerificationCode** (string, nullable): کد تایید 5 رقمی
+6. **TokenExpiresAt** (DateTime, nullable): تاریخ انقضای توکن
+
+### Enum نوع سیمکارت
+
+```csharp
+public enum SimCardType
+{
+ Hamrahe_Aval = 1, // همراه اول
+ Irancell = 2, // ایرانسل
+ Rightel = 3 // رایتل
+}
+```
+
+## API Endpoints
+
+### 1. دریافت فاصله زمانی آپلود
+
+**GET** `/api/DeviceToken/upload-interval`
+
+دریافت مقدار `UPLOAD_INTERVAL_MIN` بر اساس شناسه دستگاه یا شماره تلفن.
+
+#### پارامترها (Query String)
+
+- `deviceId` (int, optional): شناسه دستگاه
+- `devicePhoneNumber` (string, optional): شماره تلفن دستگاه
+
+**نکته:** حداقل یکی از پارامترها باید ارسال شود.
+
+#### مثال درخواست
+
+```http
+GET /api/DeviceToken/upload-interval?devicePhoneNumber=09123456789
+```
+
+#### پاسخ موفق
+
+```json
+{
+ "success": true,
+ "message": null,
+ "uploadIntervalMin": 5
+}
+```
+
+#### پاسخ خطا
+
+```json
+{
+ "success": false,
+ "message": "دستگاه یافت نشد",
+ "uploadIntervalMin": null
+}
+```
+
+---
+
+### 2. درخواست توکن دستگاه
+
+**POST** `/api/DeviceToken/request-token`
+
+تولید کد توکن 5 رقمی و ارسال آن از طریق پیامک به شماره دستگاه.
+
+#### بدنه درخواست (JSON)
+
+```json
+{
+ "devicePhoneNumber": "09123456789"
+}
+```
+
+#### مثال درخواست
+
+```http
+POST /api/DeviceToken/request-token
+Content-Type: application/json
+
+{
+ "devicePhoneNumber": "09123456789"
+}
+```
+
+#### پاسخ موفق
+
+```json
+{
+ "success": true,
+ "message": "کد تایید با موفقیت ارسال شد",
+ "tokenCode": "12345"
+}
+```
+
+**نکته:** پیامک حاوی کد توکن به شماره مشخص شده ارسال میشود. کد دارای اعتبار 10 دقیقه است.
+
+#### محاسبه کد تایید
+
+کد تایید بر اساس فرمول زیر محاسبه میشود:
+
+```
+VerificationCode = (TokenCode × 7 + 12345) % 100000
+```
+
+---
+
+### 3. تایید توکن دستگاه
+
+**POST** `/api/DeviceToken/verify-token`
+
+تایید کد تایید و ارسال تنظیمات کدشده دستگاه از طریق پیامک.
+
+#### بدنه درخواست (JSON)
+
+```json
+{
+ "devicePhoneNumber": "09123456789",
+ "verificationCode": "98765"
+}
+```
+
+#### مثال درخواست
+
+```http
+POST /api/DeviceToken/verify-token
+Content-Type: application/json
+
+{
+ "devicePhoneNumber": "09123456789",
+ "verificationCode": "98765"
+}
+```
+
+#### پاسخ موفق
+
+```json
+{
+ "success": true,
+ "message": "تنظیمات با موفقیت ارسال شد",
+ "encodedSettings": "RGV2aWNlMDF8NQ=="
+}
+```
+
+**نکته:** تنظیمات به صورت کدشده Base64 ارسال میشود. فرمت قبل از کدگذاری: `{DeviceName}|{UploadIntervalMin}`
+
+#### پاسخ خطا
+
+```json
+{
+ "success": false,
+ "message": "کد تایید نادرست است",
+ "encodedSettings": null
+}
+```
+
+---
+
+### 4. بروزرسانی تنظیمات دستگاه
+
+**PUT** `/api/DeviceSettings`
+
+API موجود که حالا فیلدهای جدید را نیز پشتیبانی میکند.
+
+#### بدنه درخواست (JSON)
+
+```json
+{
+ "id": 1,
+ "deviceId": 1,
+ "province": "تهران",
+ "city": "تهران",
+ "productType": "گلخانه",
+ "uploadIntervalMin": 5,
+ "devicePhoneNumber": "09123456789",
+ "simCardType": 1,
+ "minimumSmsIntervalMinutes": 15,
+ "minimumCallIntervalMinutes": 60
+}
+```
+
+## فلوی کاری (Workflow)
+
+### سناریو: دریافت تنظیمات دستگاه
+
+1. **دستگاه درخواست توکن میکند:**
+ ```http
+ POST /api/DeviceToken/request-token
+ Body: { "devicePhoneNumber": "09123456789" }
+ ```
+
+2. **سرور کد توکن تولید و ارسال میکند:**
+ - کد توکن 5 رقمی: مثلاً `12345`
+ - کد تایید محاسبه شده: `(12345 × 7 + 12345) % 100000 = 98760`
+ - پیامک حاوی کد توکن به شماره دستگاه ارسال میشود
+
+3. **دستگاه کد تایید را محاسبه و ارسال میکند:**
+ ```http
+ POST /api/DeviceToken/verify-token
+ Body: {
+ "devicePhoneNumber": "09123456789",
+ "verificationCode": "98760"
+ }
+ ```
+
+4. **سرور تنظیمات کدشده را ارسال میکند:**
+ - تنظیمات: `Device01|5`
+ - Base64: `RGV2aWNlMDF8NQ==`
+ - پیامک حاوی تنظیمات کدشده به شماره دستگاه ارسال میشود
+
+5. **دستگاه تنظیمات را decode کرده و اعمال میکند**
+
+## نکات امنیتی
+
+1. کد توکن فقط 10 دقیقه اعتبار دارد
+2. پس از تایید موفق، کدهای توکن و تایید از دیتابیس پاک میشوند
+3. کدگذاری Base64 یک کدگذاری ساده است و برای امنیت بیشتر میتوان از روشهای پیچیدهتر استفاده کرد
+
+## Migration
+
+Migration با نام `AddDeviceTokenAndPhoneFields` ایجاد و به دیتابیس اعمال شده است.
+
+برای اعمال دستی (در صورت نیاز):
+
+```bash
+dotnet ef database update --project GreenHome.Infrastructure --startup-project GreenHome.Api
+```
+
+## تست API ها
+
+میتوانید از Swagger UI (که در حالت Development در `/scalar/v1` در دسترس است) برای تست API ها استفاده کنید.
+
+یا از ابزارهایی مانند Postman/Insomnia با استفاده از نمونههای بالا.
+
diff --git a/src/GreenHome.Api/Controllers/DeviceTokenController.cs b/src/GreenHome.Api/Controllers/DeviceTokenController.cs
new file mode 100644
index 0000000..ab36cc5
--- /dev/null
+++ b/src/GreenHome.Api/Controllers/DeviceTokenController.cs
@@ -0,0 +1,128 @@
+using GreenHome.Application;
+using Microsoft.AspNetCore.Mvc;
+
+namespace GreenHome.Api.Controllers;
+
+///
+/// کنترلر مدیریت توکن و تنظیمات دستگاه
+///
+[ApiController]
+[Route("api/[controller]")]
+public class DeviceTokenController : ControllerBase
+{
+ private readonly IDeviceTokenService deviceTokenService;
+ private readonly ILogger logger;
+
+ public DeviceTokenController(
+ IDeviceTokenService deviceTokenService,
+ ILogger logger)
+ {
+ this.deviceTokenService = deviceTokenService;
+ this.logger = logger;
+ }
+
+ ///
+ /// دریافت فاصله زمانی آپلود دستگاه
+ ///
+ /// شناسه دستگاه (اختیاری)
+ /// شماره تلفن دستگاه (اختیاری)
+ /// فاصله زمانی آپلود به دقیقه
+ [HttpGet("upload-interval")]
+ public async Task> GetUploadInterval(
+ [FromQuery] int? deviceId,
+ [FromQuery] string? devicePhoneNumber,
+ CancellationToken cancellationToken)
+ {
+ if (!deviceId.HasValue && string.IsNullOrWhiteSpace(devicePhoneNumber))
+ {
+ return BadRequest(new GetUploadIntervalResponse
+ {
+ Success = false,
+ Message = "حداقل یکی از پارامترهای deviceId یا devicePhoneNumber باید ارسال شود"
+ });
+ }
+
+ var request = new GetUploadIntervalRequest
+ {
+ DeviceId = deviceId,
+ DevicePhoneNumber = devicePhoneNumber
+ };
+
+ var result = await deviceTokenService.GetUploadIntervalAsync(request, cancellationToken);
+
+ if (!result.Success)
+ {
+ return NotFound(result);
+ }
+
+ return Ok(result);
+ }
+
+ ///
+ /// درخواست توکن دستگاه (تولید و ارسال کد از طریق پیامک)
+ ///
+ /// درخواست شامل شماره تلفن دستگاه
+ /// نتیجه درخواست
+ [HttpPost("request-token")]
+ public async Task> RequestToken(
+ [FromBody] RequestDeviceTokenRequest request,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
+ {
+ return BadRequest(new RequestDeviceTokenResponse
+ {
+ Success = false,
+ Message = "شماره تلفن دستگاه الزامی است"
+ });
+ }
+
+ var result = await deviceTokenService.RequestDeviceTokenAsync(request, cancellationToken);
+
+ if (!result.Success)
+ {
+ return BadRequest(result);
+ }
+
+ return Ok(result);
+ }
+
+ ///
+ /// تایید توکن دستگاه (ارسال تنظیمات کدشده از طریق پیامک)
+ ///
+ /// درخواست شامل شماره تلفن و کد تایید
+ /// نتیجه تایید و تنظیمات کدشده
+ [HttpPost("verify-token")]
+ public async Task> VerifyToken(
+ [FromBody] VerifyDeviceTokenRequest request,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
+ {
+ return BadRequest(new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "شماره تلفن دستگاه الزامی است"
+ });
+ }
+
+ if (string.IsNullOrWhiteSpace(request.VerificationCode))
+ {
+ return BadRequest(new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "کد تایید الزامی است"
+ });
+ }
+
+ var result = await deviceTokenService.VerifyDeviceTokenAsync(request, cancellationToken);
+
+ if (!result.Success)
+ {
+ return BadRequest(result);
+ }
+
+ return Ok(result);
+ }
+}
+
diff --git a/src/GreenHome.Api/Program.cs b/src/GreenHome.Api/Program.cs
index e1fa0e3..54f7862 100644
--- a/src/GreenHome.Api/Program.cs
+++ b/src/GreenHome.Api/Program.cs
@@ -66,6 +66,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
// SMS Service Configuration
builder.Services.AddIppanelSms(builder.Configuration);
diff --git a/src/GreenHome.Application/Dtos.cs b/src/GreenHome.Application/Dtos.cs
index 975cf2c..4fbc4ea 100644
--- a/src/GreenHome.Application/Dtos.cs
+++ b/src/GreenHome.Application/Dtos.cs
@@ -128,6 +128,13 @@ public sealed class DeviceSettingsDto
public int MinimumCallIntervalMinutes { get; set; } = 60;
public decimal? AreaSquareMeters { get; set; }
+ public int UploadIntervalMin { get; set; } = 5;
+ public string DevicePhoneNumber { get; set; } = string.Empty;
+ public Domain.SimCardType? SimCardType { get; set; }
+ public string? TokenCode { get; set; }
+ public string? VerificationCode { get; set; }
+ public DateTime? TokenExpiresAt { get; set; }
+
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
@@ -312,4 +319,62 @@ public sealed class UserDailyReportFilter
public int? Month { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
+}
+
+// DTOs برای مدیریت توکن دستگاه
+
+///
+/// درخواست دریافت فاصله زمانی آپلود
+///
+public sealed class GetUploadIntervalRequest
+{
+ public int? DeviceId { get; set; }
+ public string? DevicePhoneNumber { get; set; }
+}
+
+///
+/// پاسخ دریافت فاصله زمانی آپلود
+///
+public sealed class GetUploadIntervalResponse
+{
+ public bool Success { get; set; }
+ public string? Message { get; set; }
+ public int? UploadIntervalMin { get; set; }
+}
+
+///
+/// درخواست دریافت توکن دستگاه
+///
+public sealed class RequestDeviceTokenRequest
+{
+ public required string DevicePhoneNumber { get; set; }
+}
+
+///
+/// پاسخ دریافت توکن دستگاه
+///
+public sealed class RequestDeviceTokenResponse
+{
+ public bool Success { get; set; }
+ public string? Message { get; set; }
+ public string? TokenCode { get; set; }
+}
+
+///
+/// درخواست تایید توکن دستگاه
+///
+public sealed class VerifyDeviceTokenRequest
+{
+ public required string DevicePhoneNumber { get; set; }
+ public required string VerificationCode { get; set; }
+}
+
+///
+/// پاسخ تایید توکن دستگاه
+///
+public sealed class VerifyDeviceTokenResponse
+{
+ public bool Success { get; set; }
+ public string? Message { get; set; }
+ public string? EncodedSettings { get; set; }
}
\ No newline at end of file
diff --git a/src/GreenHome.Application/IDeviceTokenService.cs b/src/GreenHome.Application/IDeviceTokenService.cs
new file mode 100644
index 0000000..f98d892
--- /dev/null
+++ b/src/GreenHome.Application/IDeviceTokenService.cs
@@ -0,0 +1,23 @@
+namespace GreenHome.Application;
+
+///
+/// سرویس مدیریت توکن و تنظیمات دستگاه
+///
+public interface IDeviceTokenService
+{
+ ///
+ /// دریافت فاصله زمانی آپلود بر اساس شماره تلفن یا شناسه دستگاه
+ ///
+ Task GetUploadIntervalAsync(GetUploadIntervalRequest request, CancellationToken cancellationToken);
+
+ ///
+ /// درخواست توکن دستگاه (تولید و ارسال کد)
+ ///
+ Task RequestDeviceTokenAsync(RequestDeviceTokenRequest request, CancellationToken cancellationToken);
+
+ ///
+ /// تایید توکن دستگاه (ارسال تنظیمات)
+ ///
+ Task VerifyDeviceTokenAsync(VerifyDeviceTokenRequest request, CancellationToken cancellationToken);
+}
+
diff --git a/src/GreenHome.Domain/DeviceSettings.cs b/src/GreenHome.Domain/DeviceSettings.cs
index cb94240..abfc63e 100644
--- a/src/GreenHome.Domain/DeviceSettings.cs
+++ b/src/GreenHome.Domain/DeviceSettings.cs
@@ -1,5 +1,26 @@
namespace GreenHome.Domain;
+///
+/// نوع سیم کارت
+///
+public enum SimCardType
+{
+ ///
+ /// همراه اول
+ ///
+ Hamrahe_Aval = 1,
+
+ ///
+ /// ایرانسل
+ ///
+ Irancell = 2,
+
+ ///
+ /// رایتل
+ ///
+ Rightel = 3
+}
+
public sealed class DeviceSettings
{
public int Id { get; set; }
@@ -46,6 +67,36 @@ public sealed class DeviceSettings
///
public decimal? AreaSquareMeters { get; set; }
+ ///
+ /// فاصله زمانی آپلود داده (به دقیقه)
+ ///
+ public int UploadIntervalMin { get; set; } = 5;
+
+ ///
+ /// شماره تلفن دستگاه
+ ///
+ public string DevicePhoneNumber { get; set; } = string.Empty;
+
+ ///
+ /// نوع سیم کارت
+ ///
+ public SimCardType? SimCardType { get; set; }
+
+ ///
+ /// کد توکن (5 رقمی)
+ ///
+ public string? TokenCode { get; set; }
+
+ ///
+ /// کد تایید (5 رقمی)
+ ///
+ public string? VerificationCode { get; set; }
+
+ ///
+ /// تاریخ انقضای توکن
+ ///
+ public DateTime? TokenExpiresAt { get; set; }
+
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
\ No newline at end of file
diff --git a/src/GreenHome.Infrastructure/DeviceTokenService.cs b/src/GreenHome.Infrastructure/DeviceTokenService.cs
new file mode 100644
index 0000000..c684d74
--- /dev/null
+++ b/src/GreenHome.Infrastructure/DeviceTokenService.cs
@@ -0,0 +1,249 @@
+using GreenHome.Application;
+using GreenHome.Sms.Ippanel;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace GreenHome.Infrastructure;
+
+///
+/// سرویس مدیریت توکن و تنظیمات دستگاه
+///
+public sealed class DeviceTokenService : IDeviceTokenService
+{
+ private readonly GreenHomeDbContext dbContext;
+ private readonly ISmsService smsService;
+ private readonly ILogger logger;
+
+ public DeviceTokenService(
+ GreenHomeDbContext dbContext,
+ ISmsService smsService,
+ ILogger logger)
+ {
+ this.dbContext = dbContext;
+ this.smsService = smsService;
+ this.logger = logger;
+ }
+
+ ///
+ /// دریافت فاصله زمانی آپلود بر اساس شماره تلفن یا شناسه دستگاه
+ ///
+ public async Task GetUploadIntervalAsync(
+ GetUploadIntervalRequest request,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ Domain.DeviceSettings? settings = null;
+
+ // جستجو بر اساس DeviceId یا DevicePhoneNumber
+ if (request.DeviceId.HasValue)
+ {
+ settings = await dbContext.DeviceSettings
+ .AsNoTracking()
+ .FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId.Value, cancellationToken);
+ }
+ else if (!string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
+ {
+ settings = await dbContext.DeviceSettings
+ .AsNoTracking()
+ .FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
+ }
+
+ if (settings == null)
+ {
+ return new GetUploadIntervalResponse
+ {
+ Success = false,
+ Message = "دستگاه یافت نشد"
+ };
+ }
+
+ return new GetUploadIntervalResponse
+ {
+ Success = true,
+ UploadIntervalMin = settings.UploadIntervalMin
+ };
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error getting upload interval for device");
+ return new GetUploadIntervalResponse
+ {
+ Success = false,
+ Message = "خطا در دریافت اطلاعات"
+ };
+ }
+ }
+
+ ///
+ /// درخواست توکن دستگاه (تولید و ارسال کد)
+ ///
+ public async Task RequestDeviceTokenAsync(
+ RequestDeviceTokenRequest request,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ // پیدا کردن تنظیمات دستگاه بر اساس شماره تلفن
+ var settings = await dbContext.DeviceSettings
+ .FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
+
+ if (settings == null)
+ {
+ return new RequestDeviceTokenResponse
+ {
+ Success = false,
+ Message = "دستگاه با این شماره تلفن یافت نشد"
+ };
+ }
+
+ // تولید کد توکن 5 رقمی
+ var random = new Random();
+ var tokenCode = random.Next(10000, 99999).ToString();
+
+ // تولید کد تایید بر اساس فرمول: (TokenCode * 7 + 12345) % 100000
+ var verificationCode = ((int.Parse(tokenCode) * 7 + 12345) % 100000).ToString("D5");
+
+ // ذخیره کدها
+ settings.TokenCode = tokenCode;
+ settings.VerificationCode = verificationCode;
+ settings.TokenExpiresAt = DateTime.UtcNow.AddMinutes(10); // اعتبار 10 دقیقه
+ settings.UpdatedAt = DateTime.UtcNow;
+
+ await dbContext.SaveChangesAsync(cancellationToken);
+
+ // ارسال کد توکن از طریق پیامک
+ try
+ {
+ await smsService.SendWebserviceSmsAsync(new WebserviceSmsRequest
+ {
+ Recipient = request.DevicePhoneNumber,
+ Message = $"کد تایید دستگاه شما: {tokenCode}\nاعتبار: 10 دقیقه"
+ }, cancellationToken);
+ }
+ catch (Exception smsEx)
+ {
+ logger.LogError(smsEx, "Error sending token SMS to {PhoneNumber}", request.DevicePhoneNumber);
+ return new RequestDeviceTokenResponse
+ {
+ Success = false,
+ Message = "خطا در ارسال پیامک"
+ };
+ }
+
+ logger.LogInformation("Token requested for device phone {PhoneNumber}", request.DevicePhoneNumber);
+
+ return new RequestDeviceTokenResponse
+ {
+ Success = true,
+ Message = "کد تایید با موفقیت ارسال شد",
+ TokenCode = tokenCode
+ };
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error requesting device token");
+ return new RequestDeviceTokenResponse
+ {
+ Success = false,
+ Message = "خطا در درخواست توکن"
+ };
+ }
+ }
+
+ ///
+ /// تایید توکن دستگاه (ارسال تنظیمات)
+ ///
+ public async Task VerifyDeviceTokenAsync(
+ VerifyDeviceTokenRequest request,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ // پیدا کردن تنظیمات دستگاه
+ var settings = await dbContext.DeviceSettings
+ .Include(ds => ds.Device)
+ .FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
+
+ if (settings == null)
+ {
+ return new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "دستگاه با این شماره تلفن یافت نشد"
+ };
+ }
+
+ // بررسی انقضای توکن
+ if (settings.TokenExpiresAt == null || settings.TokenExpiresAt < DateTime.UtcNow)
+ {
+ return new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "کد تایید منقضی شده است"
+ };
+ }
+
+ // بررسی کد تایید
+ if (settings.VerificationCode != request.VerificationCode)
+ {
+ return new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "کد تایید نادرست است"
+ };
+ }
+
+ // رمزگذاری ساده تنظیمات: DeviceName|UploadIntervalMin به Base64
+ var deviceName = settings.Device.DeviceName;
+ var uploadInterval = settings.UploadIntervalMin;
+ var plainText = $"{deviceName}|{uploadInterval}";
+ var encodedSettings = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(plainText));
+
+ // ارسال تنظیمات کدشده از طریق پیامک
+ try
+ {
+ await smsService.SendWebserviceSmsAsync(new WebserviceSmsRequest
+ {
+ Recipient = request.DevicePhoneNumber,
+ Message = $"تنظیمات دستگاه:\n{encodedSettings}"
+ }, cancellationToken);
+ }
+ catch (Exception smsEx)
+ {
+ logger.LogError(smsEx, "Error sending settings SMS to {PhoneNumber}", request.DevicePhoneNumber);
+ return new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "خطا در ارسال پیامک"
+ };
+ }
+
+ // پاک کردن کدها بعد از استفاده موفق
+ settings.TokenCode = null;
+ settings.VerificationCode = null;
+ settings.TokenExpiresAt = null;
+ settings.UpdatedAt = DateTime.UtcNow;
+ await dbContext.SaveChangesAsync(cancellationToken);
+
+ logger.LogInformation("Device token verified for {PhoneNumber}", request.DevicePhoneNumber);
+
+ return new VerifyDeviceTokenResponse
+ {
+ Success = true,
+ Message = "تنظیمات با موفقیت ارسال شد",
+ EncodedSettings = encodedSettings
+ };
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error verifying device token");
+ return new VerifyDeviceTokenResponse
+ {
+ Success = false,
+ Message = "خطا در تایید توکن"
+ };
+ }
+ }
+}
+
diff --git a/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.Designer.cs b/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.Designer.cs
new file mode 100644
index 0000000..542092a
--- /dev/null
+++ b/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.Designer.cs
@@ -0,0 +1,1218 @@
+//
+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("20251217154130_AddDeviceTokenAndPhoneFields")]
+ partial class AddDeviceTokenAndPhoneFields
+ {
+ ///
+ 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.AIQuery", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Answer")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompletionTokens")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("Model")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PromptTokens")
+ .HasColumnType("int");
+
+ b.Property("Question")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ResponseTimeMs")
+ .HasColumnType("bigint");
+
+ b.Property("Temperature")
+ .HasColumnType("float");
+
+ b.Property("TotalTokens")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AIQueries", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CallCooldownMinutes")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("IsEnabled")
+ .HasColumnType("bit");
+
+ b.Property("NotificationType")
+ .HasColumnType("int");
+
+ b.Property("SmsCooldownMinutes")
+ .HasColumnType("int");
+
+ b.Property("TimeType")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.ToTable("AlertConditions", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AlertConditionId")
+ .HasColumnType("int");
+
+ b.Property("AlertType")
+ .HasColumnType("int");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("NotificationType")
+ .HasColumnType("int");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("ProcessingTimeMs")
+ .HasColumnType("bigint");
+
+ b.Property("SentAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AlertConditionId");
+
+ b.HasIndex("AlertType");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("DeviceId", "SentAt");
+
+ b.HasIndex("UserId", "SentAt");
+
+ b.ToTable("AlertLogs", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AlertConditionId")
+ .HasColumnType("int");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("IsSent")
+ .HasColumnType("bit");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("MessageOutboxIds")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("NotificationType")
+ .HasColumnType("int");
+
+ b.Property("SentAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AlertConditionId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt");
+
+ b.ToTable("AlertNotifications", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AlertConditionId")
+ .HasColumnType("int");
+
+ b.Property("ComparisonType")
+ .HasColumnType("int");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.Property("SensorType")
+ .HasColumnType("int");
+
+ b.Property("Value1")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Value2")
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AlertConditionId");
+
+ b.ToTable("AlertRules", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedByUserId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedByUserId");
+
+ b.HasIndex("DeviceId", "IsActive");
+
+ b.ToTable("Checklists", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ChecklistId")
+ .HasColumnType("int");
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CompletedByUserId")
+ .HasColumnType("int");
+
+ b.Property("Notes")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("PersianDate")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompletedByUserId");
+
+ b.HasIndex("ChecklistId", "PersianDate");
+
+ b.ToTable("ChecklistCompletions", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ChecklistId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("IsRequired")
+ .HasColumnType("bit");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("nvarchar(300)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChecklistId", "Order");
+
+ b.ToTable("ChecklistItems", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ChecklistCompletionId")
+ .HasColumnType("int");
+
+ b.Property("ChecklistItemId")
+ .HasColumnType("int");
+
+ b.Property("IsChecked")
+ .HasColumnType("bit");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChecklistCompletionId");
+
+ b.HasIndex("ChecklistItemId");
+
+ b.ToTable("ChecklistItemCompletions", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Analysis")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompletionTokens")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("Model")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PersianDate")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("PersianDay")
+ .HasColumnType("int");
+
+ b.Property("PersianMonth")
+ .HasColumnType("int");
+
+ b.Property("PersianYear")
+ .HasColumnType("int");
+
+ b.Property("PromptTokens")
+ .HasColumnType("int");
+
+ b.Property("RecordCount")
+ .HasColumnType("int");
+
+ b.Property("ResponseTimeMs")
+ .HasColumnType("bigint");
+
+ b.Property("SampledRecordCount")
+ .HasColumnType("int");
+
+ b.Property("TotalTokens")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("DeviceId", "PersianDate")
+ .IsUnique();
+
+ b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
+
+ b.ToTable("DailyReports", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("Location")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("nvarchar(250)");
+
+ b.Property("NeshanLocation")
+ .IsRequired()
+ .HasMaxLength(80)
+ .HasColumnType("nvarchar(80)");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Devices", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AuthorUserId")
+ .HasColumnType("int");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(5000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorUserId");
+
+ b.HasIndex("DeviceId", "CreatedAt");
+
+ b.ToTable("DevicePosts", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("DevicePostId")
+ .HasColumnType("int");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("FileSize")
+ .HasColumnType("bigint");
+
+ b.Property("UploadedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DevicePostId");
+
+ b.ToTable("DevicePostImages", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AreaSquareMeters")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("City")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("DevicePhoneNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Latitude")
+ .HasColumnType("decimal(9,6)");
+
+ b.Property("Longitude")
+ .HasColumnType("decimal(9,6)");
+
+ b.Property("MinimumCallIntervalMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(60);
+
+ b.Property("MinimumSmsIntervalMinutes")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(15);
+
+ b.Property("ProductType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Province")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("SimCardType")
+ .HasColumnType("int");
+
+ b.Property("TokenCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TokenExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UploadIntervalMin")
+ .HasColumnType("int");
+
+ b.Property("VerificationCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceSettings", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
+ {
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.Property("ReceiveAlerts")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true);
+
+ b.HasKey("DeviceId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("DeviceUsers", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ReportImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("FileSize")
+ .HasColumnType("bigint");
+
+ b.Property("UploadedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserDailyReportId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserDailyReportId");
+
+ b.ToTable("ReportImages", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("GasPPM")
+ .HasColumnType("int");
+
+ b.Property("HumidityPercent")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Lux")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("PersianDate")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("PersianMonth")
+ .HasColumnType("int");
+
+ b.Property("PersianYear")
+ .HasColumnType("int");
+
+ b.Property("ServerTimestampUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("SoilPercent")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("TemperatureC")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("TimestampUtc")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId", "ServerTimestampUtc");
+
+ b.HasIndex("DeviceId", "TimestampUtc");
+
+ b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
+
+ b.ToTable("Telemetry", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Family")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("LastLoginAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Mobile")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Mobile")
+ .IsUnique();
+
+ b.ToTable("Users", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("DeviceId")
+ .HasColumnType("int");
+
+ b.Property("Notes")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)");
+
+ b.Property("Observations")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Operations")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PersianDate")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.Property("PersianDay")
+ .HasColumnType("int");
+
+ b.Property("PersianMonth")
+ .HasColumnType("int");
+
+ b.Property("PersianYear")
+ .HasColumnType("int");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("DeviceId", "PersianDate");
+
+ b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
+
+ b.ToTable("UserDailyReports", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(4)
+ .HasColumnType("nvarchar(4)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("IsUsed")
+ .HasColumnType("bit");
+
+ b.Property("Mobile")
+ .IsRequired()
+ .HasMaxLength(11)
+ .HasColumnType("nvarchar(11)");
+
+ b.Property("UsedAt")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Mobile", "Code", "IsUsed");
+
+ b.ToTable("VerificationCodes", (string)null);
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
+ {
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GreenHome.Domain.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("Device");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
+ {
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertLog", b =>
+ {
+ b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
+ .WithMany()
+ .HasForeignKey("AlertConditionId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ 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("AlertCondition");
+
+ b.Navigation("Device");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
+ {
+ b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
+ .WithMany()
+ .HasForeignKey("AlertConditionId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ 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("AlertCondition");
+
+ b.Navigation("Device");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
+ {
+ b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
+ .WithMany("Rules")
+ .HasForeignKey("AlertConditionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AlertCondition");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
+ {
+ b.HasOne("GreenHome.Domain.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedByUserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
+ {
+ b.HasOne("GreenHome.Domain.Checklist", "Checklist")
+ .WithMany("Completions")
+ .HasForeignKey("ChecklistId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GreenHome.Domain.User", "CompletedByUser")
+ .WithMany()
+ .HasForeignKey("CompletedByUserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Checklist");
+
+ b.Navigation("CompletedByUser");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistItem", b =>
+ {
+ b.HasOne("GreenHome.Domain.Checklist", "Checklist")
+ .WithMany("Items")
+ .HasForeignKey("ChecklistId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Checklist");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistItemCompletion", b =>
+ {
+ b.HasOne("GreenHome.Domain.ChecklistCompletion", "ChecklistCompletion")
+ .WithMany("ItemCompletions")
+ .HasForeignKey("ChecklistCompletionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GreenHome.Domain.ChecklistItem", "ChecklistItem")
+ .WithMany()
+ .HasForeignKey("ChecklistItemId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("ChecklistCompletion");
+
+ b.Navigation("ChecklistItem");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
+ {
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Device");
+ });
+
+ 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.DevicePost", b =>
+ {
+ b.HasOne("GreenHome.Domain.User", "AuthorUser")
+ .WithMany()
+ .HasForeignKey("AuthorUserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("AuthorUser");
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DevicePostImage", b =>
+ {
+ b.HasOne("GreenHome.Domain.DevicePost", "DevicePost")
+ .WithMany("Images")
+ .HasForeignKey("DevicePostId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("DevicePost");
+ });
+
+ 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.ReportImage", b =>
+ {
+ b.HasOne("GreenHome.Domain.UserDailyReport", "UserDailyReport")
+ .WithMany("Images")
+ .HasForeignKey("UserDailyReportId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("UserDailyReport");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
+ {
+ b.HasOne("GreenHome.Domain.Device", "Device")
+ .WithMany()
+ .HasForeignKey("DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GreenHome.Domain.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Device");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
+ {
+ b.Navigation("Rules");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.Checklist", b =>
+ {
+ b.Navigation("Completions");
+
+ b.Navigation("Items");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.ChecklistCompletion", b =>
+ {
+ b.Navigation("ItemCompletions");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.Device", b =>
+ {
+ b.Navigation("DeviceUsers");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.DevicePost", b =>
+ {
+ b.Navigation("Images");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.User", b =>
+ {
+ b.Navigation("DeviceUsers");
+ });
+
+ modelBuilder.Entity("GreenHome.Domain.UserDailyReport", b =>
+ {
+ b.Navigation("Images");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.cs b/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.cs
new file mode 100644
index 0000000..2f2f2eb
--- /dev/null
+++ b/src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GreenHome.Infrastructure.Migrations
+{
+ ///
+ public partial class AddDeviceTokenAndPhoneFields : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "DevicePhoneNumber",
+ table: "DeviceSettings",
+ type: "nvarchar(max)",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn(
+ name: "SimCardType",
+ table: "DeviceSettings",
+ type: "int",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "TokenCode",
+ table: "DeviceSettings",
+ type: "nvarchar(max)",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "TokenExpiresAt",
+ table: "DeviceSettings",
+ type: "datetime2",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "UploadIntervalMin",
+ table: "DeviceSettings",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "VerificationCode",
+ table: "DeviceSettings",
+ type: "nvarchar(max)",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "DevicePhoneNumber",
+ table: "DeviceSettings");
+
+ migrationBuilder.DropColumn(
+ name: "SimCardType",
+ table: "DeviceSettings");
+
+ migrationBuilder.DropColumn(
+ name: "TokenCode",
+ table: "DeviceSettings");
+
+ migrationBuilder.DropColumn(
+ name: "TokenExpiresAt",
+ table: "DeviceSettings");
+
+ migrationBuilder.DropColumn(
+ name: "UploadIntervalMin",
+ table: "DeviceSettings");
+
+ migrationBuilder.DropColumn(
+ name: "VerificationCode",
+ table: "DeviceSettings");
+ }
+ }
+}
diff --git a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs
index c0f75ba..d89d5ff 100644
--- a/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs
+++ b/src/GreenHome.Infrastructure/Migrations/GreenHomeDbContextModelSnapshot.cs
@@ -590,6 +590,10 @@ namespace GreenHome.Infrastructure.Migrations
b.Property("DeviceId")
.HasColumnType("int");
+ b.Property("DevicePhoneNumber")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
b.Property("Latitude")
.HasColumnType("decimal(9,6)");
@@ -616,9 +620,24 @@ namespace GreenHome.Infrastructure.Migrations
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
+ b.Property("SimCardType")
+ .HasColumnType("int");
+
+ b.Property("TokenCode")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TokenExpiresAt")
+ .HasColumnType("datetime2");
+
b.Property("UpdatedAt")
.HasColumnType("datetime2");
+ b.Property("UploadIntervalMin")
+ .HasColumnType("int");
+
+ b.Property("VerificationCode")
+ .HasColumnType("nvarchar(max)");
+
b.HasKey("Id");
b.HasIndex("DeviceId")