diff --git a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java index 89d8e98..a945504 100644 --- a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java +++ b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java @@ -1,7 +1,8 @@ package com.goi.integration.samsara.client; import com.goi.integration.common.dto.ExtIngestResult; -import com.goi.integration.samsara.dto.ExtInspectionIngestCommand; +import com.goi.integration.samsara.dto.VehicleInspectionIngestCommand; +import com.goi.integration.samsara.dto.VehicleStatOdometerCommand; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -27,11 +28,7 @@ public class OprIngestClient { // @Autowired // private ObjectMapper objectMapper; - public ExtIngestResult ingestInspection(ExtInspectionIngestCommand command) { -// String json = objectMapper -// .writerWithDefaultPrettyPrinter() -// .writeValueAsString(command); -// + public ExtIngestResult ingestInspection(VehicleInspectionIngestCommand command) { log.info( "[OPR_INGEST][REQUEST] source={}, type={}, fetchedAt={}", command.getSource(), @@ -39,19 +36,6 @@ public class OprIngestClient { command.getFetchedAt() ); - /* - try { - log.debug( - "[OPR_INGEST][REQUEST_BODY]\n{}", - new com.fasterxml.jackson.databind.ObjectMapper() - .writerWithDefaultPrettyPrinter() - .writeValueAsString(command) - ); - } catch (Exception e) { - log.debug("[OPR_INGEST][REQUEST_BODY] failed to serialize", e); - } - */ - try { ExtIngestResult result = oprWebClient.post() .uri("/ext/samsara/inspections/ingest") @@ -97,4 +81,63 @@ public class OprIngestClient { throw e; } } + + /* ========================= + * Odometer ingest + * ========================= */ + public ExtIngestResult ingestVehicleOdometer( + VehicleStatOdometerCommand command + ) { + + log.info( + "[OPR_INGEST][ODOMETER][REQUEST] source={}, records={}, fetchedAt={}", + command.getSource(), + command.getRecords() != null ? command.getRecords().size() : 0, + command.getFetchedAt() + ); + + try { + ExtIngestResult result = oprWebClient.post() + .uri("/ext/samsara/odometer/ingest") + .header(INTERNAL_TOKEN_HEADER, token) + .bodyValue(command) + .retrieve() + .onStatus( + status -> status.is4xxClientError() || status.is5xxServerError(), + resp -> resp.bodyToMono(String.class) + .map(body -> { + if (resp.statusCode().value() == 403) { + return new IllegalStateException( + "OPR_AUTH_FAILED: " + body + ); + } + return new RuntimeException( + "OPR_INGEST_HTTP_ERROR: " + + resp.statusCode() + " body=" + body + ); + }) + ) + .bodyToMono(ExtIngestResult.class) + .block(); + + log.info( + "[OPR_INGEST][ODOMETER][SUCCESS] received={}, inserted={}, updated={}, skipped={}", + result.getReceived(), + result.getInserted(), + result.getUpdated(), + result.getSkipped() + ); + + return result; + + } catch (Exception e) { + log.error( + "[OPR_INGEST][ODOMETER][FAILED] source={}, error={}", + command.getSource(), + e.getMessage(), + e + ); + throw e; + } + } } diff --git a/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java index 4accfa1..bf8b227 100644 --- a/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java +++ b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java @@ -77,7 +77,32 @@ public class SamsaraClient { .queryParam("vehicleIds", vehicleIdsParam) .queryParam("startTime", startTime.toString()) .queryParam("endTime", endTime.toString()) - .queryParam("types", "obdOdometerMeters") + .queryParam("types", "obdOdometerMeters,gpsOdometerMeters") + .build()) + .retrieve() + .bodyToMono(String.class) + .block(); + } + + /** + * Vehicle odometer history (OBD odometer meters) + * Raw JSON 반환 + */ + public String getVehicleEngineSeconds( + List vehicleExternalIds, + Instant startTime, + Instant endTime + ) { + + String vehicleIdsParam = String.join(",", vehicleExternalIds); + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/fleet/vehicles/stats/history") + .queryParam("vehicleIds", vehicleIdsParam) + .queryParam("startTime", startTime.toString()) + .queryParam("endTime", endTime.toString()) + .queryParam("types", "obdEngineSeconds") .build()) .retrieve() .bodyToMono(String.class) @@ -89,7 +114,7 @@ public class SamsaraClient { * Raw JSON 반환 * */ - public String getVehicleSafetyEvents(Instant startTime, Instant endTime, List vehicleExternalIds) { + public String getVehicleSafetyEvents(List vehicleExternalIds, Instant startTime, Instant endTime) { String vehicleIdsParam = String.join(",", vehicleExternalIds); diff --git a/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java b/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java index bc7e6ae..781a1df 100644 --- a/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java +++ b/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController; import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; import com.goi.integration.samsara.dto.ScheduleWorkerResponseDto; -import com.goi.integration.samsara.service.InspectionIngestWorker; +import com.goi.integration.samsara.service.VehicleInspectionIngestWorker; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class InspectionWorkerController { - private final InspectionIngestWorker inspectionIngestWorker; + private final VehicleInspectionIngestWorker inspectionIngestWorker; @PostMapping("/worker") public ScheduleWorkerResponseDto runDvir( diff --git a/src/main/java/com/goi/integration/samsara/controller/VehicleController.java b/src/main/java/com/goi/integration/samsara/controller/VehicleController.java index d1f4304..5a7d298 100644 --- a/src/main/java/com/goi/integration/samsara/controller/VehicleController.java +++ b/src/main/java/com/goi/integration/samsara/controller/VehicleController.java @@ -1,15 +1,17 @@ package com.goi.integration.samsara.controller; -import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto; +import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; import com.goi.integration.samsara.dto.VehicleGpsResponseDto; -import com.goi.integration.samsara.dto.VehicleOdometerCommand; -import com.goi.integration.samsara.service.VehicleOdometerHistoryService; -import com.goi.integration.samsara.service.VehicleOdometerService; +import com.goi.integration.samsara.dto.VehicleStatEngineSecondsCommand; +import com.goi.integration.samsara.dto.VehicleStatOdometerCommand; +import com.goi.integration.samsara.dto.VehicleStatSafetyEventCommand; import com.goi.integration.samsara.service.VehicleGpsService; +import com.goi.integration.samsara.service.VehicleStatEngineSecondsService; +import com.goi.integration.samsara.service.VehicleStatOdometerService; +import com.goi.integration.samsara.service.VehicleStatSafetyEventService; import lombok.RequiredArgsConstructor; -import java.time.Instant; import java.util.List; import org.springframework.web.bind.annotation.*; @@ -20,8 +22,9 @@ import org.springframework.web.bind.annotation.*; public class VehicleController { private final VehicleGpsService vehicleStatService; - private final VehicleOdometerHistoryService vehicleOdometerHistoryService; - private final VehicleOdometerService vehicleOdometerService; + private final VehicleStatOdometerService statOdometerService; + private final VehicleStatEngineSecondsService statEngineSecondsService; + private final VehicleStatSafetyEventService statSafetyEventService; @GetMapping("/stat/gps") public List getGps( @@ -30,42 +33,34 @@ public class VehicleController { return vehicleStatService.getVehicleGps(vehicleIds); } + /** - * Vehicle odometer history summary (window-based) + * odometer fetch 후 리턴 */ - @GetMapping("/stat/odometer/history") - public List getOdometerHistory( - @RequestParam List vehicleIds, - @RequestParam Instant startTime, - @RequestParam Instant endTime + @PostMapping("/stat/odometer/fetch") + public VehicleStatOdometerCommand fetchStatOdometer( + @RequestBody ScheduleWorkerRequestDto request ) { - return vehicleOdometerHistoryService.getOdometerHistory( - vehicleIds, startTime, endTime - ); + return statOdometerService.fetchCommand(request); } /** - * Vehicle odometer raw fetch (for opr-rest-api ingest) + * odometer fetch 후 리턴 */ - @PostMapping("/odometer/fetch") - public VehicleOdometerCommand fetchOdometer( - @RequestParam List vehicleIds, - @RequestParam Instant startTime, - @RequestParam Instant endTime + @PostMapping("/stat/engine-seconds/fetch") + public VehicleStatEngineSecondsCommand fetchStatEngineSeconds( + @RequestBody ScheduleWorkerRequestDto request ) { - return vehicleOdometerService.fetchOdometerCommand( - vehicleIds, - startTime, - endTime - ); + return statEngineSecondsService.fetchCommand(request); } -// @GetMapping("/safety/events") -// public List getSafetyEvents( -// @RequestParam Instant startTime, -// @RequestParam Instant endTime, -// @RequestParam List vehicleIds -// ) { -// return vehicleSafetyEventService.getSafetyEvents(vehicleIds, startTime, endTime); -// } + /** + * odometer fetch 후 리턴 + */ + @PostMapping("/stat/safety-event/fetch") + public VehicleStatSafetyEventCommand fetchStatSafetyEvent( + @RequestBody ScheduleWorkerRequestDto request + ) { + return statSafetyEventService.fetchCommand(request); + } } diff --git a/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java index 71c7486..8f27193 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java +++ b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java @@ -6,6 +6,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; @Data @@ -19,4 +20,6 @@ public class ScheduleWorkerRequestDto { private LocalDateTime to; private Integer maxRecords; private Map> config; + + private List vehicleExternalIds; } diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.java b/src/main/java/com/goi/integration/samsara/dto/VehicleInspectionIngestCommand.java similarity index 76% rename from src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.java rename to src/main/java/com/goi/integration/samsara/dto/VehicleInspectionIngestCommand.java index 549a179..7c5249a 100644 --- a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.java +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleInspectionIngestCommand.java @@ -12,8 +12,8 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder -public class VehicleOdometerCommand { +public class VehicleInspectionIngestCommand { private String source; private LocalDateTime fetchedAt; - private List records; + private List records; } \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleInspectionRecordDto.java similarity index 94% rename from src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java rename to src/main/java/com/goi/integration/samsara/dto/VehicleInspectionRecordDto.java index 1eccb62..7fa2597 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleInspectionRecordDto.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode; @NoArgsConstructor @AllArgsConstructor @Builder -public class ExtInspectionRecordDto { +public class VehicleInspectionRecordDto { private String externalId; // inspection id private String vehicleExternalId; // vehicle.id private String driverExternalId; // signatoryUser.id diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerHistoryResponseDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerHistoryResponseDto.java deleted file mode 100644 index 920f101..0000000 --- a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerHistoryResponseDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.goi.integration.samsara.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class VehicleOdometerHistoryResponseDto { - private String vohVehicleExternalId; - private Instant vohFirstSampleTime; - private Instant vohLastSampleTime; - private Long vohFirstOdometerMeters; - private Long vohLastOdometerMeters; - private Integer vohSampleCount; -} diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsCommand.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsCommand.java new file mode 100644 index 0000000..4f59ab7 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsCommand.java @@ -0,0 +1,19 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleStatEngineSecondsCommand { + private String source; + private LocalDateTime fetchedAt; + private List records; +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsRecordDto.java new file mode 100644 index 0000000..dd7619f --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatEngineSecondsRecordDto.java @@ -0,0 +1,22 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +import com.fasterxml.jackson.databind.JsonNode; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleStatEngineSecondsRecordDto { + private String vehicleExternalId; + private Instant sampleTime; + private Long engineSeconds; + private String payloadHash; + private JsonNode payloadJson; +} diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerCommand.java similarity index 76% rename from src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java rename to src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerCommand.java index 93ed03b..098f0ba 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerCommand.java @@ -12,8 +12,8 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder -public class ExtInspectionIngestCommand { +public class VehicleStatOdometerCommand { private String source; private LocalDateTime fetchedAt; - private List records; + private List records; } \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerRecordDto.java similarity index 72% rename from src/main/java/com/goi/integration/samsara/dto/VehicleOdometerRecordDto.java rename to src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerRecordDto.java index cde0b48..672bc66 100644 --- a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerRecordDto.java +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatOdometerRecordDto.java @@ -5,7 +5,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; +import java.time.Instant; import com.fasterxml.jackson.databind.JsonNode; @@ -13,10 +13,10 @@ import com.fasterxml.jackson.databind.JsonNode; @NoArgsConstructor @AllArgsConstructor @Builder -public class VehicleOdometerRecordDto { - private String source; // SAMSARA +public class VehicleStatOdometerRecordDto { private String vehicleExternalId; // samsara vehicle.id - private LocalDateTime sampleTime; // snapshot time (UTC) + private Instant sampleTime; // snapshot time (UTC) + private String odometerType; // "OBD" | "GPS" private Long odometerMeters; // cumulative meters private String payloadHash; // idempotent hash private JsonNode payloadJson; // minimal raw payload diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventCommand.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventCommand.java new file mode 100644 index 0000000..faf0cec --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventCommand.java @@ -0,0 +1,19 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleStatSafetyEventCommand { + private String source; + private LocalDateTime fetchedAt; + private List records; +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventRecordDto.java new file mode 100644 index 0000000..a8c45c9 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatSafetyEventRecordDto.java @@ -0,0 +1,31 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.time.LocalDate; + +import com.fasterxml.jackson.databind.JsonNode; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleStatSafetyEventRecordDto { + private String eventId; + private String vehicleId; + private String driverId; + private Instant eventTime; + private LocalDate eventDate; + private String coachingState; + private Double maxAccelerationG; + private Double latitude; + private Double longitude; + private String videoForwardUrl; + private String videoInwardUrl; + private JsonNode behaviorLabels; + private JsonNode rawPayload; +} diff --git a/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java b/src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestService.java similarity index 89% rename from src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java rename to src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestService.java index eb27423..94a239d 100644 --- a/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java +++ b/src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestService.java @@ -6,8 +6,8 @@ import com.goi.integration.common.dto.ExtIngestResult; import com.goi.integration.common.util.DateTimeUtil; import com.goi.integration.common.util.ExtPayloadHashUtil; import com.goi.integration.samsara.client.OprIngestClient; -import com.goi.integration.samsara.dto.ExtInspectionIngestCommand; -import com.goi.integration.samsara.dto.ExtInspectionRecordDto; +import com.goi.integration.samsara.dto.VehicleInspectionIngestCommand; +import com.goi.integration.samsara.dto.VehicleInspectionRecordDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -19,7 +19,7 @@ import java.util.List; @Slf4j @Service @RequiredArgsConstructor -public class InspectionIngestService { +public class VehicleInspectionIngestService { private final ObjectMapper objectMapper; private final OprIngestClient oprIngestClient; @@ -44,7 +44,7 @@ public class InspectionIngestService { } // - List records = new ArrayList<>(); + List records = new ArrayList<>(); log.info("inspection data size={}", data.size()); // data[] 각 node 가 하나의 inspection (preTrip / postTrip) @@ -67,7 +67,7 @@ public class InspectionIngestService { // record DTO records.add( - ExtInspectionRecordDto.builder() + VehicleInspectionRecordDto.builder() .externalId(externalId) .vehicleExternalId(vehicleExtId) .driverExternalId(driverExtId) @@ -82,8 +82,8 @@ public class InspectionIngestService { } // ingest command 생성 - ExtInspectionIngestCommand command = - ExtInspectionIngestCommand.builder() + VehicleInspectionIngestCommand command = + VehicleInspectionIngestCommand.builder() .source("SAMSARA") .fetchedAt(LocalDateTime.now()) .records(records) diff --git a/src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java b/src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestWorker.java similarity index 95% rename from src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java rename to src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestWorker.java index f4c815a..9266660 100644 --- a/src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java +++ b/src/main/java/com/goi/integration/samsara/service/VehicleInspectionIngestWorker.java @@ -15,10 +15,10 @@ import java.time.ZoneOffset; @Slf4j @Component @RequiredArgsConstructor -public class InspectionIngestWorker { +public class VehicleInspectionIngestWorker { private final SamsaraClient samsaraClient; - private final InspectionIngestService ingestService; + private final VehicleInspectionIngestService ingestService; public ScheduleWorkerResponseDto execute(ScheduleWorkerRequestDto request) { diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleOdometerHistoryService.java b/src/main/java/com/goi/integration/samsara/service/VehicleOdometerHistoryService.java deleted file mode 100644 index 82211cd..0000000 --- a/src/main/java/com/goi/integration/samsara/service/VehicleOdometerHistoryService.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.goi.integration.samsara.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.goi.integration.samsara.client.SamsaraClient; -import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class VehicleOdometerHistoryService { - - private final SamsaraClient samsaraClient; - private final ObjectMapper objectMapper = new ObjectMapper(); - - public List getOdometerHistory( - List vehicleExternalIds, - Instant startTime, - Instant endTime - ) { - // api 호출 - String rawJson = samsaraClient.getVehicleOdometerHistory(vehicleExternalIds, startTime, endTime); - - return parseOdometerHistory(rawJson); - } - - private List parseOdometerHistory(String rawJson) { - - List results = new ArrayList<>(); - - try { - JsonNode root = objectMapper.readTree(rawJson); - JsonNode dataArray = root.path("data"); - - for (JsonNode vehicleNode : dataArray) { - - String vehicleId = vehicleNode.path("id").asText(); - JsonNode odoArray = vehicleNode.path("obdOdometerMeters"); - - if (!odoArray.isArray() || odoArray.isEmpty()) { - // 데이터 없음 - results.add( - VehicleOdometerHistoryResponseDto.builder() - .vohVehicleExternalId(vehicleId) - .vohSampleCount(0) - .build() - ); - continue; - } - - Iterator it = odoArray.elements(); - - JsonNode first = it.next(); - JsonNode last = first; - - int count = 1; - - while (it.hasNext()) { - last = it.next(); - count++; - } - - // 거리 계산은 opr 에서. 여기선 이전 누적치를 모름. - long firstValue = first.path("value").asLong(); - long lastValue = last.path("value").asLong(); - - Instant firstTime = Instant.parse(first.path("time").asText()); - Instant lastTime = Instant.parse(last.path("time").asText()); - - results.add( - VehicleOdometerHistoryResponseDto.builder() - .vohVehicleExternalId(vehicleId) - .vohFirstSampleTime(firstTime) - .vohLastSampleTime(lastTime) - .vohFirstOdometerMeters(firstValue) - .vohLastOdometerMeters(lastValue) - .vohSampleCount(count) - .build() - ); - } - - } catch (Exception e) { - log.error("[ODOMETER][PARSE_FAIL]", e); - throw new RuntimeException("Failed to parse vehicle odometer history", e); - } - - return results; - } -} diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java b/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java deleted file mode 100644 index a31681a..0000000 --- a/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.goi.integration.samsara.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.goi.integration.common.util.DateTimeUtil; -import com.goi.integration.common.util.ExtPayloadHashUtil; -import com.goi.integration.samsara.client.SamsaraClient; -import com.goi.integration.samsara.dto.VehicleOdometerCommand; -import com.goi.integration.samsara.dto.VehicleOdometerRecordDto; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class VehicleOdometerService { - - private final SamsaraClient samsaraClient; - private final ObjectMapper objectMapper; - - /** - * Fetch samsara odometer data and build command - */ - public VehicleOdometerCommand fetchOdometerCommand( - List vehicleExternalIds, - Instant startTime, - Instant endTime - ) { - - String rawJson = - samsaraClient.getVehicleOdometerHistory( - vehicleExternalIds, - startTime, - endTime - ); - - List records = - parseRawOdometer(rawJson); - - return VehicleOdometerCommand.builder() - .source("SAMSARA") - .fetchedAt(LocalDateTime.now()) - .records(records) - .build(); - } - - /** - * Parse samsara raw JSON → odometer snapshot records - */ - private List parseRawOdometer(String rawJson) { - - List records = new ArrayList<>(); - - try { - JsonNode root = objectMapper.readTree(rawJson); - JsonNode dataArray = root.path("data"); - - if (!dataArray.isArray()) { - log.warn("[ODOMETER] no data[] in samsara response"); - return records; - } - - for (JsonNode vehicleNode : dataArray) { - - String vehicleExtId = - vehicleNode.path("id").asText(null); - - JsonNode odoArray = - vehicleNode.path("obdOdometerMeters"); - - if (!odoArray.isArray() || odoArray.isEmpty()) { - continue; - } - - for (JsonNode sample : odoArray) { - - String timeStr = - sample.path("time").asText(null); - - if (timeStr == null) { - continue; - } - - LocalDateTime sampleTime = - DateTimeUtil.parseToUTC(timeStr); - - long meters = - sample.path("value").asLong(); - - String hash = - ExtPayloadHashUtil.sha256FromStrings( - vehicleExtId, - timeStr, - String.valueOf(meters) - ); - - records.add( - VehicleOdometerRecordDto.builder() - .source("SAMSARA") - .vehicleExternalId(vehicleExtId) - .sampleTime(sampleTime) - .odometerMeters(meters) - .payloadHash(hash) - .payloadJson(sample) - .build() - ); - } - } - - } catch (Exception e) { - log.error("[ODOMETER][PARSE_FAIL]", e); - throw new RuntimeException("Failed to parse samsara odometer payload", e); - } - - return records; - } -} diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleStatEngineSecondsService.java b/src/main/java/com/goi/integration/samsara/service/VehicleStatEngineSecondsService.java new file mode 100644 index 0000000..698dfff --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/VehicleStatEngineSecondsService.java @@ -0,0 +1,181 @@ +package com.goi.integration.samsara.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goi.integration.common.util.ExtPayloadHashUtil; +import com.goi.integration.samsara.client.SamsaraClient; +import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; +import com.goi.integration.samsara.dto.VehicleStatEngineSecondsCommand; +import com.goi.integration.samsara.dto.VehicleStatEngineSecondsRecordDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; + +/** + * - Samsara API 호출 + * - raw JSON 파싱 + * - "사실 데이터"를 record 단위로 변환 + * + * 판단 / 선택 / 병합 없음 + * obdEngineSeconds 단일 타입 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class VehicleStatEngineSecondsService { + + private final SamsaraClient samsaraClient; + private final ObjectMapper objectMapper; + + /** + * Controller 진입점 + */ + public VehicleStatEngineSecondsCommand fetchCommand(ScheduleWorkerRequestDto request) { + + if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) { + throw new IllegalArgumentException("vehicleExternalIds is required"); + } + if (request.getFrom() == null || request.getTo() == null) { + throw new IllegalArgumentException("from/to is required"); + } + + Instant start = request.getFrom().toInstant(ZoneOffset.UTC); + Instant end = request.getTo().toInstant(ZoneOffset.UTC); + + log.info( + "[{}][ENGINE_SECONDS][FETCH] start={} end={} vehicles={}", + request.getJobCode(), start, end, request.getVehicleExternalIds().size() + ); + + String rawJson = samsaraClient.getVehicleEngineSeconds( + request.getVehicleExternalIds(), + start, + end + ); + + VehicleStatEngineSecondsCommand command = buildCommandFromRawJson(rawJson); + + log.info( + "[{}][ENGINE_SECONDS][FETCH_OK] records={} fetchedAt={}", + request.getJobCode(), + command.getRecords().size(), + command.getFetchedAt() + ); + + return command; + } + + /** + * raw JSON → records → command + * + * response sample: + * { + * "data": [ + * { + * "id": "...", + * "obdEngineSeconds": [{"time":"...Z","value":123}, ...] + * }, ... + * ] + * } + */ + private VehicleStatEngineSecondsCommand buildCommandFromRawJson(String rawJson) { + + try { + JsonNode root = objectMapper.readTree(rawJson); + JsonNode dataArray = root.path("data"); + + if (!dataArray.isArray()) { + log.warn("[ENGINE_SECONDS] samsara response has no data[]"); + return VehicleStatEngineSecondsCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(List.of()) + .build(); + } + + List records = new ArrayList<>(); + + for (JsonNode vehicleNode : dataArray) { + + String vehicleExtId = vehicleNode.path("id").asText(null); + if (vehicleExtId == null) { + continue; + } + + // obdEngineSeconds samples only + parseSamples( + records, + vehicleExtId, + vehicleNode.path("obdEngineSeconds") + ); + } + + return VehicleStatEngineSecondsCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(records) + .build(); + + } catch (Exception e) { + log.error("[ENGINE_SECONDS][PARSE_FAIL]", e); + throw new RuntimeException("ENGINE_SECONDS_FETCH_PARSE_FAIL", e); + } + } + + /** + * samplesArray: [{"time": "...Z", "value": 123}, ...] + */ + private void parseSamples( + List records, + String vehicleExtId, + JsonNode arrayNode + ) { + + if (arrayNode == null || !arrayNode.isArray() || arrayNode.isEmpty()) { + return; + } + + for (JsonNode sample : arrayNode) { + + String timeStr = sample.path("time").asText(null); + if (timeStr == null) { + continue; + } + + Instant sampleTime; + try { + sampleTime = Instant.parse(timeStr); + } catch (Exception e) { + continue; + } + + long engineSeconds = sample.path("value").asLong(); + + // 단일 타입이지만 해시 혹시 모를 확장을 위해 "obdEngineSeconds" 고정 문자열 포함 권장 + String hash = ExtPayloadHashUtil.sha256FromStrings( + vehicleExtId, + timeStr, + "obdEngineSeconds", + String.valueOf(engineSeconds) + ); + + records.add( + VehicleStatEngineSecondsRecordDto.builder() + .vehicleExternalId(vehicleExtId) + .sampleTime(sampleTime) + .engineSeconds(engineSeconds) + .payloadHash(hash) + .payloadJson(sample) // 원본 sample 그대로 + .build() + ); + } + } +} diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleStatOdometerService.java b/src/main/java/com/goi/integration/samsara/service/VehicleStatOdometerService.java new file mode 100644 index 0000000..a4525e5 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/VehicleStatOdometerService.java @@ -0,0 +1,189 @@ +package com.goi.integration.samsara.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goi.integration.common.util.ExtPayloadHashUtil; +import com.goi.integration.samsara.client.SamsaraClient; +import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; +import com.goi.integration.samsara.dto.VehicleStatOdometerCommand; +import com.goi.integration.samsara.dto.VehicleStatOdometerRecordDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; + +/** + * integration-service 책임: + * - Samsara API 호출 + * - raw JSON 파싱 + * - "사실 데이터"를 record 단위로 변환 + * + * 판단 / 선택 / 병합 없음 + * OBD / GPS 는 type 으로 구분된 독립 record + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class VehicleStatOdometerService { + + private final SamsaraClient samsaraClient; + private final ObjectMapper objectMapper; + + /** + * Controller 진입점 + */ + public VehicleStatOdometerCommand fetchCommand(ScheduleWorkerRequestDto request) { + + if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) { + throw new IllegalArgumentException("vehicleExternalIds is required"); + } + if (request.getFrom() == null || request.getTo() == null) { + throw new IllegalArgumentException("from/to is required"); + } + + Instant start = request.getFrom().toInstant(ZoneOffset.UTC); + Instant end = request.getTo().toInstant(ZoneOffset.UTC); + + log.info( + "[{}][ODOMETER][FETCH] start={} end={} vehicles={}", + request.getJobCode(), start, end, request.getVehicleExternalIds().size() + ); + + String rawJson = samsaraClient.getVehicleOdometerHistory( + request.getVehicleExternalIds(), + start, + end + ); + + VehicleStatOdometerCommand command = buildCommandFromRawJson(rawJson); + + log.info( + "[{}][ODOMETER][FETCH_OK] records={} fetchedAt={}", + request.getJobCode(), + command.getRecords().size(), + command.getFetchedAt() + ); + + return command; + } + + /** + * raw JSON → records → command + * + * - OBD / GPS 각각 독립 record + * - time 이 달라도 정상 처리 + * - hash 에 type 포함 + */ + private VehicleStatOdometerCommand buildCommandFromRawJson(String rawJson) { + + try { + JsonNode root = objectMapper.readTree(rawJson); + JsonNode dataArray = root.path("data"); + + if (!dataArray.isArray()) { + log.warn("[ODOMETER] samsara response has no data[]"); + return VehicleStatOdometerCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(List.of()) + .build(); + } + + List records = new ArrayList<>(); + + for (JsonNode vehicleNode : dataArray) { + + String vehicleExtId = vehicleNode.path("id").asText(null); + if (vehicleExtId == null) { + continue; + } + + // OBD samples + parseSamples( + records, + vehicleExtId, + vehicleNode.path("obdOdometerMeters"), + "OBD" + ); + + // GPS samples + parseSamples( + records, + vehicleExtId, + vehicleNode.path("gpsOdometerMeters"), + "GPS" + ); + } + + return VehicleStatOdometerCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(records) + .build(); + + } catch (Exception e) { + log.error("[ODOMETER][PARSE_FAIL]", e); + throw new RuntimeException("ODOMETER_FETCH_PARSE_FAIL", e); + } + } + + /** + * samplesArray: [{"time": "...Z", "value": 123}, ...] + * type: "OBD" | "GPS" + */ + private void parseSamples( + List records, + String vehicleExtId, + JsonNode arrayNode, + String type + ) { + + if (arrayNode == null || !arrayNode.isArray() || arrayNode.isEmpty()) { + return; + } + + for (JsonNode sample : arrayNode) { + + String timeStr = sample.path("time").asText(null); + if (timeStr == null) { + continue; + } + + Instant sampleTime; + try { + sampleTime = Instant.parse(timeStr); + } catch (Exception e) { + // invalid timestamp → skip + continue; + } + + long meters = sample.path("value").asLong(); + + // ✅ type 포함 hash (idempotent) + String hash = ExtPayloadHashUtil.sha256FromStrings( + vehicleExtId, + timeStr, + type, + String.valueOf(meters) + ); + + records.add( + VehicleStatOdometerRecordDto.builder() + .vehicleExternalId(vehicleExtId) + .sampleTime(sampleTime) + .odometerType(type) // "OBD" or "GPS" + .odometerMeters(meters) + .payloadHash(hash) + .payloadJson(sample) // 원본 sample 그대로 + .build() + ); + } + } +} diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleStatSafetyEventService.java b/src/main/java/com/goi/integration/samsara/service/VehicleStatSafetyEventService.java new file mode 100644 index 0000000..8a5b784 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/VehicleStatSafetyEventService.java @@ -0,0 +1,189 @@ +package com.goi.integration.samsara.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.goi.integration.samsara.client.SamsaraClient; +import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; +import com.goi.integration.samsara.dto.VehicleStatSafetyEventCommand; +import com.goi.integration.samsara.dto.VehicleStatSafetyEventRecordDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; + +/** + * - Samsara /fleet/safety-events API 호출 + * - raw JSON 파싱 + * - safety event 단위 record 생성 + * + * ❌ 판단 / 병합 / dispatch 매핑 없음 + * ✅ 사실 데이터 그대로 전달 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class VehicleStatSafetyEventService { + + private final SamsaraClient samsaraClient; + private final ObjectMapper objectMapper; + + /** + * Controller 진입점 + */ + public VehicleStatSafetyEventCommand fetchCommand(ScheduleWorkerRequestDto request) { + + if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) { + throw new IllegalArgumentException("vehicleExternalIds is required"); + } + if (request.getFrom() == null || request.getTo() == null) { + throw new IllegalArgumentException("from/to is required"); + } + + Instant start = request.getFrom().toInstant(ZoneOffset.UTC); + Instant end = request.getTo().toInstant(ZoneOffset.UTC); + + log.info( + "[{}][SAFETY_EVENT][FETCH] start={} end={} vehicles={}", + request.getJobCode(), start, end, request.getVehicleExternalIds().size() + ); + + String rawJson = samsaraClient.getVehicleSafetyEvents( + request.getVehicleExternalIds(), + start, + end + ); + + VehicleStatSafetyEventCommand command = buildCommandFromRawJson(rawJson); + + log.info( + "[{}][SAFETY_EVENT][FETCH_OK] records={} fetchedAt={}", + request.getJobCode(), + command.getRecords().size(), + command.getFetchedAt() + ); + + return command; + } + + /** + * raw JSON → records → command + * + * response sample: + * { + * "data": [ + * { + * "id": "...", + * "vehicle": { "id": "..." }, + * "driver": { "id": "..." }, + * "time": "...Z", + * "location": { "latitude": 0, "longitude": 0 }, + * "coachingState": "...", + * "maxAccelerationGForce": 0.58, + * "behaviorLabels": [...] + * } + * ] + * } + */ + private VehicleStatSafetyEventCommand buildCommandFromRawJson(String rawJson) { + + try { + JsonNode root = objectMapper.readTree(rawJson); + JsonNode dataArray = root.path("data"); + + if (!dataArray.isArray()) { + log.warn("[SAFETY_EVENT] samsara response has no data[]"); + return VehicleStatSafetyEventCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(List.of()) + .build(); + } + + List records = new ArrayList<>(); + + for (JsonNode eventNode : dataArray) { + + String eventId = eventNode.path("id").asText(null); + if (eventId == null) { + continue; + } + + JsonNode vehicleNode = eventNode.path("vehicle"); + String vehicleId = vehicleNode.path("id").asText(null); + if (vehicleId == null) { + continue; + } + + JsonNode driverNode = eventNode.path("driver"); + String driverId = driverNode.isMissingNode() + ? null + : driverNode.path("id").asText(null); + + String timeStr = eventNode.path("time").asText(null); + if (timeStr == null) { + continue; + } + + Instant eventTime; + try { + eventTime = Instant.parse(timeStr); + } catch (Exception e) { + continue; + } + + JsonNode locationNode = eventNode.path("location"); + + records.add( + VehicleStatSafetyEventRecordDto.builder() + .eventId(eventId) + .vehicleId(vehicleId) + .driverId(driverId) + .eventTime(eventTime) + .eventDate(eventTime.atZone(ZoneOffset.UTC).toLocalDate()) + .coachingState(eventNode.path("coachingState").asText(null)) + .maxAccelerationG( + eventNode.hasNonNull("maxAccelerationGForce") + ? eventNode.get("maxAccelerationGForce").asDouble() + : null + ) + .latitude( + locationNode.hasNonNull("latitude") + ? locationNode.get("latitude").asDouble() + : null + ) + .longitude( + locationNode.hasNonNull("longitude") + ? locationNode.get("longitude").asDouble() + : null + ) + .videoForwardUrl( + eventNode.path("downloadForwardVideoUrl").asText(null) + ) + .videoInwardUrl( + eventNode.path("downloadInwardVideoUrl").asText(null) + ) + .behaviorLabels(eventNode.path("behaviorLabels")) + .rawPayload(eventNode) + .build() + ); + } + + return VehicleStatSafetyEventCommand.builder() + .source("SAMSARA") + .fetchedAt(LocalDateTime.now()) + .records(records) + .build(); + + } catch (Exception e) { + log.error("[SAFETY_EVENT][PARSE_FAIL]", e); + throw new RuntimeException("SAFETY_EVENT_FETCH_PARSE_FAIL", e); + } + } +}