Skip to content

Shipment Service

The ShipmentAppService is the core service of the Hub module, managing all shipment-related operations including creation, updates, queries, and logistics tracking.

Overview

Location: modules/hub/src/Hub.Application/Services/ShipmentAppService.cs

Interface: IShipmentAppService

Base Class: HubEntityBaseService<Shipment, ShipmentDto>

Permissions: HubPermissions.Shipment.*

Features

  • CRUD Operations: Complete create, read, update, delete functionality
  • Advanced Filtering: Organization-based and attribute-based filtering
  • Pagination: Efficient handling of large datasets
  • Faceted Search: Dynamic facets for filtering
  • Document Integration: Linked document management
  • Multi-Organization: Tenant and organization isolation

Service Methods

Get Upcoming Shipments

Retrieves shipments that are departing within a specified date range.

Task<PagedResultDtoWithFilters<ShipmentDto>> GetUpcomingShipments(
    PagedAndSortedResultRequestWithFacetOptionsDto input,
    DateTime? startDate = null,
    DateTime? endDate = null
)

Parameters: - input: Pagination, sorting, and filtering options - startDate: Optional start date (defaults to current UTC date) - endDate: Optional end date (defaults to 14 days from now)

Returns: Paginated list of upcoming shipments with filters

Authorization: Requires HubPermissions.Shipment.View

Filters Applied: - Estimated departure within date range - Has estimated arrival - Has departure and arrival ports - Has transport mode (Air or Sea with FCL/LCL) - Has container mode

Example:

var result = await _shipmentAppService.GetUpcomingShipments(
    new PagedAndSortedResultRequestWithFacetOptionsDto
    {
        SkipCount = 0,
        MaxResultCount = 20,
        Sorting = "EstimatedDeparture DESC",
        IncludeFacets = true
    },
    startDate: DateTime.UtcNow,
    endDate: DateTime.UtcNow.AddDays(30)
);

Console.WriteLine($"Found {result.TotalCount} upcoming shipments");
foreach (var shipment in result.Items)
{
    Console.WriteLine($"Shipment {shipment.Id}: {shipment.FirstConsolLegDeparturePort} → {shipment.LastConsolLegArrivalPort}");
}

Get New Documents

Retrieves recently added documents associated with shipments.

Task<PagedResultDto<NewDocumentDto>> GetNewDocuments(
    PagedAndSortedResultRequestDto input
)

Parameters: - input: Pagination and sorting options

Returns: List of documents created in the last 28 days with shipment details

Authorization: Requires HubPermissions.Shipment.View

Example:

var documents = await _shipmentAppService.GetNewDocuments(
    new PagedAndSortedResultRequestDto
    {
        SkipCount = 0,
        MaxResultCount = 50,
        Sorting = "FileDate DESC"
    }
);

foreach (var doc in documents.Items)
{
    Console.WriteLine($"Document: {doc.FileName} (Shipment: {doc.ShipmentReference})");
}

Standard CRUD Operations

Get Single Shipment

Task<ShipmentDto> GetAsync(Guid id)

Retrieves a single shipment by ID with all related data.

Example:

var shipment = await _shipmentAppService.GetAsync(shipmentId);
Console.WriteLine($"Shipment: {shipment.ShipmentReference}");
Console.WriteLine($"Departure: {shipment.EstimatedDeparture:d}");
Console.WriteLine($"Arrival: {shipment.EstimatedArrival:d}");

Get Shipment List

Task<PagedResultDtoWithFilters<ShipmentDto>> GetListAsync(
    PagedAndSortedResultRequestWithFacetOptionsDto input
)

Retrieves a paginated list of shipments with optional filtering and faceting.

Example:

var shipments = await _shipmentAppService.GetListAsync(
    new PagedAndSortedResultRequestWithFacetOptionsDto
    {
        SkipCount = 0,
        MaxResultCount = 20,
        Sorting = "CreationTime DESC",
        IncludeFacets = true,
        Filters = new Dictionary<string, List<string>>
        {
            { "TransportMode", new List<string> { "Sea" } },
            { "ContainerMode", new List<string> { "FCL", "LCL" } }
        }
    }
);

// Display applied filters
foreach (var filter in shipments.AppliedFilters)
{
    Console.WriteLine($"Filter: {filter.Field} = {string.Join(", ", filter.Values)}");
}

// Display facets for UI
foreach (var facet in shipments.Facets)
{
    Console.WriteLine($"\nFacet: {facet.Field}");
    foreach (var value in facet.Values)
    {
        Console.WriteLine($"  {value.Value} ({value.Count})");
    }
}

Create Shipment

Task<ShipmentDto> CreateAsync(CreateShipmentDto input)

Creates a new shipment.

Authorization: Requires HubPermissions.Shipment.Create

Example:

var newShipment = await _shipmentAppService.CreateAsync(
    new CreateShipmentDto
    {
        ShipmentReference = "SHP-2024-001",
        EstimatedDeparture = DateTime.UtcNow.AddDays(7),
        EstimatedArrival = DateTime.UtcNow.AddDays(30),
        FirstConsolLegDeparturePortId = portOfLoadingId,
        LastConsolLegArrivalPortId = portOfDischargeId,
        TransportModeId = seaTransportId,
        ContainerModeId = fclModeId,
        CargoDescription = "Electronics",
        TotalPackages = 100,
        TotalGrossWeight = 5000.0
    }
);

Console.WriteLine($"Created shipment: {newShipment.Id}");

Update Shipment

Task<ShipmentDto> UpdateAsync(Guid id, UpdateShipmentDto input)

Updates an existing shipment.

Authorization: Requires HubPermissions.Shipment.Edit

Example:

var updated = await _shipmentAppService.UpdateAsync(
    shipmentId,
    new UpdateShipmentDto
    {
        EstimatedDeparture = DateTime.UtcNow.AddDays(10),
        EstimatedArrival = DateTime.UtcNow.AddDays(35),
        TotalPackages = 120
    }
);

Delete Shipment

Task DeleteAsync(Guid id)

Soft deletes a shipment (if auditing is enabled).

Authorization: Requires HubPermissions.Shipment.Delete

Example:

await _shipmentAppService.DeleteAsync(shipmentId);

Domain Model

Shipment Entity

public class Shipment : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
    // Identification
    public string ShipmentReference { get; set; }
    public Guid? TenantId { get; set; }

    // Dates
    public DateTime? CargoReadyDate { get; set; }
    public DateTime? EstimatedPickup { get; set; }
    public DateTime? EstimatedDeparture { get; set; }
    public DateTime? EstimatedArrival { get; set; }
    public DateTime? ActualDeparture { get; set; }
    public DateTime? ActualArrival { get; set; }

    // Ports
    public Guid? FirstConsolLegDeparturePortId { get; set; }
    public Port FirstConsolLegDeparturePort { get; set; }

    public Guid? LastConsolLegArrivalPortId { get; set; }
    public Port LastConsolLegArrivalPort { get; set; }

    // Transport
    public Guid? TransportModeId { get; set; }
    public CodeEntity TransportMode { get; set; }

    public Guid? ContainerModeId { get; set; }
    public CodeEntity ContainerMode { get; set; }

    // Cargo
    public string CargoDescription { get; set; }
    public int? TotalPackages { get; set; }
    public double? TotalGrossWeight { get; set; }
    public double? TotalVolume { get; set; }

    // Environmental
    public double? Co2Emissions { get; set; }

    // Relations
    public ICollection<ShipmentAddress> Addresses { get; set; }
    public ICollection<Document> Documents { get; set; }
    public ICollection<Order> Orders { get; set; }
}

DTOs

ShipmentDto

public class ShipmentDto
{
    public Guid Id { get; set; }
    public string ShipmentReference { get; set; }
    public DateTime? EstimatedDeparture { get; set; }
    public DateTime? EstimatedArrival { get; set; }
    public PortDto FirstConsolLegDeparturePort { get; set; }
    public PortDto LastConsolLegArrivalPort { get; set; }
    public CodeEntityDto TransportMode { get; set; }
    public CodeEntityDto ContainerMode { get; set; }
    public string CargoDescription { get; set; }
    public int? TotalPackages { get; set; }
    public double? TotalGrossWeight { get; set; }
    // ... more properties
}

CreateShipmentDto

public class CreateShipmentDto
{
    [Required]
    [StringLength(100)]
    public string ShipmentReference { get; set; }

    public DateTime? EstimatedDeparture { get; set; }
    public DateTime? EstimatedArrival { get; set; }

    public Guid? FirstConsolLegDeparturePortId { get; set; }
    public Guid? LastConsolLegArrivalPortId { get; set; }

    public Guid? TransportModeId { get; set; }
    public Guid? ContainerModeId { get; set; }

    [StringLength(500)]
    public string CargoDescription { get; set; }

    [Range(0, int.MaxValue)]
    public int? TotalPackages { get; set; }

    [Range(0, double.MaxValue)]
    public double? TotalGrossWeight { get; set; }
}

Filtering and Faceting

Organization Filtering

All shipment queries are automatically filtered by the current user's organization context:

protected override async Task<IQueryable<Shipment>> WithDetailsAsync()
{
    var query = await base.WithDetailsAsync();
    var filterCtx = Get<CurrentFilterContextProvider>();

    // Apply organization filter
    return query.ApplyOrganizationFilter(filterCtx);
}

Document Filtering

Documents can be conditionally included based on permissions:

return query.IncludeFilteredDocumentsIf(
    filterCtx,
    await AuthorizationService.IsGrantedAnyAsync(HubPermissions.Document.View)
);

Dynamic Facets

The service supports dynamic facet generation for: - Transport Mode - Container Mode - Departure Port - Arrival Port - Status - Organization

Performance Considerations

Tracking

For read-only operations, tracking is disabled:

using (shipmentRepository.DisableTracking())
{
    var shipments = await shipmentRepository.GetListAsync();
    // No change tracking overhead
}

Eager Loading

Related entities are eagerly loaded to avoid N+1 queries:

var queryable = await _repository.GetQueryableAsync();
queryable = queryable
    .Include(s => s.FirstConsolLegDeparturePort)
    .Include(s => s.LastConsolLegArrivalPort)
    .Include(s => s.TransportMode)
    .Include(s => s.ContainerMode);

Pagination

Always use pagination for large datasets:

var query = queryable
    .OrderBy(sorting)
    .Skip(skipCount)
    .Take(maxResultCount);

Testing

Unit Test Example

public class ShipmentAppService_Tests : HubApplicationTestBase
{
    private readonly IShipmentAppService _shipmentAppService;
    private readonly IRepository<Shipment, Guid> _shipmentRepository;

    public ShipmentAppService_Tests()
    {
        _shipmentAppService = GetRequiredService<IShipmentAppService>();
        _shipmentRepository = GetRequiredService<IRepository<Shipment, Guid>>();
    }

    [Fact]
    public async Task Should_Get_Upcoming_Shipments()
    {
        // Arrange
        var testShipment = await _shipmentRepository.InsertAsync(
            new Shipment
            {
                ShipmentReference = "TEST-001",
                EstimatedDeparture = DateTime.UtcNow.AddDays(7),
                EstimatedArrival = DateTime.UtcNow.AddDays(30)
            }
        );

        // Act
        var result = await _shipmentAppService.GetUpcomingShipments(
            new PagedAndSortedResultRequestWithFacetOptionsDto
            {
                MaxResultCount = 10
            }
        );

        // Assert
        result.ShouldNotBeNull();
        result.TotalCount.ShouldBeGreaterThan(0);
        result.Items.ShouldContain(s => s.Id == testShipment.Id);
    }
}

REST API Endpoints

The service is automatically exposed via REST API:

GET    /api/hub/shipment/{id}
GET    /api/hub/shipment
GET    /api/hub/shipment/upcoming-shipments
GET    /api/hub/shipment/new-documents
POST   /api/hub/shipment
PUT    /api/hub/shipment/{id}
DELETE /api/hub/shipment/{id}

Best Practices

  1. Always use pagination for list operations
  2. Apply organization filters to respect data isolation
  3. Use tracking selectively - disable for read-only operations
  4. Validate dates - ensure departure is before arrival
  5. Handle missing data - ports and transport modes may be optional
  6. Log important operations - especially create, update, delete
  7. Test with multiple organizations - verify isolation

See Also