diff --git a/src/main/java/com/shipping/freightops/controller/FreightOrderController.java b/src/main/java/com/shipping/freightops/controller/FreightOrderController.java index 7d0bdb6..99e7ffd 100644 --- a/src/main/java/com/shipping/freightops/controller/FreightOrderController.java +++ b/src/main/java/com/shipping/freightops/controller/FreightOrderController.java @@ -1,23 +1,26 @@ package com.shipping.freightops.controller; import com.itextpdf.text.DocumentException; -import com.shipping.freightops.dto.CreateFreightOrderRequest; -import com.shipping.freightops.dto.FreightOrderResponse; -import com.shipping.freightops.dto.PageResponse; -import com.shipping.freightops.dto.UpdateDiscountRequest; +import com.shipping.freightops.dto.*; import com.shipping.freightops.entity.FreightOrder; +import com.shipping.freightops.entity.TrackingEvent; +import com.shipping.freightops.repository.FreightOrderRepository; import com.shipping.freightops.service.FreightOrderService; import com.shipping.freightops.service.InvoiceService; +import com.shipping.freightops.service.TrackingEventService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import java.io.FileNotFoundException; import java.net.URI; +import java.time.LocalDateTime; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; /** @@ -32,10 +35,18 @@ public class FreightOrderController { private final FreightOrderService service; private final InvoiceService invoiceService; + private final FreightOrderRepository freightOrderRepository; + private final TrackingEventService trackingEventService; - public FreightOrderController(FreightOrderService service, InvoiceService invoiceService) { + public FreightOrderController( + FreightOrderService service, + InvoiceService invoiceService, + FreightOrderRepository freightOrderRepository, + TrackingEventService trackingEventService) { this.service = service; this.invoiceService = invoiceService; + this.freightOrderRepository = freightOrderRepository; + this.trackingEventService = trackingEventService; } /** Create a new freight order. */ @@ -108,4 +119,32 @@ public ResponseEntity updateDiscount( FreightOrder order = service.updateDiscount(id, request); return ResponseEntity.ok(FreightOrderResponse.fromEntity(order)); } + + @PostMapping("/{id}/events") + public ResponseEntity createEvent( + @Valid @RequestBody TrackingEventRequest eventRequest, @PathVariable Long id, Errors errors) { + if (errors.hasErrors()) { + throw new IllegalArgumentException("invalid data"); + } + TrackingEvent event = new TrackingEvent(); + FreightOrder order = + freightOrderRepository + .findById(id) + .orElseThrow(() -> new IllegalArgumentException("Order not found")); + event.setFreightOrder(order); + event.setDescription(eventRequest.getDescription()); + event.setEventTime(LocalDateTime.now()); + event.setEventType(eventRequest.getEventType()); + event.setLocation(eventRequest.getLocation()); + event.setDescription(eventRequest.getDescription()); + event.setPerformedBy(eventRequest.getPerformedBy()); + TrackingEvent savedEvent = trackingEventService.createEvent(event); + return ResponseEntity.ok().body(savedEvent); + } + + @GetMapping("/{id}/events") + public ResponseEntity> getAllEvents(@PathVariable Long id) { + List events = trackingEventService.getAllEventsByOrderId(id); + return ResponseEntity.ok().body(events); + } } diff --git a/src/main/java/com/shipping/freightops/dto/OrderTrackingResponse.java b/src/main/java/com/shipping/freightops/dto/OrderTrackingResponse.java index 5d19fbd..21a97c1 100644 --- a/src/main/java/com/shipping/freightops/dto/OrderTrackingResponse.java +++ b/src/main/java/com/shipping/freightops/dto/OrderTrackingResponse.java @@ -1,11 +1,14 @@ package com.shipping.freightops.dto; import com.shipping.freightops.entity.FreightOrder; +import com.shipping.freightops.entity.TrackingEvent; import com.shipping.freightops.enums.ContainerSize; import com.shipping.freightops.enums.ContainerType; import com.shipping.freightops.enums.OrderStatus; import com.shipping.freightops.enums.VoyageStatus; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; public class OrderTrackingResponse { private Long orderId; @@ -20,6 +23,7 @@ public class OrderTrackingResponse { private LocalDateTime departureTime; private LocalDateTime estimatedArrival; private VoyageStatus voyageStatus; + private List events = new ArrayList<>(); public static OrderTrackingResponse fromEntity(FreightOrder order) { OrderTrackingResponse dto = new OrderTrackingResponse(); @@ -28,6 +32,7 @@ public static OrderTrackingResponse fromEntity(FreightOrder order) { dto.ContainerCode = order.getContainer().getContainerCode(); dto.containerSize = order.getContainer().getSize(); dto.containerType = order.getContainer().getType(); + dto.events = order.getEvents(); if (order.getVoyage() != null) { dto.voyageNumber = order.getVoyage().getVoyageNumber(); dto.vesselName = order.getVoyage().getVessel().getName(); @@ -40,6 +45,14 @@ public static OrderTrackingResponse fromEntity(FreightOrder order) { return dto; } + public void setEvents(List events) { + this.events = events; + } + + public List getEvents() { + return events; + } + public String getVoyageNumber() { return voyageNumber; } diff --git a/src/main/java/com/shipping/freightops/dto/TrackingEventRequest.java b/src/main/java/com/shipping/freightops/dto/TrackingEventRequest.java new file mode 100644 index 0000000..dcfbd71 --- /dev/null +++ b/src/main/java/com/shipping/freightops/dto/TrackingEventRequest.java @@ -0,0 +1,57 @@ +package com.shipping.freightops.dto; + +import com.shipping.freightops.enums.EventType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class TrackingEventRequest { + @NotNull private EventType eventType; + + @NotBlank(message = "description is required") + private String description; + + private String location; + private String performedBy; + + public TrackingEventRequest() {} + + public TrackingEventRequest( + EventType eventType, String description, String location, String performedBy) { + this.eventType = eventType; + this.description = description; + this.location = location; + this.performedBy = performedBy; + } + + public EventType getEventType() { + return eventType; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getPerformedBy() { + return performedBy; + } + + public void setPerformedBy(String performedBy) { + this.performedBy = performedBy; + } +} diff --git a/src/main/java/com/shipping/freightops/entity/FreightOrder.java b/src/main/java/com/shipping/freightops/entity/FreightOrder.java index f7195f9..c671fcd 100644 --- a/src/main/java/com/shipping/freightops/entity/FreightOrder.java +++ b/src/main/java/com/shipping/freightops/entity/FreightOrder.java @@ -1,16 +1,11 @@ package com.shipping.freightops.entity; import com.shipping.freightops.enums.OrderStatus; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; +import jakarta.persistence.*; import jakarta.validation.constraints.*; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; /** A freight booking made by the internal ops team, assigning a container to a voyage. */ @Entity @@ -43,13 +38,8 @@ public class FreightOrder extends BaseEntity { @Column(nullable = false) private String orderedBy; - public String getOrderedBy() { - return orderedBy; - } - - public void setOrderedBy(String orderedBy) { - this.orderedBy = orderedBy; - } + @OneToMany(mappedBy = "freightOrder", cascade = CascadeType.ALL) + private List events = new ArrayList<>(); @Column(length = 500) private String notes; @@ -78,6 +68,24 @@ public void setOrderedBy(String orderedBy) { @Column(nullable = true, length = 500) private String discountReason; + public FreightOrder() {} + + public void setEvents(List events) { + this.events = events; + } + + public List getEvents() { + return events; + } + + public String getOrderedBy() { + return orderedBy; + } + + public void setOrderedBy(String orderedBy) { + this.orderedBy = orderedBy; + } + public Customer getCustomer() { return customer; } @@ -86,8 +94,6 @@ public void setCustomer(Customer customer) { this.customer = customer; } - public FreightOrder() {} - public Voyage getVoyage() { return voyage; } diff --git a/src/main/java/com/shipping/freightops/entity/TrackingEvent.java b/src/main/java/com/shipping/freightops/entity/TrackingEvent.java new file mode 100644 index 0000000..b2843a1 --- /dev/null +++ b/src/main/java/com/shipping/freightops/entity/TrackingEvent.java @@ -0,0 +1,90 @@ +package com.shipping.freightops.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.shipping.freightops.enums.EventType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; + +@Entity +public class TrackingEvent extends BaseEntity { + @ManyToOne(optional = false) + @JsonIgnore + private FreightOrder freightOrder; + + private EventType eventType; + + @Column(nullable = false) + @NotBlank(message = "description is required") + private String description; + + private String location; + private String performedBy; + private LocalDateTime eventTime; + + public TrackingEvent() {} + + public TrackingEvent( + FreightOrder freightOrder, + EventType eventType, + String description, + String location, + String performedBy, + LocalDateTime eventTime) { + this.freightOrder = freightOrder; + this.description = description; + this.location = location; + this.performedBy = performedBy; + this.eventTime = eventTime; + } + + public FreightOrder getFreightOrder() { + return freightOrder; + } + + public void setFreightOrder(FreightOrder freightOrder) { + this.freightOrder = freightOrder; + } + + public EventType getEventType() { + return eventType; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getPerformedBy() { + return performedBy; + } + + public void setPerformedBy(String performedBy) { + this.performedBy = performedBy; + } + + public LocalDateTime getEventTime() { + return eventTime; + } + + public void setEventTime(LocalDateTime eventTime) { + this.eventTime = eventTime; + } +} diff --git a/src/main/java/com/shipping/freightops/enums/EventType.java b/src/main/java/com/shipping/freightops/enums/EventType.java new file mode 100644 index 0000000..f3dd0a4 --- /dev/null +++ b/src/main/java/com/shipping/freightops/enums/EventType.java @@ -0,0 +1,11 @@ +package com.shipping.freightops.enums; + +public enum EventType { + STATUS_CHANGE, + GATE_IN, + GATE_OUT, + LOADED, + DISCHARGED, + CUSTOMS_CLEARED, + NOTE +} diff --git a/src/main/java/com/shipping/freightops/repository/TrackingEventRepository.java b/src/main/java/com/shipping/freightops/repository/TrackingEventRepository.java new file mode 100644 index 0000000..dce6940 --- /dev/null +++ b/src/main/java/com/shipping/freightops/repository/TrackingEventRepository.java @@ -0,0 +1,9 @@ +package com.shipping.freightops.repository; + +import com.shipping.freightops.entity.TrackingEvent; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TrackingEventRepository extends JpaRepository { + List findAllByFreightOrder_IdOrderByCreatedAtAsc(Long id); +} diff --git a/src/main/java/com/shipping/freightops/service/FreightOrderService.java b/src/main/java/com/shipping/freightops/service/FreightOrderService.java index 921c9f6..a9ff516 100644 --- a/src/main/java/com/shipping/freightops/service/FreightOrderService.java +++ b/src/main/java/com/shipping/freightops/service/FreightOrderService.java @@ -9,6 +9,7 @@ import com.shipping.freightops.entity.FreightOrder; import com.shipping.freightops.entity.Voyage; import com.shipping.freightops.enums.ContainerSize; +import com.shipping.freightops.enums.EventType; import com.shipping.freightops.enums.OrderStatus; import com.shipping.freightops.enums.VoyageStatus; import com.shipping.freightops.exception.BadRequestException; @@ -19,6 +20,7 @@ import com.shipping.freightops.repository.VoyageRepository; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -38,6 +40,7 @@ public class FreightOrderService { private final VoyagePriceRepository voyagePriceRepository; private final BookingProperties bookingProperties; private static final Logger log = LoggerFactory.getLogger(FreightOrderService.class); + private final TrackingEventService trackingEventService; public FreightOrderService( FreightOrderRepository orderRepository, @@ -46,7 +49,8 @@ public FreightOrderService( AgentRepository agentRepository, CustomerRepository customerRepository, VoyagePriceRepository voyagePriceRepository, - BookingProperties bookingProperties) { + BookingProperties bookingProperties, + TrackingEventService trackingEventService) { this.orderRepository = orderRepository; this.voyageRepository = voyageRepository; this.containerRepository = containerRepository; @@ -54,6 +58,7 @@ public FreightOrderService( this.customerRepository = customerRepository; this.voyagePriceRepository = voyagePriceRepository; this.bookingProperties = bookingProperties; + this.trackingEventService = trackingEventService; } @Transactional @@ -123,6 +128,15 @@ public FreightOrder createOrder(CreateFreightOrderRequest request) { FreightOrder savedOrder = orderRepository.save(order); handleAutoCutoff(voyage); + // Adding event tracking logique + TrackingEvent event = new TrackingEvent(); + event.setFreightOrder(savedOrder); + event.setDescription("order created with status created"); + event.setEventTime(LocalDateTime.now()); + event.setEventType(EventType.STATUS_CHANGE); + event.setPerformedBy(order.getAgent().getName()); + trackingEventService.createEvent(event); + savedOrder.getEvents().add(event); return savedOrder; } diff --git a/src/main/java/com/shipping/freightops/service/InvoiceService.java b/src/main/java/com/shipping/freightops/service/InvoiceService.java index 42cab34..9e22159 100644 --- a/src/main/java/com/shipping/freightops/service/InvoiceService.java +++ b/src/main/java/com/shipping/freightops/service/InvoiceService.java @@ -1,9 +1,11 @@ package com.shipping.freightops.service; import com.itextpdf.text.*; +import com.itextpdf.text.pdf.ColumnText; import com.itextpdf.text.pdf.PdfPCell; import com.itextpdf.text.pdf.PdfPTable; import com.itextpdf.text.pdf.PdfWriter; +import com.shipping.freightops.config.AppProperties; import com.shipping.freightops.entity.FreightOrder; import com.shipping.freightops.entity.Invoice; import com.shipping.freightops.enums.OrderStatus; @@ -15,6 +17,7 @@ import java.time.format.DateTimeFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class InvoiceService { @@ -23,9 +26,15 @@ public class InvoiceService { @Autowired private FreightOrderRepository freightOrderRepository; + private static final int QR_SIZE_PX = 100; + private static final float QR_SIZE_PT = 100f; + @Autowired private BarcodeService barcodeService; + @Autowired private AppProperties appProperties; + private static final BaseColor BRAND_TEAL = new BaseColor(95, 134, 112); private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + @Transactional public byte[] generateInvoice(Long orderId) throws DocumentException, FileNotFoundException { FreightOrder order = freightOrderRepository @@ -41,7 +50,7 @@ public byte[] generateInvoice(Long orderId) throws DocumentException, FileNotFou Document document = new Document(PageSize.A4, 36, 36, 50, 36); ByteArrayOutputStream out = new ByteArrayOutputStream(); - PdfWriter.getInstance(document, out); + PdfWriter writer = PdfWriter.getInstance(document, out); document.open(); // Fonts @@ -160,6 +169,9 @@ public byte[] generateInvoice(Long orderId) throws DocumentException, FileNotFou footer.setAlignment(Element.ALIGN_CENTER); document.add(footer); + // --- 6. QR Code (bottom-right, absolute position) --- + addTrackingQr(writer, document, orderId); + document.close(); // Logique de persistance @@ -169,6 +181,39 @@ public byte[] generateInvoice(Long orderId) throws DocumentException, FileNotFou } // Helpers + private void addTrackingQr(PdfWriter writer, Document document, Long orderId) + throws DocumentException { + String trackUrl = appProperties.getBaseUrl() + "/api/v1/track/order/" + orderId; + byte[] qrBytes = barcodeService.generateQrCode(trackUrl, QR_SIZE_PX, QR_SIZE_PX); + + try { + float rightMargin = document.rightMargin(); + float bottomMargin = document.bottomMargin(); + float pageWidth = document.getPageSize().getWidth(); + + float labelHeight = 10f; + float padding = 4f; + float x = pageWidth - rightMargin - QR_SIZE_PT; + float yQr = bottomMargin + labelHeight + padding; + + Image qrImage = Image.getInstance(qrBytes); + qrImage.scaleAbsolute(QR_SIZE_PT, QR_SIZE_PT); + qrImage.setAbsolutePosition(x, yQr); + document.add(qrImage); + + Font tiny = FontFactory.getFont(FontFactory.HELVETICA, 7, BaseColor.GRAY); + ColumnText.showTextAligned( + writer.getDirectContent(), + Element.ALIGN_CENTER, + new Phrase("Scan to track your shipment", tiny), + x + QR_SIZE_PT / 2, + bottomMargin, + 0); + } catch (Exception e) { + throw new DocumentException("Failed to add tracking QR code: " + e.getMessage()); + } + } + private void addRow(PdfPTable t, String l, String v, Font f1, Font f2) { PdfPCell c1 = getBorderlessCell(l + ":", f1); c1.setHorizontalAlignment(Element.ALIGN_RIGHT); diff --git a/src/main/java/com/shipping/freightops/service/TrackingEventService.java b/src/main/java/com/shipping/freightops/service/TrackingEventService.java new file mode 100644 index 0000000..a860b46 --- /dev/null +++ b/src/main/java/com/shipping/freightops/service/TrackingEventService.java @@ -0,0 +1,23 @@ +package com.shipping.freightops.service; + +import com.shipping.freightops.entity.*; +import com.shipping.freightops.repository.TrackingEventRepository; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class TrackingEventService { + private final TrackingEventRepository trackingEventRepository; + + public TrackingEventService(TrackingEventRepository trackingEventRepository) { + this.trackingEventRepository = trackingEventRepository; + } + + public TrackingEvent createEvent(TrackingEvent event) { + return trackingEventRepository.save(event); + } + + public List getAllEventsByOrderId(Long id) { + return trackingEventRepository.findAllByFreightOrder_IdOrderByCreatedAtAsc(id); + } +} diff --git a/src/test/java/com/shipping/freightops/controller/ContainerControllerTest.java b/src/test/java/com/shipping/freightops/controller/ContainerControllerTest.java index 57d2cad..efcac3c 100644 --- a/src/test/java/com/shipping/freightops/controller/ContainerControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/ContainerControllerTest.java @@ -54,8 +54,8 @@ void setUp() { new Container("TSTU1234567", ContainerSize.TWENTY_FOOT, ContainerType.DRY)); // Ports - Port departure = portRepository.save(new Port("AEJEA", "Jebel Ali", "UAE")); - Port arrival = portRepository.save(new Port("CNSHA", "Shanghai", "China")); + Port departure = portRepository.save(new Port("AEJEK", "Jebel Ali", "UAE")); + Port arrival = portRepository.save(new Port("CNSHK", "Shanghai", "China")); // Vessels Vessel vessel1 = vesselRepository.save(new Vessel("MV Active", "V001", 1000)); diff --git a/src/test/java/com/shipping/freightops/controller/FreightOrderControllerTest.java b/src/test/java/com/shipping/freightops/controller/FreightOrderControllerTest.java index 6844e19..12f967d 100644 --- a/src/test/java/com/shipping/freightops/controller/FreightOrderControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/FreightOrderControllerTest.java @@ -5,15 +5,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; import com.shipping.freightops.dto.CreateFreightOrderRequest; +import com.shipping.freightops.dto.TrackingEventRequest; import com.shipping.freightops.dto.UpdateDiscountRequest; import com.shipping.freightops.entity.*; -import com.shipping.freightops.enums.AgentType; -import com.shipping.freightops.enums.ContainerSize; -import com.shipping.freightops.enums.ContainerType; -import com.shipping.freightops.enums.OrderStatus; +import com.shipping.freightops.enums.*; import com.shipping.freightops.repository.*; import com.shipping.freightops.service.FreightOrderService; +import com.shipping.freightops.service.TrackingEventService; import java.math.BigDecimal; import java.time.LocalDateTime; import org.hamcrest.CoreMatchers; @@ -56,6 +56,7 @@ class FreightOrderControllerTest { private Customer savedCustomer; private Agent savedAgent; private Long freightOrderId; + @Autowired private TrackingEventService trackingEventService; @BeforeEach void setUp() { @@ -433,4 +434,71 @@ void getFreightOrderInvoiceWithOrderStatusNotDelivered() throws Exception { .perform(get("/api/v1/freight-orders/" + freightOrderId + "/invoice")) .andExpect(status().isConflict()); } + + @Test + @DisplayName("POST /api/v1/freight-orders/{id}/events → 200 OK") + void createManualEvent_returnsOk() throws Exception { + TrackingEventRequest request = new TrackingEventRequest(); + request.setEventType(EventType.GATE_IN); + request.setDescription("Container entered terminal"); + request.setLocation("Jebel Ali Terminal 2"); + request.setPerformedBy("scanner-T2"); + + mockMvc + .perform( + post("/api/v1/freight-orders/{id}/events", freightOrderId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.eventType").value("GATE_IN")) + .andExpect(jsonPath("$.description").value("Container entered terminal")) + .andExpect(jsonPath("$.location").value("Jebel Ali Terminal 2")); + } + + @Test + @DisplayName("Auto-event creation on order creation") + void createOrder_automaticallyCreatesInitialEvent() throws Exception { + CreateFreightOrderRequest request = new CreateFreightOrderRequest(); + request.setVoyageId(savedVoyage.getId()); + request.setContainerId(savedContainer.getId()); + request.setCustomerId(savedCustomer.getId()); + request.setAgentId(savedAgent.getId()); + request.setOrderedBy("ops-team"); + + String response = + mockMvc + .perform( + post("/api/v1/freight-orders") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + Integer newOrderId = JsonPath.parse(response).read("$.id"); + + mockMvc + .perform(get("/api/v1/freight-orders/{id}/events", newOrderId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].eventType").value("STATUS_CHANGE")) + .andExpect(jsonPath("$[0].description").exists()); + } + + @Test + @DisplayName("GET /api/v1/freight-orders/{id}/events → 200 OK") + void getAllEvents_returnsList() throws Exception { + // Ajout préalable d'un événement pour s'assurer que la liste n'est pas vide + TrackingEvent event = new TrackingEvent(); + event.setFreightOrder(freightOrderRepository.findById(freightOrderId).get()); + event.setEventType(EventType.NOTE); + event.setDescription("Test note"); + event.setDescription("take note"); + event.setEventTime(LocalDateTime.now()); + + mockMvc + .perform(get("/api/v1/freight-orders/{id}/events", freightOrderId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()); + } } diff --git a/src/test/java/com/shipping/freightops/controller/InvoiceControllerTest.java b/src/test/java/com/shipping/freightops/controller/InvoiceControllerTest.java new file mode 100644 index 0000000..d7cfeef --- /dev/null +++ b/src/test/java/com/shipping/freightops/controller/InvoiceControllerTest.java @@ -0,0 +1,154 @@ +package com.shipping.freightops.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.itextpdf.text.pdf.PdfReader; +import com.itextpdf.text.pdf.parser.PdfTextExtractor; +import com.shipping.freightops.entity.Agent; +import com.shipping.freightops.entity.Container; +import com.shipping.freightops.entity.Customer; +import com.shipping.freightops.entity.FreightOrder; +import com.shipping.freightops.entity.Port; +import com.shipping.freightops.entity.Vessel; +import com.shipping.freightops.entity.Voyage; +import com.shipping.freightops.entity.VoyagePrice; +import com.shipping.freightops.enums.AgentType; +import com.shipping.freightops.enums.ContainerSize; +import com.shipping.freightops.enums.ContainerType; +import com.shipping.freightops.enums.OrderStatus; +import com.shipping.freightops.enums.VoyageStatus; +import com.shipping.freightops.repository.AgentRepository; +import com.shipping.freightops.repository.ContainerRepository; +import com.shipping.freightops.repository.CustomerRepository; +import com.shipping.freightops.repository.FreightOrderRepository; +import com.shipping.freightops.repository.InvoiceRepository; +import com.shipping.freightops.repository.PortRepository; +import com.shipping.freightops.repository.VesselRepository; +import com.shipping.freightops.repository.VoyagePriceRepository; +import com.shipping.freightops.repository.VoyageRepository; +import jakarta.persistence.EntityManager; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class InvoiceControllerTest { + + @Autowired private MockMvc mockMvc; + @Autowired private PortRepository portRepository; + @Autowired private VesselRepository vesselRepository; + @Autowired private ContainerRepository containerRepository; + @Autowired private CustomerRepository customerRepository; + @Autowired private VoyageRepository voyageRepository; + @Autowired private VoyagePriceRepository voyagePriceRepository; + @Autowired private EntityManager entityManager; + @Autowired private InvoiceRepository invoiceRepository; + @Autowired private FreightOrderRepository freightOrderRepository; + @Autowired private AgentRepository agentRepository; + + private Long deliveredOrderId; + + @BeforeEach + void setUp() { + invoiceRepository.deleteAll(); + freightOrderRepository.deleteAll(); + agentRepository.deleteAll(); + voyagePriceRepository.deleteAll(); + voyageRepository.deleteAll(); + containerRepository.deleteAll(); + customerRepository.deleteAll(); + vesselRepository.deleteAll(); + portRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + Port departure = portRepository.save(new Port("AEJEA", "Jebel Ali", "UAE")); + Port arrival = portRepository.save(new Port("CNSHA", "Shanghai", "China")); + Vessel vessel = vesselRepository.save(new Vessel("MV Invoice", "1122334", 3000)); + + Voyage voyage = new Voyage(); + voyage.setVoyageNumber("INV-VOY-001"); + voyage.setVessel(vessel); + voyage.setDeparturePort(departure); + voyage.setArrivalPort(arrival); + voyage.setDepartureTime(LocalDateTime.now().minusDays(10)); + voyage.setArrivalTime(LocalDateTime.now().minusDays(3)); + voyage.setStatus(VoyageStatus.COMPLETED); + voyage.setMaxCapacityTeu(vessel.getCapacityTeu()); + voyage.setBookingOpen(false); + Voyage savedVoyage = voyageRepository.save(voyage); + + VoyagePrice price = new VoyagePrice(); + price.setVoyage(savedVoyage); + price.setContainerSize(ContainerSize.TWENTY_FOOT); + price.setBasePriceUsd(BigDecimal.valueOf(1000)); + voyagePriceRepository.save(price); + + Container container = + containerRepository.save( + new Container("INVT1234567", ContainerSize.TWENTY_FOOT, ContainerType.DRY)); + + Customer customer = new Customer(); + customer.setCompanyName("Invoice Test Corp"); + customer.setContactName("Jane Doe"); + customer.setEmail("jane@invoicetest.com"); + customer.setAddress("123 Test Street"); + Customer savedCustomer = customerRepository.save(customer); + + Agent agent = new Agent(); + agent.setName("Invoice Agent"); + agent.setEmail("inv-agent@test.com"); + agent.setCommissionPercent(BigDecimal.valueOf(5)); + agent.setType(AgentType.INTERNAL); + agent.setActive(true); + agentRepository.save(agent); + + FreightOrder order = new FreightOrder(); + order.setVoyage(savedVoyage); + order.setContainer(container); + order.setCustomer(savedCustomer); + order.setAgent(agent); + order.setStatus(OrderStatus.DELIVERED); + order.setBasePriceUsd(BigDecimal.valueOf(1000)); + order.setDiscountPercent(BigDecimal.valueOf(10)); + order.setFinalPrice(BigDecimal.valueOf(900)); + order.setDiscountReason("Loyalty discount"); + order.setOrderedBy("test-user"); + order.setNotes("test notes"); + deliveredOrderId = freightOrderRepository.save(order).getId(); + } + + @Test + @DisplayName("GET /api/v1/freight-orders/{id}/invoice → returns valid PDF with tracking QR") + void generateInvoice_returnsValidPdfWithQr() throws Exception { + mockMvc + .perform(get("/api/v1/freight-orders/{id}/invoice", deliveredOrderId)) + .andExpect(status().isOk()) + .andExpect( + result -> { + byte[] bytes = result.getResponse().getContentAsByteArray(); + assertTrue(bytes.length > 0, "PDF must not be empty"); + assertEquals("%PDF", new String(bytes, 0, 4), "Response must be a valid PDF"); + + PdfReader reader = new PdfReader(bytes); + String pageText = PdfTextExtractor.getTextFromPage(reader, 1); + reader.close(); + + assertTrue( + pageText.contains("Scan to track your shipment"), + "PDF must contain QR tracking label"); + }); + } +} diff --git a/src/test/java/com/shipping/freightops/controller/PortControllerTest.java b/src/test/java/com/shipping/freightops/controller/PortControllerTest.java index c1528b4..59bdcb1 100644 --- a/src/test/java/com/shipping/freightops/controller/PortControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/PortControllerTest.java @@ -12,6 +12,7 @@ import com.shipping.freightops.entity.Port; import com.shipping.freightops.repository.FreightOrderRepository; import com.shipping.freightops.repository.PortRepository; +import com.shipping.freightops.repository.VoyagePriceRepository; import com.shipping.freightops.repository.VoyageRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -31,11 +32,13 @@ class PortControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired private PortRepository portRepository; @Autowired private FreightOrderRepository freightOrderRepository; + @Autowired private VoyagePriceRepository voyagePriceRepository; @Autowired private VoyageRepository voyageRepository; @BeforeEach void setUp() { freightOrderRepository.deleteAll(); + voyagePriceRepository.deleteAll(); voyageRepository.deleteAll(); portRepository.deleteAll(); } diff --git a/src/test/java/com/shipping/freightops/controller/TrackingControllerTest.java b/src/test/java/com/shipping/freightops/controller/TrackingControllerTest.java index 36dd7fa..cf30153 100644 --- a/src/test/java/com/shipping/freightops/controller/TrackingControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/TrackingControllerTest.java @@ -4,13 +4,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.shipping.freightops.dto.CreateFreightOrderRequest; import com.shipping.freightops.entity.*; import com.shipping.freightops.enums.AgentType; import com.shipping.freightops.enums.ContainerSize; import com.shipping.freightops.enums.ContainerType; import com.shipping.freightops.enums.OrderStatus; import com.shipping.freightops.repository.*; -import com.shipping.freightops.service.TrackingService; +import com.shipping.freightops.service.FreightOrderService; import java.math.BigDecimal; import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; @@ -35,14 +36,17 @@ public class TrackingControllerTest { @Autowired private CustomerRepository customerRepository; @Autowired private AgentRepository agentRepository; @Autowired private FreightOrderRepository freightOrderRepository; - @Autowired private TrackingService trackingService; + @Autowired private VoyagePriceRepository voyagePriceRepository; + @Autowired private FreightOrderService freightOrderService; private Long orderId; + private Long orderId2; @BeforeEach void setup() { // Clear repositories before each test freightOrderRepository.deleteAll(); + voyagePriceRepository.deleteAll(); voyageRepository.deleteAll(); containerRepository.deleteAll(); vesselRepository.deleteAll(); @@ -51,8 +55,8 @@ void setup() { agentRepository.deleteAll(); // Setup test data for tracking tests - Port departurePort = portRepository.save(new Port("AEJEA", "Jebel Ali", "UAE")); - Port arrivalPort = portRepository.save(new Port("CNSHA", "Shanghai", "China")); + Port departurePort = portRepository.save(new Port("AEJEC", "Jebel Ali", "UAE")); + Port arrivalPort = portRepository.save(new Port("CNSHC", "Shanghai", "China")); Vessel vessel = vesselRepository.save(new Vessel("MV Test", "1111111", 3000)); Agent agent = new Agent(); agent.setName("Test Agent"); @@ -92,6 +96,50 @@ void setup() { FreightOrder savedOrder = freightOrderRepository.save(order); orderId = savedOrder.getId(); + Port departure2 = portRepository.save(new Port("AEJEB", "Jebel Ali 2", "UAE")); + Port arrival2 = portRepository.save(new Port("CNSHB", "Shanghai 2", "China")); + Vessel vessel2 = vesselRepository.save(new Vessel("MV Test 2", "9999993", 3000)); + + Agent agent2 = new Agent(); + agent2.setName("Test Agent 2"); + agent2.setEmail("agent2@test.com"); + agent2.setCommissionPercent(BigDecimal.valueOf(5)); + agent2.setType(AgentType.INTERNAL); + Agent savedAgent2 = agentRepository.save(agent2); + + Voyage voyage2 = new Voyage(); + voyage2.setVoyageNumber("VOY-002"); + voyage2.setVessel(vessel2); + voyage2.setDeparturePort(departure2); + voyage2.setArrivalPort(arrival2); + voyage2.setDepartureTime(LocalDateTime.now().plusDays(3)); + voyage2.setArrivalTime(LocalDateTime.now().plusDays(10)); + voyage2.setMaxCapacityTeu(vessel2.getCapacityTeu()); + voyage2.setBookingOpen(true); + Voyage savedVoyage2 = voyageRepository.save(voyage2); + + Customer customer2 = + customerRepository.save(new Customer("Test Customer Inc.", "John Doe", "john@test.com")); + Container container2 = + containerRepository.save( + new Container("TSTU1234567", ContainerSize.TWENTY_FOOT, ContainerType.DRY)); + + VoyagePrice price2 = new VoyagePrice(); + price2.setVoyage(savedVoyage2); + price2.setContainerSize(ContainerSize.TWENTY_FOOT); + price2.setBasePriceUsd(BigDecimal.valueOf(1000)); + voyagePriceRepository.save(price2); + + CreateFreightOrderRequest request2 = new CreateFreightOrderRequest(); + request2.setVoyageId(savedVoyage2.getId()); + request2.setContainerId(container2.getId()); + request2.setCustomerId(customer2.getId()); + request2.setAgentId(savedAgent2.getId()); + request2.setOrderedBy("ops-team"); + request2.setNotes("Audit trail starting"); + + FreightOrder savedOrder2 = freightOrderService.createOrder(request2); + this.orderId2 = savedOrder2.getId(); } @Test @@ -154,4 +202,17 @@ void testTrackContainer_containerWithNoOrders_returnsOkWithEmptyVoyages() throws .andExpect(jsonPath("$.voyages").isArray()) .andExpect(jsonPath("$.voyages").isEmpty()); } + + @Test + @DisplayName("GET /api/v1/track/order/{orderId} - valid order ID returns tracking details") + void testTrackOrder_withListEvents() throws Exception { + mockMvc + .perform(get("/api/v1/track/order/" + orderId2)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orderId").value(orderId2)) + .andExpect(jsonPath("$.status").value("PENDING")) + .andExpect(jsonPath("$.voyageNumber").value("VOY-002")) + .andExpect(jsonPath("$.containerCode").value("TSTU1234567")) + .andExpect(jsonPath("$.events").isArray()); + } } diff --git a/src/test/java/com/shipping/freightops/controller/VesselControllerTest.java b/src/test/java/com/shipping/freightops/controller/VesselControllerTest.java index d9dd4df..5a9fe0a 100644 --- a/src/test/java/com/shipping/freightops/controller/VesselControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/VesselControllerTest.java @@ -9,6 +9,7 @@ import com.shipping.freightops.dto.CreateVesselRequest; import com.shipping.freightops.entity.Vessel; import com.shipping.freightops.repository.VesselRepository; +import com.shipping.freightops.repository.VoyagePriceRepository; import com.shipping.freightops.repository.VoyageRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -30,6 +31,7 @@ public class VesselControllerTest { @Autowired private MockMvc mockMvc; + @Autowired private VoyagePriceRepository voyagePriceRepository; @Autowired private VoyageRepository voyageRepository; @Autowired private VesselRepository vesselRepository; @Autowired private ObjectMapper objectMapper; @@ -38,6 +40,7 @@ public class VesselControllerTest { @BeforeEach void setUp() { + voyagePriceRepository.deleteAll(); voyageRepository.deleteAll(); vesselRepository.deleteAll(); Vessel vessel = new Vessel("MV Test", "9999999", 3000); diff --git a/src/test/java/com/shipping/freightops/controller/VesselOwnerControllerTest.java b/src/test/java/com/shipping/freightops/controller/VesselOwnerControllerTest.java index 3f03fd2..1735a4e 100644 --- a/src/test/java/com/shipping/freightops/controller/VesselOwnerControllerTest.java +++ b/src/test/java/com/shipping/freightops/controller/VesselOwnerControllerTest.java @@ -12,6 +12,7 @@ import com.shipping.freightops.entity.VesselOwner; import com.shipping.freightops.repository.VesselOwnerRepository; import com.shipping.freightops.repository.VesselRepository; +import com.shipping.freightops.repository.VoyagePriceRepository; import com.shipping.freightops.repository.VoyageRepository; import java.math.BigDecimal; import org.junit.jupiter.api.BeforeEach; @@ -32,16 +33,18 @@ class VesselOwnerControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired private VesselOwnerRepository vesselOwnerRepository; @Autowired private VesselRepository vesselRepository; + @Autowired private VoyagePriceRepository voyagePriceRepository; @Autowired private VoyageRepository voyageRepository; private Vessel savedVessel; @BeforeEach void setUp() { + voyagePriceRepository.deleteAll(); voyageRepository.deleteAll(); vesselOwnerRepository.deleteAll(); vesselRepository.deleteAll(); - savedVessel = vesselRepository.save(new Vessel("MV Test", "1234567", 500)); + savedVessel = vesselRepository.save(new Vessel("MV Test", "1234566", 500)); } // ── ADD OWNER ──