[INSPECTION]
- Ingest inspection raw data via opr-rest-api
This commit is contained in:
parent
777cb5bd13
commit
26d05d35f0
8
pom.xml
8
pom.xml
|
|
@ -41,10 +41,6 @@
|
|||
</profiles>
|
||||
<dependencies>
|
||||
<!-- spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
|
|
@ -106,6 +102,10 @@
|
|||
<version>${allure.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.goi.integration;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class IntegrationServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(IntegrationServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
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<String, ScheduleJobConfigDto> 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<ScheduleJobConfigDto> getJobConfig(String jobCode) {
|
||||
return Optional.ofNullable(configs.get(jobCode));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.goi.integration.common.config;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Component
|
||||
public class InternalWebClientFactory {
|
||||
|
||||
public WebClient create(String baseUrl, String token) {
|
||||
return WebClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeader("X-INTERNAL-SERVICE", "integration-service")
|
||||
.defaultHeader("X-INTERNAL-TOKEN", token)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
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"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.goi.integration.common.config;
|
||||
|
||||
import com.goi.integration.common.config.ScheduleJobConfigDto;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ScheduleJobConfigProvider {
|
||||
Optional<ScheduleJobConfigDto> getJobConfig(String jobCode);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.goi.integration.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@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) {
|
||||
return ExtIngestResult.builder()
|
||||
.source(source)
|
||||
.recordType(recordType)
|
||||
.received(0)
|
||||
.inserted(0)
|
||||
.updated(0)
|
||||
.skipped(0)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.goi.integration.common.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
public final class DateTimeUtil {
|
||||
|
||||
private DateTimeUtil() {}
|
||||
|
||||
/**
|
||||
* JsonNode → LocalDateTime
|
||||
* (ISO-8601, Z / offset 지원)
|
||||
*/
|
||||
public static LocalDateTime parse(JsonNode node) {
|
||||
if (node == null || node.isMissingNode() || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return parse(node.asText(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* String → LocalDateTime
|
||||
* ex) 2025-12-22T13:30:24.365Z
|
||||
* ex) 2025-12-22T13:30:24+00:00
|
||||
*/
|
||||
public static LocalDateTime parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return OffsetDateTime.parse(value).toLocalDateTime();
|
||||
} catch (DateTimeParseException e) {
|
||||
return null; // ingest 안정성 우선
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalDateTime parseToToronto(String value) {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
|
||||
return OffsetDateTime
|
||||
.parse(value)
|
||||
.atZoneSameInstant(ZoneId.of("America/Toronto"))
|
||||
.toLocalDateTime();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.goi.integration.common.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public final class ExtPayloadHashUtil {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
private ExtPayloadHashUtil() {}
|
||||
|
||||
/** record(JsonNode) → canonical hash */
|
||||
public static String sha256FromNode(JsonNode node) {
|
||||
try {
|
||||
JsonNode normalized = normalize(node);
|
||||
String canonicalJson = mapper.writeValueAsString(normalized);
|
||||
return sha256(canonicalJson);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to hash payload", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** raw JSON string → hash (fallback 용) */
|
||||
public static String sha256FromJson(String rawJson) {
|
||||
try {
|
||||
JsonNode node = mapper.readTree(rawJson);
|
||||
return sha256FromNode(node);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to hash raw json", e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- internal ---------- */
|
||||
|
||||
@SuppressWarnings({ "serial", "deprecation" })
|
||||
private static JsonNode normalize(JsonNode node) {
|
||||
if (node.isObject()) {
|
||||
ObjectNode obj = mapper.createObjectNode();
|
||||
new TreeMap<String, JsonNode>() {{
|
||||
node.fields().forEachRemaining(e -> put(e.getKey(), normalize(e.getValue())));
|
||||
}}.forEach(obj::set);
|
||||
return obj;
|
||||
}
|
||||
if (node.isArray()) {
|
||||
ArrayNode arr = mapper.createArrayNode();
|
||||
node.forEach(n -> arr.add(normalize(n)));
|
||||
return arr;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : hash) hex.append(String.format("%02x", b));
|
||||
return hex.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String sha256FromJsonNode(JsonNode node) {
|
||||
try {
|
||||
JsonNode normalized = normalize(node);
|
||||
String canonicalJson = mapper.writeValueAsString(normalized);
|
||||
return sha256(canonicalJson);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to hash JsonNode payload", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.goi.integration.samsara.client;
|
||||
|
||||
import com.goi.integration.common.dto.ExtIngestResult;
|
||||
import com.goi.integration.samsara.dto.ExtSamsaraInspectionIngestCommand;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OprIngestClient {
|
||||
|
||||
private static final String INTERNAL_TOKEN_HEADER = "X-INTERNAL-TOKEN";
|
||||
|
||||
@Qualifier("oprWebClient")
|
||||
private final WebClient oprWebClient;
|
||||
|
||||
@Value("${ext.opr.internal-token}")
|
||||
private String token;
|
||||
|
||||
// @Autowired
|
||||
// private ObjectMapper objectMapper;
|
||||
|
||||
public ExtIngestResult ingestInspection(ExtSamsaraInspectionIngestCommand command) {
|
||||
// String json = objectMapper
|
||||
// .writerWithDefaultPrettyPrinter()
|
||||
// .writeValueAsString(command);
|
||||
//
|
||||
log.info(
|
||||
"[OPR_INGEST][REQUEST] source={}, type={}, records={}, fetchedAt={}",
|
||||
command.getSource(),
|
||||
command.getRecordType(),
|
||||
command.getRecords() != null ? command.getRecords().size() : 0,
|
||||
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")
|
||||
.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][SUCCESS] received={}, inserted={}, updated={}, skipped={}",
|
||||
result.getReceived(),
|
||||
result.getInserted(),
|
||||
result.getUpdated(),
|
||||
result.getSkipped()
|
||||
);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 실패 로그
|
||||
log.error(
|
||||
"[OPR_INGEST][FAILED] source={}, type={}, error={}",
|
||||
command.getSource(),
|
||||
command.getRecordType(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.goi.integration.samsara.client;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Component
|
||||
public class SamsaraClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SamsaraClient(
|
||||
@Value("${ext.samsara.base-url:https://api.samsara.com}") String baseUrl,
|
||||
@Value("${ext.samsara.api-token}") String apiToken
|
||||
) {
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiToken)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* DVIR history raw JSON (string)로 받아도 되고, DTO로 매핑해도 됨.
|
||||
* 일단 초기엔 String으로 받아서 JsonNode로 파싱하는게 가장 유연함.
|
||||
*/
|
||||
public String getDvirHistory(int limit, Instant startTime, Instant endTime) {
|
||||
return webClient.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/fleet/dvirs/history")
|
||||
.queryParam("limit", limit)
|
||||
.queryParam("startTime", startTime.toString())
|
||||
.queryParam("endTime", endTime.toString())
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.goi.integration.samsara.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Configuration
|
||||
public class OprClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient oprWebClient(
|
||||
WebClient.Builder builder,
|
||||
@Value("${ext.opr.base-url}") String baseUrl
|
||||
) {
|
||||
return builder
|
||||
.baseUrl(baseUrl)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.goi.integration.samsara.controller;
|
||||
|
||||
import com.goi.integration.samsara.job.SamsaraDvirIngestJob;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/test")
|
||||
@RequiredArgsConstructor
|
||||
public class SamsaraTestController {
|
||||
|
||||
private final SamsaraDvirIngestJob dvirJob;
|
||||
|
||||
@PostMapping("/samsara-dvir")
|
||||
public String runDvirNow() {
|
||||
dvirJob.run();
|
||||
return "SAMSARA_DVIR_TRIGGERED";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
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 ExtSamsaraInspectionIngestCommand {
|
||||
private String source;
|
||||
private String recordType;
|
||||
private LocalDateTime fetchedAt;
|
||||
private List<ExtSamsaraInspectionRecordDto> records;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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 ExtSamsaraInspectionRecordDto {
|
||||
private String externalId; // inspection id
|
||||
private String vehicleExternalId; // vehicle.id
|
||||
private String driverExternalId; // signatoryUser.id
|
||||
private String inspectionType; // preTrip / postTrip
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private LocalDateTime signedAt;
|
||||
private String payloadHash; // SHA-256
|
||||
private JsonNode payloadJson; // raw inspection JSON
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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.SamsaraInspectionIngestService;
|
||||
|
||||
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 SamsaraDvirIngestJob {
|
||||
|
||||
private static final String JOB_CODE = "SAMSARA_DVIR";
|
||||
|
||||
private final ScheduleJobConfigProvider configProvider;
|
||||
private final SamsaraClient samsaraClient;
|
||||
private final SamsaraInspectionIngestService 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.goi.integration.samsara.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.ExtSamsaraInspectionIngestCommand;
|
||||
import com.goi.integration.samsara.dto.ExtSamsaraInspectionRecordDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SamsaraInspectionIngestService {
|
||||
|
||||
private static final String JOB_CODE = "SAMSARA_DVIR";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OprIngestClient oprIngestClient;
|
||||
|
||||
/**
|
||||
* Samsara DVIR raw JSON 응답을 받아
|
||||
* 1) record 단위로 분해
|
||||
* 2) hash 생성
|
||||
* 3) opr-rest-api ingest endpoint 호출
|
||||
*/
|
||||
public ExtIngestResult ingestFromRawJson(String rawJson) {
|
||||
|
||||
try {
|
||||
// 전체 JSON 파싱
|
||||
JsonNode root = objectMapper.readTree(rawJson);
|
||||
JsonNode data = root.path("data");
|
||||
|
||||
// data[] 없음
|
||||
if (!data.isArray()) {
|
||||
log.warn("Samsara DVIR response has no data[] array");
|
||||
return ExtIngestResult.empty("SAMSARA", "DVIR");
|
||||
}
|
||||
|
||||
//
|
||||
List<ExtSamsaraInspectionRecordDto> records = new ArrayList<>();
|
||||
log.info("[{}] data={}", JOB_CODE, data.size());
|
||||
|
||||
// data[] 각 node 가 하나의 inspection (preTrip / postTrip)
|
||||
for (JsonNode node : data) {
|
||||
// parsing
|
||||
String externalId = node.path("id").asText(null);
|
||||
String vehicleExtId = node.path("vehicle").path("id").asText(null);
|
||||
String driverExtId = node.path("authorSignature")
|
||||
.path("signatoryUser")
|
||||
.path("id")
|
||||
.asText(null);
|
||||
String inspectionType = node.path("type").asText(null);
|
||||
LocalDateTime startTime = DateTimeUtil.parseToToronto(node.path("startTime").asText(null));
|
||||
LocalDateTime endTime = DateTimeUtil.parseToToronto(node.path("endTime").asText(null));
|
||||
LocalDateTime signedAt = DateTimeUtil.parseToToronto(
|
||||
node.path("authorSignature").path("signedAtTime").asText(null)
|
||||
);
|
||||
// record 단위 hash 생성 (opr-rest-api 에서 idempotent ingest 판단용)
|
||||
String hash = ExtPayloadHashUtil.sha256FromJsonNode(node);
|
||||
|
||||
// record DTO
|
||||
records.add(
|
||||
ExtSamsaraInspectionRecordDto.builder()
|
||||
.externalId(externalId)
|
||||
.vehicleExternalId(vehicleExtId)
|
||||
.driverExternalId(driverExtId)
|
||||
.inspectionType(inspectionType)
|
||||
.startTime(startTime)
|
||||
.endTime(endTime)
|
||||
.signedAt(signedAt)
|
||||
.payloadJson(node) // 원본 payload (JSON 그대로 저장)
|
||||
.payloadHash(hash)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
// ingest command 생성
|
||||
ExtSamsaraInspectionIngestCommand command =
|
||||
ExtSamsaraInspectionIngestCommand.builder()
|
||||
.source("SAMSARA")
|
||||
.recordType("DVIR")
|
||||
.fetchedAt(LocalDateTime.now())
|
||||
.records(records)
|
||||
.build();
|
||||
|
||||
// opr-rest-api ingest endpoint 호출
|
||||
return oprIngestClient.ingestInspection(command);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to ingest samsara DVIR payload", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,5 +14,39 @@ management:
|
|||
exposure:
|
||||
include: health
|
||||
server:
|
||||
port: 8091
|
||||
servlet:
|
||||
context-path: /integration-service
|
||||
context-path: /integration-service
|
||||
# ============================
|
||||
# External Integrations
|
||||
# ============================
|
||||
ext:
|
||||
opr:
|
||||
base-url: http://localhost:8083/opr-rest-api # opr-rest-api 주소
|
||||
ingest-path: /ext/samsara/inspections/ingest
|
||||
internal-token: ${OPR_INTERNAL_TOKEN}
|
||||
hcm:
|
||||
base-url: http://localhost:8081/hcm-rest-api
|
||||
internal-token: ${HCM_INTERNAL_TOKEN}
|
||||
acc:
|
||||
base-url: http://localhost:8084/acc-rest-api
|
||||
internal-token: ${ACC_INTERNAL_TOKEN}
|
||||
samsara:
|
||||
base-url: https://api.samsara.com
|
||||
api-token: ${SAMSARA_API_TOKEN} # 반드시 env 로
|
||||
timeout:
|
||||
connect-ms: 5000
|
||||
read-ms: 10000
|
||||
|
||||
jobs:
|
||||
dvir:
|
||||
cron: "0 */10 * * * *" # 10분
|
||||
# ============================
|
||||
# logging
|
||||
# ============================
|
||||
logging:
|
||||
file:
|
||||
name: logs/integration-service.log
|
||||
level:
|
||||
root: INFO
|
||||
com.goi.integration: INFO
|
||||
Loading…
Reference in New Issue