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": "*"
+}