chore: initial commit — UdyamLinc backend, phases A-E
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>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user