diff --git a/pom.xml b/pom.xml index 70ae13d..9d7099a 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,11 @@ spring-security-test test + + com.vladmihalcea + hibernate-types-60 + 2.21.1 + template layered-architecture-template diff --git a/src/main/java/com/goi/erp/common/util/DateTimeUtil.java b/src/main/java/com/goi/erp/common/util/DateTimeUtil.java new file mode 100644 index 0000000..7ef1e52 --- /dev/null +++ b/src/main/java/com/goi/erp/common/util/DateTimeUtil.java @@ -0,0 +1,50 @@ +package com.goi.erp.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(); + } +} diff --git a/src/main/java/com/goi/erp/config/InternalAuthFilter.java b/src/main/java/com/goi/erp/config/InternalAuthFilter.java new file mode 100644 index 0000000..41e8c9d --- /dev/null +++ b/src/main/java/com/goi/erp/config/InternalAuthFilter.java @@ -0,0 +1,37 @@ +package com.goi.erp.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class InternalAuthFilter extends OncePerRequestFilter { + + @Value("${internal.integration.token}") + private String expectedToken; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + String token = request.getHeader("X-INTERNAL-TOKEN"); + + if (request.getRequestURI().startsWith("/ext/")) { + if (token == null || !token.equals(expectedToken)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/goi/erp/config/SecurityConfig.java b/src/main/java/com/goi/erp/config/SecurityConfig.java index 098033f..8790862 100644 --- a/src/main/java/com/goi/erp/config/SecurityConfig.java +++ b/src/main/java/com/goi/erp/config/SecurityConfig.java @@ -32,6 +32,7 @@ public class SecurityConfig { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안함 .authorizeHttpRequests(auth -> auth .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/ext/**").permitAll() .anyRequest().authenticated() ) // 요청 권한 설정 .addFilterBefore(new CorsFilter(corsConfigurationSource()), UsernamePasswordAuthenticationFilter.class) // JWT 필터 전에 CorsFilter 등록 diff --git a/src/main/java/com/goi/erp/controller/TestController.java b/src/main/java/com/goi/erp/controller/TestController.java new file mode 100644 index 0000000..fd50c53 --- /dev/null +++ b/src/main/java/com/goi/erp/controller/TestController.java @@ -0,0 +1,23 @@ +package com.goi.erp.controller; + +import com.goi.erp.service.ExtSamsaraInspectionProcessor; + +import lombok.RequiredArgsConstructor; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +@RequiredArgsConstructor +public class TestController { + + private final ExtSamsaraInspectionProcessor processor; + + @PostMapping("/process-samsara-inspection") + public String process() { + processor.processUnprocessed(); + return "OK"; + } +} diff --git a/src/main/java/com/goi/erp/controller/VehicleDispatchController.java b/src/main/java/com/goi/erp/controller/VehicleDispatchController.java new file mode 100644 index 0000000..e991a00 --- /dev/null +++ b/src/main/java/com/goi/erp/controller/VehicleDispatchController.java @@ -0,0 +1,260 @@ +package com.goi.erp.controller; + +import com.goi.erp.common.permission.PermissionSet; +import com.goi.erp.dto.VehicleDispatchRequestDto; +import com.goi.erp.entity.VehicleDispatch; +import com.goi.erp.repository.VehicleDispatchRepository; +import com.goi.erp.common.permission.PermissionChecker; +import com.goi.erp.token.PermissionAuthenticationToken; +import com.goi.erp.service.VehicleDispatchService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/vehicle-dispatch") +@RequiredArgsConstructor +public class VehicleDispatchController { + + private final VehicleDispatchService dispatchService; + private final VehicleDispatchRepository dispatchRepository; + + // 기본값 + private final int defaultPage = 0; + private final int defaultSize = 50; + private final int maxSize = 500; + + /* ============================================================ + CREATE (MANUAL) + ============================================================ */ + @PostMapping("/manual") + public ResponseEntity createManualDispatch( + @RequestBody VehicleDispatchRequestDto requestDto + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canCreateOPR(permissionSet)) { + throw new AccessDeniedException("You do not have permission to create dispatch"); + } + + VehicleDispatch created = + dispatchService.createManualDispatch(requestDto, auth.getName()); + + return new ResponseEntity<>(created, HttpStatus.CREATED); + } + + /* ============================================================ + PATCH (MANUAL) + ============================================================ */ + @PatchMapping("/manual/{uuid}") + public ResponseEntity patchManualDispatch( + @PathVariable UUID uuid, + @RequestBody VehicleDispatchRequestDto requestDto + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canUpdateOPR(permissionSet)) { + throw new AccessDeniedException("You do not have permission to update dispatch"); + } + + VehicleDispatch updated = + dispatchService.patchManualDispatch(uuid, requestDto, auth.getName()); + + return ResponseEntity.ok(updated); + } + + /* ============================================================ + STATE CHANGE (MANUAL) + ============================================================ */ + @PostMapping("/{uuid}/pause") + public ResponseEntity pauseDispatch( + @PathVariable UUID uuid, + @RequestParam(required = false) LocalDateTime pausedAt + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canUpdateOPR(permissionSet)) { + throw new AccessDeniedException("You do not have permission to update dispatch"); + } + + dispatchService.pauseDispatch(uuid, pausedAt); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{uuid}/close") + public ResponseEntity closeDispatch( + @PathVariable UUID uuid, + @RequestParam(required = false) LocalDateTime endAt, + @RequestParam(required = false) BigDecimal endOdometerEnd, // DB: ved_odometer_end 로 저장 + @RequestParam(required = false) String reason + ) { + PermissionAuthenticationToken auth = + (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canUpdateOPR(permissionSet)) { + throw new AccessDeniedException("You do not have permission to update dispatch"); + } + + dispatchService.closeDispatch( + uuid, + endAt, + endOdometerEnd, + (reason == null || reason.isBlank()) ? "MANUAL_CLOSE" : reason + ); + + return ResponseEntity.ok().build(); + } + + /* ============================================================ + READ ONE + ============================================================ */ + @GetMapping("/{uuid}") + public ResponseEntity getOne( + @PathVariable UUID uuid + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadOPRAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read dispatch data"); + } + + return dispatchRepository.findByVedUuid(uuid) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /* ============================================================ + READ ALL (Paged) + ============================================================ */ + @GetMapping + public ResponseEntity> getAll( + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer size + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadOPRAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read dispatch data"); + } + + int p = (page == null) ? defaultPage : page; + int s = (size == null) ? defaultSize : size; + if (s > maxSize) s = maxSize; + + Pageable pageable = PageRequest.of(p, s); + return ResponseEntity.ok(dispatchRepository.findAll(pageable)); + } + + /* ============================================================ + READ BY DATE (가장 자주 쓰는 API) + - date만: 그날 전체 + - date + vehId: 그 차량의 그날 + - date + driverId: 그 기사 그날 + ============================================================ */ + @GetMapping("/by-date") + public ResponseEntity> getByDate( + @RequestParam LocalDate date, + @RequestParam(required = false) Long vehId, + @RequestParam(required = false) Long driverId + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadOPRAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read dispatch data"); + } + + if (vehId != null && driverId != null) { + // 둘 다 넣는 케이스가 있을까 + throw new IllegalArgumentException("Use either vehId or driverId, not both"); + } + + if (vehId != null) { + return ResponseEntity.ok( + dispatchRepository.findByVedVehIdAndVedDispatchDate(vehId, date) + ); + } + + if (driverId != null) { + return ResponseEntity.ok( + dispatchRepository.findByVedDriverIdAndVedDispatchDate(driverId, date) + ); + } + + return ResponseEntity.ok( + dispatchRepository.findByVedDispatchDate(date) + ); + } + + /* ============================================================ + JOB TRIGGER + - paused to closed + ============================================================ */ + @PostMapping("/jobs/close-paused") + public ResponseEntity closePaused( + @RequestParam(defaultValue = "60") long minutes + ) { + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canUpdateOPR(permissionSet)) { + throw new AccessDeniedException("You do not have permission to update dispatch"); + } + + dispatchService.closePausedDispatches(Duration.ofMinutes(minutes)); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/goi/erp/controller/VehicleExternalMapController.java b/src/main/java/com/goi/erp/controller/VehicleExternalMapController.java new file mode 100644 index 0000000..8753bdb --- /dev/null +++ b/src/main/java/com/goi/erp/controller/VehicleExternalMapController.java @@ -0,0 +1,33 @@ +package com.goi.erp.controller; + +import com.goi.erp.dto.VehicleExternalMapResponseDto; +import com.goi.erp.service.VehicleExternalMapService; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/employee") +@RequiredArgsConstructor +public class VehicleExternalMapController { + + private final VehicleExternalMapService externalMapService; + + /** + * 외부 시스템의 employeeId → 내부 employee 매핑 조회 + * 목적: customer_daily_order.driver_id 설정 + * + * 예: GET /employee/external?solutionType=SAMSARA&externalId=28147... + */ + @GetMapping("/external") + public ResponseEntity getVehicleMapping( + @RequestParam String solutionType, + @RequestParam String externalId + ) { + VehicleExternalMapResponseDto dto = + externalMapService.findMapping(solutionType, externalId); + + return ResponseEntity.ok(dto); + } +} diff --git a/src/main/java/com/goi/erp/controller/ingest/ExtSamsaraInspectionIngestController.java b/src/main/java/com/goi/erp/controller/ingest/ExtSamsaraInspectionIngestController.java new file mode 100644 index 0000000..cde05d1 --- /dev/null +++ b/src/main/java/com/goi/erp/controller/ingest/ExtSamsaraInspectionIngestController.java @@ -0,0 +1,58 @@ +package com.goi.erp.controller.ingest; + +import com.goi.erp.dto.ExtApiErrorResponse; +import com.goi.erp.dto.ExtIngestResult; +import com.goi.erp.dto.ExtSamsaraInspectionIngestCommand; +import com.goi.erp.service.ExtSamsaraInspectionIngestService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/ext/samsara/inspections") +@RequiredArgsConstructor +public class ExtSamsaraInspectionIngestController { + + private final ExtSamsaraInspectionIngestService ingestService; + + /** + * Samsara DVIR raw ingest endpoint + * Called by integration-service + */ + @PostMapping("/ingest") + public ResponseEntity ingest( + @RequestBody ExtSamsaraInspectionIngestCommand command + ) { + try { + ExtIngestResult result = ingestService.ingest(command); + return ResponseEntity.ok(result); + + } catch (Exception e) { + + log.error( + "[SAMSARA_DVIR][INGEST_ERROR] records={}, error={}", + command.getRecords() != null ? command.getRecords().size() : 0, + e.getMessage(), + e + ); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body( + ExtApiErrorResponse.builder() + .service("opr-rest-api") + .errorCode("SAMSARA_DVIR_INGEST_FAILED") + .message("Failed to ingest Samsara DVIR data") + .detail(e.getMessage()) + .timestamp(LocalDateTime.now()) + .build() + ); + } + } +} diff --git a/src/main/java/com/goi/erp/dto/ExtApiErrorResponse.java b/src/main/java/com/goi/erp/dto/ExtApiErrorResponse.java new file mode 100644 index 0000000..7520f8f --- /dev/null +++ b/src/main/java/com/goi/erp/dto/ExtApiErrorResponse.java @@ -0,0 +1,16 @@ +package com.goi.erp.dto; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ExtApiErrorResponse { + private String service; // opr-rest-api + private String errorCode; // 예: INGEST_FAILED + private String message; // 사람이 읽는 메시지 + private String detail; // stack / 원인 + private LocalDateTime timestamp; +} diff --git a/src/main/java/com/goi/erp/dto/ExtIngestResult.java b/src/main/java/com/goi/erp/dto/ExtIngestResult.java new file mode 100644 index 0000000..15568df --- /dev/null +++ b/src/main/java/com/goi/erp/dto/ExtIngestResult.java @@ -0,0 +1,19 @@ +package com.goi.erp.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; +} diff --git a/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionIngestCommand.java b/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionIngestCommand.java new file mode 100644 index 0000000..1c0fb90 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionIngestCommand.java @@ -0,0 +1,20 @@ +package com.goi.erp.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 records; +} diff --git a/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionRecordDto.java b/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionRecordDto.java new file mode 100644 index 0000000..39423d7 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/ExtSamsaraInspectionRecordDto.java @@ -0,0 +1,26 @@ +package com.goi.erp.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; + private String vehicleExternalId; + private String driverExternalId; + private String inspectionType; + private LocalDateTime startTime; + private LocalDateTime endTime; + private LocalDateTime signedAt; + private String payloadHash; + private JsonNode payloadJson; +} diff --git a/src/main/java/com/goi/erp/dto/VehicleDispatchRequestDto.java b/src/main/java/com/goi/erp/dto/VehicleDispatchRequestDto.java new file mode 100644 index 0000000..748a15a --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleDispatchRequestDto.java @@ -0,0 +1,35 @@ +package com.goi.erp.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class VehicleDispatchRequestDto { + + private Long vedVehId; + private Long vedDriverId; + private Long vedSubDriverId; + private String vedExternalVehicleId; // SAMSARA vehicle id + private String vedExternalDriverId; + private LocalDate vedDispatchDate; + private Integer vedShift; + private String vedStatus; + private LocalDateTime vedStartAt; + private LocalDateTime vedPausedAt; + private LocalDateTime vedEndAt; + private String vedEndReason; + private BigDecimal vedOdometerStart; + private BigDecimal vedOdometerEnd; + private BigDecimal vedOdometerIncrement; + private String vedOdometerSource; // obd / gpsDistance / gpsOdometer + private String vedSource; // AUTO / MANUAL + private Integer vedEventCount; + + // inspection 연계용 (AUTO 생성 시 사용) + private Long vedInspectionId; + private UUID vedInspectionUuid; +} diff --git a/src/main/java/com/goi/erp/dto/VehicleDispatchResponseDto.java b/src/main/java/com/goi/erp/dto/VehicleDispatchResponseDto.java new file mode 100644 index 0000000..bb0d74e --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleDispatchResponseDto.java @@ -0,0 +1,44 @@ +package com.goi.erp.dto; + +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Builder +public class VehicleDispatchResponseDto { + + // 식별자 + private UUID vedUuid; + + // 차량 정보 (View용) + private String vehicleNo; + private String vehicleName; + private String licensePlate; + + // 운전자 정보 + private String driverName; + private String driverEmpNo; + private String subDriverName; + + // 배차 정보 + private String vedStatus; + private LocalDate vedDispatchDate; + private Integer vedShift; + private LocalDateTime vedStartAt; + private LocalDateTime vedPausedAt; + private LocalDateTime vedEndAt; + private String vedEndReason; + + private BigDecimal vedOdometerStart; + private BigDecimal vedOdometerEnd; + private BigDecimal vedOdometerIncrement; + private String vedOdometerSource; + + private Integer vedEventCount; + private String vedSource; +} diff --git a/src/main/java/com/goi/erp/dto/VehicleExternalMapRequestDto.java b/src/main/java/com/goi/erp/dto/VehicleExternalMapRequestDto.java new file mode 100644 index 0000000..3705e94 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleExternalMapRequestDto.java @@ -0,0 +1,11 @@ +package com.goi.erp.dto; + +import lombok.Data; + +@Data +public class VehicleExternalMapRequestDto { + private Long vexVehicleId; // 매핑할 내부 vehicle ID + private String vexSolutionType; // 'SAMSARA', 'MIS', ... + private String vexExternalId; // 외부 ID + private String vexStatus; // A / I (옵션) +} diff --git a/src/main/java/com/goi/erp/dto/VehicleExternalMapResponseDto.java b/src/main/java/com/goi/erp/dto/VehicleExternalMapResponseDto.java new file mode 100644 index 0000000..376a1ef --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleExternalMapResponseDto.java @@ -0,0 +1,20 @@ +package com.goi.erp.dto; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class VehicleExternalMapResponseDto { + + private UUID vexUuid; + private Long vexVehicleId; + + private String vexSolutionType; + private String vexExternalId; + + private String vexStatus; + + private String vexCreatedBy; + private String vexUpdatedBy; +} diff --git a/src/main/java/com/goi/erp/dto/VehicleInspectionDefectDto.java b/src/main/java/com/goi/erp/dto/VehicleInspectionDefectDto.java new file mode 100644 index 0000000..cb3177e --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleInspectionDefectDto.java @@ -0,0 +1,24 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VehicleInspectionDefectDto { + + private String defectType; + private String comment; + private Boolean isResolved; + private Long resolvedBy; + private String resolvedByExternalId; + private LocalDateTime resolvedAt; + private String vidExternalDefectId; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/goi/erp/dto/VehicleInspectionDto.java b/src/main/java/com/goi/erp/dto/VehicleInspectionDto.java new file mode 100644 index 0000000..4a6b65e --- /dev/null +++ b/src/main/java/com/goi/erp/dto/VehicleInspectionDto.java @@ -0,0 +1,42 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VehicleInspectionDto { + + private Long vehId; + private String externalVehicleId; + private UUID vehicleUuid; + private Long driverId; + private UUID driverUuid; + private UUID subDriverUuid; + private Long subDriverId; + private String externalDriverId; + private String externalSubDriverId; + + private LocalDate inspectionDate; + private String inspectionType; // preTrip / postTrip + private LocalDateTime startAt; + private LocalDateTime endAt; + + private String result; // safe / unsafe + private Long odometer; + private Boolean hasDefect; + + private String source; // SAMSARA / PAPER + private Long sourceId; + + private List defects; +} diff --git a/src/main/java/com/goi/erp/entity/ExtSamsaraRawInspection.java b/src/main/java/com/goi/erp/entity/ExtSamsaraRawInspection.java new file mode 100644 index 0000000..4d77c7c --- /dev/null +++ b/src/main/java/com/goi/erp/entity/ExtSamsaraRawInspection.java @@ -0,0 +1,99 @@ +package com.goi.erp.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table( + name = "ext_samsara_raw_inspection", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_ext_samsara_raw_inspection_src_extid", + columnNames = {"esri_source", "esri_external_id"} + ) + } +) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class ExtSamsaraRawInspection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "esri_id") + private Long esriId; + + @Column(name = "esri_source", nullable = false, length = 20) + private String esriSource; // SAMSARA + + @Column(name = "esri_record_type", nullable = false, length = 20) + private String esriRecordType; // DVIR + + @Column(name = "esri_external_id", nullable = false, length = 50) + private String esriExternalId; // inspection id + + @Column(name = "esri_vehicle_ext_id", length = 50) + private String esriVehicleExtId; + + @Column(name = "esri_driver_ext_id", length = 50) + private String esriDriverExtId; + + @Column(name = "esri_start_time") + private LocalDateTime esriStartTime; + + @Column(name = "esri_end_time") + private LocalDateTime esriEndTime; + + @Column(name = "esri_signed_at") + private LocalDateTime esriSignedAt; + + @Column(name = "esri_inspection_type", length = 20) + private String esriInspectionType; // preTrip / postTrip + + @Column(name = "esri_hash", nullable = false, length = 64) + private String esriHash; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name="esri_payload", columnDefinition = "jsonb", nullable = false) + private JsonNode esriPayload; + + @Column(name = "esri_fetched_at", nullable = false) + private LocalDateTime esriFetchedAt; + + @Column( + name = "esri_fetched_date", + insertable = false, + updatable = false + ) + private LocalDate esriFetchedDate; + + @Column(name = "esri_processed") + private Boolean esriProcessed; + + @Column(name = "esri_processed_at") + private LocalDateTime esriProcessedAt; +} + diff --git a/src/main/java/com/goi/erp/entity/VehicleDispatch.java b/src/main/java/com/goi/erp/entity/VehicleDispatch.java new file mode 100644 index 0000000..cb2483e --- /dev/null +++ b/src/main/java/com/goi/erp/entity/VehicleDispatch.java @@ -0,0 +1,107 @@ +package com.goi.erp.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table( + name = "vehicle_dispatch", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_vehicle_dispatch_shift", + columnNames = {"ved_veh_id", "ved_dispatch_date", "ved_shift"} + ) + } +) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class VehicleDispatch { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ved_id") + private Long vedId; + + @Column(name = "ved_uuid", unique = true) + private UUID vedUuid; + + @Column(name = "ved_status", length = 10) + private String vedStatus; // O: Open | C: Closed | P: Paused + + @Column(name = "ved_veh_id") + private Long vedVehId; + + @Column(name = "ved_driver_id") + private Long vedDriverId; + + @Column(name = "ved_sub_driver_id") + private Long vedSubDriverId; + + @Column(name = "ved_dispatch_date", nullable = false) + private LocalDate vedDispatchDate; + + @Column(name = "ved_shift") + private Integer vedShift; + + @Column(name = "ved_start_at") + private LocalDateTime vedStartAt; + + @Column(name = "ved_paused_at") + private LocalDateTime vedPausedAt; + + @Column(name = "ved_end_at") + private LocalDateTime vedEndAt; + + @Column(name = "ved_end_reason") + private String vedEndReason; + + @Column(name = "ved_odometer_start", precision = 12, scale = 2) + private BigDecimal vedOdometerStart; + + @Column(name = "ved_odometer_end", precision = 12, scale = 2) + private BigDecimal vedOdometerEnd; + + @Column(name = "ved_odometer_increment", precision = 12, scale = 2) + private BigDecimal vedOdometerIncrement; + + @Column(name = "ved_odometer_source", length = 20) + private String vedOdometerSource; // obd / gpsDistance / gpsOdometer + + @Column(name = "ved_event_count") + private Integer vedEventCount; + + @Column(name = "ved_source", length = 20) + private String vedSource; // AUTO / MANUAL + + @Column(name = "ved_created_at") + private LocalDateTime vedCreatedAt; + + @Column(name = "ved_created_by", length = 50) + private String vedCreatedBy; + + @Column(name = "ved_updated_at") + private LocalDateTime vedUpdatedAt; + + @Column(name = "ved_updated_by", length = 50) + private String vedUpdatedBy; +} diff --git a/src/main/java/com/goi/erp/entity/VehicleExternalMap.java b/src/main/java/com/goi/erp/entity/VehicleExternalMap.java new file mode 100644 index 0000000..3638249 --- /dev/null +++ b/src/main/java/com/goi/erp/entity/VehicleExternalMap.java @@ -0,0 +1,66 @@ +package com.goi.erp.entity; + +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class VehicleExternalMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "vex_id") + private Long vexId; + + @Column(name = "vex_uuid", nullable = false) + private UUID vexUuid; + + @Column(name = "vex_vehicle_id", nullable = false) + private Long vexVehicleId; + + @Column(name = "vex_solution_type", nullable = false, length = 50) + private String vexSolutionType; + + @Column(name = "vex_external_id", nullable = false, length = 100) + private String vexExternalId; + + @Column(name = "vex_status", length = 1) + private String vexStatus; // A / I + + @CreatedDate + @Column(name = "vex_created_at", updatable = false) + private LocalDateTime vexCreatedAt; + + @CreatedBy + @Column(name = "vex_created_by") + private String vexCreatedBy; + + @LastModifiedDate + @Column(name = "vex_updated_at") + private LocalDateTime vexUpdatedAt; + + @LastModifiedBy + @Column(name = "vex_updated_by") + private String vexUpdatedBy; +} diff --git a/src/main/java/com/goi/erp/entity/VehicleInspection.java b/src/main/java/com/goi/erp/entity/VehicleInspection.java new file mode 100644 index 0000000..f5a329f --- /dev/null +++ b/src/main/java/com/goi/erp/entity/VehicleInspection.java @@ -0,0 +1,88 @@ +package com.goi.erp.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table( + name = "vehicle_inspection", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_vehicle_inspection_source", + columnNames = {"vei_source", "vei_source_id"} + ) + } +) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class VehicleInspection { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "vei_id") + private Long veiId; + + @Column(name = "vei_uuid", unique = true) + private UUID veiUuid; + + @Column(name = "vei_veh_id") + private Long veiVehId; + + @Column(name = "vei_driver_id") + private Long veiDriverId; + + @Column(name = "vei_sub_driver_id") + private Long veiSubDriverId; + + @Column(name = "vei_inspection_date", nullable = false) + private LocalDate veiInspectionDate; + + @Column(name = "vei_inspection_type", length = 20) + private String veiInspectionType; // preTrip / postTrip + + @Column(name = "vei_start_at") + private LocalDateTime veiStartAt; + + @Column(name = "vei_end_at") + private LocalDateTime veiEndAt; + + @Column(name = "vei_result", length = 20) + private String veiResult; // safe / unsafe + + @Column(name = "vei_odometer") + private Long veiOdometer; + + @Column(name = "vei_has_defect") + private Boolean veiHasDefect; + + @Column(name = "vei_source", length = 20) + private String veiSource; // SAMSARA / PAPER + + @Column(name = "vei_source_id") + private Long veiSourceId; // external id or self id + + @Column(name = "vei_created_at") + private LocalDateTime veiCreatedAt; + + @Column(name = "vei_dispatch_id") + private Long veiDispatchId; +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/entity/VehicleInspectionDefect.java b/src/main/java/com/goi/erp/entity/VehicleInspectionDefect.java new file mode 100644 index 0000000..445dc85 --- /dev/null +++ b/src/main/java/com/goi/erp/entity/VehicleInspectionDefect.java @@ -0,0 +1,56 @@ +package com.goi.erp.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "vehicle_inspection_defect") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class VehicleInspectionDefect { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "vid_id") + private Long vidId; + + @Column(name = "vid_vei_id", nullable = false) + private Long vidVeiId; // vehicle_inspection.vei_id + + @Column(name = "vid_defect_type", length = 50) + private String vidDefectType; + + @Column(name = "vid_comment") + private String vidComment; + + @Column(name = "vid_external_defect_id") + private String vidExternalDefectId; + + @Column(name = "vid_is_resolved") + private Boolean vidIsResolved; + + @Column(name = "vid_resolved_by") + private Long vidResolvedBy; + + @Column(name = "vid_resolved_at") + private LocalDateTime vidResolvedAt; + + @Column(name = "vid_created_at") + private LocalDateTime vidCreatedAt; +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/ExtSamsaraRawInspectionRepository.java b/src/main/java/com/goi/erp/repository/ExtSamsaraRawInspectionRepository.java new file mode 100644 index 0000000..208239a --- /dev/null +++ b/src/main/java/com/goi/erp/repository/ExtSamsaraRawInspectionRepository.java @@ -0,0 +1,44 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.ExtSamsaraRawInspection; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface ExtSamsaraRawInspectionRepository + extends JpaRepository { + + /** + * idempotent ingest 용 + * (source + externalId 는 UNIQUE) + */ + Optional findByEsriSourceAndEsriExternalId( + String esriSource, + String esriExternalId + ); + + /** + * 아직 처리되지 않은 raw inspection 조회 + */ + List findByEsriProcessedFalse(); + + /** + * 특정 날짜에 수집된 raw inspection 중 미처리 데이터 + * (scheduler / batch parsing 용) + */ + List findByEsriProcessedFalseAndEsriFetchedDate( + LocalDate esriFetchedDate + ); + + /** + * 특정 차량의 inspection 이력 조회 (디버깅/분석용) + */ + List findByEsriVehicleExtId( + String esriVehicleExtId + ); +} diff --git a/src/main/java/com/goi/erp/repository/VehicleDispatchRepository.java b/src/main/java/com/goi/erp/repository/VehicleDispatchRepository.java new file mode 100644 index 0000000..0e7c3af --- /dev/null +++ b/src/main/java/com/goi/erp/repository/VehicleDispatchRepository.java @@ -0,0 +1,57 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.VehicleDispatch; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface VehicleDispatchRepository extends JpaRepository { + + List findByVedVehIdAndVedDispatchDate(Long vedVehId, LocalDate vedDispatchDate); + List findByVedDriverIdAndVedDispatchDate(Long vedDriverId, LocalDate vedDispatchDate); + List findByVedDispatchDate(LocalDate vedDispatchDate); + Page findAll(Pageable pageable); + + Optional findByVedUuid(UUID vedUuid); + + // + @Query(""" + SELECT d + FROM VehicleDispatch d + WHERE d.vedVehId = :vehId + AND d.vedStatus IN ('O','P') + """) + Optional findOpenDispatchByVehId(Long vehId); + + // + @Query(""" + SELECT d + FROM VehicleDispatch d + WHERE d.vedStatus = 'P' + AND d.vedPausedAt <= :threshold + """) + List findPausedBefore(LocalDateTime threshold); + + // + @Query(""" + SELECT COALESCE(MAX(d.vedShift), -1) + FROM VehicleDispatch d + WHERE d.vedVehId = :vehId + AND d.vedDispatchDate = :date + """) + Integer findMaxShift(Long vehId, LocalDate date); + + // + default Integer findNextShift(Long vehId, LocalDate date) { + Integer max = findMaxShift(vehId, date); + return max == null ? 0 : max + 1; + } +} diff --git a/src/main/java/com/goi/erp/repository/VehicleExternalMapRepository.java b/src/main/java/com/goi/erp/repository/VehicleExternalMapRepository.java new file mode 100644 index 0000000..2d50fb8 --- /dev/null +++ b/src/main/java/com/goi/erp/repository/VehicleExternalMapRepository.java @@ -0,0 +1,17 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.VehicleExternalMap; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface VehicleExternalMapRepository extends JpaRepository { + + Optional findByVexSolutionTypeAndVexExternalIdAndVexStatus( + String vexSolutionType, + String vexExternalId, + String vexStatus + ); +} diff --git a/src/main/java/com/goi/erp/repository/VehicleInspectionDefectRepository.java b/src/main/java/com/goi/erp/repository/VehicleInspectionDefectRepository.java new file mode 100644 index 0000000..8d9841e --- /dev/null +++ b/src/main/java/com/goi/erp/repository/VehicleInspectionDefectRepository.java @@ -0,0 +1,18 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.VehicleInspectionDefect; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface VehicleInspectionDefectRepository extends JpaRepository { + + List findByVidVeiId(Long vidVeiId); + List findByVidVeiIdAndVidIsResolvedFalse(Long vidVeiId); + + Optional findByVidVeiIdAndVidExternalDefectId(Long veiId, String vidExternalDefectId); +} diff --git a/src/main/java/com/goi/erp/repository/VehicleInspectionRepository.java b/src/main/java/com/goi/erp/repository/VehicleInspectionRepository.java new file mode 100644 index 0000000..a98bc30 --- /dev/null +++ b/src/main/java/com/goi/erp/repository/VehicleInspectionRepository.java @@ -0,0 +1,16 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.VehicleInspection; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.Optional; + +@Repository +public interface VehicleInspectionRepository extends JpaRepository { + + Optional findByVeiSourceAndVeiSourceId(String veiSource, Long veiSourceId); + Optional findByVeiVehIdAndVeiInspectionDate(Long veiVehId, LocalDate veiInspectionDate); +} diff --git a/src/main/java/com/goi/erp/service/ExtSamsaraInspectionIngestService.java b/src/main/java/com/goi/erp/service/ExtSamsaraInspectionIngestService.java new file mode 100644 index 0000000..4d13745 --- /dev/null +++ b/src/main/java/com/goi/erp/service/ExtSamsaraInspectionIngestService.java @@ -0,0 +1,139 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.ExtIngestResult; +import com.goi.erp.dto.ExtSamsaraInspectionIngestCommand; +import com.goi.erp.dto.ExtSamsaraInspectionRecordDto; +import com.goi.erp.entity.ExtSamsaraRawInspection; +import com.goi.erp.repository.ExtSamsaraRawInspectionRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ExtSamsaraInspectionIngestService { + + private final ExtSamsaraRawInspectionRepository repository; + + /** + * Ingest entry point + */ + @Transactional + public ExtIngestResult ingest(ExtSamsaraInspectionIngestCommand command) { + + int inserted = 0; + int updated = 0; + int skipped = 0; + @SuppressWarnings("unused") + int failed = 0; + + for (ExtSamsaraInspectionRecordDto record : command.getRecords()) { + + IngestAction action; + + try { + action = ingestSingle(command, record); + } catch (Exception e) { + failed++; + continue; + } + + switch (action) { + case INSERTED -> inserted++; + case UPDATED -> updated++; + case SKIPPED -> skipped++; + case FAILED -> failed++; + } + } + + return ExtIngestResult.builder() + .source(command.getSource()) + .recordType(command.getRecordType()) + .received(command.getRecords().size()) + .inserted(inserted) + .updated(updated) + .skipped(skipped) + // .failed(failed) // 필요하면 추후 추가 + .build(); + } + + /** + * Single record ingest (idempotent) + */ + private IngestAction ingestSingle( + ExtSamsaraInspectionIngestCommand command, + ExtSamsaraInspectionRecordDto record + ) { + + return repository + .findByEsriSourceAndEsriExternalId( + command.getSource(), + record.getExternalId() + ) + .map(existing -> updateIfChanged(existing, command, record)) + .orElseGet(() -> { + insertNew(command, record); + return IngestAction.INSERTED; + }); + } + + + /** + * Insert new raw inspection + */ + private void insertNew( + ExtSamsaraInspectionIngestCommand command, + ExtSamsaraInspectionRecordDto record + ) { + + ExtSamsaraRawInspection entity = ExtSamsaraRawInspection.builder() + .esriSource(command.getSource()) + .esriRecordType(command.getRecordType()) + .esriExternalId(record.getExternalId()) + .esriVehicleExtId(record.getVehicleExternalId()) + .esriDriverExtId(record.getDriverExternalId()) + .esriInspectionType(record.getInspectionType()) + .esriStartTime(record.getStartTime()) + .esriEndTime(record.getEndTime()) + .esriSignedAt(record.getSignedAt()) + .esriHash(record.getPayloadHash()) + .esriFetchedAt(command.getFetchedAt()) + .esriProcessed(false) + .esriPayload(record.getPayloadJson()) + .build(); + + repository.save(entity); + } + + /** + * Update only if hash changed + */ + private IngestAction updateIfChanged( + ExtSamsaraRawInspection existing, + ExtSamsaraInspectionIngestCommand command, + ExtSamsaraInspectionRecordDto record + ) { + + if (existing.getEsriHash().equals(record.getPayloadHash())) { + return IngestAction.SKIPPED; + } + + existing.setEsriVehicleExtId(record.getVehicleExternalId()); + existing.setEsriDriverExtId(record.getDriverExternalId()); + existing.setEsriInspectionType(record.getInspectionType()); + existing.setEsriStartTime(record.getStartTime()); + existing.setEsriEndTime(record.getEndTime()); + existing.setEsriSignedAt(record.getSignedAt()); + existing.setEsriHash(record.getPayloadHash()); + existing.setEsriFetchedAt(command.getFetchedAt()); + existing.setEsriPayload(record.getPayloadJson()); + existing.setEsriProcessed(false); + existing.setEsriProcessedAt(null); + + repository.save(existing); + return IngestAction.UPDATED; + } + +} diff --git a/src/main/java/com/goi/erp/service/ExtSamsaraInspectionProcessor.java b/src/main/java/com/goi/erp/service/ExtSamsaraInspectionProcessor.java new file mode 100644 index 0000000..85f7aa8 --- /dev/null +++ b/src/main/java/com/goi/erp/service/ExtSamsaraInspectionProcessor.java @@ -0,0 +1,130 @@ +package com.goi.erp.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.goi.erp.dto.VehicleInspectionDefectDto; +import com.goi.erp.dto.VehicleInspectionDto; +import com.goi.erp.entity.ExtSamsaraRawInspection; +import com.goi.erp.repository.ExtSamsaraRawInspectionRepository; +import com.goi.erp.common.util.DateTimeUtil; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ExtSamsaraInspectionProcessor { + + private final ExtSamsaraRawInspectionRepository rawRepo; + private final VehicleInspectionService vehicleInspectionService; + + @Transactional + public void processUnprocessed() { + + List raws = rawRepo.findByEsriProcessedFalse(); + + if (raws.isEmpty()) { + log.info("[DVIR_PROCESSOR] no unprocessed raw inspections"); + return; + } + // log 용 + int processed = 0; + int failed = 0; + + for (ExtSamsaraRawInspection raw : raws) { + try { + processSingle(raw); + processed++; + } catch (Exception e) { + failed++; + log.error( + "[DVIR_PROCESSOR] failed rawId={} externalId={} msg={}", + raw.getEsriId(), + raw.getEsriExternalId(), + e.getMessage(), + e + ); + } + } + + log.info("[DVIR_PROCESSOR] done processed={} failed={}", processed, failed); + } + + private void processSingle(ExtSamsaraRawInspection raw) { + + JsonNode payload = raw.getEsriPayload(); + + // Inspection + VehicleInspectionDto dto = new VehicleInspectionDto(); + dto.setSource("SAMSARA"); + dto.setSourceId(Long.valueOf(raw.getEsriExternalId())); + dto.setExternalVehicleId(raw.getEsriVehicleExtId()); + dto.setExternalDriverId(raw.getEsriDriverExtId()); + dto.setInspectionType(raw.getEsriInspectionType()); + + dto.setInspectionDate( + raw.getEsriStartTime() != null + ? raw.getEsriStartTime().toLocalDate() + : raw.getEsriFetchedAt().toLocalDate() + ); + + dto.setStartAt(raw.getEsriStartTime()); + dto.setEndAt(raw.getEsriEndTime()); + dto.setResult(payload.path("safetyStatus").asText(null)); + dto.setOdometer(payload.path("odometerMeters").asLong(0L)); + + log.info( + "[DVIR] rawId={} odometerMeters node={} value={}", + raw.getEsriExternalId(), + payload.get("odometerMeters"), + payload.path("odometerMeters").asLong(-1) + ); + + // Defects + JsonNode defectsNode = payload.path("vehicleDefects"); + List defectDtos = new ArrayList<>(); + + if (defectsNode.isArray()) { + for (JsonNode d : defectsNode) { + VehicleInspectionDefectDto defectDto = new VehicleInspectionDefectDto(); + defectDto.setVidExternalDefectId(d.path("id").asText(null)); + defectDto.setDefectType(d.path("defectType").asText(null)); + defectDto.setComment(d.path("comment").asText(null)); + defectDto.setIsResolved(d.path("isResolved").asBoolean(false)); + JsonNode resolvedBy = d.path("resolvedBy"); + if (!resolvedBy.isMissingNode()) { + defectDto.setResolvedByExternalId(resolvedBy.path("id").asText(null)); + } + defectDto.setResolvedAt(DateTimeUtil.parse(d.path("resolvedAtTime"))); + defectDto.setCreatedAt(DateTimeUtil.parse(d.path("createdAtTime"))); + defectDtos.add(defectDto); + } + } + dto.setDefects(defectDtos); + dto.setHasDefect(!defectDtos.isEmpty()); + + log.info( + "[DVIR DTO] sourceId={} result={} odometer={} hasDefect={} defects={}", + dto.getSourceId(), + dto.getResult(), + dto.getOdometer(), + dto.getHasDefect(), + dto.getDefects() != null ? dto.getDefects().size() : null + ); + + // inspection + defect 저장은 Service 에서 + vehicleInspectionService.saveInspection(dto); + + // raw 처리 완료 + raw.setEsriProcessed(true); + raw.setEsriProcessedAt(LocalDateTime.now()); + rawRepo.save(raw); + } +} diff --git a/src/main/java/com/goi/erp/service/HcmEmployeeClient.java b/src/main/java/com/goi/erp/service/HcmEmployeeClient.java index d07adb7..2a1aead 100644 --- a/src/main/java/com/goi/erp/service/HcmEmployeeClient.java +++ b/src/main/java/com/goi/erp/service/HcmEmployeeClient.java @@ -25,9 +25,9 @@ public class HcmEmployeeClient { @Value("${hcm.api.base-url}") private String hcmBaseUrl; - public Long getEmpIdFromExternalId(String externalId) { + public Long getEmpIdFromExternalId(String externalSource, String externalId) { - String url = hcmBaseUrl + "/employee/external" + "?solutionType=MIS&externalId=" + externalId; + String url = hcmBaseUrl + "/employee/external" + "?solutionType=" + externalSource + "&externalId=" + externalId; try { // set token in header diff --git a/src/main/java/com/goi/erp/service/IngestAction.java b/src/main/java/com/goi/erp/service/IngestAction.java new file mode 100644 index 0000000..4bf2e27 --- /dev/null +++ b/src/main/java/com/goi/erp/service/IngestAction.java @@ -0,0 +1,8 @@ +package com.goi.erp.service; + +public enum IngestAction { + INSERTED, + UPDATED, + SKIPPED, + FAILED +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/service/VehicleDispatchService.java b/src/main/java/com/goi/erp/service/VehicleDispatchService.java new file mode 100644 index 0000000..18ebc2f --- /dev/null +++ b/src/main/java/com/goi/erp/service/VehicleDispatchService.java @@ -0,0 +1,275 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.VehicleDispatchRequestDto; +import com.goi.erp.entity.VehicleDispatch; +import com.goi.erp.entity.VehicleInspection; +import com.goi.erp.repository.VehicleDispatchRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class VehicleDispatchService { + + private final VehicleDispatchRepository dispatchRepository; + + /* =============================== + * CREATE + * =============================== */ + public VehicleDispatch createFromPreTripInspection( + Long vehId, + Long driverId, + Long subDriverId, + LocalDate dispatchDate, + LocalDateTime startAt, + BigDecimal startOdometer, + String source + ) { + Integer nextShift = dispatchRepository.findNextShift(vehId, dispatchDate); + + VehicleDispatch dispatch = VehicleDispatch.builder() + .vedUuid(UUID.randomUUID()) + .vedVehId(vehId) + .vedDriverId(driverId) + .vedSubDriverId(subDriverId) + .vedDispatchDate(dispatchDate) + .vedShift(nextShift) + .vedStartAt(startAt) + .vedOdometerStart(startOdometer) + .vedStatus("O") + .vedSource(source != null ? source : "AUTO") + .build(); + + return dispatchRepository.save(dispatch); + } + + public VehicleDispatch createManualDispatch( + VehicleDispatchRequestDto dto, + String createdBy + ) { + VehicleDispatch opened = + dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId()) + .orElse(null); + + if (opened != null) return opened; + + Integer nextShift = + dispatchRepository.findNextShift(dto.getVedVehId(), dto.getVedDispatchDate()); + + VehicleDispatch dispatch = VehicleDispatch.builder() + .vedUuid(UUID.randomUUID()) + .vedVehId(dto.getVedVehId()) + .vedDriverId(dto.getVedDriverId()) + .vedSubDriverId(dto.getVedSubDriverId()) + .vedDispatchDate(dto.getVedDispatchDate()) + .vedShift(nextShift) + .vedStartAt(dto.getVedStartAt() != null ? dto.getVedStartAt() : LocalDateTime.now()) + .vedOdometerStart(dto.getVedOdometerStart()) + .vedOdometerSource(dto.getVedOdometerSource()) + .vedStatus("O") + .vedSource("MANUAL") + .vedCreatedBy(createdBy) + .build(); + + return dispatchRepository.save(dispatch); + } + + /* =============================== + * STATE TRANSITIONS + * =============================== */ + public void pauseDispatch(UUID dispatchUuid, LocalDateTime pausedAt) { + VehicleDispatch d = getOpen(dispatchUuid); + + d.setVedStatus("P"); + d.setVedPausedAt(pausedAt != null ? pausedAt : LocalDateTime.now()); + + dispatchRepository.save(d); + } + + public void closeDispatch( + UUID dispatchUuid, + LocalDateTime endAt, + BigDecimal endOdometer, + String reason + ) { + VehicleDispatch d = dispatchRepository.findByVedUuid(dispatchUuid) + .orElseThrow(() -> new IllegalArgumentException("dispatch not found")); + + if ("C".equals(d.getVedStatus())) { + log.debug( + "[DISPATCH][CLOSE] already closed. dispatchUuid={}", + dispatchUuid + ); + return; + } + + d.setVedStatus("C"); + d.setVedEndAt(endAt != null ? endAt : LocalDateTime.now()); + d.setVedEndReason(reason); + + if (endOdometer != null) { + d.setVedOdometerEnd(endOdometer); + + if (d.getVedOdometerStart() != null) { + d.setVedOdometerIncrement( + endOdometer.subtract(d.getVedOdometerStart()) + ); + } + } + + dispatchRepository.save(d); + } + + + /* =============================== + * INSPECTION 에 의한 배차 종료 + * =============================== */ + public void evaluateDispatchCloseByInspection(VehicleInspection inspection) { + + VehicleDispatch open = + dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId()) + .orElse(null); + + if (open == null) { + log.info( + "[DISPATCH][EVAL] no open dispatch. inspectionType={} vehId={} driverId={}", + inspection.getVeiInspectionType(), + inspection.getVeiVehId(), + inspection.getVeiDriverId() + ); + return; + } + + // Rule 1: PostTrip + if ("postTrip".equalsIgnoreCase(inspection.getVeiInspectionType())) { + closeDispatch( + open.getVedUuid(), + inspection.getVeiEndAt(), + inspection.getVeiOdometer() != null ? BigDecimal.valueOf(inspection.getVeiOdometer()): null, + "POST_TRIP_INSPECTION" + ); + log.info( + "[DISPATCH][CLOSE] by POST_TRIP inspection. dispatchUuid={} endAt={}", + open.getVedUuid(), + inspection.getVeiEndAt() + ); + return; + } + + // Rule 2: same vehicle, different driver, preTrip + if ("preTrip".equalsIgnoreCase(inspection.getVeiInspectionType()) + && !inspection.getVeiDriverId().equals(open.getVedDriverId())) { + + closeDispatch( + open.getVedUuid(), + inspection.getVeiStartAt(), + null, + "NEW_PRETRIP_SAME_VEHICLE" + ); + log.info( + "[DISPATCH][CLOSE] reason={} dispatchUuid={} inspectionType={}", + "NEW_PRETRIP_SAME_VEHICLE", + open.getVedUuid(), + inspection.getVeiInspectionType() + ); + return; + } + + // Rule 3: same driver, different vehicle, preTrip + if ("preTrip".equalsIgnoreCase(inspection.getVeiInspectionType()) + && inspection.getVeiDriverId().equals(open.getVedDriverId()) + && !inspection.getVeiVehId().equals(open.getVedVehId())) { + + closeDispatch( + open.getVedUuid(), + inspection.getVeiStartAt(), + null, + "NEW_PRETRIP_OTHER_VEHICLE" + ); + } + } + + /* =============================== + * JOB / 60분 RULE로 배차 종료 + * =============================== */ + public void closePausedDispatches(Duration threshold) { + LocalDateTime now = LocalDateTime.now(); + + List paused = + dispatchRepository.findPausedBefore(now.minus(threshold)); + + for (VehicleDispatch d : paused) { + closeDispatch( + d.getVedUuid(), + now, + null, + "ENGINE_OFF_60MIN" + ); + } + } + + /* =============================== + * MANUAL 배차 업데이트 + * =============================== */ + public VehicleDispatch patchManualDispatch( + UUID dispatchUuid, + VehicleDispatchRequestDto dto, + String updatedBy + ) { + VehicleDispatch d = dispatchRepository.findByVedUuid(dispatchUuid) + .orElseThrow(() -> new IllegalArgumentException("dispatch not found")); + + if ("C".equals(d.getVedStatus())) { + throw new IllegalStateException("closed dispatch cannot be modified"); + } + + if (dto.getVedDriverId() != null) d.setVedDriverId(dto.getVedDriverId()); + if (dto.getVedSubDriverId() != null) d.setVedSubDriverId(dto.getVedSubDriverId()); + if (dto.getVedStartAt() != null) d.setVedStartAt(dto.getVedStartAt()); + if (dto.getVedOdometerSource() != null) d.setVedOdometerSource(dto.getVedOdometerSource()); + if (dto.getVedEventCount() != null) d.setVedEventCount(dto.getVedEventCount()); + + // odometer + if (dto.getVedOdometerStart() != null) { + d.setVedOdometerStart(dto.getVedOdometerStart()); + } + if (dto.getVedOdometerEnd() != null) { + d.setVedOdometerEnd(dto.getVedOdometerEnd()); + + if (d.getVedOdometerStart() != null) { + d.setVedOdometerIncrement( + dto.getVedOdometerEnd().subtract(d.getVedOdometerStart()) + ); + } + } + + d.setVedUpdatedAt(LocalDateTime.now()); + d.setVedUpdatedBy(updatedBy); + + return dispatchRepository.save(d); + } + + + // + private VehicleDispatch getOpen(UUID uuid) { + VehicleDispatch d = dispatchRepository.findByVedUuid(uuid) + .orElseThrow(() -> new IllegalArgumentException("dispatch not found")); + + if (!"O".equals(d.getVedStatus())) { + throw new IllegalStateException("dispatch not open"); + } + return d; + } +} + diff --git a/src/main/java/com/goi/erp/service/VehicleExternalMapService.java b/src/main/java/com/goi/erp/service/VehicleExternalMapService.java new file mode 100644 index 0000000..88ef9ca --- /dev/null +++ b/src/main/java/com/goi/erp/service/VehicleExternalMapService.java @@ -0,0 +1,46 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.VehicleExternalMapResponseDto; +import com.goi.erp.entity.VehicleExternalMap; +import com.goi.erp.repository.VehicleExternalMapRepository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class VehicleExternalMapService { + + private final VehicleExternalMapRepository externalMapRepository; + + /** + * 외부 솔루션의 ID로 내부 employee 매핑 정보 조회 + * 목적: customer_daily_order.driver_id 에 들어갈 내부 emp_id 조회 + */ + @Transactional(readOnly = true) + public VehicleExternalMapResponseDto findMapping(String solutionType, String externalId) { + + VehicleExternalMap map = externalMapRepository + .findByVexSolutionTypeAndVexExternalIdAndVexStatus( + solutionType, + externalId, + "A" // 활성 매핑만 사용 + ) + .orElseThrow(() -> + new RuntimeException("Vehicle external mapping not found for externalId: " + externalId) + ); + + // Entity → ResponseDto 변환 + VehicleExternalMapResponseDto dto = new VehicleExternalMapResponseDto(); + dto.setVexUuid(map.getVexUuid()); + dto.setVexVehicleId(map.getVexVehicleId()); + dto.setVexSolutionType(map.getVexSolutionType()); + dto.setVexExternalId(map.getVexExternalId()); + dto.setVexStatus(map.getVexStatus()); + dto.setVexCreatedBy(map.getVexCreatedBy()); + dto.setVexUpdatedBy(map.getVexUpdatedBy()); + + return dto; + } +} diff --git a/src/main/java/com/goi/erp/service/VehicleInspectionService.java b/src/main/java/com/goi/erp/service/VehicleInspectionService.java new file mode 100644 index 0000000..65e02ce --- /dev/null +++ b/src/main/java/com/goi/erp/service/VehicleInspectionService.java @@ -0,0 +1,238 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.VehicleInspectionDefectDto; +import com.goi.erp.dto.VehicleInspectionDto; +import com.goi.erp.entity.VehicleDispatch; +import com.goi.erp.entity.VehicleInspection; +import com.goi.erp.entity.VehicleInspectionDefect; +import com.goi.erp.repository.VehicleDispatchRepository; +import com.goi.erp.repository.VehicleInspectionDefectRepository; +import com.goi.erp.repository.VehicleInspectionRepository; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class VehicleInspectionService { + + private final VehicleInspectionRepository inspectionRepository; + private final VehicleInspectionDefectRepository defectRepository; + private final VehicleDispatchRepository dispatchRepository; + private final HcmEmployeeClient hcmEmployeeClient; + private final VehicleExternalMapService vehicleExternalMapService; + private final VehicleDispatchService vehicleDispatchService; + // private final VehicleService vehicleService; + + /** + * Create or Update Vehicle Inspection (idempotent) + * -> defects upsert + * -> evaluate dispatch close by inspection + * -> create dispatch (preTrip) if needed + */ + @Transactional + public VehicleInspection saveInspection(VehicleInspectionDto dto) { + VehicleInspection inspection = inspectionRepository + .findByVeiSourceAndVeiSourceId(dto.getSource(), dto.getSourceId()) + .orElseGet(VehicleInspection::new); + + // ===== ID resolve ===== + Long driverId = resolveDriverId(dto); + Long subDriverId = resolveSubDriverId(dto); + Long vehicleId = resolveVehicleId(dto); + + log.info( + "[INSPECTION] start source={} sourceId={} type={} vehId={} driverId={}", + dto.getSource(), + dto.getSourceId(), + dto.getInspectionType(), + vehicleId, + driverId + ); + + // ===== Inspection ===== + inspection.setVeiUuid(UUID.randomUUID()); + inspection.setVeiVehId(vehicleId); + inspection.setVeiDriverId(driverId); + inspection.setVeiSubDriverId(subDriverId); + inspection.setVeiInspectionDate(dto.getInspectionDate()); + inspection.setVeiInspectionType(dto.getInspectionType()); + inspection.setVeiStartAt(dto.getStartAt()); + inspection.setVeiEndAt(dto.getEndAt()); + inspection.setVeiResult(dto.getResult()); + inspection.setVeiOdometer(dto.getOdometer()); + inspection.setVeiHasDefect(dto.getHasDefect()); + inspection.setVeiSource(dto.getSource()); + inspection.setVeiSourceId(dto.getSourceId()); + + inspection = inspectionRepository.save(inspection); + + // ===== Defects ===== + if (dto.getDefects() != null) { + saveDefects(inspection.getVeiId(), dto.getDefects()); + } + + // ===== Dispatch close evaluation (Rule 1~3) ===== + // - postTrip이면 여기서 closeDispatch 호출됨 + // - preTrip new driver/vehicle 조건도 여기서 기존 open close됨 + vehicleDispatchService.evaluateDispatchCloseByInspection(inspection); + + // ===== Dispatch create (preTrip) ===== + // - 이미 open dispatch가 있으면 생성하지 않음 + // - odometerStart = inspection.odometer (가능하면) + if (shouldCreateDispatch(inspection)) { + // open 여부 + boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId()).isPresent(); + + if (!hasOpenDispatch) { + + BigDecimal odometerStart = + (inspection.getVeiOdometer() != null) + ? BigDecimal.valueOf(inspection.getVeiOdometer()) + : null; + + VehicleDispatch dispatch = + vehicleDispatchService.createFromPreTripInspection( + inspection.getVeiVehId(), + inspection.getVeiDriverId(), + inspection.getVeiSubDriverId(), + inspection.getVeiInspectionDate(), + inspection.getVeiStartAt(), + odometerStart, + "AUTO" + ); + + // inspection에 dispatch 연결 + inspection.setVeiDispatchId(dispatch.getVedId()); + inspection = inspectionRepository.save(inspection); + } else { + log.info( + "[DISPATCH][CREATE] skipped. open dispatch already exists. vehId={}", + inspection.getVeiVehId() + ); + } + } else { + log.debug( + "[DISPATCH][CREATE] skip. reason=shouldCreateDispatch=false inspectionId={}", + inspection.getVeiId() + ); + } + + return inspection; + } + + /** + * defects 저장 (externalDefectId 기준 upsert) + * - externalDefectId가 null이면 "수기"로 간주 → 그냥 매번 INSERT 하면 중복 가능성이 커짐 + * => 수기는 (veiId + defectType + comment + createdAt) 조합으로 찾아 + */ + private void saveDefects(Long veiId, List defects) { + // 없으면 + if (defects == null || defects.isEmpty()) return; + + // defect 별 + for (VehicleInspectionDefectDto d : defects) { + + VehicleInspectionDefect defect; + + // 외부 defect id가 있으면 그걸로 idempotent + if (d.getVidExternalDefectId() != null && !d.getVidExternalDefectId().isBlank()) { + defect = defectRepository + .findByVidVeiIdAndVidExternalDefectId(veiId, d.getVidExternalDefectId()) + .orElseGet(VehicleInspectionDefect::new); + } else { + // 수기 defect: 최소한의 중복 방지 (원하면 repository 메서드로 더 정교하게) + defect = new VehicleInspectionDefect(); + } + + defect.setVidVeiId(veiId); + defect.setVidExternalDefectId(d.getVidExternalDefectId()); + defect.setVidDefectType(d.getDefectType()); + defect.setVidComment(d.getComment()); + defect.setVidIsResolved(d.getIsResolved()); + defect.setVidCreatedAt(d.getCreatedAt()); + defect.setVidResolvedAt(d.getResolvedAt()); + defect.setVidResolvedBy(resolveEmpId(d)); + + defectRepository.save(defect); + } + } + + private Long resolveDriverId(VehicleInspectionDto dto) { + + if (dto.getDriverId() != null) return dto.getDriverId(); + + if (dto.getExternalDriverId() != null) { + return hcmEmployeeClient.getEmpIdFromExternalId( + "SAMSARA", + dto.getExternalDriverId() + ); + } + + if (dto.getDriverUuid() != null) { + return hcmEmployeeClient.getEmpIdFromUuid(dto.getDriverUuid()); + } + + return null; + } + + private Long resolveSubDriverId(VehicleInspectionDto dto) { + + if (dto.getSubDriverId() != null) return dto.getSubDriverId(); + + if (dto.getExternalSubDriverId() != null) { + return hcmEmployeeClient.getEmpIdFromExternalId( + "SAMSARA", + dto.getExternalSubDriverId() + ); + } + + if (dto.getSubDriverUuid() != null) { + return hcmEmployeeClient.getEmpIdFromUuid(dto.getSubDriverUuid()); + } + + return null; + } + + private Long resolveVehicleId(VehicleInspectionDto dto) { + + if (dto.getVehId() != null) return dto.getVehId(); + + if (dto.getExternalVehicleId() != null) { + return vehicleExternalMapService + .findMapping("SAMSARA", dto.getExternalVehicleId()) + .getVexVehicleId(); + } + + return null; + } + + private Long resolveEmpId(VehicleInspectionDefectDto dto) { + + if (dto.getResolvedBy() != null) return dto.getResolvedBy(); + + if (dto.getResolvedByExternalId() != null) { + return hcmEmployeeClient.getEmpIdFromExternalId( + "SAMSARA", + dto.getResolvedByExternalId() + ); + } + + return null; + } + + private boolean shouldCreateDispatch(VehicleInspection inspection) { + return "preTrip".equalsIgnoreCase(inspection.getVeiInspectionType()) + && inspection.getVeiDispatchId() == null + && inspection.getVeiVehId() != null + && inspection.getVeiDriverId() != null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 85c3fa1..1a444ff 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,4 +35,7 @@ server: # ================================ hcm: api: - base-url: http://localhost:8081/hcm-rest-api \ No newline at end of file + base-url: http://localhost:8081/hcm-rest-api +internal: + integration: + token: ${INTEGRATION_INTERNAL_TOKEN} \ No newline at end of file