diff --git a/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt b/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt index dd93385..9f9ca72 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt @@ -2,8 +2,11 @@ package com.frankenergie.smartasset.controller import com.frankenergie.smartasset.model.OrderUpdateRequest import com.frankenergie.smartasset.model.OrderUpdateResponse +import com.frankenergie.smartasset.model.PurchaseSummary import com.frankenergie.smartasset.service.OrderBookService +import com.frankenergie.smartasset.service.PurchaseTrackerService import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -11,11 +14,20 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api") -class OrderUpdateController(private val orderBookService: OrderBookService) { +class OrderUpdateController( + private val orderBookService: OrderBookService, + private val purchaseTrackerService: PurchaseTrackerService +) { @PostMapping("/orderupdate") fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity { val response = orderBookService.processOrder(request) return ResponseEntity.ok(response) } + + @GetMapping("/purchases/average") + fun getPurchaseAverage(): ResponseEntity { + val summary = purchaseTrackerService.getSummary() + return ResponseEntity.ok(summary) + } } diff --git a/src/main/kotlin/com/frankenergie/smartasset/model/ExecutedPurchase.kt b/src/main/kotlin/com/frankenergie/smartasset/model/ExecutedPurchase.kt new file mode 100644 index 0000000..22fc4d6 --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/model/ExecutedPurchase.kt @@ -0,0 +1,16 @@ +package com.frankenergie.smartasset.model + +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDateTime + +data class ExecutedPurchase( + val groupId: String, + val deliveryStart: LocalDateTime, + val deliveryEnd: LocalDateTime, + val quantity: BigDecimal, + val pricePerMWh: BigDecimal, + val timestamp: Instant = Instant.now() +) { + val totalCost: BigDecimal get() = quantity * pricePerMWh +} diff --git a/src/main/kotlin/com/frankenergie/smartasset/model/PurchaseSummary.kt b/src/main/kotlin/com/frankenergie/smartasset/model/PurchaseSummary.kt new file mode 100644 index 0000000..f7883bf --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/model/PurchaseSummary.kt @@ -0,0 +1,10 @@ +package com.frankenergie.smartasset.model + +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal + +data class PurchaseSummary( + @JsonProperty("total_mwh") val totalMWh: BigDecimal, + @JsonProperty("total_cost") val totalCost: BigDecimal, + @JsonProperty("average_price_per_mwh") val averagePricePerMWh: BigDecimal +) diff --git a/src/main/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerService.kt b/src/main/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerService.kt new file mode 100644 index 0000000..145bb01 --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerService.kt @@ -0,0 +1,37 @@ +package com.frankenergie.smartasset.service + +import com.frankenergie.smartasset.model.ExecutedPurchase +import com.frankenergie.smartasset.model.PurchaseSummary +import org.springframework.stereotype.Service +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.concurrent.ConcurrentLinkedQueue + +@Service +class PurchaseTrackerService { + + private val purchases: ConcurrentLinkedQueue = ConcurrentLinkedQueue() + + fun recordPurchase(purchase: ExecutedPurchase) { + purchases.add(purchase) + } + + fun getAllPurchases(): List = purchases.toList() + + fun getSummary(): PurchaseSummary { + val allPurchases = purchases.toList() + val totalMWh = allPurchases.sumOf { it.quantity } + val totalCost = allPurchases.sumOf { it.totalCost } + val averagePrice = if (totalMWh > BigDecimal.ZERO) { + totalCost.divide(totalMWh, 2, RoundingMode.HALF_UP) + } else { + BigDecimal.ZERO + } + + return PurchaseSummary(totalMWh, totalCost, averagePrice) + } + + fun clear() { + purchases.clear() + } +} diff --git a/src/test/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerServiceTest.kt b/src/test/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerServiceTest.kt new file mode 100644 index 0000000..07f14f3 --- /dev/null +++ b/src/test/kotlin/com/frankenergie/smartasset/service/PurchaseTrackerServiceTest.kt @@ -0,0 +1,129 @@ +package com.frankenergie.smartasset.service + +import com.frankenergie.smartasset.model.ExecutedPurchase +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.time.LocalDateTime + +class PurchaseTrackerServiceTest { + + private lateinit var tracker: PurchaseTrackerService + + @BeforeEach + fun setUp() { + tracker = PurchaseTrackerService() + } + + @Test + fun `getSummary returns zeros when no purchases`() { + val summary = tracker.getSummary() + + assertEquals(BigDecimal.ZERO, summary.totalMWh) + assertEquals(BigDecimal.ZERO, summary.totalCost) + assertEquals(BigDecimal.ZERO, summary.averagePricePerMWh) + } + + @Test + fun `getSummary calculates average price correctly`() { + tracker.recordPurchase( + ExecutedPurchase( + groupId = "A", + deliveryStart = LocalDateTime.of(2024, 1, 15, 10, 0), + deliveryEnd = LocalDateTime.of(2024, 1, 15, 10, 15), + quantity = BigDecimal("10"), + pricePerMWh = BigDecimal("50.00") + ) + ) + tracker.recordPurchase( + ExecutedPurchase( + groupId = "B", + deliveryStart = LocalDateTime.of(2024, 1, 15, 10, 15), + deliveryEnd = LocalDateTime.of(2024, 1, 15, 10, 30), + quantity = BigDecimal("10"), + pricePerMWh = BigDecimal("60.00") + ) + ) + + val summary = tracker.getSummary() + + assertEquals(BigDecimal("20"), summary.totalMWh) + assertEquals(BigDecimal("1100.00"), summary.totalCost) + assertEquals(BigDecimal("55.00"), summary.averagePricePerMWh) + } + + @Test + fun `getSummary calculates weighted average correctly`() { + tracker.recordPurchase( + ExecutedPurchase( + groupId = "A", + deliveryStart = LocalDateTime.of(2024, 1, 15, 10, 0), + deliveryEnd = LocalDateTime.of(2024, 1, 15, 10, 15), + quantity = BigDecimal("5"), + pricePerMWh = BigDecimal("40.00") + ) + ) + tracker.recordPurchase( + ExecutedPurchase( + groupId = "A", + deliveryStart = LocalDateTime.of(2024, 1, 15, 10, 15), + deliveryEnd = LocalDateTime.of(2024, 1, 15, 10, 30), + quantity = BigDecimal("15"), + pricePerMWh = BigDecimal("60.00") + ) + ) + + val summary = tracker.getSummary() + + // 5 * 40 + 15 * 60 = 200 + 900 = 1100 + // 1100 / 20 = 55 + assertEquals(BigDecimal("20"), summary.totalMWh) + assertEquals(BigDecimal("1100.00"), summary.totalCost) + assertEquals(BigDecimal("55.00"), summary.averagePricePerMWh) + } + + @Test + fun `getAllPurchases returns all recorded purchases`() { + tracker.recordPurchase( + ExecutedPurchase( + "A", + LocalDateTime.of(2024, 1, 15, 10, 0), + LocalDateTime.of(2024, 1, 15, 10, 15), + BigDecimal("10"), + BigDecimal("50.00") + ) + ) + tracker.recordPurchase( + ExecutedPurchase( + "B", + LocalDateTime.of(2024, 1, 15, 10, 15), + LocalDateTime.of(2024, 1, 15, 10, 30), + BigDecimal("5"), + BigDecimal("55.00") + ) + ) + + val purchases = tracker.getAllPurchases() + + assertEquals(2, purchases.size) + } + + @Test + fun `clear removes all purchases`() { + tracker.recordPurchase( + ExecutedPurchase( + "A", + LocalDateTime.of(2024, 1, 15, 10, 0), + LocalDateTime.of(2024, 1, 15, 10, 15), + BigDecimal("10"), + BigDecimal("50.00") + ) + ) + + tracker.clear() + + assertTrue(tracker.getAllPurchases().isEmpty()) + assertEquals(BigDecimal.ZERO, tracker.getSummary().totalMWh) + } +}