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".
-
"PO 1"(Incoming) ("Αποθήκη Α"->"Supermarket A") -
Split σε Freight Unit
"FU 1"("Αποθήκη Α"->"Supermarket A") -
Pre-routing σε Freight Order
"FO 1"("Αποθήκη Α"->"Supermarket A") -
Plan a trip
"TR 1"("Αποθήκη Α"->"Supermarket A") -
Forward trip
"TR 1"to"Mytis"
Δημιουργείται το
"PO 2"purchase order. Το πεδίο"OutgoingPurchaseOrderId"του Trip"TR 1"τώρα έχει την τιμή"PO 2".
- Δημιουργία αντίγραφου purchase order με κωδικό
"PO 2"στον tenant του"Mytis".
Scenario B
Η "ISOMAT" παραλαμβάνει τα ψυγεία, ας πούμε από το "Αποθήκη Α", τα μεταφέρει ως το "Πρακτορείο Mytis" και αναθέτει την υπόλοιπη παραγγελία στον συνεργάτη της, "Mytis".
-
"PO 1"(Incoming) ("Αποθήκη Α"->"Supermarket A") -
Split σε Freight Unit
"FU 1"("Αποθήκη Α"->"Supermarket A") -
Pre-routing σε 2 Freight Orders
-
"FO 1"("Αποθήκη Α"->"Πρακτορείο Mytis") -
"FO 2"("Πρακτορείο Mytis"->"Supermarket A")
- Plan a trip
"TR 1"από το"FO 1"("Αποθήκη Α"->"Πρακτορείο Mytis")
Αυτό θα το διεκπαιρεώσει η
"ISOMAT".
-
Plan a trip
"TR 2"από το"FO 2"("Πρακτορείο Mytis"->"Supermarket A") -
Forward trip
"TR 2"to"Mytis"
Δημιουργείται το
"PO 2"purchase order. Το πεδίο"OutgoingPurchaseOrderId"του Trip"TR 2"τώρα έχει την τιμή"PO 2".
- Δημιουργία αντίγραφου 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:
-
UI
-
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
expressis actually important here.
Immediately be able to plan a trip από το Purchase order. Το backend μας θα αναλάβει τη δημιουργία των ενδιάμεσων οντοτήτων (Freight unit, Freight order).
Rejection
Απόρριψη του Purchase order.
Requirements
-
Both
CompaniesMUST be partners with each other. -
Partnersmust have the global"CompanyId".
System architecture
Important changes to current schema
- Προσθήκη πεδίου
"IncomingPurchaseOrderId" (int FK)(Για τα Outgoing μόνο),"TripId" (int FK)(Για τα Outgoing μόνο),"IsOutgoing" (bit)στον πίνακα"FreightUnits".
Το κάθε
"..PurchaseOrderId"θα βλέπει το"OmniId"του άλλου.
- Προσθήκη πεδίου
"OmniId" (Guid)στους πίνακες"Partners","Pois","FreightUnits"(και μελλοντικά παντού).
Forwarding
Αυτή η διαδικασία περιλαμβάνει τη δημιουργία 2 ίδιων Purchase Order, το καθένα σε διαφορετικό Tenant και με διαφορερικό type.
Forwarding - High level περιγραφή
-
Επιλογή entity (Purchase order (incoming), Freight unit, Freight order, Trip) in
"ISOMAT"tenant -
Επιλογή "Assign to partner" in
"ISOMAT"tenant -
Δημιουργία επόμενων entities + Purchase order (outgoing) in
"ISOMAT"tenant -
Δημιουργία Purchase order (incoming) in
"Mytis"tenant
Forwarding - Στην πράξη
Forwarding - Στην πράξη - Endpoints
-
[POST] purchase-orders/{id:int}/forward -
[POST] freight-units/{id:int}/forward -
[POST] freight-orders/{id:int}/forward -
[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-3 κανονικά μέσω της τρέχουσας υλοποίησης.
-
Για το βήμα 4, το Azure function παίρνει το
OmniIdτου Purchase Order (outgoing) από το event hub και το διαβάζει πλήρως. -
Το Azure function επιβεβαιώνει πως το PO είναι Outgoing στον tenant που υπάρχει.
-
Το Azure function δημιουργεί ένα ακριβές αντίγραφο με διαφορετικό
Typeστον target tenant -
Συμπληρώνει το
"ParentOmniId"στο νέο incoming Purchase Order του target tenant (E.g."Mytis") ως το"OmniId"του outgoing Purchase Orderτου current tenant (E.g."ISOMAT"). -
Συμπληρώνει το
"IncomingPurchaseOrderId"στο outgoing Purchase Order του current tenant (E.g."ISOMAT").
Status update
Status update - High level περιγραφή
-
Due to event change (
"Mytis"tenant), Trip status changes in"Mytis"tenant -
Due to Trip status change (
"Mytis"tenant), Freight orders statuses changes in"Mytis"tenant -
Due to each Freight order status change (
"Mytis"tenant), Freight units statuses change in"Mytis"tenant -
Due to each Freight unit status change (
"Mytis"tenant), Purchase orders (incoming) statuses change in"Mytis"tenant -
Due to Purchase order (incoming) status change (
"Mytis"tenant), Purchase order (outgoing) status changes in"ISOMAT"tenant -
Due to Purchase order (outgoing) status change (
"ISOMAT"tenant), Trip status changes in"ISOMAT"tenant -
Due to Trip status change (
"ISOMAT"tenant), Freight orders statuses changes in"ISOMAT"tenant -
Due to each Freight order status change (
"ISOMAT"tenant), Freight units statuses change in"ISOMAT"tenant -
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-4 κανονικά μέσω της τρέχουσας υλοποίησης.
-
Για το βήμα 5, το service βλέπει πως το PurchaseOrder είναι incoming και στέλνει στο event hub.
-
Το Azure function διαβάζει το event από το hub και ανανεώνει το Purchase order (outgoing) του 2ου tenant.
Το matching γίνεται μέσω του Code property.
- Βήματα 6-9 κανονικά μέσω της τρέχουσας υλοποίησης.
Implementation
Εδώ θα βρείτε την υλοποίηση του παραπάνω.
Prerequisites - High level
- 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.
-
When adding a partner, if the partner exists as a separate tenant in the platform, they must receive some kind of notification/request.
-
If they accept, these 2 companies will now be able to interact with each other.
-
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
ForwardDto(new)
public record EntityForwarderDto(object Entity, EntityType EntityType, int CompanyId);
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);
}
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;
}
}
- Add
PublishToEntityReceiverAsynctoIEventHubsService
+ The required producer connection string
- Inherit
ForwardServicein:
-
PurchaseOrdersService -
FreightUnitsService -
FreightOrdersService -
TripsService
Data.EFCore
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");
}
ModelBuilderExtensions
// IOmniEntity
if (typeof(IOmniEntity).IsAssignableFrom(entityClrType))
{
var applyMethod = typeof(EntityTypeBuilderExtensions)
.GetMethod(nameof(EntityTypeBuilderExtensions.ApplyOmniEntity))
?.MakeGenericMethod(entityClrType);
applyMethod?.Invoke(null, [entityTypeBuilder]);
}
FreightUnitBaseConfig
Not going to write this here, too...
Straight to the code
Domain
IOmniEntity(new)
public interface IOmniEntity
{
Guid OmniId { get; set; }
}
EntityTypeenum
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
-
Get all entities (
ForwardDto) -
Foreach, check type using
EntityTypeenum -
Handle each type in a switch block
Frontend
There should be 2 main features here.
Also, TODOs:
- Visual differences between incoming / outgoing PurchaseOrders
Relate partner to another tenant
TODO: To be discussed.
Assign entity to partner
- Grid button
-
Icon:
heroicons_solid:arrow-uturn-right -
Tooltip:
"Assign to partner"
- 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".