average price endpoint
This commit is contained in:
@@ -2,8 +2,11 @@ package com.frankenergie.smartasset.controller
|
|||||||
|
|
||||||
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
||||||
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
||||||
|
import com.frankenergie.smartasset.model.PurchaseSummary
|
||||||
import com.frankenergie.smartasset.service.OrderBookService
|
import com.frankenergie.smartasset.service.OrderBookService
|
||||||
|
import com.frankenergie.smartasset.service.PurchaseTrackerService
|
||||||
import org.springframework.http.ResponseEntity
|
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.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
@@ -11,11 +14,20 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
class OrderUpdateController(private val orderBookService: OrderBookService) {
|
class OrderUpdateController(
|
||||||
|
private val orderBookService: OrderBookService,
|
||||||
|
private val purchaseTrackerService: PurchaseTrackerService
|
||||||
|
) {
|
||||||
|
|
||||||
@PostMapping("/orderupdate")
|
@PostMapping("/orderupdate")
|
||||||
fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> {
|
fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> {
|
||||||
val response = orderBookService.processOrder(request)
|
val response = orderBookService.processOrder(request)
|
||||||
return ResponseEntity.ok(response)
|
return ResponseEntity.ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/purchases/average")
|
||||||
|
fun getPurchaseAverage(): ResponseEntity<PurchaseSummary> {
|
||||||
|
val summary = purchaseTrackerService.getSummary()
|
||||||
|
return ResponseEntity.ok(summary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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<ExecutedPurchase> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
fun recordPurchase(purchase: ExecutedPurchase) {
|
||||||
|
purchases.add(purchase)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllPurchases(): List<ExecutedPurchase> = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user