average price endpoint

This commit is contained in:
2026-07-01 12:55:46 +02:00
parent 1fd30130cf
commit 4ba70d7d6e
5 changed files with 205 additions and 1 deletions

View File

@@ -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)
}
} }

View File

@@ -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
}

View File

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

View File

@@ -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()
}
}

View File

@@ -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)
}
}