Skip to main content

POFUFO

(Purchase Orders Freight Units Freight Orders)

Meeting 2025-12-01

Notes from Stavros


M2M: A -> B -> C

A -> B: Isomat

B -> C: Mytis

-----------------



Isomat

PO incoming A -> C

FU A -> C

FO1 A -> B

FO2 B -> C

Trip1 A -> B

Trip2 B -> C (external)

PO outgoing B -> C (to Mytis) (code 342343, linked to Trip2)



Mytis

PO incoming B -> C (from Isomat) (code 342343)

FU B -> C

FO B -> C

Trip B -> C

Meeting 2025-12-04

High level

Use case

Μια εταιρεία "Company A" κάνει παραγγελία από την "ISOMAT" να μεταφέρει 3 ψυγεία που βρίσκονται στο "Αποθήκη Α" μέχρι το "Supermarket A".

Αυτή η παραγγελία παρουσιάζεται στην "ISOMAT" ως "PO 1" όπου PoiTo = "Supermarket A".

Scenario A

Η "ISOMAT" αναθέτει όλη την παραγγελία στον συνεργάτη της, "Mytis".

  1. "PO 1" (Incoming) ("Αποθήκη Α" -> "Supermarket A")

  2. Split σε Freight Unit "FU 1" ("Αποθήκη Α" -> "Supermarket A")

  3. Pre-routing σε Freight Order "FO 1" ("Αποθήκη Α" -> "Supermarket A")

  4. Plan a trip "TR 1" ("Αποθήκη Α" -> "Supermarket A")

  5. Forward trip "TR 1" to "Mytis"

Δημιουργείται το "PO 2" purchase order. Το πεδίο "OutgoingPurchaseOrderId" του Trip "TR 1" τώρα έχει την τιμή "PO 2".

  1. Δημιουργία αντίγραφου purchase order με κωδικό "PO 2" στον tenant του "Mytis".

Scenario B

Η "ISOMAT" παραλαμβάνει τα ψυγεία, ας πούμε από το "Αποθήκη Α", τα μεταφέρει ως το "Πρακτορείο Mytis" και αναθέτει την υπόλοιπη παραγγελία στον συνεργάτη της, "Mytis".

  1. "PO 1" (Incoming) ("Αποθήκη Α" -> "Supermarket A")

  2. Split σε Freight Unit "FU 1" ("Αποθήκη Α" -> "Supermarket A")

  3. Pre-routing σε 2 Freight Orders

  • "FO 1" ("Αποθήκη Α" -> "Πρακτορείο Mytis")

  • "FO 2" ("Πρακτορείο Mytis" -> "Supermarket A")

  1. Plan a trip "TR 1" από το "FO 1" ("Αποθήκη Α" -> "Πρακτορείο Mytis")

Αυτό θα το διεκπαιρεώσει η "ISOMAT".

  1. Plan a trip "TR 2" από το "FO 2" ("Πρακτορείο Mytis" -> "Supermarket A")

  2. Forward trip "TR 2" to "Mytis"

Δημιουργείται το "PO 2" purchase order. Το πεδίο "OutgoingPurchaseOrderId" του Trip "TR 2" τώρα έχει την τιμή "PO 2".

  1. Δημιουργία αντίγραφου purchase order με κωδικό "PO 2" στον tenant του "Mytis".

Scenario C

Hidden for now but it will be implemented eventually after the base logic complete.

Automatic forwarding of an incoming Purchase order through:

  1. UI

  2. M2M API

This updated version will also be given to ISOMAT instead of the current custom implementation.

Σε αυτή την περίπτωση, δημιουργούμε όλα τα ενδιάμεσα στάδια μέσω backend service για να καταλήξουμε από το "PO 1" στο "PO 2" όπως περιγράφεται στο Scenario A.

What's next

Και στα 3 σενάρια ως τώρα, έχουμε φτάσει στο σημείο που ο Partner γνωστός ως "Forwarding agent" (π.χ. "Mytis") έχει ένα incoming Purchase order. Πως θα το διαχειριστεί αυτό λοιπόν?

Στην πράξη, η "Forwarding company" (π.χ. "ISOMAT") δεν πρέπει να έχει το δικαίωμα να προγραμματίσει το ίδιο το trip του "Forwarding agent".

Επιλογές:

Manual planning

Όταν υπάρχουν incoming Purchase orders, το πλήρες σενάριο είναι η Split to Freights units, Prerouting / Combine to Freight orders, Plan a trip, ...

Express planning

The term express is actually important here.

Immediately be able to plan a trip από το Purchase order. Το backend μας θα αναλάβει τη δημιουργία των ενδιάμεσων οντοτήτων (Freight unit, Freight order).

Rejection

Απόρριψη του Purchase order.

Requirements

  • Both Companies MUST be partners with each other.

  • Partners must have the global "CompanyId".

System architecture

Important changes to current schema

  1. Προσθήκη πεδίου "IncomingPurchaseOrderId" (int FK) (Για τα Outgoing μόνο), "TripId" (int FK) (Για τα Outgoing μόνο), "IsOutgoing" (bit) στον πίνακα "FreightUnits".

Το κάθε "..PurchaseOrderId" θα βλέπει το "OmniId" του άλλου.

  1. Προσθήκη πεδίου "OmniId" (Guid) στους πίνακες "Partners", "Pois", "FreightUnits" (και μελλοντικά παντού).

Forwarding

Αυτή η διαδικασία περιλαμβάνει τη δημιουργία 2 ίδιων Purchase Order, το καθένα σε διαφορετικό Tenant και με διαφορερικό type.

Forwarding - High level περιγραφή

  1. Επιλογή entity (Purchase order (incoming), Freight unit, Freight order, Trip) in "ISOMAT" tenant

  2. Επιλογή "Assign to partner" in "ISOMAT" tenant

  3. Δημιουργία επόμενων entities + Purchase order (outgoing) in "ISOMAT" tenant

  4. Δημιουργία Purchase order (incoming) in "Mytis" tenant

Forwarding - Στην πράξη

Forwarding - Στην πράξη - Endpoints
  1. [POST] purchase-orders/{id:int}/forward

  2. [POST] freight-units/{id:int}/forward

  3. [POST] freight-orders/{id:int}/forward

  4. [POST] trips/{id:int}/forward

Request body


{

"partnerId": "integer"

}

Forwarding - Στην πράξη - User Interface
  • Στα grid των ενδιαφερόμενων entities, θα μπει κουμπί "Forward".

  • Θα ενεργοποιείται όταν υπάρχουν >= 1 επιλεγμένες οντότητες.

  • Θα ανοίγει modal για την επιλογή του Partner (Forwarding agent).

  • Θα καλείται το αντίστοιχο endpoint.

Forwarding - Στην πράξη - Logic

Suggested implementation: Azure function + event hub

  1. Βήματα 1-3 κανονικά μέσω της τρέχουσας υλοποίησης.

  2. Για το βήμα 4, το Azure function παίρνει το OmniId του Purchase Order (outgoing) από το event hub και το διαβάζει πλήρως.

  3. Το Azure function επιβεβαιώνει πως το PO είναι Outgoing στον tenant που υπάρχει.

  4. Το Azure function δημιουργεί ένα ακριβές αντίγραφο με διαφορετικό Type στον target tenant

  5. Συμπληρώνει το "ParentOmniId" στο νέο incoming Purchase Order του target tenant (E.g. "Mytis") ως το "OmniId" του outgoing Purchase Orderτου current tenant (E.g. "ISOMAT").

  6. Συμπληρώνει το "IncomingPurchaseOrderId" στο outgoing Purchase Order του current tenant (E.g. "ISOMAT").

Status update

Status update - High level περιγραφή

  1. Due to event change ("Mytis" tenant), Trip status changes in "Mytis" tenant

  2. Due to Trip status change ("Mytis" tenant), Freight orders statuses changes in "Mytis" tenant

  3. Due to each Freight order status change ("Mytis" tenant), Freight units statuses change in "Mytis" tenant

  4. Due to each Freight unit status change ("Mytis" tenant), Purchase orders (incoming) statuses change in "Mytis" tenant

  5. Due to Purchase order (incoming) status change ("Mytis" tenant), Purchase order (outgoing) status changes in "ISOMAT" tenant

  6. Due to Purchase order (outgoing) status change ("ISOMAT" tenant), Trip status changes in "ISOMAT" tenant

  7. Due to Trip status change ("ISOMAT" tenant), Freight orders statuses changes in "ISOMAT" tenant

  8. Due to each Freight order status change ("ISOMAT" tenant), Freight units statuses change in "ISOMAT" tenant

  9. Due to each Freight unit status change ("ISOMAT" tenant), Purchase orders (incoming) statuses change in "ISOMAT" tenant

9 βήματα, 2 tenants.

Status update - Στην πράξη

Obvious implementation: Azure function + event hub

  1. Βήματα 1-4 κανονικά μέσω της τρέχουσας υλοποίησης.

  2. Για το βήμα 5, το service βλέπει πως το PurchaseOrder είναι incoming και στέλνει στο event hub.

  3. Το Azure function διαβάζει το event από το hub και ανανεώνει το Purchase order (outgoing) του 2ου tenant.

Το matching γίνεται μέσω του Code property.

  1. Βήματα 6-9 κανονικά μέσω της τρέχουσας υλοποίησης.

Implementation

Εδώ θα βρείτε την υλοποίηση του παραπάνω.

Prerequisites - High level

  1. Partners

We have to change the logic behind adding partners.

Yes, it is nice to have partners in the current way we handle them, as simple information etc.

We have to add interactivity.

  1. When adding a partner, if the partner exists as a separate tenant in the platform, they must receive some kind of notification/request.

  2. If they accept, these 2 companies will now be able to interact with each other.

  3. If they refuse, forwarding trips and other future interactivity should not be allowed.

Database


alter table Assets

add OmniId nvarchar(36)

go



alter table Assets

add OmniPartnerId int

go



alter table Assets

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Assets

add constraint FK_Assets_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_Assets_OmniId_OwnerId

on Assets (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table DictionaryItems

add OmniId nvarchar(36)

go



alter table DictionaryItems

add OmniPartnerId int

go



alter table DictionaryItems

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table DictionaryItems

add constraint FK_DictionaryItems_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_DictionaryItems_OmniId_OwnerId

on DictionaryItems (OmniId, OwnerId)

where OmniId is not null;

go



alter table Documents

add OmniId nvarchar(36)

go



alter table Documents

add OmniPartnerId int

go



alter table Documents

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Documents

add constraint FK_Documents_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_Documents_OmniId_OwnerId

on Documents (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table FreightUnits

add OmniId nvarchar(36)

go



alter table FreightUnits

add OmniPartnerId int

go



alter table FreightUnits

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table FreightUnits

add IsOutgoing bit default 0 not null

go



alter table FreightUnits

add IncomingPurchaseOrderId int

go



alter table FreightUnits

add OutgoingPurchaseOrderOmniId nvarchar(36)

go



alter table FreightUnits

add TripId int

go



alter table FreightUnits

add constraint FK_FreightUnits_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



alter table FreightUnits

add constraint FK_FreightUnits_IncomingPurchaseOrderId

foreign key (IncomingPurchaseOrderId)

references FreightUnits (Id)

on delete no action;

go



alter table FreightUnits

add constraint FK_FreightUnits_TripId

foreign key (TripId)

references Trips (Id)

on delete set null;

go



create unique index IX_FreightUnits_OmniId_OwnerId

on FreightUnits (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table Partners

add OmniId nvarchar(36)

go



alter table Partners

add OmniPartnerId int

go



alter table Partners

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Partners

add CompanyId int

go



alter table Partners

add constraint FK_Partners_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



alter table Partners

add constraint FK_Partners_CompanyId

foreign key (CompanyId)

references Companies (Id)

on delete set null;

go



create unique index IX_Partners_OmniId_OwnerId

on Partners (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table Persons

add OmniId nvarchar(36)

go



alter table Persons

add OmniPartnerId int

go



alter table Persons

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Persons

add constraint FK_Persons_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_Persons_OmniId_OwnerId

on Persons (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table Pois

add OmniId nvarchar(36)

go



alter table Pois

add OmniPartnerId int

go



alter table Pois

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Pois

add constraint FK_Pois_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_Pois_OmniId_OwnerId

on Pois (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table ProofOfDelivery

add OmniId nvarchar(36)

go



alter table ProofOfDelivery

add OmniPartnerId int

go



alter table ProofOfDelivery

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table ProofOfDelivery

add constraint FK_ProofOfDelivery_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



alter table ProofOfDelivery

add OwnerId int

go



alter table ProofOfDelivery

add constraint FK_ProofOfDelivery_OwnerId

foreign key (OwnerId) references Companies

go



UPDATE ProofOfDelivery

SET OwnerId = FU.OwnerId

FROM ProofOfDelivery POD

JOIN FreightUnits FU ON POD.FreightUnitId = FU.Id

WHERE POD.OwnerId IS NULL

go



alter table ProofOfDelivery

alter column OwnerId int not null

go



create unique index IX_ProofOfDelivery_OmniId_OwnerId

on ProofOfDelivery (OmniId, OwnerId)

where OmniId is not null;

go



alter table Trips

add OmniId nvarchar(36)

go



alter table Trips

add OmniPartnerId int

go



alter table Trips

add OmniPartnersSharedWithIds nvarchar(max)

go



alter table Trips

add constraint FK_Trips_OmniPartnerId

foreign key (OmniPartnerId)

references Partners (Id)

on delete no action;

go



create unique index IX_Trips_OmniId_OwnerId

on Trips (OmniId, OwnerId)

where OmniId is not null and IsDeleted = 0;

go



alter table FreightUnits add ActualPickupFrom datetime2 go

alter table FreightUnits add ActualPickupTo datetime2 go

alter table FreightUnits add PickupEta datetime2 go

alter table FreightUnits add ActualDeliveryFrom datetime2 go

alter table FreightUnits add ActualDeliveryTo datetime2 go

alter table FreightUnits add DeliveryEta datetime2 go

alter table FreightOrders add ActualPickupFrom datetime2 go

alter table FreightOrders add ActualPickupTo datetime2 go

alter table FreightOrders add PickupEta datetime2 go

alter table FreightOrders add ActualDeliveryFrom datetime2 go

alter table FreightOrders add ActualDeliveryTo datetime2 go

alter table FreightOrders add DeliveryEta datetime2 go

Backend

Application

  1. ForwardDto (new)

public record EntityForwarderDto(object Entity, EntityType EntityType, int CompanyId);

  1. IForwardService (new)

public interface IEntityForwarderService<TEntity, in TKey, TViewDto, TRepository>

where TEntity : class, IEntity<TKey>, IOmniEntity

where TViewDto : class

where TRepository : IRepository<TEntity, TKey, TViewDto>

{

/// <summary>

/// Forwards an entity to another tenant.

/// </summary>

/// <param name="id">The id of the entity to forward.</param>

/// <param name="entityType">The type of the entity in a runtime-accessible enum.</param>

/// <param name="partnerId">The id of the partner to receive this entity.</param>

/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>

Task<TViewDto> ForwardAsync(

TKey id,

EntityType entityType,

int partnerId,

CancellationToken cancellationToken = default);

}

  1. ForwardService (new)

public sealed class EntityForwarderService<TEntity, TKey, TViewDto, TRepository>(

IEventHubsService eventHubsService,

ILogger logger,

IMapper mapper,

IPartnersRepository partnersRepository,

TRepository repository)

: IEntityForwarderService<TEntity, TKey, TViewDto, TRepository>

where TEntity : class, IEntity<TKey>, IOmniEntity

where TViewDto : class

where TRepository : IRepository<TEntity, TKey, TViewDto>

{

public async Task<TViewDto> ForwardAsync(

TKey id,

EntityType entityType,

int partnerId,

CancellationToken cancellationToken = default)

{

var entity = await repository.GetByIdAsync(id, untracked: true, cancellationToken: cancellationToken)

?? throw new RtmApiNotFoundException($"Entity of type '{typeof(TEntity).Name}' with ID '{id}' not found.");

var partner = await partnersRepository.GetByIdAsync(partnerId, untracked: true, cancellationToken: cancellationToken)

?? throw new RtmApiNotFoundException($"Partner with ID '{partnerId}' not found.");



if (partner.CompanyId is null)

throw new RtmApiBadRequestException($"Partner with ID '{partnerId}' is not related to a registered company.");



var forwardDto = new EntityForwarderDto(

Entity: entity,

EntityType: entityType,

CompanyId: (int)partner.CompanyId);



await eventHubsService.PublishToEntityReceiverAsync(forwardDto, logger, cancellationToken);



var dto = mapper.Map<TViewDto>(entity);



return dto;

}

}

  1. Add PublishToEntityReceiverAsync to IEventHubsService

+ The required producer connection string

  1. Inherit ForwardService in:
  • PurchaseOrdersService

  • FreightUnitsService

  • FreightOrdersService

  • TripsService

Data.EFCore

  1. EntityTypeBuilderExtensions

public static void ApplyOmniEntity<T>(this EntityTypeBuilder<T> builder) where T : class, IOmniEntity

{

// Get builder's table name

var tableName = builder.Metadata.GetTableName();



builder.Property(entity => entity.OmniId)

.HasConversion<string>()

.HasMaxLength(36)

.HasColumnName("OmniId")

.IsRequired();



if (builder.Metadata.ClrType.IsAssignableTo(typeof(IHasOwner)))

builder.HasIndex(entity => new {

((IHasOwner)entity).OwnerId,

entity.OmniId

})

.IsUnique()

.HasDatabaseName($"IX_{tableName}_OmniId_OwnerId");

else if (builder.Metadata.ClrType.IsAssignableTo(typeof(IHasOptionalOwner)))

builder.HasIndex(entity => new {

((IHasOptionalOwner)entity).OwnerId,

entity.OmniId

})

.IsUnique()

.HasDatabaseName($"IX_{tableName}_OmniId_OwnerId");

else

builder.HasIndex(entity => entity.OmniId)

.IsUnique()

.HasDatabaseName($"IX_{tableName}_OmniId");

}

  1. ModelBuilderExtensions

// IOmniEntity

if (typeof(IOmniEntity).IsAssignableFrom(entityClrType))

{

var applyMethod = typeof(EntityTypeBuilderExtensions)

.GetMethod(nameof(EntityTypeBuilderExtensions.ApplyOmniEntity))

?.MakeGenericMethod(entityClrType);



applyMethod?.Invoke(null, [entityTypeBuilder]);

}

  1. FreightUnitBaseConfig

Not going to write this here, too...

Straight to the code

Domain

  1. IOmniEntity (new)

public interface IOmniEntity

{

Guid OmniId { get; set; }

}

  1. EntityType enum

PurchaseOrder = 11,

FreightUnit = 12,

FreightOrder = 13,

Tracing.API

  • PurchaseOrdersController

[HttpPost("{id:int}/forward")]

public async Task<ActionResult> ForwardPurchaseOrder(

[FromRoute(Name = "id")] int id,

[FromQuery(Name = "partnerId")] int partnerId,

CancellationToken cancellationToken = default)

{

var dto = await purchaseOrdersService.ForwardAsync(id, EntityType.PurchaseOrder, partnerId, cancellationToken);

// if IsOutgoing = true, calls base.ForwardAsync(id, EntityType.PurchaseOrder, partnerId, cancellationToken);

// else, creates FreightUnit, FreightOrder, then Trip and then Outgoing PO and forwards it



return Ok(dto);

}

  • FreightUnitsController

[HttpPost("{id:int}/forward-purchase-order")]

public async Task<ActionResult> ForwardPurchaseOrder(

[FromRoute(Name = "id")] int id,

[FromQuery(Name = "partnerId")] int partnerId,

CancellationToken cancellationToken = default)

{

var dto = await freightUnitsService.ForwardPurchaseOrderAsync(id, partnerId, cancellationToken);

// Creates FreightOrder, then Trip and then Outgoing PO and forwards it



return Ok(dto);

}

  • FreightOrdersController

[HttpPost("{id:int}/forward-purchase-order")]

public async Task<ActionResult> ForwardPurchaseOrder(

[FromRoute(Name = "id")] int id,

[FromQuery(Name = "partnerId")] int partnerId,

CancellationToken cancellationToken = default)

{

var dto = await freightOrdersService.ForwardPurchaseOrderAsync(id, partnerId, cancellationToken);

// Creates Trip and then Outgoing PO and forwards it



return Ok(dto);

}

  • TripsController

[HttpPost("{id:int}/forward")]

public async Task<ActionResult> ForwardTrip(

[FromRoute(Name = "id")] int id,

[FromQuery(Name = "partnerId")] int partnerId,

CancellationToken cancellationToken = default)

{

var dto = await tripsService.ForwardAsync(id, EntityType.Trip, partnerId, cancellationToken);



return Ok(dto);

}



[HttpPost("{id:int}/forward-purchase-order")]

public async Task<ActionResult> ForwardPurchaseOrder(

[FromRoute(Name = "id")] int id,

[FromQuery(Name = "partnerId")] int partnerId,

CancellationToken cancellationToken = default)

{

var dto = await tripsService.ForwardAsPurchaseOrdersAsync(id, partnerId, cancellationToken);

// Creates Outgoing PO and forwards it



return Ok(dto);

}

EntityReceiver

EntityReceiver - High level
  1. Get all entities (ForwardDto)

  2. Foreach, check type using EntityType enum

  3. Handle each type in a switch block

Frontend

There should be 2 main features here.

  1. Relate partner to another tenant

  2. Forward entity to partner

Also, TODOs:

  • Visual differences between incoming / outgoing PurchaseOrders

Relate partner to another tenant

TODO: To be discussed.

Assign entity to partner

  1. Grid button
  • Icon: heroicons_solid:arrow-uturn-right

  • Tooltip: "Assign to partner"

  1. Modal

Opens when the above button is pressed.

It will include some helper texts and a select box to choose a partner.

This should be done either by "Name" or "VAT number". Most likely the "Name".