Both repositories were queried by enterprise alone, so every reason code and every forecast point ever written came back. A run-287 model score was being rendered beside run-145 rule sentences, and one enterprise's six-month chart drew 102 points from seventeen different runs. Now read through findByEnterpriseIdAndRunId..., resolved from the newest run. The same unscoped query existed in EnterpriseAppService and is fixed there too. Also adds GET /officer/app-enrolled: which borrowers on this caller's book have a phone login. Not every borrower is enrolled and that is the design — the platform scores a book from core banking and UPI whether or not the owner installs anything — so the console needs to know rather than offering to open an app that does not exist. Scoped by intersecting with the caller's own book, so it cannot be used to discover that an id exists on another bank's. EnterpriseRow gains firstBreachHorizon, firstBreachMonth and topDriver so the dashboard can rank by lead time instead of by composite score. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
4.2 KiB
Java
108 lines
4.2 KiB
Java
package com.lincfusion.udyamlinc.officer;
|
|
|
|
import com.lincfusion.udyamlinc.common.ApiResponse;
|
|
import com.lincfusion.udyamlinc.security.TenantContext;
|
|
import com.lincfusion.udyamlinc.dto.UdyamRequests.AlertActionRequest;
|
|
import com.lincfusion.udyamlinc.dto.UdyamRequests.ScheduleVisitRequest;
|
|
import com.lincfusion.udyamlinc.dto.UdyamRequests.VisitOutcomeRequest;
|
|
import com.lincfusion.udyamlinc.dto.UdyamResponses.*;
|
|
import com.lincfusion.udyamlinc.risk.RiskAlert;
|
|
import jakarta.validation.Valid;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* The field officer console API. Every route here is gated on SCOPE_OFFICER by
|
|
* {@code SecurityConfig}, so an enterprise token can never reach it.
|
|
*/
|
|
@RestController
|
|
@RequestMapping("/officer")
|
|
@RequiredArgsConstructor
|
|
public class OfficerController {
|
|
|
|
private final OfficerService officerService;
|
|
|
|
@GetMapping("/portfolio")
|
|
public ApiResponse<PortfolioResponse> portfolio(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = OfficerService.ALL_BLOCKS) String block,
|
|
@RequestParam(required = false) String month) {
|
|
return ApiResponse.ok(officerService.portfolio(tenant, block, month));
|
|
}
|
|
|
|
@GetMapping("/enterprises")
|
|
public ApiResponse<List<EnterpriseRow>> enterprises(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = OfficerService.ALL_BLOCKS) String block) {
|
|
return ApiResponse.ok(officerService.enterprises(tenant, block));
|
|
}
|
|
|
|
/** Ids on this book that have a phone-app login. */
|
|
@GetMapping("/app-enrolled")
|
|
public ApiResponse<List<String>> appEnrolled(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = "ALL") String block) {
|
|
return ApiResponse.ok(officerService.appEnrolled(tenant, block));
|
|
}
|
|
|
|
@GetMapping("/enterprises/{enterpriseId}")
|
|
public ApiResponse<EnterpriseDetailResponse> detail(TenantContext tenant, @PathVariable String enterpriseId) {
|
|
return ApiResponse.ok(officerService.detail(tenant, enterpriseId));
|
|
}
|
|
|
|
@GetMapping("/alerts")
|
|
public ApiResponse<List<RiskAlert>> alerts(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = OfficerService.ALL_BLOCKS) String block,
|
|
@RequestParam(required = false) String state) {
|
|
return ApiResponse.ok(officerService.alerts(tenant, block, state));
|
|
}
|
|
|
|
@PutMapping("/alerts/{alertId}")
|
|
public ApiResponse<RiskAlert> disposition(
|
|
TenantContext tenant,
|
|
@PathVariable String alertId,
|
|
@Valid @RequestBody AlertActionRequest request,
|
|
@AuthenticationPrincipal String actor) {
|
|
return ApiResponse.ok("Alert updated", officerService.disposition(tenant, alertId, request.state(), request.note(), actor));
|
|
}
|
|
|
|
@GetMapping("/visits")
|
|
public ApiResponse<List<FieldVisit>> visits(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = OfficerService.ALL_BLOCKS) String block) {
|
|
return ApiResponse.ok(officerService.visits(tenant, block));
|
|
}
|
|
|
|
@PostMapping("/visits")
|
|
public ApiResponse<FieldVisit> schedule(
|
|
TenantContext tenant,
|
|
@Valid @RequestBody ScheduleVisitRequest request,
|
|
@AuthenticationPrincipal String officer) {
|
|
return ApiResponse.ok("Visit scheduled", officerService.schedule(tenant, request, officer));
|
|
}
|
|
|
|
@PutMapping("/visits/{visitId}/outcome")
|
|
public ApiResponse<FieldVisit> logOutcome(
|
|
@PathVariable String visitId,
|
|
@Valid @RequestBody VisitOutcomeRequest request) {
|
|
return ApiResponse.ok("Outcome logged",
|
|
officerService.logOutcome(visitId, request.outcome(), request.officerOverride()));
|
|
}
|
|
|
|
@GetMapping("/playbook")
|
|
public ApiResponse<List<PlaybookActionResponse>> playbook() {
|
|
return ApiResponse.ok(PlaybookCatalog.asResponses());
|
|
}
|
|
|
|
@GetMapping("/sectors")
|
|
public ApiResponse<List<SectorRollupResponse>> sectors(
|
|
TenantContext tenant,
|
|
@RequestParam(defaultValue = OfficerService.ALL_BLOCKS) String block) {
|
|
return ApiResponse.ok(officerService.sectorRollup(tenant, block));
|
|
}
|
|
}
|