diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b80ce98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# ────────────────────────────────────────────── +# Docker +# ────────────────────────────────────────────── + +# Local override compose files +docker-compose.override.yml + +# ────────────────────────────────────────────── +# Node / Frontend +# ────────────────────────────────────────────── + +**/node_modules/ +**/dist/ +**/build/ +.next/ +.nuxt/ +.output/ + +# ────────────────────────────────────────────── +# OS / Editor noise +# ────────────────────────────────────────────── + +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# VS Code workspace settings (keep launch/tasks, ignore local overrides) +.vscode/settings.json +.vscode/*.code-workspace diff --git a/BotNetApi/.gitignore b/BotNetApi/.gitignore new file mode 100644 index 0000000..ae03047 --- /dev/null +++ b/BotNetApi/.gitignore @@ -0,0 +1,36 @@ +# ────────────────────────────────────────────── +# .NET / ASP.NET Core — BotNetApi +# ────────────────────────────────────────────── + +# Build output +bin/ +obj/ + +# Published output +publish/ +out/ + +# NuGet packages (restored automatically, never committed) +*.nupkg +*.snupkg +packages/ +!packages/build/ + +# User-specific Visual Studio files +*.user +*.suo +*.userosscache +*.sln.docstates +.vs/ + +# Rider IDE +.idea/ + +# ────────────────────────────────────────────── +# Local environment / secrets +# ────────────────────────────────────────────── + +# Keep production appsettings, ignore local overrides with real connection strings +appsettings.Development.json +secrets.json +UserSecrets/ diff --git a/BotNetApi/BotNetApi.csproj b/BotNetApi/BotNetApi.csproj new file mode 100644 index 0000000..41ff692 --- /dev/null +++ b/BotNetApi/BotNetApi.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/BotNetApi/Controllers/BotsController.cs b/BotNetApi/Controllers/BotsController.cs new file mode 100644 index 0000000..cc57505 --- /dev/null +++ b/BotNetApi/Controllers/BotsController.cs @@ -0,0 +1,102 @@ +using BotNetApi.DTOs; +using BotNetApi.Services; +using Microsoft.AspNetCore.Mvc; + +namespace BotNetApi.Controllers; + +[ApiController] +[Route("api/bots")] +public class BotsController : ControllerBase +{ + private readonly IBotService _botService; + + public BotsController(IBotService botService) + { + _botService = botService; + } + + // GET /api/bots + [HttpGet] + public async Task GetAll() + { + var bots = await _botService.GetAllAsync(); + return Ok(bots); + } + + // GET /api/bots/findNearest?latitude=47.66&longitude=-117.43 + // Declared before {id:int} — prevents any potential route ambiguity + [HttpGet("findNearest")] + public async Task FindNearest([FromQuery] double latitude, [FromQuery] double longitude) + { + var bot = await _botService.FindNearestAvailableAsync(latitude, longitude); + + if (bot is null) + return NotFound(new { message = "No available bots found near the specified location." }); + + return Ok(bot); + } + + // GET /api/bots/{id} + [HttpGet("{id:int}")] + public async Task GetById(int id) + { + var bot = await _botService.GetByIdAsync(id); + return bot is null ? NotFound() : Ok(bot); + } + + // POST /api/bots + [HttpPost] + public async Task Create([FromBody] CreateBotDto dto) + { + var created = await _botService.CreateAsync(dto); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + // PUT /api/bots/{id} + [HttpPut("{id:int}")] + public async Task Update(int id, [FromBody] UpdateBotDto dto) + { + var updated = await _botService.UpdateAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } + + // DELETE /api/bots/{id} + [HttpDelete("{id:int}")] + public async Task Delete(int id) + { + var deleted = await _botService.DeleteAsync(id); + return deleted ? NoContent() : NotFound(); + } + + // PUT /api/bots/{id}/recharge + [HttpPut("{id:int}/recharge")] + public async Task Recharge(int id) + { + var updated = await _botService.RechargeAsync(id); + return updated is null ? NotFound() : Ok(updated); + } + + // PUT /api/bots/{id}/stock + [HttpPut("{id:int}/stock")] + public async Task UpdateStock(int id, [FromBody] UpdateStockDto dto) + { + var updated = await _botService.UpdateStockAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } + + // PUT /api/bots/{id}/location + [HttpPut("{id:int}/location")] + public async Task UpdateLocation(int id, [FromBody] UpdateLocationDto dto) + { + var updated = await _botService.UpdateLocationAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } + + // PUT /api/bots/{id}/servicing-status + [HttpPut("{id:int}/servicing-status")] + public async Task UpdateServicingStatus(int id, [FromBody] UpdateServicingStatusDto dto) + { + var updated = await _botService.UpdateServicingStatusAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } +} diff --git a/BotNetApi/DTOs/BotResponseDto.cs b/BotNetApi/DTOs/BotResponseDto.cs new file mode 100644 index 0000000..3716f6d --- /dev/null +++ b/BotNetApi/DTOs/BotResponseDto.cs @@ -0,0 +1,14 @@ +namespace BotNetApi.DTOs; + +public class BotResponseDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string StockLevel { get; set; } = string.Empty; + public int BatteryLevel { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public DateTime LastUpdated { get; set; } + public bool IsOnline { get; set; } + public bool IsServicingCustomer { get; set; } +} diff --git a/BotNetApi/DTOs/CreateBotDto.cs b/BotNetApi/DTOs/CreateBotDto.cs new file mode 100644 index 0000000..0958f3f --- /dev/null +++ b/BotNetApi/DTOs/CreateBotDto.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using BotNetApi.Models; + +namespace BotNetApi.DTOs; + +public class CreateBotDto +{ + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [Required] + public StockLevel StockLevel { get; set; } + + [Range(0, 100, ErrorMessage = "BatteryLevel must be between 0 and 100.")] + public int BatteryLevel { get; set; } = 100; + + [Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")] + public double Latitude { get; set; } + + [Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")] + public double Longitude { get; set; } + + public bool IsOnline { get; set; } = true; +} diff --git a/BotNetApi/DTOs/UpdateBotDto.cs b/BotNetApi/DTOs/UpdateBotDto.cs new file mode 100644 index 0000000..75c2c6f --- /dev/null +++ b/BotNetApi/DTOs/UpdateBotDto.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using BotNetApi.Models; + +namespace BotNetApi.DTOs; + +public class UpdateBotDto +{ + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [Required] + public StockLevel StockLevel { get; set; } + + [Range(0, 100, ErrorMessage = "BatteryLevel must be between 0 and 100.")] + public int BatteryLevel { get; set; } + + [Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")] + public double Latitude { get; set; } + + [Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")] + public double Longitude { get; set; } + + public bool IsOnline { get; set; } + + public bool IsServicingCustomer { get; set; } +} diff --git a/BotNetApi/DTOs/UpdateLocationDto.cs b/BotNetApi/DTOs/UpdateLocationDto.cs new file mode 100644 index 0000000..2ab2a55 --- /dev/null +++ b/BotNetApi/DTOs/UpdateLocationDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace BotNetApi.DTOs; + +public class UpdateLocationDto +{ + [Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")] + public double Latitude { get; set; } + + [Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")] + public double Longitude { get; set; } +} diff --git a/BotNetApi/DTOs/UpdateServicingStatusDto.cs b/BotNetApi/DTOs/UpdateServicingStatusDto.cs new file mode 100644 index 0000000..45b0ea6 --- /dev/null +++ b/BotNetApi/DTOs/UpdateServicingStatusDto.cs @@ -0,0 +1,6 @@ +namespace BotNetApi.DTOs; + +public class UpdateServicingStatusDto +{ + public bool IsServicingCustomer { get; set; } +} diff --git a/BotNetApi/DTOs/UpdateStockDto.cs b/BotNetApi/DTOs/UpdateStockDto.cs new file mode 100644 index 0000000..15acad6 --- /dev/null +++ b/BotNetApi/DTOs/UpdateStockDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using BotNetApi.Models; + +namespace BotNetApi.DTOs; + +public class UpdateStockDto +{ + [Required] + public StockLevel StockLevel { get; set; } +} diff --git a/BotNetApi/Data/AppDbContext.cs b/BotNetApi/Data/AppDbContext.cs new file mode 100644 index 0000000..dcc0c3a --- /dev/null +++ b/BotNetApi/Data/AppDbContext.cs @@ -0,0 +1,75 @@ +using BotNetApi.Models; +using Microsoft.EntityFrameworkCore; + +namespace BotNetApi.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Bots => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(b => b.Id); + entity.Property(b => b.Name).IsRequired().HasMaxLength(100); + + // Store enum as a readable string rather than an integer + entity.Property(b => b.StockLevel).HasConversion(); + + // Seed data — real coordinates around downtown Spokane, WA + entity.HasData( + new Bot + { + Id = 1, + Name = "BOT-ALPHA", + StockLevel = StockLevel.High, + BatteryLevel = 92, + Latitude = 47.6588, + Longitude = -117.4260, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), + IsOnline = true, + IsServicingCustomer = false + }, + new Bot + { + Id = 2, + Name = "BOT-BRAVO", + StockLevel = StockLevel.Medium, + BatteryLevel = 61, + Latitude = 47.6721, + Longitude = -117.3982, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), + IsOnline = true, + IsServicingCustomer = true // busy — should be skipped in nearest-bot search + }, + new Bot + { + Id = 3, + Name = "BOT-CHARLIE", + StockLevel = StockLevel.Low, + BatteryLevel = 8, // critically low — should be skipped + Latitude = 47.6543, + Longitude = -117.4390, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), + IsOnline = false, + IsServicingCustomer = false + }, + new Bot + { + Id = 4, + Name = "BOT-DELTA", + StockLevel = StockLevel.High, + BatteryLevel = 77, + Latitude = 47.6489, + Longitude = -117.4143, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), + IsOnline = true, + IsServicingCustomer = false + } + ); + }); + } +} diff --git a/BotNetApi/Mappings/BotMappings.cs b/BotNetApi/Mappings/BotMappings.cs new file mode 100644 index 0000000..8bb5352 --- /dev/null +++ b/BotNetApi/Mappings/BotMappings.cs @@ -0,0 +1,37 @@ +using BotNetApi.DTOs; +using BotNetApi.Models; + +namespace BotNetApi.Mappings; + +/// +/// Manual mapping extension methods — keeps things simple and easy to follow in class. +/// +public static class BotMappings +{ + public static BotResponseDto ToResponseDto(this Bot bot) => + new BotResponseDto + { + Id = bot.Id, + Name = bot.Name, + StockLevel = bot.StockLevel.ToString(), + BatteryLevel = bot.BatteryLevel, + Latitude = bot.Latitude, + Longitude = bot.Longitude, + LastUpdated = bot.LastUpdated, + IsOnline = bot.IsOnline, + IsServicingCustomer = bot.IsServicingCustomer + }; + + public static Bot ToEntity(this CreateBotDto dto) => + new Bot + { + Name = dto.Name, + StockLevel = dto.StockLevel, + BatteryLevel = dto.BatteryLevel, + Latitude = dto.Latitude, + Longitude = dto.Longitude, + LastUpdated = DateTime.UtcNow, + IsOnline = dto.IsOnline, + IsServicingCustomer = false + }; +} diff --git a/BotNetApi/Migrations/20260517142157_InitialCreate.Designer.cs b/BotNetApi/Migrations/20260517142157_InitialCreate.Designer.cs new file mode 100644 index 0000000..38475c7 --- /dev/null +++ b/BotNetApi/Migrations/20260517142157_InitialCreate.Designer.cs @@ -0,0 +1,120 @@ +// +using System; +using BotNetApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BotNetApi.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260517142157_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("BotNetApi.Models.Bot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BatteryLevel") + .HasColumnType("int"); + + b.Property("IsOnline") + .HasColumnType("bit"); + + b.Property("IsServicingCustomer") + .HasColumnType("bit"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("Latitude") + .HasColumnType("float"); + + b.Property("Longitude") + .HasColumnType("float"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("StockLevel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Bots"); + + b.HasData( + new + { + Id = 1, + BatteryLevel = 92, + IsOnline = true, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.658799999999999, + Longitude = -117.426, + Name = "BOT-ALPHA", + StockLevel = "High" + }, + new + { + Id = 2, + BatteryLevel = 61, + IsOnline = true, + IsServicingCustomer = true, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.6721, + Longitude = -117.3982, + Name = "BOT-BRAVO", + StockLevel = "Medium" + }, + new + { + Id = 3, + BatteryLevel = 8, + IsOnline = false, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.654299999999999, + Longitude = -117.43899999999999, + Name = "BOT-CHARLIE", + StockLevel = "Low" + }, + new + { + Id = 4, + BatteryLevel = 77, + IsOnline = true, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.648899999999998, + Longitude = -117.4143, + Name = "BOT-DELTA", + StockLevel = "High" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BotNetApi/Migrations/20260517142157_InitialCreate.cs b/BotNetApi/Migrations/20260517142157_InitialCreate.cs new file mode 100644 index 0000000..31a0d51 --- /dev/null +++ b/BotNetApi/Migrations/20260517142157_InitialCreate.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace BotNetApi.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Bots", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + StockLevel = table.Column(type: "nvarchar(max)", nullable: false), + BatteryLevel = table.Column(type: "int", nullable: false), + Latitude = table.Column(type: "float", nullable: false), + Longitude = table.Column(type: "float", nullable: false), + LastUpdated = table.Column(type: "datetime2", nullable: false), + IsOnline = table.Column(type: "bit", nullable: false), + IsServicingCustomer = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Bots", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Bots", + columns: new[] { "Id", "BatteryLevel", "IsOnline", "IsServicingCustomer", "LastUpdated", "Latitude", "Longitude", "Name", "StockLevel" }, + values: new object[,] + { + { 1, 92, true, false, new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), 47.658799999999999, -117.426, "BOT-ALPHA", "High" }, + { 2, 61, true, true, new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), 47.6721, -117.3982, "BOT-BRAVO", "Medium" }, + { 3, 8, false, false, new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), 47.654299999999999, -117.43899999999999, "BOT-CHARLIE", "Low" }, + { 4, 77, true, false, new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), 47.648899999999998, -117.4143, "BOT-DELTA", "High" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Bots"); + } + } +} diff --git a/BotNetApi/Migrations/AppDbContextModelSnapshot.cs b/BotNetApi/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..f5bc3f1 --- /dev/null +++ b/BotNetApi/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,117 @@ +// +using System; +using BotNetApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace BotNetApi.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("BotNetApi.Models.Bot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BatteryLevel") + .HasColumnType("int"); + + b.Property("IsOnline") + .HasColumnType("bit"); + + b.Property("IsServicingCustomer") + .HasColumnType("bit"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("Latitude") + .HasColumnType("float"); + + b.Property("Longitude") + .HasColumnType("float"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("StockLevel") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Bots"); + + b.HasData( + new + { + Id = 1, + BatteryLevel = 92, + IsOnline = true, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.658799999999999, + Longitude = -117.426, + Name = "BOT-ALPHA", + StockLevel = "High" + }, + new + { + Id = 2, + BatteryLevel = 61, + IsOnline = true, + IsServicingCustomer = true, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.6721, + Longitude = -117.3982, + Name = "BOT-BRAVO", + StockLevel = "Medium" + }, + new + { + Id = 3, + BatteryLevel = 8, + IsOnline = false, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.654299999999999, + Longitude = -117.43899999999999, + Name = "BOT-CHARLIE", + StockLevel = "Low" + }, + new + { + Id = 4, + BatteryLevel = 77, + IsOnline = true, + IsServicingCustomer = false, + LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Latitude = 47.648899999999998, + Longitude = -117.4143, + Name = "BOT-DELTA", + StockLevel = "High" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BotNetApi/Models/Bot.cs b/BotNetApi/Models/Bot.cs new file mode 100644 index 0000000..789b200 --- /dev/null +++ b/BotNetApi/Models/Bot.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace BotNetApi.Models; + +public class Bot +{ + public int Id { get; set; } + + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + public StockLevel StockLevel { get; set; } + + /// Battery percentage from 0 to 100. + public int BatteryLevel { get; set; } + + public double Latitude { get; set; } + public double Longitude { get; set; } + + public DateTime LastUpdated { get; set; } + + public bool IsOnline { get; set; } + + public bool IsServicingCustomer { get; set; } +} diff --git a/BotNetApi/Models/StockLevel.cs b/BotNetApi/Models/StockLevel.cs new file mode 100644 index 0000000..28b68a3 --- /dev/null +++ b/BotNetApi/Models/StockLevel.cs @@ -0,0 +1,8 @@ +namespace BotNetApi.Models; + +public enum StockLevel +{ + High, + Medium, + Low +} diff --git a/BotNetApi/Program.cs b/BotNetApi/Program.cs new file mode 100644 index 0000000..934a85e --- /dev/null +++ b/BotNetApi/Program.cs @@ -0,0 +1,50 @@ +using BotNetApi.Data; +using BotNetApi.Services; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// ── Database ─────────────────────────────────────────────────────────────────── +// Swap the connection string in appsettings.json to point at Azure SQL for production. +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// ── Services ─────────────────────────────────────────────────────────────────── +builder.Services.AddScoped(); + +// ── Controllers ──────────────────────────────────────────────────────────────── +// JsonStringEnumConverter lets clients send enum values as strings ("High", "Medium", "Low") +builder.Services.AddControllers() + .AddJsonOptions(opts => + opts.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter())); + +// ── Swagger / OpenAPI ────────────────────────────────────────────────────────── +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// ── Development middleware ───────────────────────────────────────────────────── +if (app.Environment.IsDevelopment()) +{ + // Auto-apply migrations and seed data on startup (dev convenience only) + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + try + { + db.Database.Migrate(); + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "Database migration failed on startup. Ensure LocalDB is installed and the connection string is correct."); + } + + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/BotNetApi/Properties/launchSettings.json b/BotNetApi/Properties/launchSettings.json new file mode 100644 index 0000000..6dc870b --- /dev/null +++ b/BotNetApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7260;http://localhost:5021", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BotNetApi/Services/BotService.cs b/BotNetApi/Services/BotService.cs new file mode 100644 index 0000000..792c03b --- /dev/null +++ b/BotNetApi/Services/BotService.cs @@ -0,0 +1,148 @@ +using BotNetApi.Data; +using BotNetApi.DTOs; +using BotNetApi.Mappings; +using Microsoft.EntityFrameworkCore; + +namespace BotNetApi.Services; + +public class BotService : IBotService +{ + // Bots below this battery level are excluded from nearest-bot searches + private const int MinBatteryThreshold = 15; + + private readonly AppDbContext _db; + + public BotService(AppDbContext db) + { + _db = db; + } + + public async Task> GetAllAsync() + { + var bots = await _db.Bots.ToListAsync(); + return bots.Select(b => b.ToResponseDto()); + } + + public async Task GetByIdAsync(int id) + { + var bot = await _db.Bots.FindAsync(id); + return bot?.ToResponseDto(); + } + + public async Task CreateAsync(CreateBotDto dto) + { + var bot = dto.ToEntity(); + _db.Bots.Add(bot); + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task UpdateAsync(int id, UpdateBotDto dto) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return null; + + bot.Name = dto.Name; + bot.StockLevel = dto.StockLevel; + bot.BatteryLevel = dto.BatteryLevel; + bot.Latitude = dto.Latitude; + bot.Longitude = dto.Longitude; + bot.IsOnline = dto.IsOnline; + bot.IsServicingCustomer = dto.IsServicingCustomer; + bot.LastUpdated = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task DeleteAsync(int id) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return false; + + _db.Bots.Remove(bot); + await _db.SaveChangesAsync(); + return true; + } + + public async Task RechargeAsync(int id) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return null; + + bot.BatteryLevel = 100; + bot.LastUpdated = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task UpdateStockAsync(int id, UpdateStockDto dto) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return null; + + bot.StockLevel = dto.StockLevel; + bot.LastUpdated = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task UpdateLocationAsync(int id, UpdateLocationDto dto) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return null; + + bot.Latitude = dto.Latitude; + bot.Longitude = dto.Longitude; + bot.LastUpdated = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task UpdateServicingStatusAsync(int id, UpdateServicingStatusDto dto) + { + var bot = await _db.Bots.FindAsync(id); + if (bot is null) return null; + + bot.IsServicingCustomer = dto.IsServicingCustomer; + bot.LastUpdated = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + return bot.ToResponseDto(); + } + + public async Task FindNearestAvailableAsync(double latitude, double longitude) + { + var bots = await _db.Bots.ToListAsync(); + + // Filter: must be online, not busy, and have enough battery + var nearest = bots + .Where(b => b.IsOnline && !b.IsServicingCustomer && b.BatteryLevel >= MinBatteryThreshold) + .OrderBy(b => CalculateDistanceKm(latitude, longitude, b.Latitude, b.Longitude)) + .FirstOrDefault(); + + return nearest?.ToResponseDto(); + } + + // Haversine formula — calculates straight-line distance between two coordinates in kilometers + private static double CalculateDistanceKm(double lat1, double lon1, double lat2, double lon2) + { + const double EarthRadiusKm = 6371.0; + + var dLat = ToRadians(lat2 - lat1); + var dLon = ToRadians(lon2 - lon1); + + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) + * Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + + var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + + return EarthRadiusKm * c; + } + + private static double ToRadians(double degrees) => degrees * Math.PI / 180.0; +} diff --git a/BotNetApi/Services/IBotService.cs b/BotNetApi/Services/IBotService.cs new file mode 100644 index 0000000..aec0385 --- /dev/null +++ b/BotNetApi/Services/IBotService.cs @@ -0,0 +1,21 @@ +using BotNetApi.DTOs; + +namespace BotNetApi.Services; + +public interface IBotService +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task CreateAsync(CreateBotDto dto); + Task UpdateAsync(int id, UpdateBotDto dto); + Task DeleteAsync(int id); + + // Bot actions + Task RechargeAsync(int id); + Task UpdateStockAsync(int id, UpdateStockDto dto); + Task UpdateLocationAsync(int id, UpdateLocationDto dto); + Task UpdateServicingStatusAsync(int id, UpdateServicingStatusDto dto); + + // Search + Task FindNearestAvailableAsync(double latitude, double longitude); +} diff --git a/BotNetApi/USINGBOTNET.md b/BotNetApi/USINGBOTNET.md new file mode 100644 index 0000000..f842f05 --- /dev/null +++ b/BotNetApi/USINGBOTNET.md @@ -0,0 +1,464 @@ +# BotNetApi — Developer Integration Guide + +## Purpose + +BotNetApi is the backend service for the vending machine bot delivery network. + +It acts as the central source of truth for: + +- Bot status +- Bot locations +- Battery levels +- Inventory stock levels +- Availability/service state + +Other systems in the project communicate with the bots exclusively through this API. + +Primary consumers: + +1. Frontend web application +2. Bot simulator application + +--- + +# High Level Architecture + +```text +Bot Simulators + | + v + ASP.NET Core API + | + v + Azure SQL Database + ^ + | +Frontend Web App +``` + +The bot simulators push updates into the API. + +The frontend pulls data from the API to display: + +- Bot locations +- Availability +- Battery status +- Nearest bot results + +The frontend and simulators should NOT communicate directly with the database. + +--- + +# Tech Stack + +- ASP.NET Core Web API (.NET 10) +- Entity Framework Core +- Azure SQL Database +- Swagger/OpenAPI + +--- + +# Base API Route + +```text +/api/bots +``` + +Example local development URL: + +```text +http://localhost:5021/api/bots +https://localhost:7260/api/bots (HTTPS) +``` + +--- + +# Full Endpoint Reference + +## CRUD + +| Method | Route | Description | +| -------- | ---------------- | -------------------- | +| `GET` | `/api/bots` | Return all bots | +| `GET` | `/api/bots/{id}` | Return a single bot | +| `POST` | `/api/bots` | Add a new bot | +| `PUT` | `/api/bots/{id}` | Full update of a bot | +| `DELETE` | `/api/bots/{id}` | Remove a bot | + +## Bot Actions + +| Method | Route | Description | +| ------ | --------------------------------- | ------------------- | +| `PUT` | `/api/bots/{id}/recharge` | Set battery to 100 | +| `PUT` | `/api/bots/{id}/stock` | Update stock level | +| `PUT` | `/api/bots/{id}/location` | Update GPS location | +| `PUT` | `/api/bots/{id}/servicing-status` | Set servicing state | + +## Search + +| Method | Route | Description | +| ------ | -------------------------------------------- | -------------------------- | +| `GET` | `/api/bots/findNearest?latitude=&longitude=` | Find nearest available bot | + +--- + +# Bot Data Model + +Each bot contains: + +| Field | Type | Description | +| ------------------- | -------- | --------------------------------- | +| id | int | Unique bot identifier | +| name | string | Friendly bot name | +| stockLevel | enum | High / Medium / Low | +| batteryLevel | int | 0–100 | +| latitude | double | Current GPS latitude | +| longitude | double | Current GPS longitude | +| lastUpdated | datetime | Last update timestamp (UTC) | +| isOnline | bool | Whether the bot is online | +| isServicingCustomer | bool | Whether the bot is currently busy | + +--- + +# Important System Rules + +## Availability Rules + +A bot is considered AVAILABLE only if: + +```text +isOnline == true +isServicingCustomer == false +batteryLevel >= 15 +``` + +The nearest-bot endpoint automatically filters out unavailable bots. + +--- + +# Expected Frontend Usage + +The frontend will primarily: + +## 1. Display All Bots + +```http +GET /api/bots +``` + +Use for: + +- Map displays +- Status dashboards +- Admin pages + +--- + +## 2. Find Nearest Available Bot + +```http +GET /api/bots/findNearest?latitude=47.6588&longitude=-117.4260 +``` + +Use for: + +- User requests +- "Find nearest vending bot" feature +- Delivery assignment UI + +The API handles: + +- Distance calculation +- Availability filtering +- Ignoring busy bots +- Ignoring low battery bots + +The frontend does NOT need to calculate nearest bots itself. + +--- + +## 3. Display Individual Bot Details + +```http +GET /api/bots/{id} +``` + +--- + +# Expected Bot Simulator Usage + +The simulator should periodically push updates into the API. + +Typical simulator flow: + +## Create Bot + +```http +POST /api/bots +``` + +Example: + +```json +{ + "name": "BOT-ECHO", + "stockLevel": "High", + "batteryLevel": 100, + "latitude": 47.6588, + "longitude": -117.426, + "isOnline": true +} +``` + +> `isServicingCustomer` is omitted — new bots always start as not servicing a customer. +> `stockLevel` accepts string values: `"High"`, `"Medium"`, or `"Low"`. + +--- + +## Update Location + +```http +PUT /api/bots/12/location +``` + +```json +{ + "latitude": 47.6612, + "longitude": -117.431 +} +``` + +Expected usage: + +- Called frequently +- Simulates movement around Spokane + +--- + +## Update Stock + +```http +PUT /api/bots/12/stock +``` + +```json +{ + "stockLevel": "Medium" +} +``` + +--- + +## Recharge Battery + +```http +PUT /api/bots/12/recharge +``` + +Automatically sets: + +```text +batteryLevel = 100 +``` + +--- + +## Mark Bot as Busy + +```http +PUT /api/bots/12/servicing-status +``` + +```json +{ + "isServicingCustomer": true +} +``` + +When servicing is complete: + +```json +{ + "isServicingCustomer": false +} +``` + +This directly affects nearest-bot selection. + +--- + +## Delete Bot + +```http +DELETE /api/bots/{id} +``` + +Permanently removes a bot from the system. Use only when decommissioning a bot. + +**Response:** `204 No Content` + +--- + +# Nearest Bot Behavior + +The endpoint: + +```http +GET /api/bots/findNearest +``` + +works as follows: + +1. Loads all bots +2. Filters out: + - Offline bots + - Busy bots + - Bots under 15% battery +3. Calculates geographic distance +4. Sorts nearest-to-farthest +5. Returns the first valid bot + +If no bots qualify: + +```http +404 Not Found +``` + +or similar response. + +--- + +# Example Response + +```json +{ + "id": 4, + "name": "Bot-4", + "stockLevel": "High", + "batteryLevel": 82, + "latitude": 47.6592, + "longitude": -117.4235, + "lastUpdated": "2026-05-17T18:12:55Z", + "isOnline": true, + "isServicingCustomer": false +} +``` + +--- + +# Expected Update Frequency + +## Bot Simulator + +Recommended: + +- Location updates every few seconds +- Battery updates periodically +- Service state changes as needed + +## Frontend + +Recommended: + +- Poll every few seconds + +OR + +- Add SignalR later if real-time updates become necessary + +SignalR is intentionally NOT included yet to keep the project simple. + +--- + +# Database Notes + +Development: + +- SQL Server LocalDB or SQL Express + +Production: + +- Azure SQL Database + +Entity Framework Core migrations will manage schema creation. + +**Migration commands:** + +```bash +dotnet ef migrations add +dotnet ef database update +``` + +--- + +# Seed Data + +Four bots are seeded at real Spokane, WA coordinates on first run: + +| ID | Name | Battery | Online | Servicing | Notes | +| --- | ----------- | ------- | ------ | --------- | ------------------------------------------------ | +| 1 | BOT-ALPHA | 92% | Yes | No | Available | +| 2 | BOT-BRAVO | 61% | Yes | Yes | Skipped by `findNearest` — busy | +| 3 | BOT-CHARLIE | 8% | No | No | Skipped by `findNearest` — offline + low battery | +| 4 | BOT-DELTA | 77% | Yes | No | Available | + +--- + +# Swagger Support + +Swagger UI will be available during development: + +```text +http://localhost:5021/swagger +https://localhost:7260/swagger (HTTPS) +``` + +Developers can: + +- Test endpoints +- View request/response schemas +- Experiment without Postman + +--- + +# Current Scope + +Included: + +- CRUD operations +- Bot status management +- Nearest available bot lookup +- EF Core persistence + +Not included yet: + +- Authentication +- Authorization +- Real-time websocket updates +- Queueing systems +- Distributed services +- Bot routing/pathfinding +- Reservations +- Multi-city support + +--- + +# Assumptions + +- All bots operate within Spokane, Washington +- GPS coordinates are trusted +- Simulators are responsible for realistic movement +- Frontend handles visualization only +- API is the source of truth for availability + +--- + +# Design Philosophy + +This API is intentionally: + +- Simple +- Beginner-friendly +- Easy to explain in a classroom setting +- Structured similarly to real production APIs +- Built so it can later evolve into a larger distributed system without major rewrites diff --git a/BotNetApi/USINGBOTNETAPI.md b/BotNetApi/USINGBOTNETAPI.md new file mode 100644 index 0000000..d857845 --- /dev/null +++ b/BotNetApi/USINGBOTNETAPI.md @@ -0,0 +1,464 @@ +# BotNetApi — Developer Integration Guide + +## Purpose + +BotNetApi is the backend service for the vending machine bot delivery network. + +It acts as the central source of truth for: + +- Bot status +- Bot locations +- Battery levels +- Inventory stock levels +- Availability/service state + +Other systems in the project communicate with the bots exclusively through this API. + +Primary consumers: + +1. Frontend web application +2. Bot simulator application + +--- + +# High Level Architecture + +```text +Bot Simulators + | + v + ASP.NET Core API + | + v + Azure SQL Database + ^ + | +Frontend Web App +``` + +The bot simulators push updates into the API. + +The frontend pulls data from the API to display: + +- Bot locations +- Availability +- Battery status +- Nearest bot results + +The frontend and simulators should NOT communicate directly with the database. + +--- + +# Tech Stack + +- ASP.NET Core Web API (.NET 10) +- Entity Framework Core +- Azure SQL Database +- Swagger/OpenAPI + +--- + +# Base API Route + +```text +/api/bots +``` + +Example local development URL: + +```text +http://localhost:5021/api/bots +https://localhost:7260/api/bots (HTTPS) +``` + +--- + +# Full Endpoint Reference + +## CRUD + +| Method | Route | Description | +|----------|--------------------|------------------------| +| `GET` | `/api/bots` | Return all bots | +| `GET` | `/api/bots/{id}` | Return a single bot | +| `POST` | `/api/bots` | Add a new bot | +| `PUT` | `/api/bots/{id}` | Full update of a bot | +| `DELETE` | `/api/bots/{id}` | Remove a bot | + +## Bot Actions + +| Method | Route | Description | +|--------|-----------------------------------|-----------------------| +| `PUT` | `/api/bots/{id}/recharge` | Set battery to 100 | +| `PUT` | `/api/bots/{id}/stock` | Update stock level | +| `PUT` | `/api/bots/{id}/location` | Update GPS location | +| `PUT` | `/api/bots/{id}/servicing-status` | Set servicing state | + +## Search + +| Method | Route | Description | +|--------|----------------------------------------------|----------------------------| +| `GET` | `/api/bots/findNearest?latitude=&longitude=` | Find nearest available bot | + +--- + +# Bot Data Model + +Each bot contains: + +| Field | Type | Description | +| ------------------- | -------- | --------------------------------- | +| id | int | Unique bot identifier | +| name | string | Friendly bot name | +| stockLevel | enum | High / Medium / Low | +| batteryLevel | int | 0–100 | +| latitude | double | Current GPS latitude | +| longitude | double | Current GPS longitude | +| lastUpdated | datetime | Last update timestamp (UTC) | +| isOnline | bool | Whether the bot is online | +| isServicingCustomer | bool | Whether the bot is currently busy | + +--- + +# Important System Rules + +## Availability Rules + +A bot is considered AVAILABLE only if: + +```text +isOnline == true +isServicingCustomer == false +batteryLevel >= 15 +``` + +The nearest-bot endpoint automatically filters out unavailable bots. + +--- + +# Expected Frontend Usage + +The frontend will primarily: + +## 1. Display All Bots + +```http +GET /api/bots +``` + +Use for: + +- Map displays +- Status dashboards +- Admin pages + +--- + +## 2. Find Nearest Available Bot + +```http +GET /api/bots/findNearest?latitude=47.6588&longitude=-117.4260 +``` + +Use for: + +- User requests +- "Find nearest vending bot" feature +- Delivery assignment UI + +The API handles: + +- Distance calculation +- Availability filtering +- Ignoring busy bots +- Ignoring low battery bots + +The frontend does NOT need to calculate nearest bots itself. + +--- + +## 3. Display Individual Bot Details + +```http +GET /api/bots/{id} +``` + +--- + +# Expected Bot Simulator Usage + +The simulator should periodically push updates into the API. + +Typical simulator flow: + +## Create Bot + +```http +POST /api/bots +``` + +Example: + +```json +{ + "name": "BOT-ECHO", + "stockLevel": "High", + "batteryLevel": 100, + "latitude": 47.6588, + "longitude": -117.426, + "isOnline": true +} +``` + +> `isServicingCustomer` is omitted — new bots always start as not servicing a customer. +> `stockLevel` accepts string values: `"High"`, `"Medium"`, or `"Low"`. + +--- + +## Update Location + +```http +PUT /api/bots/12/location +``` + +```json +{ + "latitude": 47.6612, + "longitude": -117.431 +} +``` + +Expected usage: + +- Called frequently +- Simulates movement around Spokane + +--- + +## Update Stock + +```http +PUT /api/bots/12/stock +``` + +```json +{ + "stockLevel": "Medium" +} +``` + +--- + +## Recharge Battery + +```http +PUT /api/bots/12/recharge +``` + +Automatically sets: + +```text +batteryLevel = 100 +``` + +--- + +## Mark Bot as Busy + +```http +PUT /api/bots/12/servicing-status +``` + +```json +{ + "isServicingCustomer": true +} +``` + +When servicing is complete: + +```json +{ + "isServicingCustomer": false +} +``` + +This directly affects nearest-bot selection. + +--- + +## Delete Bot + +```http +DELETE /api/bots/{id} +``` + +Permanently removes a bot from the system. Use only when decommissioning a bot. + +**Response:** `204 No Content` + +--- + +# Nearest Bot Behavior + +The endpoint: + +```http +GET /api/bots/findNearest +``` + +works as follows: + +1. Loads all bots +2. Filters out: + - Offline bots + - Busy bots + - Bots under 15% battery +3. Calculates geographic distance +4. Sorts nearest-to-farthest +5. Returns the first valid bot + +If no bots qualify: + +```http +404 Not Found +``` + +or similar response. + +--- + +# Example Response + +```json +{ + "id": 4, + "name": "Bot-4", + "stockLevel": "High", + "batteryLevel": 82, + "latitude": 47.6592, + "longitude": -117.4235, + "lastUpdated": "2026-05-17T18:12:55Z", + "isOnline": true, + "isServicingCustomer": false +} +``` + +--- + +# Expected Update Frequency + +## Bot Simulator + +Recommended: + +- Location updates every few seconds +- Battery updates periodically +- Service state changes as needed + +## Frontend + +Recommended: + +- Poll every few seconds + +OR + +- Add SignalR later if real-time updates become necessary + +SignalR is intentionally NOT included yet to keep the project simple. + +--- + +# Database Notes + +Development: + +- SQL Server LocalDB or SQL Express + +Production: + +- Azure SQL Database + +Entity Framework Core migrations will manage schema creation. + +**Migration commands:** + +```bash +dotnet ef migrations add +dotnet ef database update +``` + +--- + +# Seed Data + +Four bots are seeded at real Spokane, WA coordinates on first run: + +| ID | Name | Battery | Online | Servicing | Notes | +|----|-------------|---------|--------|-----------|------------------------------------------------| +| 1 | BOT-ALPHA | 92% | Yes | No | Available | +| 2 | BOT-BRAVO | 61% | Yes | Yes | Skipped by `findNearest` — busy | +| 3 | BOT-CHARLIE | 8% | No | No | Skipped by `findNearest` — offline + low battery | +| 4 | BOT-DELTA | 77% | Yes | No | Available | + +--- + +# Swagger Support + +Swagger UI will be available during development: + +```text +http://localhost:5021/swagger +https://localhost:7260/swagger (HTTPS) +``` + +Developers can: + +- Test endpoints +- View request/response schemas +- Experiment without Postman + +--- + +# Current Scope + +Included: + +- CRUD operations +- Bot status management +- Nearest available bot lookup +- EF Core persistence + +Not included yet: + +- Authentication +- Authorization +- Real-time websocket updates +- Queueing systems +- Distributed services +- Bot routing/pathfinding +- Reservations +- Multi-city support + +--- + +# Assumptions + +- All bots operate within Spokane, Washington +- GPS coordinates are trusted +- Simulators are responsible for realistic movement +- Frontend handles visualization only +- API is the source of truth for availability + +--- + +# Design Philosophy + +This API is intentionally: + +- Simple +- Beginner-friendly +- Easy to explain in a classroom setting +- Structured similarly to real production APIs +- Built so it can later evolve into a larger distributed system without major rewrites diff --git a/BotNetApi/appsettings.json b/BotNetApi/appsettings.json new file mode 100644 index 0000000..2a2dda3 --- /dev/null +++ b/BotNetApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=tcp:.database.windows.net,1433;Initial Catalog=BotNetApiDb;Persist Security Info=False;User ID=;Password=;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}