Skip to content

Commit ebac2f3

Browse files
committed
feat(dispatch): evaluate price automation policies
1 parent 1d8e916 commit ebac2f3

5 files changed

Lines changed: 75 additions & 8 deletions

File tree

‎services/dispatch-service/src/main/java/io/voltweave/dispatch/access/IntelligenceDispatchClient.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public Optional<AutomationPlan> automationPlan(AutomationPolicy policy) {
8383
"/internal/v1/vpps/{vppId}/automation-plans", policy.vppId()
8484
)
8585
.body(new AutomationPlanRequest(
86-
policy.organizationId(), policy.peakImportLimitKw(),
86+
policy.organizationId(), policy.triggerType(),
87+
policy.peakImportLimitKw(), policy.priceThreshold(),
8788
policy.maxDispatchPowerKw(), policy.maxDispatchDurationMinutes(),
8889
BigDecimal.valueOf(policy.reserveMarginPercent())
8990
))
@@ -143,7 +144,9 @@ public record ReplacementPlan(
143144

144145
private record AutomationPlanRequest(
145146
UUID organizationId,
147+
String triggerType,
146148
BigDecimal peakImportLimitKw,
149+
BigDecimal priceThreshold,
147150
BigDecimal maxDispatchPowerKw,
148151
int maxDispatchDurationMinutes,
149152
BigDecimal reserveMarginPercent

‎services/dispatch-service/src/main/java/io/voltweave/dispatch/application/AutomationEvaluationJob.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void evaluate() {
3131
automationService.expireCandidates();
3232
for (var policy : portfolioClient.activeAutomationPolicies()) {
3333
try {
34-
if (!"PEAK_LIMIT".equals(policy.triggerType())) {
34+
if ("MANUAL".equals(policy.triggerType())) {
3535
continue;
3636
}
3737
intelligenceClient.automationPlan(policy)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.voltweave.dispatch.application;
2+
3+
import static org.mockito.Mockito.mock;
4+
import static org.mockito.Mockito.never;
5+
import static org.mockito.Mockito.verify;
6+
import static org.mockito.Mockito.when;
7+
8+
import java.math.BigDecimal;
9+
import java.time.Duration;
10+
import java.time.Instant;
11+
import java.util.List;
12+
import java.util.Optional;
13+
import java.util.UUID;
14+
15+
import org.junit.jupiter.api.Test;
16+
17+
import io.voltweave.dispatch.access.IntelligenceDispatchClient;
18+
import io.voltweave.dispatch.access.PortfolioAccessClient;
19+
import io.voltweave.dispatch.application.model.AutomationPlan;
20+
import io.voltweave.dispatch.application.model.AutomationPolicy;
21+
22+
class AutomationEvaluationJobTests {
23+
@Test
24+
void evaluatesPricePolicyAndSkipsManualPolicy() {
25+
var portfolio = mock(PortfolioAccessClient.class);
26+
var intelligence = mock(IntelligenceDispatchClient.class);
27+
var automation = mock(AutomationApplicationService.class);
28+
var price = policy("PRICE_THRESHOLD");
29+
var manual = policy("MANUAL");
30+
var plan = new AutomationPlan(
31+
Instant.parse("2026-08-13T17:00:00Z"), Duration.ofMinutes(30),
32+
UUID.randomUUID(), true
33+
);
34+
when(portfolio.activeAutomationPolicies()).thenReturn(List.of(price, manual));
35+
when(intelligence.automationPlan(price)).thenReturn(Optional.of(plan));
36+
37+
new AutomationEvaluationJob(portfolio, intelligence, automation).evaluate();
38+
39+
verify(automation).expireCandidates();
40+
verify(automation).createCandidate(price, plan);
41+
verify(intelligence, never()).automationPlan(manual);
42+
}
43+
44+
private static AutomationPolicy policy(String trigger) {
45+
return new AutomationPolicy(
46+
UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), trigger,
47+
"AUTO_DISPATCH", "PEAK_LIMIT".equals(trigger) ? BigDecimal.TEN : null,
48+
"PRICE_THRESHOLD".equals(trigger) ? new BigDecimal("0.20") : null,
49+
10, new BigDecimal("5"), 30, 2
50+
);
51+
}
52+
}

‎services/intelligence-service/src/main/java/io/voltweave/intelligence/optimization/api/controller/InternalAutomationPlanController.java‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@ public ResponseEntity<AutomationPlanResponse> plan(
2828
@PathVariable UUID vppId,
2929
@Valid @RequestBody PlanAutomationRequest request
3030
) {
31-
return ResponseEntity.of(planningService.peakLimitPlan(
32-
request.organizationId(), vppId, request.peakImportLimitKw(),
33-
request.maxDispatchPowerKw(), request.maxDispatchDurationMinutes(),
34-
request.reserveMarginPercent()
35-
).map(AutomationPlanResponse::from));
31+
var plan = switch (request.triggerType()) {
32+
case "PEAK_LIMIT" -> planningService.peakLimitPlan(
33+
request.organizationId(), vppId, request.peakImportLimitKw(),
34+
request.maxDispatchPowerKw(), request.maxDispatchDurationMinutes(),
35+
request.reserveMarginPercent()
36+
);
37+
case "PRICE_THRESHOLD" -> planningService.priceThresholdPlan(
38+
request.organizationId(), vppId, request.priceThreshold(),
39+
request.maxDispatchPowerKw(), request.maxDispatchDurationMinutes(),
40+
request.reserveMarginPercent()
41+
);
42+
default -> throw new IllegalArgumentException("Unsupported automation trigger");
43+
};
44+
return ResponseEntity.of(plan.map(AutomationPlanResponse::from));
3645
}
3746
}

‎services/intelligence-service/src/main/java/io/voltweave/intelligence/optimization/api/request/PlanAutomationRequest.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import jakarta.validation.constraints.Max;
99
import jakarta.validation.constraints.Min;
1010
import jakarta.validation.constraints.NotNull;
11+
import jakarta.validation.constraints.NotBlank;
1112

1213
public record PlanAutomationRequest(
1314
@NotNull UUID organizationId,
14-
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal peakImportLimitKw,
15+
@NotBlank String triggerType,
16+
@DecimalMin(value = "0", inclusive = false) BigDecimal peakImportLimitKw,
17+
@DecimalMin(value = "0", inclusive = false) BigDecimal priceThreshold,
1518
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxDispatchPowerKw,
1619
@Min(15) @Max(1440) int maxDispatchDurationMinutes,
1720
@NotNull @DecimalMin("0") @DecimalMax("100") BigDecimal reserveMarginPercent

0 commit comments

Comments
 (0)