diff --git a/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java b/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java deleted file mode 100644 index 4cdedd1..0000000 --- a/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.goi.integration.common.config; - -import com.goi.integration.common.config.ScheduleJobConfigDto; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.Optional; - -@Component -public class InMemoryScheduleJobConfigProvider implements ScheduleJobConfigProvider { - - private final Map configs = Map.of( - "SAMSARA_DVIR", - ScheduleJobConfigDto.builder() - .sjcJobCode("SAMSARA_DVIR") - .sjcEnabled(true) - .sjcCronExpression("0 */10 * * * *") - .sjcLookbackHours(24) - .sjcOverlapMinutes(10) - .sjcMaxRecords(252) - .sjcTimezone("UTC") - .build() - ); - - @Override - public Optional getJobConfig(String jobCode) { - return Optional.ofNullable(configs.get(jobCode)); - } -} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java deleted file mode 100644 index d88ab9c..0000000 --- a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.goi.integration.common.config; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ScheduleJobConfigDto { - private String sjcJobCode; // SAMSARA_DVIR - private String sjcCronExpression; // "0 */10 * * * *" - private Integer sjcLookbackHours; // 24 - private Integer sjcOverlapMinutes;// 10 - private Integer sjcMaxRecords; // 252 - private Boolean sjcEnabled; // true - private String sjcTimezone; // "UTC" -} diff --git a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java deleted file mode 100644 index 692c00d..0000000 --- a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.goi.integration.common.config; - -import com.goi.integration.common.config.ScheduleJobConfigDto; - -import java.util.Optional; - -public interface ScheduleJobConfigProvider { - Optional getJobConfig(String jobCode); -} - diff --git a/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java b/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java index ece3a8b..f3ba848 100644 --- a/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java +++ b/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java @@ -11,16 +11,14 @@ import lombok.NoArgsConstructor; @Builder public class ExtIngestResult { private String source; - private String recordType; private int received; private int inserted; private int updated; private int skipped; - public static ExtIngestResult empty(String source, String recordType) { + public static ExtIngestResult empty(String source) { return ExtIngestResult.builder() .source(source) - .recordType(recordType) .received(0) .inserted(0) .updated(0) diff --git a/src/main/java/com/goi/integration/common/util/DateTimeUtil.java b/src/main/java/com/goi/integration/common/util/DateTimeUtil.java index 001dce8..716767a 100644 --- a/src/main/java/com/goi/integration/common/util/DateTimeUtil.java +++ b/src/main/java/com/goi/integration/common/util/DateTimeUtil.java @@ -39,6 +39,37 @@ public final class DateTimeUtil { } } + /** + * JsonNode → LocalDateTime (UTC) + */ + public static LocalDateTime parseToUTC(JsonNode node) { + if (node == null || node.isMissingNode() || node.isNull()) { + return null; + } + return parseToUTC(node.asText(null)); + } + + /** + * String → LocalDateTime (UTC) + * ex) 2025-12-22T13:30:24.365Z + * ex) 2025-12-22T13:30:24+00:00 + * ex) 2025-12-22T08:30:24-05:00 + */ + public static LocalDateTime parseToUTC(String value) { + if (value == null || value.isBlank()) { + return null; + } + + try { + return OffsetDateTime + .parse(value) + .atZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime(); + } catch (DateTimeParseException e) { + return null; // ingest 안정성 우선 + } + } + public static LocalDateTime parseToToronto(String value) { if (value == null || value.isBlank()) return null; diff --git a/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java b/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java index 8a63be2..928c100 100644 --- a/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java +++ b/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java @@ -35,6 +35,25 @@ public final class ExtPayloadHashUtil { throw new RuntimeException("Failed to hash raw json", e); } } + + /** + * 문자열 조합 기반 SHA-256 + * (odometer snapshot idempotent / 변경 감지용) + */ + public static String sha256FromStrings(String... values) { + try { + StringBuilder sb = new StringBuilder(); + for (String v : values) { + if (v != null) { + sb.append(v); + } + sb.append('|'); // 구분자 + } + return sha256(sb.toString()); + } catch (Exception e) { + throw new RuntimeException("Failed to hash from strings", e); + } + } /* ---------- internal ---------- */ 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 b290437..89d8e98 100644 --- a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java +++ b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java @@ -33,9 +33,8 @@ public class OprIngestClient { // .writeValueAsString(command); // log.info( - "[OPR_INGEST][REQUEST] source={}, type={}, records={}, fetchedAt={}", + "[OPR_INGEST][REQUEST] source={}, type={}, fetchedAt={}", command.getSource(), - command.getRecordType(), command.getRecords() != null ? command.getRecords().size() : 0, command.getFetchedAt() ); @@ -90,9 +89,8 @@ public class OprIngestClient { } catch (Exception e) { // 실패 로그 log.error( - "[OPR_INGEST][FAILED] source={}, type={}, error={}", + "[OPR_INGEST][FAILED] source={}, error={}", command.getSource(), - command.getRecordType(), e.getMessage(), e ); diff --git a/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java b/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java new file mode 100644 index 0000000..bc7e6ae --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/controller/InspectionWorkerController.java @@ -0,0 +1,31 @@ +package com.goi.integration.samsara.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + + + +@RestController +@RequestMapping("/samsara/inspection") +@RequiredArgsConstructor +@Slf4j +public class InspectionWorkerController { + + private final InspectionIngestWorker inspectionIngestWorker; + + @PostMapping("/worker") + public ScheduleWorkerResponseDto runDvir( + @RequestBody ScheduleWorkerRequestDto request + ) { + return inspectionIngestWorker.execute(request); + } +} diff --git a/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java b/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java deleted file mode 100644 index 820d96e..0000000 --- a/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.goi.integration.samsara.controller; - -import com.goi.integration.samsara.job.DvirIngestJob; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/internal/test") -@RequiredArgsConstructor -public class SamsaraTestController { - - private final DvirIngestJob dvirJob; - - @PostMapping("/samsara-dvir") - public String runDvirNow() { - dvirJob.run(); - return "SAMSARA_DVIR_TRIGGERED"; - } -} 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 a36abb1..d1f4304 100644 --- a/src/main/java/com/goi/integration/samsara/controller/VehicleController.java +++ b/src/main/java/com/goi/integration/samsara/controller/VehicleController.java @@ -2,7 +2,9 @@ package com.goi.integration.samsara.controller; import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto; 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.service.VehicleGpsService; import lombok.RequiredArgsConstructor; @@ -19,6 +21,7 @@ public class VehicleController { private final VehicleGpsService vehicleStatService; private final VehicleOdometerHistoryService vehicleOdometerHistoryService; + private final VehicleOdometerService vehicleOdometerService; @GetMapping("/stat/gps") public List getGps( @@ -41,6 +44,22 @@ public class VehicleController { ); } + /** + * Vehicle odometer raw fetch (for opr-rest-api ingest) + */ + @PostMapping("/odometer/fetch") + public VehicleOdometerCommand fetchOdometer( + @RequestParam List vehicleIds, + @RequestParam Instant startTime, + @RequestParam Instant endTime + ) { + return vehicleOdometerService.fetchOdometerCommand( + vehicleIds, + startTime, + endTime + ); + } + // @GetMapping("/safety/events") // public List getSafetyEvents( // @RequestParam Instant startTime, diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java index 5d738fa..93ed03b 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java +++ b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java @@ -14,7 +14,6 @@ import java.util.List; @Builder public class ExtInspectionIngestCommand { private String source; - private String recordType; private LocalDateTime fetchedAt; private List records; } \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.java new file mode 100644 index 0000000..71c7486 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerRequestDto.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.LocalDateTime; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ScheduleWorkerRequestDto { + + private String jobCode; + private LocalDateTime from; + private LocalDateTime to; + private Integer maxRecords; + private Map> config; +} diff --git a/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerResponseDto.java b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerResponseDto.java new file mode 100644 index 0000000..15c031e --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/ScheduleWorkerResponseDto.java @@ -0,0 +1,22 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ScheduleWorkerResponseDto { + + private boolean success; + + private int processedCount; + private int successCount; + private int failCount; + + private String errorCode; + private String errorMessage; +} diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.java b/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.java new file mode 100644 index 0000000..549a179 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerCommand.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 VehicleOdometerCommand { + 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/VehicleOdometerRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerRecordDto.java new file mode 100644 index 0000000..cde0b48 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleOdometerRecordDto.java @@ -0,0 +1,23 @@ +package com.goi.integration.samsara.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.databind.JsonNode; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleOdometerRecordDto { + private String source; // SAMSARA + private String vehicleExternalId; // samsara vehicle.id + private LocalDateTime sampleTime; // snapshot time (UTC) + 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/job/DvirIngestJob.java b/src/main/java/com/goi/integration/samsara/job/DvirIngestJob.java deleted file mode 100644 index b7ba393..0000000 --- a/src/main/java/com/goi/integration/samsara/job/DvirIngestJob.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.goi.integration.samsara.job; - -import com.goi.integration.common.config.ScheduleJobConfigDto; -import com.goi.integration.common.config.ScheduleJobConfigProvider; -import com.goi.integration.common.dto.ExtIngestResult; -import com.goi.integration.samsara.client.SamsaraClient; -import com.goi.integration.samsara.service.InspectionIngestService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.Instant; - -@Slf4j -@Component -@RequiredArgsConstructor -public class DvirIngestJob { - - private static final String JOB_CODE = "SAMSARA_DVIR"; - - private final ScheduleJobConfigProvider configProvider; - private final SamsaraClient samsaraClient; - private final InspectionIngestService ingestService; - - @Scheduled(cron = "${ext.samsara.jobs.dvir.cron:0 */10 * * * *}") - public void run() { - - ScheduleJobConfigDto cfg = configProvider.getJobConfig(JOB_CODE) - .filter(ScheduleJobConfigDto::getSjcEnabled) - .orElse(null); - - if (cfg == null) { - log.info("[{}] disabled or config missing", JOB_CODE); - return; - } - - int limit = (cfg.getSjcMaxRecords() != null) ? cfg.getSjcMaxRecords() : 252; - - Instant now = Instant.now(); - long lookbackSec = Duration.ofHours(cfg.getSjcLookbackHours() != null ? cfg.getSjcLookbackHours() : 24).getSeconds(); - long overlapSec = Duration.ofMinutes(cfg.getSjcOverlapMinutes() != null ? cfg.getSjcOverlapMinutes() : 0).getSeconds(); - - Instant start = now.minusSeconds(lookbackSec + overlapSec); - Instant end = now; - - log.info("[{}] calling samsara dvir history start={} end={} limit={}", JOB_CODE, start, end, limit); - - String rawJson = samsaraClient.getDvirHistory(limit, start, end); - - // opr-rest-api ingest 호출 - ExtIngestResult result = ingestService.ingestFromRawJson(rawJson); - - log.info("[{}] ingest result received={}, inserted={}, updated={}, skipped={}", - JOB_CODE, result.getReceived(), result.getInserted(), result.getUpdated(), result.getSkipped()); - } -} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java b/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java index cf6759e..eb27423 100644 --- a/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java +++ b/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java @@ -21,8 +21,6 @@ import java.util.List; @RequiredArgsConstructor public class InspectionIngestService { - private static final String JOB_CODE = "SAMSARA_DVIR"; - private final ObjectMapper objectMapper; private final OprIngestClient oprIngestClient; @@ -42,12 +40,12 @@ public class InspectionIngestService { // data[] 없음 if (!data.isArray()) { log.warn("Samsara DVIR response has no data[] array"); - return ExtIngestResult.empty("SAMSARA", "DVIR"); + return ExtIngestResult.empty("SAMSARA"); } // List records = new ArrayList<>(); - log.info("[{}] data={}", JOB_CODE, data.size()); + log.info("inspection data size={}", data.size()); // data[] 각 node 가 하나의 inspection (preTrip / postTrip) for (JsonNode node : data) { @@ -87,7 +85,6 @@ public class InspectionIngestService { ExtInspectionIngestCommand command = ExtInspectionIngestCommand.builder() .source("SAMSARA") - .recordType("DVIR") .fetchedAt(LocalDateTime.now()) .records(records) .build(); diff --git a/src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java b/src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java new file mode 100644 index 0000000..f4c815a --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/InspectionIngestWorker.java @@ -0,0 +1,61 @@ +package com.goi.integration.samsara.service; + +import com.goi.integration.common.dto.ExtIngestResult; +import com.goi.integration.samsara.client.SamsaraClient; +import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto; +import com.goi.integration.samsara.dto.ScheduleWorkerResponseDto; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.ZoneOffset; + +@Slf4j +@Component +@RequiredArgsConstructor +public class InspectionIngestWorker { + + private final SamsaraClient samsaraClient; + private final InspectionIngestService ingestService; + + public ScheduleWorkerResponseDto execute(ScheduleWorkerRequestDto request) { + + Instant start = request.getFrom().toInstant(ZoneOffset.UTC); + Instant end = request.getTo().toInstant(ZoneOffset.UTC); + int limit = request.getMaxRecords() != null ? request.getMaxRecords() : 252; + + log.info("[{}] worker start={} end={} limit={}", request.getJobCode(), start, end, limit); + + try { + String rawJson = samsaraClient.getDvirHistory(limit, start, end); + + ExtIngestResult result = ingestService.ingestFromRawJson(rawJson); + + log.info("[{}] ingest success received={}, inserted={}, updated={}, skipped={}", + request.getJobCode(), + result.getReceived(), + result.getInserted(), + result.getUpdated(), + result.getSkipped() + ); + + return ScheduleWorkerResponseDto.builder() + .success(true) + .processedCount(result.getReceived()) + .successCount(result.getInserted() + result.getUpdated()) + .failCount(result.getSkipped()) + .build(); + + } catch (Exception e) { + log.error("[{}] ingest failed", request.getJobCode(), e); + + return ScheduleWorkerResponseDto.builder() + .success(false) + .errorCode("INSPECTION_INGEST_ERROR") + .errorMessage(e.getMessage()) + .build(); + } + } +} diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java b/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java new file mode 100644 index 0000000..a31681a --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/VehicleOdometerService.java @@ -0,0 +1,125 @@ +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; + } +}