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")