diff --git a/README.md b/README.md index 1dffd47..30eb047 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,34 @@ credentials `freight/freight`). The server starts on **http://localhost:8080**. On first boot, `data.sql` seeds sample ports, a vessel, and containers. -### 3. Try the API +### 3. Configure Email (Optional - for invoice sending) + +The application includes email functionality using SMTP. For local development, use **MailHog** to capture emails without sending them: + +**Start MailHog:** + +```bash +docker run --rm -p 1025:1025 -p 8025:8025 mailhog/mailhog +``` + +- SMTP server: `localhost:1025` (configured in `application.properties`) +- Web UI: **http://localhost:8025** — view all captured emails in real-time + +The `application.properties` is pre-configured: +```properties +spring.mail.host=localhost +spring.mail.port=1025 +app.email.enabled=true +app.email.from-address=noreply@apgl-shipping.com +app.email.reply-to=support@apgl-shipping.com +``` + +To disable email sending (e.g., in tests): +```properties +app.email.enabled=false +``` + +### 4. Try the API **Create a vessel:** @@ -116,6 +143,13 @@ curl -X POST http://localhost:8080/api/v1/freight-orders \ curl http://localhost:8080/api/v1/freight-orders/1/invoice --output invoice.pdf ``` +**Send invoice to customer email:** +```bash +curl -X POST http://localhost:8080/api/v1/invoices/1/send +``` + +(Requires order status = DELIVERED; email is sent to customer's registered email address) + ## Running Tests Tests use an **H2 in-memory database** — no PostgreSQL needed. diff --git a/pom.xml b/pom.xml index 011057a..b6ebe6f 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,8 @@ 21 + 5.14.2 + 1.15.10 @@ -91,6 +93,10 @@ javase 3.5.3 + + org.springframework.boot + spring-boot-starter-mail + com.itextpdf @@ -114,6 +120,24 @@ rome 2.1.0 + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + test + @@ -122,7 +146,27 @@ org.springframework.boot spring-boot-maven-plugin - + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dnet.bytebuddy.experimental=true + + + com.spotify.fmt fmt-maven-plugin diff --git a/src/main/java/com/shipping/freightops/config/EmailProperties.java b/src/main/java/com/shipping/freightops/config/EmailProperties.java new file mode 100644 index 0000000..797b2f9 --- /dev/null +++ b/src/main/java/com/shipping/freightops/config/EmailProperties.java @@ -0,0 +1,42 @@ +package com.shipping.freightops.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "app.email") +public class EmailProperties { + + /** Whether email sending is enabled. Default: true */ + private boolean enabled = true; + + /** "From" address for all outgoing emails. Default: noreply@apgl-shipping.com */ + private String fromAddress = "noreply@apgl-shipping.com"; + + /** "Reply-To" address for all outgoing emails. Default: support@apgl-shipping.com */ + private String replyTo = "support@apgl-shipping.com"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getFromAddress() { + return fromAddress; + } + + public void setFromAddress(String fromAddress) { + this.fromAddress = fromAddress; + } + + public String getReplyTo() { + return replyTo; + } + + public void setReplyTo(String replyTo) { + this.replyTo = replyTo; + } +} diff --git a/src/main/java/com/shipping/freightops/controller/InvoiceController.java b/src/main/java/com/shipping/freightops/controller/InvoiceController.java new file mode 100644 index 0000000..e57bda8 --- /dev/null +++ b/src/main/java/com/shipping/freightops/controller/InvoiceController.java @@ -0,0 +1,46 @@ +package com.shipping.freightops.controller; + +import com.itextpdf.text.DocumentException; +import com.shipping.freightops.service.InvoiceEmailService; +import com.shipping.freightops.service.InvoiceService; +import java.io.FileNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/invoices") +public class InvoiceController { + + @Autowired private InvoiceService invoiceService; + + @Autowired private InvoiceEmailService invoiceEmailService; + + @GetMapping("/{orderId}") + public ResponseEntity getInvoice(@PathVariable Long orderId) + throws DocumentException, FileNotFoundException { + byte[] pdfBytes = invoiceService.generateInvoice(orderId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDisposition( + ContentDisposition.attachment().filename("invoice-" + orderId + ".pdf").build()); + headers.setContentLength(pdfBytes.length); + + return ResponseEntity.ok().headers(headers).body(pdfBytes); + } + + @PostMapping("/{orderId}/send") + public ResponseEntity sendInvoiceToCustomer(@PathVariable Long orderId) + throws DocumentException, FileNotFoundException { + invoiceEmailService.sendInvoiceToCustomer(orderId); + return ResponseEntity.ok("Invoice sent successfully to customer email"); + } +} diff --git a/src/main/java/com/shipping/freightops/entity/TrackingEvent.java b/src/main/java/com/shipping/freightops/entity/TrackingEvent.java index 1da9268..8084910 100644 --- a/src/main/java/com/shipping/freightops/entity/TrackingEvent.java +++ b/src/main/java/com/shipping/freightops/entity/TrackingEvent.java @@ -38,6 +38,7 @@ public TrackingEvent( String performedBy, LocalDateTime eventTime) { this.freightOrder = freightOrder; + this.eventType = eventType; this.description = description; this.location = location; this.performedBy = performedBy; diff --git a/src/main/java/com/shipping/freightops/service/EmailService.java b/src/main/java/com/shipping/freightops/service/EmailService.java new file mode 100644 index 0000000..d3bb1d8 --- /dev/null +++ b/src/main/java/com/shipping/freightops/service/EmailService.java @@ -0,0 +1,14 @@ +package com.shipping.freightops.service; + +public interface EmailService { + + void sendEmail(String to, String subject, String body); + + void sendEmailWithAttachment( + String to, + String subject, + String body, + String attachmentName, + byte[] attachmentContent, + String mimeType); +} diff --git a/src/main/java/com/shipping/freightops/service/InvoiceEmailService.java b/src/main/java/com/shipping/freightops/service/InvoiceEmailService.java new file mode 100644 index 0000000..dbf3d4c --- /dev/null +++ b/src/main/java/com/shipping/freightops/service/InvoiceEmailService.java @@ -0,0 +1,83 @@ +package com.shipping.freightops.service; + +import com.itextpdf.text.DocumentException; +import com.shipping.freightops.entity.FreightOrder; +import com.shipping.freightops.enums.OrderStatus; +import com.shipping.freightops.repository.FreightOrderRepository; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class InvoiceEmailService { + + private final FreightOrderRepository freightOrderRepository; + + private final InvoiceService invoiceService; + + private final EmailService emailService; + + @Transactional + public void sendInvoiceToCustomer(Long orderId) throws DocumentException, FileNotFoundException { + FreightOrder order = getOrderOrThrow(orderId); + + validateDelivered(order); + + byte[] invoicePdf = invoiceService.generateInvoice(orderId); + + sendInvoiceEmail(order, invoicePdf); + + log.info("Invoice sent to customer: {}", order.getCustomer().getEmail()); + } + + /** Fetch order or throw if not found */ + private FreightOrder getOrderOrThrow(Long orderId) { + return freightOrderRepository + .findById(orderId) + .orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId)); + } + + /** Ensure order is in DELIVERED status */ + private void validateDelivered(FreightOrder order) { + if (order.getStatus() != OrderStatus.DELIVERED) { + throw new IllegalStateException( + "Cannot send invoice for order in " + + order.getStatus() + + " status. Order must be DELIVERED."); + } + } + + /** Build email request with subject, body, and attachment */ + private void sendInvoiceEmail(FreightOrder order, byte[] invoicePdf) { + String invoiceNo = generateInvoiceNumber(order); + String voyageNo = generateVoyageNumber(order); + String subject = String.format("Invoice %s - Voyage %s", invoiceNo, voyageNo); + String body = + String.format( + "Dear %s,\n\nPlease find your invoice attached. Thank you for your business.\n\nBest regards,\nAPGL Freight Operations", + order.getCustomer().getCompanyName()); + + emailService.sendEmailWithAttachment( + order.getCustomer().getEmail(), + subject, + body, + invoiceNo + ".pdf", + invoicePdf, + "application/pdf"); + } + + /** Generate invoice number based on year and order ID */ + private String generateInvoiceNumber(FreightOrder order) { + return String.format("INV-%d-%05d", LocalDate.now().getYear(), order.getId()); + } + + /** Generate voyage number in format VOY-XXXXX */ + private String generateVoyageNumber(FreightOrder order) { + return String.format("VOY-%05d", order.getVoyage().getId()); + } +} diff --git a/src/main/java/com/shipping/freightops/service/impl/NoOpEmailService.java b/src/main/java/com/shipping/freightops/service/impl/NoOpEmailService.java new file mode 100644 index 0000000..b57accc --- /dev/null +++ b/src/main/java/com/shipping/freightops/service/impl/NoOpEmailService.java @@ -0,0 +1,33 @@ +package com.shipping.freightops.service.impl; + +import com.shipping.freightops.service.EmailService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@ConditionalOnMissingBean(JavaMailSender.class) +public class NoOpEmailService implements EmailService { + + @Override + public void sendEmail(String to, String subject, String body) { + log.debug("NoOp email service: skipping email to {} with subject '{}'", to, subject); + } + + @Override + public void sendEmailWithAttachment( + String to, + String subject, + String body, + String attachmentName, + byte[] attachmentContent, + String mimeType) { + log.debug( + "NoOp email service: skipping email to {} with subject '{}' and attachment '{}'", + to, + subject, + attachmentName); + } +} diff --git a/src/main/java/com/shipping/freightops/service/impl/SmtpEmailService.java b/src/main/java/com/shipping/freightops/service/impl/SmtpEmailService.java new file mode 100644 index 0000000..58f16d7 --- /dev/null +++ b/src/main/java/com/shipping/freightops/service/impl/SmtpEmailService.java @@ -0,0 +1,113 @@ +package com.shipping.freightops.service.impl; + +import com.shipping.freightops.config.EmailProperties; +import com.shipping.freightops.service.EmailService; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +@Slf4j +@Service +@ConditionalOnBean(JavaMailSender.class) +public class SmtpEmailService implements EmailService { + + @Autowired private JavaMailSender mailSender; + + @Autowired private EmailProperties emailProperties; + + @Override + public void sendEmail(String to, String subject, String body) { + validateEmailInput(to, subject, body); + + if (!emailProperties.isEnabled()) { + log.debug("Email sending is disabled. Skipping email to: {}", to); + return; + } + + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); + + helper.setFrom(emailProperties.getFromAddress()); + helper.setReplyTo(emailProperties.getReplyTo()); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(body, true); // true = HTML content + + mailSender.send(message); + log.info("Email sent successfully to: {}", to); + } catch (MessagingException e) { + log.error("Failed to send email to: {}", to, e); + throw new RuntimeException("Email send failed: " + e.getMessage(), e); + } + } + + @Override + public void sendEmailWithAttachment( + String to, + String subject, + String body, + String attachmentName, + byte[] attachmentContent, + String mimeType) { + validateEmailInput(to, subject, body); + validateAttachmentInput(attachmentName, attachmentContent, mimeType); + + if (!emailProperties.isEnabled()) { + log.debug("Email sending is disabled. Skipping email with attachment to: {}", to); + return; + } + + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(emailProperties.getFromAddress()); + helper.setReplyTo(emailProperties.getReplyTo()); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(body, true); // true = HTML content + + DataSource dataSource = + new jakarta.mail.util.ByteArrayDataSource(attachmentContent, mimeType); + helper.addAttachment(attachmentName, dataSource); + + mailSender.send(message); + log.info("Email with attachment '{}' sent successfully to: {}", attachmentName, to); + } catch (MessagingException e) { + log.error("Failed to send email with attachment to: {}", to, e); + throw new RuntimeException("Email send failed: " + e.getMessage(), e); + } + } + + private void validateEmailInput(String to, String subject, String body) { + if (!StringUtils.hasText(to)) { + throw new IllegalArgumentException("Recipient email address cannot be null or empty"); + } + if (!StringUtils.hasText(subject)) { + throw new IllegalArgumentException("Email subject cannot be null or empty"); + } + if (!StringUtils.hasText(body)) { + throw new IllegalArgumentException("Email body cannot be null or empty"); + } + } + + private void validateAttachmentInput(String attachmentName, byte[] content, String mimeType) { + if (!StringUtils.hasText(attachmentName)) { + throw new IllegalArgumentException("Attachment name cannot be null or empty"); + } + if (content == null || content.length == 0) { + throw new IllegalArgumentException("Attachment content cannot be null or empty"); + } + if (!StringUtils.hasText(mimeType)) { + throw new IllegalArgumentException("MIME type cannot be null or empty"); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 72219b6..cac5285 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -35,3 +35,12 @@ app.news.feeds=https://gcaptain.com/feed/,https://theloadstar.com/feed/ app.news.max-headlines=5 app.news.connect-timeout=10 app.news.read-timeout=30 +# Email +spring.mail.host=localhost +spring.mail.port=1025 +spring.mail.username= +spring.mail.password= +# Email service +app.email.enabled=true +app.email.from-address=noreply@apgl-shipping.com +app.email.reply-to=support@apgl-shipping.com diff --git a/src/test/java/com/shipping/freightops/service/EmailServiceTest.java b/src/test/java/com/shipping/freightops/service/EmailServiceTest.java new file mode 100644 index 0000000..d1aaec5 --- /dev/null +++ b/src/test/java/com/shipping/freightops/service/EmailServiceTest.java @@ -0,0 +1,94 @@ +package com.shipping.freightops.service; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.shipping.freightops.config.EmailProperties; +import com.shipping.freightops.service.impl.SmtpEmailService; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class EmailServiceTest { + + @Mock private JavaMailSender mailSender; + + @InjectMocks private SmtpEmailService emailService; + + private EmailProperties emailProperties; + + @BeforeEach + void setUp() { + emailProperties = new EmailProperties(); + emailProperties.setEnabled(true); + emailProperties.setFromAddress("noreply@apgl-shipping.com"); + emailProperties.setReplyTo("support@apgl-shipping.com"); + ReflectionTestUtils.setField(emailService, "emailProperties", emailProperties); + } + + @Test + @DisplayName("sends email when enabled") + void sendsEmailWhenEnabled() throws MessagingException { + MimeMessage mimeMessage = new MimeMessage((jakarta.mail.Session) null); + when(mailSender.createMimeMessage()).thenReturn(mimeMessage); + + emailService.sendEmail("customer@example.com", "Test Subject", "Test Body"); + + verify(mailSender).send(mimeMessage); + } + + @Test + @DisplayName("does not send email when disabled") + void doesNotSendEmailWhenDisabled() { + emailProperties.setEnabled(false); + + emailService.sendEmail("customer@example.com", "Subject", "Body"); + + verifyNoInteractions(mailSender); + } + + @Test + @DisplayName("sends email with attachment when enabled") + void sendsEmailWithAttachmentWhenEnabled() throws MessagingException { + MimeMessage mimeMessage = new MimeMessage((jakarta.mail.Session) null); + when(mailSender.createMimeMessage()).thenReturn(mimeMessage); + + byte[] pdfContent = {(byte) 0x25, (byte) 0x50, (byte) 0x44, (byte) 0x46}; + emailService.sendEmailWithAttachment( + "customer@example.com", + "Invoice", + "See attachment", + "invoice.pdf", + pdfContent, + "application/pdf"); + + verify(mailSender).send(mimeMessage); + } + + @Test + @DisplayName("does not send email with attachment when disabled") + void doesNotSendEmailWithAttachmentWhenDisabled() { + emailProperties.setEnabled(false); + + byte[] pdfContent = {(byte) 0x25, (byte) 0x50, (byte) 0x44, (byte) 0x46}; + emailService.sendEmailWithAttachment( + "customer@example.com", + "Invoice", + "See attachment", + "invoice.pdf", + pdfContent, + "application/pdf"); + + verifyNoInteractions(mailSender); + } +} diff --git a/src/test/java/com/shipping/freightops/service/InvoiceEmailServiceTest.java b/src/test/java/com/shipping/freightops/service/InvoiceEmailServiceTest.java new file mode 100644 index 0000000..5f81d6f --- /dev/null +++ b/src/test/java/com/shipping/freightops/service/InvoiceEmailServiceTest.java @@ -0,0 +1,156 @@ +package com.shipping.freightops.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.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.FreightOrderRepository; +import java.math.BigDecimal; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class InvoiceEmailServiceTest { + + @Mock private InvoiceService invoiceService; + + @Mock private FreightOrderRepository freightOrderRepository; + + @Mock private EmailService emailService; + + @InjectMocks private InvoiceEmailService invoiceEmailService; + + @Nested + @DisplayName("sendInvoiceToCustomer") + class SendInvoiceToCustomer { + + @Test + @DisplayName("sends invoice PDF to customer email when order DELIVERED") + void sendsInvoiceWhenDelivered() throws Exception { + FreightOrder order = buildDeliveredOrder(); + byte[] pdfBytes = {0x25, 0x50, 0x44, 0x46}; // PDF magic bytes + + when(freightOrderRepository.findById(1L)).thenReturn(Optional.of(order)); + when(invoiceService.generateInvoice(1L)).thenReturn(pdfBytes); + + invoiceEmailService.sendInvoiceToCustomer(1L); + + ArgumentCaptor toCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor subjectCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor attachmentNameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor mimeTypeCaptor = ArgumentCaptor.forClass(String.class); + + verify(emailService) + .sendEmailWithAttachment( + toCaptor.capture(), + subjectCaptor.capture(), + bodyCaptor.capture(), + attachmentNameCaptor.capture(), + contentCaptor.capture(), + mimeTypeCaptor.capture()); + + assertThat(toCaptor.getValue()).isEqualTo("customer@example.com"); + assertThat(subjectCaptor.getValue()).contains("Invoice").contains("Voyage"); + assertThat(bodyCaptor.getValue()).contains("Acme Corp"); + assertThat(attachmentNameCaptor.getValue()).endsWith(".pdf"); + assertThat(mimeTypeCaptor.getValue()).isEqualTo("application/pdf"); + assertThat(contentCaptor.getValue()).isNotEmpty(); + } + + @Test + @DisplayName("throws when order not found") + void throwsWhenOrderNotFound() { + when(freightOrderRepository.findById(99L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> invoiceEmailService.sendInvoiceToCustomer(99L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Order not found"); + } + + @Test + @DisplayName("throws when order not in DELIVERED status") + void throwsWhenNotDelivered() { + FreightOrder order = buildDeliveredOrder(); + order.setStatus(OrderStatus.IN_TRANSIT); + when(freightOrderRepository.findById(1L)).thenReturn(Optional.of(order)); + + assertThatThrownBy(() -> invoiceEmailService.sendInvoiceToCustomer(1L)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot send invoice"); + } + } + + // ── HELPERS ──────────────────────────────────────────────── + + private FreightOrder buildDeliveredOrder() { + Customer customer = new Customer(); + customer.setId(1L); + customer.setCompanyName("Acme Corp"); + customer.setEmail("customer@example.com"); + customer.setAddress("123 Main St"); + + Port port = new Port(); + port.setId(1L); + port.setName("Port of Hamburg"); + + Vessel vessel = new Vessel(); + vessel.setId(1L); + vessel.setName("Test Vessel"); + vessel.setImoNumber("1234567"); + vessel.setCapacityTeu(1000); + + Voyage voyage = new Voyage(); + voyage.setId(1L); + voyage.setVoyageNumber("V001"); + voyage.setDeparturePort(port); + voyage.setArrivalPort(port); + voyage.setVessel(vessel); + + Container container = new Container(); + container.setId(1L); + container.setContainerCode("TEST123456"); + container.setSize(ContainerSize.TWENTY_FOOT); + container.setType(ContainerType.DRY); + + Agent agent = new Agent(); + agent.setId(1L); + agent.setName("Test Agent"); + agent.setEmail("agent@example.com"); + agent.setType(AgentType.INTERNAL); + + FreightOrder order = new FreightOrder(); + order.setId(1L); + order.setCustomer(customer); + order.setVoyage(voyage); + order.setContainer(container); + order.setAgent(agent); + order.setOrderedBy("ops-team"); + order.setStatus(OrderStatus.DELIVERED); + order.setBasePriceUsd(new BigDecimal("1000.00")); + order.setFinalPrice(new BigDecimal("1000.00")); + order.setDiscountPercent(BigDecimal.ZERO); + + return order; + } +}