Spring Boot 4 / Java 17 / Postgres. Masters and serving (A), Agmarknet and climate ingestion (B), feature store with 14 derivations (C), SpEL rule engine and risk scoring (D). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
5.1 KiB
Java
110 lines
5.1 KiB
Java
package com.lincfusion.udyamlinc.ingestion;
|
|
|
|
import com.lincfusion.udyamlinc.common.ApiResponse;
|
|
import com.lincfusion.udyamlinc.master.District;
|
|
import com.lincfusion.udyamlinc.master.DistrictRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.data.domain.PageRequest;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.time.LocalDate;
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* The operations surface for ingestion: what ran, what it fetched, and how stale
|
|
* each district's data now is.
|
|
*
|
|
* <p>Mounted under {@code /admin} so the existing rule applies — any officer may
|
|
* read, only a supervisor may trigger a run.
|
|
*/
|
|
@RestController
|
|
@RequestMapping("/admin/ingestion")
|
|
@RequiredArgsConstructor
|
|
public class IngestionController {
|
|
|
|
private final IngestionScheduler scheduler;
|
|
private final IngestionRunRepository runRepository;
|
|
private final MarketSignalRepository marketSignalRepository;
|
|
private final ClimateSignalRepository climateSignalRepository;
|
|
private final ApiLogRepository apiLogRepository;
|
|
private final DistrictRepository districtRepository;
|
|
|
|
// ── what ran ────────────────────────────────────────────
|
|
|
|
@GetMapping("/runs")
|
|
public ApiResponse<List<IngestionRun>> runs(@RequestParam(required = false) String source,
|
|
@RequestParam(defaultValue = "30") int limit) {
|
|
PageRequest page = PageRequest.of(0, Math.min(limit, 200));
|
|
return ApiResponse.ok(source == null
|
|
? runRepository.findAllByOrderByStartedAtDesc(page)
|
|
: runRepository.findBySourceOrderByStartedAtDesc(source, page));
|
|
}
|
|
|
|
@GetMapping("/calls")
|
|
public ApiResponse<List<ApiLog>> calls(@RequestParam(required = false) String source,
|
|
@RequestParam(defaultValue = "50") int limit) {
|
|
PageRequest page = PageRequest.of(0, Math.min(limit, 200));
|
|
return ApiResponse.ok(source == null
|
|
? apiLogRepository.findAllByOrderByCalledAtDesc(page)
|
|
: apiLogRepository.findBySourceOrderByCalledAtDesc(source, page));
|
|
}
|
|
|
|
// ── what it wrote ───────────────────────────────────────
|
|
|
|
@GetMapping("/signals/market")
|
|
public ApiResponse<List<MarketSignal>> marketSignals(@RequestParam(defaultValue = "12") int weeks) {
|
|
return ApiResponse.ok(marketSignalRepository
|
|
.findByWeekStartGreaterThanEqualOrderByWeekStartDesc(Weeks.startOf(LocalDate.now()).minusWeeks(weeks)));
|
|
}
|
|
|
|
@GetMapping("/signals/climate")
|
|
public ApiResponse<List<ClimateSignal>> climateSignals(@RequestParam(defaultValue = "12") int weeks) {
|
|
return ApiResponse.ok(climateSignalRepository
|
|
.findByWeekStartGreaterThanEqualOrderByWeekStartDesc(Weeks.startOf(LocalDate.now()).minusWeeks(weeks)));
|
|
}
|
|
|
|
/**
|
|
* Per-district freshness — the number a trip-wire consults before it is
|
|
* allowed to fire. A rule firing on a three-week-old price is worse than no
|
|
* rule.
|
|
*/
|
|
@GetMapping("/freshness")
|
|
public ApiResponse<List<Map<String, Object>>> freshness() {
|
|
LocalDate today = LocalDate.now();
|
|
List<Map<String, Object>> out = new ArrayList<>();
|
|
for (District district : districtRepository.findByActiveTrue()) {
|
|
LocalDate market = marketSignalRepository
|
|
.findFirstByDistrictIdOrderByValidForDateDesc(district.getId())
|
|
.map(MarketSignal::getValidForDate).orElse(null);
|
|
LocalDate climate = climateSignalRepository
|
|
.findFirstByDistrictIdOrderByValidForDateDesc(district.getId())
|
|
.map(ClimateSignal::getValidForDate).orElse(null);
|
|
Map<String, Object> row = new java.util.LinkedHashMap<>();
|
|
row.put("districtCode", district.getCode());
|
|
row.put("districtName", district.getName());
|
|
row.put("marketValidFor", market);
|
|
row.put("marketStalenessDays", market == null ? null : ChronoUnit.DAYS.between(market, today));
|
|
row.put("climateValidFor", climate);
|
|
row.put("climateStalenessDays", climate == null ? null : ChronoUnit.DAYS.between(climate, today));
|
|
out.add(row);
|
|
}
|
|
return ApiResponse.ok(out);
|
|
}
|
|
|
|
// ── trigger ─────────────────────────────────────────────
|
|
|
|
/** Supervisor only, by the {@code /admin/**} write rule in SecurityConfig. */
|
|
@PostMapping("/run")
|
|
public ApiResponse<List<IngestionRun>> runAll() {
|
|
return ApiResponse.ok("Ingestion triggered", scheduler.runAll("MANUAL"));
|
|
}
|
|
|
|
@PostMapping("/run/{source}")
|
|
public ApiResponse<IngestionRun> runOne(@PathVariable String source) {
|
|
return ApiResponse.ok("Ingestion triggered", scheduler.run(source.toUpperCase(), "MANUAL"));
|
|
}
|
|
}
|