Initial commit

develop
Matija Koželj 2 years ago
parent c5a0428600
commit 8aea377d60

@ -0,0 +1,5 @@
namespace Logistics.Documents;
public static class DocumentUrls
{
public const string Receives = "/logistics/documents/receives";
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Common\Common.Model\Common.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Logistics.Documents</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Common.Model\Common.Model.csproj" />
<ProjectReference Include="..\..\Customers\Contacts.Types.Model\Contacts.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,12 @@
using Common.Documents;
namespace Logistics.Documents.Receive;
public interface IReceiveDocument : IDocument<int>
{
int? Supplier { get; init; }
DateTimeOffset? ReceiveDate { get; init; }
int ItemCount { get; init; }
int OpenItemCount { get; init; }
}

@ -0,0 +1,84 @@
using System.Collections.Immutable;
using Common.Documents;
using Connected.Annotations;
using Connected.ServiceModel;
namespace Logistics.Documents.Receive;
/// <summary>
/// Represents service for the <see cref="IReceiveDocument"/> document.
/// </summary>
[Service]
[ServiceUrl(DocumentUrls.Receives)]
public interface IReceiveDocumentService : IDocumentService<int, long>
{
/// <summary>
/// Inserts a new <see cref="IReceiveDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the properties of the new document.</param>
/// <returns>The id of the newly inserted document.</returns>
Task<int> Insert(InsertReceiveDocumentArgs args);
/// <summary>
/// Updates <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing changed properties of the document.</param>
Task Update(UpdateReceiveDocumentArgs args);
/// <summary>
/// Performs partial update on the <see cref="IReceiveDocument"/> for the properties specified
/// in arguments.
/// </summary>
/// <param name="args">The arguments containing properties that need to be updated.</param>
Task Patch(PatchArgs<int> args);
/// <summary>
/// Deletes <see cref="IReceiveDocument"/> from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the document that is about to be deleted.</param>
Task Delete(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects <see cref="IReceiveDocument"/> for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id.</param>
/// <returns><see cref="IReceiveDocument"/> if found, <c>null</c> otherwise.</returns>
Task<IReceiveDocument?> Select(PrimaryKeyArgs<int> args);
/// <summary>
/// Searches <see cref="IReceiveDocument">documents</see> for the specified criteria.
/// </summary>
/// <param name="args">The arguments containing the query criteria.</param>
/// <returns>The list of <see cref="IReceiveDocument"/> documents that matches the search criteria.</returns>
Task<ImmutableList<IReceiveDocument>> Query(QueryArgs? args);
/// <summary>
/// Inserts a new <see cref="IReceiveItem"/> into the <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the properties of the new item.</param>
/// <returns>The id of the newly inserted <see cref="IReceiveItem"/> item.</returns>
Task<long> InsertItem(InsertReceiveItemArgs args);
/// <summary>
/// Updates <see cref="IReceiveItem"/>.
/// </summary>
/// <param name="args">The arguments containing the properties to be updated.</param>
Task UpdateItem(UpdateReceiveItemArgs args);
/// <summary>
/// Permanently deleted the <see cref="IReceiveItem"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the item to be deleted.</param>
Task DeleteItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Queries the <see cref="IReceiveItem"/> items for the specified <see cref="IReceiveDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the document for which the items to be
/// queried.</param>
/// <returns>The list of items that belong to the specified document.</returns>
Task<ImmutableList<IReceiveItem>> QueryItems(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects the <see cref="IReceiveItem"/> item for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the item.</param>
/// <returns>The <see cref="IReceiveItem"/> if found, <c>null</c> otherwise.</returns>
Task<IReceiveItem?> SelectItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Select the <see cref="IReceiveItem"/> for the specified entity and entity id from the
/// specified document.
/// </summary>
/// <param name="args">The arguments containing criteria values.</param>
/// <returns>A first <see cref="IReceiveItem"/> that matches the criteria, <c>null</c> otherwise.</returns>
Task<IReceiveItem?> SelectItem(SelectReceiveItemArgs args);
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Logistics.Documents.Receive;
public interface IReceiveItem : IEntityContainer<long>
{
int Document { get; init; }
float Quantity { get; init; }
float PostedQuantity { get; init; }
}

@ -0,0 +1,41 @@
using Connected.Data;
namespace Logistics.Documents.Receive;
/// <summary>
/// Represents connected (many-to-many) entity between <see cref="IReceivePostingDocument"/>
/// and <see cref="IReceiveItem"/>.
/// </summary>
/// <remarks>
/// Master receive document contains one or more <see cref="IReceiveItem"/> items. Receive document
/// is then divided into one or more <see cref="IReceivePostingDocument"/> documents which contain
/// two lists of items:
/// <list type="bullet">
/// <item><see cref="IReceivePlannedItem"/></item>
/// <item><see cref="IReceivePostingItem"/></item>
/// </list>
/// This entity represents planned items which represents the plan of how what kind of item and how
/// much should be posted to each <see cref="IReceivePostingDocument"/>. This acts only as a guide to the user
/// not the actual items and quantities that arrived into warehouse.
/// </remarks>
public interface IReceivePlannedItem : IPrimaryKey<long>
{
/// <summary>
/// The id of the <see cref="IReceivePostingDocument"/> to which
/// this planned entity belongs.
/// </summary>
int Document { get; init; }
/// <summary>
/// The id of the <see cref="IReceiveItem"/> item to which
/// this planned entity belongs.
/// </summary>
int Item { get; init; }
/// <summary>
/// The planned entity which should be posted into this
/// item.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The actual posted quantity for this item.
/// </summary>
float PostedQuantity { get; init; }
}

@ -0,0 +1,10 @@
using Common.Documents;
namespace Logistics.Documents.Receive;
public interface IReceivePostingDocument : IDocument<int>
{
int Document { get; init; }
int OpenItemCount { get; init; }
int ItemCount { get; init; }
}

@ -0,0 +1,84 @@
using System.Collections.Immutable;
using Common.Documents;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Logistics.Documents.Receive;
/// <summary>
/// Represents service for the <see cref="IReceivePostingDocument"/> document.
/// </summary>
[Service]
[ServiceUrl(DocumentUrls.Receives)]
public interface IReceivePostingDocumentService : IDocumentService<int, long>
{
event ServiceEventHandler<PrimaryKeyEventArgs<long>> PlannedItemUpdated;
/// <summary>
/// Inserts a new <see cref="IReceivePostingDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the properties of the new document.</param>
/// <returns>The id of the newly inserted document.</returns>
Task<int> Insert(InsertReceivePostingDocumentArgs args);
/// <summary>
/// Updates <see cref="IReceivePostingDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing changed properties of the document.</param>
Task Update(UpdateReceivePostingDocumentArgs args);
/// <summary>
/// Performs partial update on the <see cref="IReceivePostingDocument"/> for the properties specified
/// in arguments.
/// </summary>
/// <param name="args">The arguments containing properties that need to be updated.</param>
Task Patch(PatchArgs<int> args);
/// <summary>
/// Deletes <see cref="IReceivePostingDocument"/> from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the document that is about to be deleted.</param>
Task Delete(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects <see cref="IReceivePostingDocument"/> for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id.</param>
/// <returns><see cref="IReceivePostingDocument"/> is found, <c>null</c> otherwise.</returns>
Task<IReceivePostingDocument?> Select(PrimaryKeyArgs<int> args);
/// <summary>
/// Queries <see cref="IReceivePostingDocument"/> for the specified <see cref="IReceiveDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the id of the parent receive document.</param>
/// <returns><see cref="IReceivePostingDocument"/> if found, <c>null</c> otherwise.</returns>
Task<ImmutableList<IReceivePostingDocument>> Query(PrimaryKeyArgs<int> args);
/// <summary>
/// Inserts a new <see cref="IReceivePostingItem"/> into the <see cref="IReceivePostingDocument"/> document.
/// </summary>
/// <param name="args">The arguments containing the properties of the new item.</param>
/// <returns>The id of the newly inserted <see cref="IReceivePostingItem"/> item.</returns>
Task<long> InsertItem(InsertReceivePostingItemArgs args);
Task PatchPlanedItem(PatchArgs<long> args);
/// <summary>
/// Queries the <see cref="IReceivePostingItem"/> items for the specified <see cref="IReceivePostingDocument"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the document for which the items to be
/// queried.</param>
/// <returns>The list of items that belong to the specified document.</returns>
Task<ImmutableList<IReceivePostingItem>> QueryItems(PrimaryKeyArgs<int> args);
/// <summary>
/// Selects the <see cref="IReceivePostingItem"/> item for the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the item.</param>
/// <returns>The <see cref="IReceivePostingItem"/> if found, <c>null</c> otherwise.</returns>
Task<IReceivePostingItem?> SelectItem(PrimaryKeyArgs<long> args);
/// <summary>
/// Updates <see cref="IReceivePlannedItem"/>.
/// </summary>
/// <param name="args">The arguments containing the changed properties of the item.</param>
Task UpdatePlannedItem(UpdateReceivePlannedItemArgs args);
Task<IReceivePlannedItem?> SelectPlannedItem(PrimaryKeyArgs<long> args);
Task<IReceivePlannedItem?> SelectPlannedItem(SelectReceivePlannedItemArgs args);
Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<int> args);
Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Logistics.Documents.Receive;
public interface IReceivePostingItem : IPrimaryKey<long>
{
int Document { get; init; }
long Serial { get; init; }
float Quantity { get; init; }
int Location { get; init; }
}

@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using Common.Documents;
using Connected.Annotations;
using Connected.ServiceModel;
namespace Logistics.Documents.Receive;
/// <summary>
/// The arguments used when inserting a new <see cref="IReceiveItem"/> item
/// via <see cref="IReceiveDocumentService"/> service.
/// </summary>
public class InsertReceiveItemArgs : Dto
{
/// <summary>
/// The id of the <see cref="IReceiveDocument"/> document.
/// Must exists in the storage.
/// </summary>
[Range(1, int.MaxValue)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string EntityType { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
[Range(0, float.MaxValue)]
public float Quantity { get; set; }
}
public sealed class UpdateReceiveItemArgs : PrimaryKeyArgs<long>
{
[MinValue(0)]
public float PostedQuantity { get; set; }
}
public sealed class InsertReceiveDocumentArgs : InsertDocumentArgs
{
public int? Warehouse { get; set; }
public int? Supplier { get; set; }
}
public sealed class UpdateReceiveDocumentArgs : UpdateDocumentArgs<int>
{
public int? Warehouse { get; set; }
public int? Supplier { get; set; }
}
public sealed class SelectReceiveItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
}

@ -0,0 +1,72 @@
using System.ComponentModel.DataAnnotations;
using Common.Documents;
using Connected.Annotations;
using Connected.ServiceModel;
namespace Logistics.Documents.Receive;
public sealed class InsertReceivePostingDocumentArgs : InsertDocumentArgs
{
[MinValue(1)]
public int Document { get; set; }
}
public sealed class UpdateReceivePostingDocumentArgs : UpdateDocumentArgs<int>
{
[MinValue(0)]
public int ItemCount { get; set; }
[MinValue(0)]
public int OpenItemCount { get; set; }
}
/// <summary>
/// The arguments used when inserting a new <see cref="IReceivePostingItem"/> item
/// via <see cref="IReceiveDocumentService"/> service.
/// </summary>
public class InsertReceivePostingItemArgs : Dto
{
/// <summary>
/// The id of the <see cref="IReceivePostingDocument"/> document.
/// Must exists in the storage.
/// </summary>
[MinValue(1)]
public int Document { get; set; }
[MinValue(1)]
public int Location { get; set; }
[MinValue(0)]
public float Quantity { get; set; }
[MinValue(1)]
public long? Serial { get; set; }
}
public class InsertReceivePlannedItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[MinValue(0)]
public float Quantity { get; set; }
public string Entity { get; set; }
public string EntityId { get; set; }
}
public class UpdateReceivePlannedItemArgs : PrimaryKeyArgs<long>
{
[MinValue(0)]
public float PostedQuantity { get; set; }
}
public class SelectReceivePlannedItemArgs : Dto
{
[MinValue(1)]
public int Document { get; set; }
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
}

@ -0,0 +1,9 @@
using Connected;
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Logistics.Documents;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Common.Model\Common.Model.csproj" />
<ProjectReference Include="..\..\Common\Common\Common.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\Logistics.Documents.Model\Logistics.Documents.Model.csproj" />
<ProjectReference Include="..\Logistics.Types.Model\Logistics.Types.Model.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SR.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="SR.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SR.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

@ -0,0 +1,24 @@
using Common.Documents;
using Connected.Annotations;
using Connected.Entities.Annotations;
using Logistics.Types;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveDocument"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceiveDocument : Document<int>, IReceiveDocument
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceiveDocument)}";
/// <inheritdoc cref="IReceiveDocument.Supplier"/>
[Ordinal(1), Nullable]
public int? Supplier { get; init; }
/// <inheritdoc cref="IReceiveDocument.ReceiveDate"/>
[Ordinal(2), Nullable]
public DateTimeOffset? ReceiveDate { get; init; }
/// <inheritdoc cref="IReceiveDocument.ItemCount"/>
public int ItemCount { get; init; }
/// <inheritdoc cref="IReceiveDocument.OpenItemCount"/>
public int OpenItemCount { get; init; }
}

@ -0,0 +1,162 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Documents.Receive;
internal sealed class ReceiveDocumentItemOps
{
public sealed class Insert : ServiceFunction<InsertReceiveItemArgs, long>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<long> OnInvoke()
{
var result = await Storage.Open<ReceiveItem>().Update(Arguments.AsEntity<ReceiveItem>(State.New));
return result.Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, typeof(ReceiveDocumentService), nameof(IReceiveDocumentService.ItemInserted), new PrimaryKeyArgs<long> { Id = Result });
}
}
public sealed class Delete : ServiceAction<PrimaryKeyArgs<long>>
{
public Delete(IStorageProvider storage, IEventService events, ICacheContext cache, IReceiveDocumentService documents)
{
Storage = storage;
Events = events;
Cache = cache;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private ICacheContext Cache { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (SetState(await Documents.SelectItem(Arguments)) is not IReceiveItem entity)
return;
await Storage.Open<ReceiveItem>().Update(Arguments.AsEntity<ReceiveItem>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.ItemDeleted), Arguments);
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceiveItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceiveItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceiveItem>() where e.Document == Arguments.Id select e).AsEntities<IReceiveItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceiveItem?>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveItem?> OnInvoke()
{
return await Cache.Get(ReceiveItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceiveItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class SelectByEntity : NullableServiceFunction<SelectReceiveItemArgs, IReceiveItem?>
{
public SelectByEntity(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveItem?> OnInvoke()
{
return await Cache.Get(ReceiveItem.EntityKey,
f => f.Document == Arguments.Document
&& string.Equals(f.Entity, Arguments.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(f.EntityId, Arguments.EntityId, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<ReceiveItem>()
where e.Document == Arguments.Document
&& string.Equals(e.Entity, Arguments.Entity, StringComparison.OrdinalIgnoreCase)
&& string.Equals(e.EntityId, Arguments.EntityId, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceiveItemArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectItem(Arguments.Id) is not ReceiveItem entity)
return;
await Storage.Open<ReceiveItem>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
return (await Documents.SelectItem(Arguments.Id)) as ReceiveItem;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveItem.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.ItemUpdated), Arguments);
}
}
}

@ -0,0 +1,144 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Documents.Receive;
internal sealed class ReceiveDocumentOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IReceiveDocumentService documents, IStorageProvider storage, ICacheContext cache, IEventService events)
{
Documents = documents;
Storage = storage;
Cache = cache;
Events = events;
}
private IReceiveDocumentService Documents { get; }
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Documents.Select(Arguments)) is not IReceiveDocument document)
return;
/*
* Delete all items
*/
foreach (var item in await Documents.QueryItems(document.Id))
await Documents.DeleteItem(item.Id);
/*
* Delete document
*/
await Storage.Open<ReceiveDocument>().Update(Arguments.AsEntity<ReceiveDocument>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertReceiveDocumentArgs, int>
{
public Insert(IStorageProvider storage, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<ReceiveDocument>().Update(Arguments.AsEntity<ReceiveDocument>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Inserted), new PrimaryKeyArgs<int> { Id = Result });
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IReceiveDocument>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
public IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceiveDocument>> OnInvoke()
{
return await (from e in Storage.Open<ReceiveDocument>() select e).WithArguments(Arguments).AsEntities<IReceiveDocument>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IReceiveDocument?>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceiveDocument?> OnInvoke()
{
return await Cache.Get<ReceiveDocument>(ReceiveDocument.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceiveDocument>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceiveDocumentArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceiveDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.Select(Arguments.Id) is not ReceiveDocument entity)
return;
await Storage.Open<ReceiveDocument>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
return (await Documents.Select(Arguments.Id)) as ReceiveDocument;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(ReceiveDocument.EntityKey, Arguments.Id);
await Events.Enqueue(this, Documents, nameof(IReceiveDocumentService.Updated), Arguments);
}
}
}

@ -0,0 +1,82 @@
using System.Collections.Immutable;
using Common.Documents;
using Connected.Entities;
using Connected.ServiceModel;
using ItemOps = Logistics.Documents.Receive.ReceiveDocumentItemOps;
using Ops = Logistics.Documents.Receive.ReceiveDocumentOps;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveDocumentService"/>
internal sealed class ReceiveDocumentService : DocumentService<int, long>, IReceiveDocumentService
{
/// <summary>
/// Create a new <see cref="ReceiveDocument"/> instance
/// </summary>
/// <param name="context">The DI scope used by this instance.</param>
public ReceiveDocumentService(IContext context) : base(context)
{
}
/// <inheritdoc cref="IReceiveDocumentService.Delete(PrimaryKeyArgs{int})"/>
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.DeleteItem(PrimaryKeyArgs{long})"/>
public async Task DeleteItem(PrimaryKeyArgs<long> args)
{
await Invoke(GetOperation<ItemOps.Delete>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Insert(InsertReceiveDocumentArgs)"/>
public async Task<int> Insert(InsertReceiveDocumentArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.InsertItem(InsertReceiveItemArgs)"/>
public async Task<long> InsertItem(InsertReceiveItemArgs args)
{
return await Invoke(GetOperation<ItemOps.Insert>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Patch(PatchArgs{int})"/>
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not ReceiveDocument entity)
return;
await Update(args.Patch<UpdateReceiveDocumentArgs, ReceiveDocument>(entity));
}
/// <inheritdoc cref="IReceiveDocumentService.Query(QueryArgs?)"/>
public async Task<ImmutableList<IReceiveDocument>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="IReceiveDocumentService.QueryItems(PrimaryKeyArgs{int})"/>
public async Task<ImmutableList<IReceiveItem>> QueryItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<ItemOps.Query>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Select(PrimaryKeyArgs{int})"/>
public async Task<IReceiveDocument?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.SelectItem(PrimaryKeyArgs{long})"/>
public async Task<IReceiveItem?> SelectItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<ItemOps.Select>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.SelectItem(SelectReceiveItemArgs)"/>
public async Task<IReceiveItem?> SelectItem(SelectReceiveItemArgs args)
{
return await Invoke(GetOperation<ItemOps.SelectByEntity>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.Update(UpdateReceiveDocumentArgs)"/>
public async Task Update(UpdateReceiveDocumentArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="IReceiveDocumentService.UpdateItem(UpdateReceiveItemArgs)"/>
public async Task UpdateItem(UpdateReceiveItemArgs args)
{
await Invoke(GetOperation<ItemOps.Update>(), args);
}
}

@ -0,0 +1,23 @@
using Common;
using Connected.Annotations;
using Connected.Entities;
using Connected.Entities.Annotations;
using Logistics.Types;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceiveItem"/>
[Table(Schema = CommonSchemas.DocumentSchema)]
internal sealed record ReceiveItem : EntityContainer<long>, IReceiveItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceiveItem)}";
/// <inheritdoc cref="IReceiveItem.Document"/>
[Ordinal(0), Index]
public int Document { get; init; }
/// <inheritdoc cref="IReceiveItem.Quantity"/>
[Ordinal(1)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceiveItem.PostedQuantity"/>
[Ordinal(4)]
public float PostedQuantity { get; init; }
}

@ -0,0 +1,25 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Logistics.Types;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePlannedItem"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePlannedItem : ConsistentEntity<long>, IReceivePlannedItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePlannedItem)}";
/// <inheritdoc cref="IReceivePlannedItem.Document"/>
[Ordinal(0), Index]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.Item"/>
[Ordinal(1), Index]
public int Item { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.Quantity"/>
[Ordinal(2)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceivePlannedItem.PostedQuantity"/>
[Ordinal(3)]
public float PostedQuantity { get; init; }
}

@ -0,0 +1,128 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Interop;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Documents.Receive;
internal sealed class ReceivePlannedItemsOps
{
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePlannedItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePlannedItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Document == Arguments.Id select e).AsEntities<IReceivePlannedItem>();
}
}
public sealed class QueryByItem : ServiceFunction<PrimaryKeyArgs<long>, ImmutableList<IReceivePlannedItem>>
{
public QueryByItem(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePlannedItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Item == Arguments.Id select e).AsEntities<IReceivePlannedItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceivePlannedItem>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePlannedItem?> OnInvoke()
{
return await Cache.Get(ReceivePlannedItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class SelectByEntity : NullableServiceFunction<SelectReceivePlannedItemArgs, IReceivePlannedItem>
{
public SelectByEntity(IStorageProvider storage, ICacheContext cache, IReceiveDocumentService documents, IReceivePostingDocumentService postingDocuments)
{
Storage = storage;
Cache = cache;
Documents = documents;
PostingDocuments = postingDocuments;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IReceiveDocumentService Documents { get; }
private IReceivePostingDocumentService PostingDocuments { get; }
protected override async Task<IReceivePlannedItem?> OnInvoke()
{
if (await PostingDocuments.Select(Arguments.Document) is not IReceivePostingDocument postingDocument)
return null;
if (await Documents.SelectItem(Arguments.AsArguments<SelectReceiveItemArgs>(new { postingDocument.Document })) is not IReceiveItem item)
return null;
return await Cache.Get<IReceivePlannedItem>(ReceivePlannedItem.EntityKey, f => f.Item == item.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePlannedItem>() where e.Item == item.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceivePlannedItemArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectPlannedItem(Arguments.Id) is not ReceivePlannedItem entity)
return;
await Storage.Open<ReceivePlannedItem>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceivePlannedItem.EntityKey, Arguments.Id);
return (await Documents.SelectPlannedItem(Arguments.Id)) as ReceivePlannedItem;
});
await Cache.Remove(ReceivePlannedItem.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.PlannedItemUpdated), Arguments);
}
}
}

@ -0,0 +1,22 @@
using Common.Documents;
using Connected.Annotations;
using Connected.Entities.Annotations;
using Logistics.Types;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePostingDocument"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePostingDocument : Document<int>, IReceivePostingDocument
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePostingDocument)}";
/// <inheritdoc cref="IReceivePostingDocument.Document"/>
[Ordinal(0)]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePostingDocument.OpenItemCount"/>
[Ordinal(1)]
public int OpenItemCount { get; init; }
/// <inheritdoc cref="IReceivePostingDocument.ItemCount"/>
[Ordinal(2)]
public int ItemCount { get; init; }
}

@ -0,0 +1,133 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Documents.Receive;
internal sealed class ReceivePostingDocumentOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
await Storage.Open<ReceivePostingDocument>().Update(Arguments.AsEntity<ReceivePostingDocument>(State.Deleted));
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertReceivePostingDocumentArgs, int>
{
public Insert(IStorageProvider storage, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<ReceivePostingDocument>().Update(Arguments.AsEntity<ReceivePostingDocument>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Inserted), new PrimaryKeyArgs<int> { Id = Result });
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePostingDocument>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePostingDocument>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePostingDocument>() where e.Document == Arguments.Id select e).AsEntities<IReceivePostingDocument>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IReceivePostingDocument>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePostingDocument?> OnInvoke()
{
return await Cache.Get(ReceivePostingDocument.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePostingDocument>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdateReceivePostingDocumentArgs>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Cache = cache;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.Select(Arguments.Id) is not ReceivePostingDocument entity)
return;
await Storage.Open<ReceivePostingDocument>().Update(entity, Arguments, async () =>
{
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
return (await Documents.Select(Arguments.Id)) as ReceivePostingDocument;
});
await Cache.Remove(ReceivePostingDocument.EntityKey, Arguments.Id);
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.Updated), Arguments);
}
}
}

@ -0,0 +1,98 @@
using System.Collections.Immutable;
using Common.Documents;
using Connected.Entities;
using Connected.Notifications;
using Connected.ServiceModel;
using ItemOps = Logistics.Documents.Receive.ReceivePostingItemOps;
using Ops = Logistics.Documents.Receive.ReceivePostingDocumentOps;
using PlannedOps = Logistics.Documents.Receive.ReceivePlannedItemsOps;
namespace Logistics.Documents.Receive;
internal sealed class ReceivePostingDocumentService : DocumentService<int, long>, IReceivePostingDocumentService
{
public event ServiceEventHandler<PrimaryKeyEventArgs<long>> PlannedItemUpdated;
public ReceivePostingDocumentService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public async Task<int> Insert(InsertReceivePostingDocumentArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task<long> InsertItem(InsertReceivePostingItemArgs args)
{
return await Invoke(GetOperation<ItemOps.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not ReceivePostingDocument entity)
return;
await Update(entity.Merge(args, State.Default).AsArguments<UpdateReceivePostingDocumentArgs>());
}
public async Task PatchPlanedItem(PatchArgs<long> args)
{
if (await SelectPlannedItem(args.Id) is not ReceivePlannedItem entity)
return;
await UpdatePlannedItem(entity.Merge(args, State.Default).AsArguments<UpdateReceivePlannedItemArgs>());
}
public async Task<ImmutableList<IReceivePostingDocument>> Query(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Query>(), args);
}
public async Task<ImmutableList<IReceivePostingItem>> QueryItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<ItemOps.Query>(), args);
}
public async Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<PlannedOps.Query>(), args);
}
public async Task<ImmutableList<IReceivePlannedItem>> QueryPlannedItems(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<PlannedOps.QueryByItem>(), args);
}
public async Task<IReceivePostingDocument?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IReceivePostingItem?> SelectItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<ItemOps.Select>(), args);
}
public async Task<IReceivePlannedItem?> SelectPlannedItem(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<PlannedOps.Select>(), args);
}
public async Task<IReceivePlannedItem?> SelectPlannedItem(SelectReceivePlannedItemArgs args)
{
return await Invoke(GetOperation<PlannedOps.SelectByEntity>(), args);
}
public async Task Update(UpdateReceivePostingDocumentArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
public Task UpdatePlannedItem(UpdateReceivePlannedItemArgs args)
{
throw new NotImplementedException();
}
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Logistics.Types;
namespace Logistics.Documents.Receive;
/// <inheritdoc cref="IReceivePostingItem"/>
[Table(Schema = Domain.Code)]
internal sealed record ReceivePostingItem : ConsistentEntity<long>, IReceivePostingItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(ReceivePostingItem)}";
/// <inheritdoc cref="IReceivePostingItem.Document"/>
[Ordinal(0)]
public int Document { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Serial"/>
[Ordinal(1)]
public long Serial { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Quantity"/>
[Ordinal(2)]
public float Quantity { get; init; }
/// <inheritdoc cref="IReceivePostingItem.Location"/>
[Ordinal(3)]
public int Location { get; init; }
}

@ -0,0 +1,70 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Documents.Receive;
internal sealed class ReceivePostingItemOps
{
public sealed class Insert : ServiceFunction<InsertReceivePostingItemArgs, long>
{
public Insert(IStorageProvider storage, IEventService events, IReceivePostingDocumentService documents)
{
Storage = storage;
Events = events;
Documents = documents;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
private IReceivePostingDocumentService Documents { get; }
protected override async Task<long> OnInvoke()
{
return (await Storage.Open<ReceivePostingItem>().Update(Arguments.AsEntity<ReceivePostingItem>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Documents, nameof(IReceivePostingDocumentService.ItemInserted), new PrimaryKeyArgs<long> { Id = Result });
}
}
public sealed class Query : ServiceFunction<PrimaryKeyArgs<int>, ImmutableList<IReceivePostingItem>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IReceivePostingItem>> OnInvoke()
{
return await (from e in Storage.Open<ReceivePostingItem>() where e.Document == Arguments.Id select e).AsEntities<IReceivePostingItem>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, IReceivePostingItem>
{
public Select(IStorageProvider storage, ICacheContext cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
protected override async Task<IReceivePostingItem?> OnInvoke()
{
return await Cache.Get(ReceivePostingItem.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<ReceivePostingItem>() where e.Id == Arguments.Id select e).AsEntity();
});
}
}
}

@ -0,0 +1,64 @@
using Common.Documents;
using Connected.Annotations;
using Connected.Data;
using Connected.Security.Identity;
using Connected.Validation;
using Contacts.Types;
using Logistics.Types.Warehouses;
using System.ComponentModel.DataAnnotations;
namespace Logistics.Documents.Receive;
[Priority(0)]
internal sealed class InsertReceiveDocumentValidator : InsertDocumentValidator<InsertReceiveDocumentArgs>
{
public InsertReceiveDocumentValidator(IUserService users, IBusinessPartnerService businessPartners, IWarehouseService warehouses) : base(users)
{
BusinessPartners = businessPartners;
Warehouses = warehouses;
}
private IBusinessPartnerService BusinessPartners { get; }
private IWarehouseService Warehouses { get; }
protected override async Task OnValidating()
{
await ValidateSupplier();
await ValidateWarehouse();
}
private async Task ValidateSupplier()
{
/*
* If supplier is not set there is no need for a validation.
*/
if (Arguments.Supplier is null)
return;
/*
* Check is business partner exists.
*/
if (await BusinessPartners.Select((int)Arguments.Supplier) is not IBusinessPartner supplier)
throw ValidationExceptions.NotFound(nameof(Arguments.Supplier), Arguments.Supplier);
/*
* Check if business partner has Supplier role which means it's actually a supplier.
*/
if (!supplier.Roles.HasFlag(CustomerRoles.Supplier))
throw new ValidationException($"{SR.ValPartnerNotSupplier} ({Arguments.Supplier})");
}
private async Task ValidateWarehouse()
{
if (Arguments.Warehouse is null)
return;
/*
* Check is warehouse exists.
*/
if (await Warehouses.Select(Arguments.Warehouse) is not IWarehouse warehouse)
throw ValidationExceptions.NotFound(nameof(Arguments.Warehouse), Arguments.Warehouse);
/*
* Only Enabled warehouses can be used.
*/
if (warehouse.Status == Status.Disabled)
throw ValidationExceptions.Disabled(nameof(Arguments.Warehouse));
}
}

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Logistics.Documents {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SR {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SR() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Logistics.Documents.SR", typeof(SR).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Business partner is not a supplier..
/// </summary>
internal static string ValPartnerNotSupplier {
get {
return ResourceManager.GetString("ValPartnerNotSupplier", resourceCulture);
}
}
}
}

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ValPartnerNotSupplier" xml:space="preserve">
<value>Business partner is not a supplier.</value>
</data>
</root>

@ -0,0 +1,11 @@
using Connected;
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Process)]
namespace Logistics.Documents;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,74 @@
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Logistics.Documents.Receive;
using Microsoft.Extensions.Logging;
namespace Logistics.Documents.Listeners;
[Middleware<IReceivePostingDocumentService>(nameof(IReceivePostingDocumentService.PlannedItemUpdated))]
internal sealed class PlannedItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
public PlannedItemListener(ILogger<PlannedItemListener> logger, IReceivePostingDocumentService documents, IReceiveDocumentService receiveDocuments)
{
Logger = logger;
Documents = documents;
ReceiveDocuments = receiveDocuments;
}
private ILogger<PlannedItemListener> Logger { get; }
private IReceivePostingDocumentService Documents { get; }
private IReceiveDocumentService ReceiveDocuments { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectPlannedItem(Arguments.Id) is not IReceivePlannedItem item)
{
Logger.LogWarning("The IReceivePlannedItem not found ({id}}.", Arguments.Id);
return;
}
if (await ReceiveDocuments.SelectItem(item.Item) is not IReceiveItem receiveItem)
{
Logger.LogWarning("The IReceiveItem not found ({id}}.", item.Item);
return;
}
if (await Documents.Select(item.Document) is not IReceivePostingDocument document)
{
Logger.LogWarning("The IReceivePostingDocument not found ({id}}.", item.Document);
return;
}
await UpdateOpenItems(document);
await UpdatePostedQuantity(receiveItem);
}
private async Task UpdateOpenItems(IReceivePostingDocument document)
{
var items = await Documents.QueryPlannedItems(new PrimaryKeyArgs<int> { Id = document.Id });
await Documents.Patch(new PatchArgs<int>
{
Id = document.Id,
Properties = new Dictionary<string, object>
{
{nameof(IReceivePostingDocument.OpenItemCount), items.Count(f => f.PostedQuantity < f.Quantity) }
}
});
}
private async Task UpdatePostedQuantity(IReceiveItem item)
{
var items = await Documents.QueryPlannedItems(new PrimaryKeyArgs<long> { Id = item.Id });
await Documents.PatchPlanedItem(new PatchArgs<long>
{
Id = item.Id,
Properties = new Dictionary<string, object>
{
{nameof(IReceiveItem.PostedQuantity), items.Sum(f => f.PostedQuantity) }
}
});
}
}

@ -0,0 +1,89 @@
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Logistics.Documents.Receive;
using Logistics.Stock;
using Logistics.Types.Serials;
using Microsoft.Extensions.Logging;
namespace Logistics.Documents.Listeners;
/// <summary>
/// Represents the event listener to the <see cref="IReceivePostingDocumentService"/> Updated event.
/// </summary>
/// <remarks>
/// This middleware reacts when the item is inserted and updates the <see cref="IStockItem"/>.
/// </remarks>
[Middleware<IReceivePostingDocumentService>(nameof(IReceivePostingDocumentService.ItemInserted))]
internal sealed class PostingItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
/// <summary>
/// Creates a new instance of the <see cref="PostingItemListener"/>
/// </summary>
public PostingItemListener(ILogger<PostingItemListener> logger, IStockService stock, IReceivePostingDocumentService documents, ISerialService serials)
{
Logger = logger;
Stock = stock;
Documents = documents;
Serials = serials;
}
private ILogger<PostingItemListener> Logger { get; }
private IStockService Stock { get; }
private IReceivePostingDocumentService Documents { get; }
private ISerialService Serials { get; }
protected override async Task OnInvoke()
{
/*
* Stage 1 is to prepare all data neede to perform operation
*
* Load posting item
*/
if (await Documents.SelectItem(Arguments.Id) is not IReceivePostingItem item)
{
Logger.LogWarning("The IReceivePostingItem not found ({id}}.", Arguments.Id);
return;
}
/*
* Now load the serial number
*/
if (await Serials.Select(item.Serial) is not ISerial serial)
{
Logger.LogWarning("The ISerial not found ({id}}.", item.Serial);
return;
}
/*
* Now load the serial number
*/
if (await Documents.SelectPlannedItem(new SelectReceivePlannedItemArgs
{
Document = item.Document,
Entity = serial.Entity,
EntityId = serial.EntityId
}) is not ISerial plannedItem)
{
Logger.LogWarning("The IReceivePlannedItem not found ({entity}, {entityId}).", serial.Entity, serial.EntityId);
return;
}
/*
* The idea here is simple:
* update (increase) the stock for the specified item
* and posted quantity and update the statictics for
* the immediate parents.
*/
await Stock.Update(new UpdateStockArgs
{
Location = item.Location,
Quantity = item.Quantity,
Serial = item.Serial
});
/*
* Now update the planned item with posted quantity
*/
await Documents.UpdatePlannedItem(new UpdateReceivePlannedItemArgs
{
Id = plannedItem.Id,
PostedQuantity = item.Quantity
});
}
}

@ -0,0 +1,40 @@
using Connected.Middleware.Annotations;
using Connected.Notifications;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Logistics.Documents.Receive;
using Microsoft.Extensions.Logging;
namespace Logistics.Documents.Listeners;
[Middleware<IReceiveDocumentService>(nameof(IReceiveDocumentService.ItemUpdated))]
internal sealed class ReceiveItemListener : EventListener<PrimaryKeyEventArgs<long>>
{
public ReceiveItemListener(ILogger<ReceiveItemListener> logger, IReceiveDocumentService documents)
{
Logger = logger;
Documents = documents;
}
private ILogger<ReceiveItemListener> Logger { get; }
private IReceiveDocumentService Documents { get; }
protected override async Task OnInvoke()
{
if (await Documents.SelectItem(Arguments.Id) is not IReceiveItem item)
{
Logger.LogWarning("The IReceiveItem not found ({id}}.", Arguments.Id);
return;
}
var items = await Documents.QueryItems(item.Document);
await Documents.Patch(new PatchArgs<int>
{
Id = item.Document,
Properties = new Dictionary<string, object>
{
{nameof(IReceiveDocument.OpenItemCount), items.Count(f=>f.PostedQuantity<f.Quantity)}
}
});
}
}

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Logistics.Documents</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Common\Common\Common.csproj" />
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Data\Connected.Data.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Hosting\Connected.Hosting.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Runtime\Connected.Runtime.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Logistics.Documents.Model\Logistics.Documents.Model.csproj" />
<ProjectReference Include="..\Logistics.Stock.Model\Logistics.Stock.Model.csproj" />
<ProjectReference Include="..\Logistics.Types.Model\Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,24 @@
using Connected.Data.DataProtection;
using Connected.Data.EntityProtection;
using Connected.Middleware;
using Connected.ServiceModel;
using Logistics.Documents.Receive;
namespace Logistics.Documents.Protection;
internal sealed class ReceiveProtector : MiddlewareComponent, IEntityProtector<IReceiveDocument>
{
public ReceiveProtector(IReceiveDocumentService documents)
{
Documents = documents;
}
public IReceiveDocumentService Documents { get; }
public async Task Invoke(EntityProtectionArgs<IReceiveDocument> args)
{
if (await Documents.Select(new PrimaryKeyArgs<int> { Id = args.Entity.Id }) is not IReceiveDocument document)
return;
throw new NotImplementedException();
}
}

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

@ -0,0 +1,9 @@
using Connected.Data;
namespace Logistics.Stock.Aggregations;
public interface IStockAggregation : IPrimaryKey<long>
{
long Stock { get; init; }
DateTimeOffset Date { get; init; }
float Quantity { get; init; }
}

@ -0,0 +1,33 @@
using Connected.Data;
namespace Logistics.Stock;
/// <summary>
/// The stock descriptor which describes what kind of entity it
/// represents. The entity could be Product, Semi product or any
/// other type of entity.
/// </summary>
public interface IStock : IPrimaryKey<long>
{
/// <summary>
/// The type of the entity.
/// </summary>
string Entity { get; init; }
/// <summary>
/// The primary key of the entity.
/// </summary>
string EntityId { get; init; }
/// <summary>
/// The total quantity currently available.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The minimum quantity that should be always available
/// in the stock.
/// </summary>
float? Min { get; init; }
/// <summary>
/// The maximum quantity that should be stored in
/// the stock.
/// </summary>
float? Max { get; init; }
}

@ -0,0 +1,40 @@
using Connected.Data;
namespace Logistics.Stock;
/// <summary>
/// Represents a single stock item.
/// </summary>
/// <remarks>
/// Goods are typically stored in the warehouse. Warehouse is
/// organized into locations or storage bins and each location contains
/// zero or more goods.
/// </remarks>
public interface IStockItem : IPrimaryKey<long>
{
/// <summary>
/// The <see cref="IStock"/> to which the item belong.
/// </summary>
/// <remarks>
/// Stock contains information about the type of the entity whereas
/// the stock item contains information about actual storage.
/// </remarks>
long Stock { get; init; }
/// <summary>
/// The location where the goods are stored.
/// </summary>
int Location { get; init; }
/// <summary>
/// The serial number of the goods.
/// </summary>
/// <remarks>
/// Each item has a serial number which uniquely identifies
/// the items even from the same type but from
/// different documents.
/// </remarks>
long Serial { get; init; }
/// <summary>
/// The quantity left in this location. Once the quantity reaches zero
/// the item gets deleted from the location.
/// </summary>
float Quantity { get; init; }
}

@ -0,0 +1,38 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Logistics.Stock;
/// <summary>
/// Represents the service which manipulates with stock items.
/// </summary>
[Service]
[ServiceUrl(StockUrls.Stock)]
public interface IStockService : IServiceNotifications<long>
{
/// <summary>
/// Updates the stock items at the specified location.
/// </summary>
/// <param name="args"></param>
Task Update(UpdateStockArgs args);
Task<IStock?> Select(PrimaryKeyArgs<long> args);
Task<IStock?> Select(EntityArgs args);
/// <summary>
/// Queries all stock items for the specified stock.
/// </summary>
/// <param name="args">The arguments containing the id of the stock</param>
/// <returns>The list of stock items that belong to the specified stock id.</returns>
Task<ImmutableList<IStockItem>> QueryItems(PrimaryKeyArgs<long> args);
/// <summary>
/// Queries stock items for the specified stock that are present in the specified
/// warehouse location.
/// </summary>
/// <param name="args">The arguments containing the crieria used by query.</param>
/// <returns>The list of stock items that are present in the specified warehouse location and
/// belong to the specified stock id.</returns>
Task<ImmutableList<IStockItem>> QueryItems(QueryStockItemsArgs args);
Task<IStockItem?> SelectItem(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Logistics.Stock</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,36 @@
using Connected.Annotations;
using Connected.ServiceModel;
namespace Logistics.Stock;
/// <summary>
/// Represents the arguments when updating the stock items.
/// </summary>
public sealed class UpdateStockArgs : Dto
{
/// <summary>
/// The serial number of the item.
/// </summary>
[MinValue(1)]
public long Serial { get; set; }
/// <summary>
/// The warehouse location where the items are stored.
/// </summary>
[MinValue(1)]
public int Location { get; set; }
/// <summary>
/// The changed quantity. Can be a positive or negative
/// value.
/// </summary>
public float Quantity { get; set; }
}
public sealed class QueryStockItemsArgs : PrimaryKeyArgs<long>
{
[MinValue(1)]
public int Location { get; set; }
/// <summary>
/// The optional serial number.
/// </summary>
public long? Serial { get; set; }
}

@ -0,0 +1,5 @@
namespace Logistics.Stock;
public static class StockUrls
{
public const string Stock = "/logistics/stock";
}

@ -0,0 +1,9 @@
using Connected;
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Logistics.Stock;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Runtime\Connected.Runtime.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Logistics.Stock.Model\Logistics.Stock.Model.csproj" />
<ProjectReference Include="..\Logistics.Types.Model\Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,60 @@
using Connected.Collections.Queues;
using Connected.Middleware;
using Logistics.Types.WarehouseLocations;
using Microsoft.Extensions.Logging;
namespace Logistics.Stock.Services;
internal sealed class StockAggregator : MiddlewareComponent, IQueueClient<PrimaryKeyQueueArgs<long>>
{
public StockAggregator(ILogger<StockAggregator> logger, IWarehouseLocationService locations, IStockService stock)
{
Logger = logger;
Locations = locations;
Stock = stock;
}
private ILogger<StockAggregator> Logger { get; }
private IWarehouseLocationService Locations { get; }
private IStockService Stock { get; }
public async Task Invoke(IQueueMessage message, PrimaryKeyQueueArgs<long> args)
{
if (await Stock.SelectItem(args.Id) is not IStockItem stock)
{
Logger.LogWarning("IStockItem not found {id}", args.Id);
return;
}
await Calculate(stock, stock.Location);
}
private async Task Calculate(IStockItem stock, int locationId)
{
if (await Locations.Select(locationId) is not IWarehouseLocation location)
{
Logger.LogWarning("IWarehouseLocation not found {id}", locationId);
return;
}
if (location.Parent is null)
return;
var parent = (int)location.Parent;
var sum = (await Stock.QueryItems(new QueryStockItemsArgs
{
Id = stock.Id,
Location = parent,
Serial = stock.Serial
})).Sum(f => f.Quantity);
await Stock.Update(new UpdateStockArgs
{
Location = parent,
Quantity = sum,
Serial = stock.Serial
});
await Calculate(stock, parent);
}
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Logistics.Types;
namespace Logistics.Stock;
[Table(Schema = Domain.Code)]
internal sealed record Stock : ConsistentEntity<long>, IStock
{
[Ordinal(0), Length(128), Index(Name = $"ix_{Domain.Code}_{nameof(Entity)}_{nameof(EntityId)}", Unique = true)]
public string Entity { get; init; } = default!;
[Ordinal(1), Length(128), Index(Name = $"ix_{Domain.Code}_{nameof(Entity)}_{nameof(EntityId)}", Unique = true)]
public string EntityId { get; init; } = default!;
[Ordinal(2)]
public float Quantity { get; init; }
[Ordinal(3)]
public float? Min { get; init; }
[Ordinal(4)]
public float? Max { get; init; }
}

@ -0,0 +1,24 @@
using Connected.Annotations;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
using Logistics.Types;
namespace Logistics.Stock;
/// <inheritdoc cref="IStockItem"/>
[Table(Schema = Domain.Code)]
internal sealed record StockItem : ConsistentEntity<long>, IStockItem
{
public const string EntityKey = $"{Domain.Code}.{nameof(StockItem)}";
/// <inheritdoc cref="IStockItem.Stock"/>
[Ordinal(0)]
public long Stock { get; init; }
/// <inheritdoc cref="IStockItem.Location"/>
[Ordinal(1)]
public int Location { get; init; }
/// <inheritdoc cref="IStockItem.Serial"/>
[Ordinal(2)]
public long Serial { get; init; }
/// <inheritdoc cref="IStockItem.Quantity"/>
[Ordinal(3)]
public float Quantity { get; init; }
}

@ -0,0 +1,187 @@
using Connected.Caching;
using Connected.Collections.Queues;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
using Connected.Threading;
using Logistics.Stock.Services;
using Logistics.Types.Serials;
using Logistics.Types.WarehouseLocations;
namespace Logistics.Stock;
internal sealed class StockOps
{
public const string StockQueue = "Stock";
static StockOps()
{
Locker = new();
}
private static AsyncLockerSlim Locker { get; }
/// <summary>
/// This method ensures that a stock (parent) record exists.
/// </summary>
/// <remarks>
/// The stock record is not created explicitly since this would introduce unnecessary complexity. It is
/// instead created on the fly when the first request is made. The tricky part is it must be thread safe
/// so we need an async locker since lock statement does not support async calls.
/// </remarks>
private static async Task<IStock> Ensure(IStorageProvider storage, IStockService stock, EntityArgs args)
{
/*
* First check for existence so we don't need to perform a lock if the record is found.
*/
if (await stock.Select(args) is IStock existing)
return existing;
/*
* Doesn't exist.
* Perform an async lock to ensure no one else is trying to insert the item.
*/
return await Locker.LockAsync(async () =>
{
/*
* Read again if two or more threads were competing for the insert. The thing is this
* is happening quite frequently even in semi loaded warehouse systems.
*/
if (await stock.Select(args) is IStock existing2)
return existing2;
/*
* Still nothing. We are safe to insert a new stock descriptor. Note that in scalable environments
* there is still a possibillity that two requests would made it here but from different processes.
* Thus we should have a unique constraint on the entity ensuring only one request will win, all the others
* lose. This also means the provider owning the entity must support unique constraints.
*/
var entity = await storage.Open<Stock>().Update(args.AsEntity<Stock>(State.New));
var result = await stock.Select(entity.Id);
/*
* This should not happen anyway but we'll do it for the sake of sompiler warning.
*/
if (result is null)
throw new NullReferenceException(nameof(IStock));
return result;
});
}
public sealed class Update : ServiceFunction<UpdateStockArgs, long>
{
public Update(IStorageProvider storage, ICacheContext cache, IEventService events, IStockService stock,
ISerialService serials, IQueueService queue, IWarehouseLocationService locations)
{
Storage = storage;
Cache = cache;
Events = events;
Stock = stock;
Serials = serials;
Queue = queue;
Locations = locations;
}
private IStorageProvider Storage { get; }
private ICacheContext Cache { get; }
private IEventService Events { get; }
private IStockService Stock { get; }
private ISerialService Serials { get; }
private IQueueService Queue { get; }
private IWarehouseLocationService Locations { get; }
private bool IsLeaf { get; set; }
protected override async Task<long> OnInvoke()
{
/*
* We need this info for queueing aggregations.
*/
IsLeaf = (await Locations.Select(Arguments.Location)).ItemCount == 0;
/*
* Validators should validate the existence. Serials don't get deleted.
*/
if (await Serials.Select(Arguments.Serial) is not ISerial serial)
return 0;
/*
* Ensure the stock record exists.
*/
var stock = await Ensure(Storage, Stock, serial.AsArguments<EntityArgs, long>());
/*
* Now we must check if the stock item exists for the specified serial and
* warehouse location. If so we'll only update the quantity.
*/
if (await FindExisting(stock) is not StockItem existing)
return await InsertItem(stock);
else
return await UpdateItem(existing);
}
private async Task<long> InsertItem(IStock stock)
{
return await Locker.LockAsync(async () =>
{
/*
* Query again if someone overtook us.
*/
if (await FindExisting(stock) is not StockItem existing)
{
/*
* Still doesn't exist, it's safe to insert it since we are in the locked area.
*/
return (await Storage.Open<StockItem>().Update(Arguments.AsEntity<StockItem>(State.New))).Id;
}
else
{
/*
* Indeed, there was a record inserted in the meantime.
*/
return await UpdateItem(existing);
}
});
}
/// <summary>
/// Performs the update on the existing stock item.
/// </summary>
/// <param name="item">The stock item to be updated.</param>
private async Task<long> UpdateItem(StockItem item)
{
await Storage.Open<StockItem>().Update(item, Arguments, async () =>
{
await Cache.Remove(StockItem.EntityKey, item.Id);
return SetState((await Stock.SelectItem(item.Id)) as StockItem);
},
async (e) =>
{
var quantity = item.Quantity + Arguments.Quantity;
await Task.CompletedTask;
return e.Merge(Arguments, State.Default, new { Quantity = quantity });
});
return item.Id;
}
private async Task<StockItem?> FindExisting(IStock stock)
{
var items = await Stock.QueryItems(new QueryStockItemsArgs
{
Id = stock.Id,
Location = Arguments.Location,
Serial = Arguments.Serial
});
if (items.IsEmpty || items[0] is not StockItem existing)
return null;
return existing;
}
protected override async Task OnCommitted()
{
await Cache.Remove(StockItem.EntityKey, Result);
await Events.Enqueue(this, Stock, nameof(IStockService.Updated), new PrimaryKeyArgs<long> { Id = Result });
if (IsLeaf)
await Queue.Enqueue<StockAggregator, PrimaryKeyQueueArgs<long>>(new PrimaryKeyQueueArgs<long> { Id = Result });
}
}
}

@ -0,0 +1,41 @@
using System.Collections.Immutable;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Stock;
internal sealed class StockService : EntityService<long>, IStockService
{
public StockService(IContext context) : base(context)
{
}
public Task<ImmutableList<IStockItem>> QueryItems(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task<ImmutableList<IStockItem>> QueryItems(QueryStockItemsArgs args)
{
throw new NotImplementedException();
}
public Task<IStock?> Select(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task<IStock?> Select(EntityArgs args)
{
throw new NotImplementedException();
}
public Task<IStockItem?> SelectItem(PrimaryKeyArgs<long> args)
{
throw new NotImplementedException();
}
public Task Update(UpdateStockArgs args)
{
throw new NotImplementedException();
}
}

@ -0,0 +1,6 @@
namespace Logistics.Types;
public static class Domain
{
public const string Name = "Logistics";
public const string Code = "lgs";
}

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Logistics.Types</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connected\Connected\Connected.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,8 @@
namespace Logistics.Types;
public static class LogisticsUrls
{
public const string Warehouses = "/logistics/types/warehouses";
public const string WarehouseLocations = "/logistics/types/warehouseLocations";
public const string Packing = "/logistics/types/packing";
public const string Serials = "/logistics/types/serials";
}

@ -0,0 +1,21 @@
using Connected.Data;
namespace Logistics.Types.Packaging;
public interface IPacking : IPrimaryKey<int>
{
string Ean { get; init; }
string Entity { get; init; }
string EntityId { get; init; }
float? Quantity { get; init; }
float? NetWeight { get; init; }
float? GrossWeight { get; init; }
int? Width { get; init; }
int? Height { get; init; }
int? Depth { get; init; }
int? ItemCount { get; init; }
Status Status { get; init; }
}

@ -0,0 +1,34 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Logistics.Types.Packaging;
[Service]
[ServiceUrl(LogisticsUrls.Packing)]
public interface IPackingService : IServiceNotifications<int>
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IPacking>> Query(QueryArgs? args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IPacking>> Query(PrimaryKeyListArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IPacking?> Select(PrimaryKeyArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IPacking?> Select(SelectPackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<int> Insert(InsertPackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdatePackingArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Delete | ServiceMethodVerbs.Post)]
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Logistics.Types.Packaging;
public sealed class InsertPackingArgs : EntityArgs
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
public float? Quantity { get; set; }
public float? NetWeight { get; set; }
public float? GrossWeight { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public int? Depth { get; set; }
public int? ItemCount { get; set; }
public Status Status { get; set; } = Status.Disabled;
}
public sealed class UpdatePackingArgs : PrimaryKeyArgs<int>
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
public float? Quantity { get; set; }
public float? NetWeight { get; set; }
public float? GrossWeight { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public int? Depth { get; set; }
public int? ItemCount { get; set; }
public Status Status { get; set; } = Status.Disabled;
}
public sealed class SelectPackingArgs : Dto
{
[Required, MaxLength(32)]
public string Ean { get; set; } = default!;
}

@ -0,0 +1,54 @@
using Connected.Data;
namespace Logistics.Types.Serials;
/// <summary>
/// Represents a serial number in the logistic environment.
/// </summary>
/// <remarks>
/// The primary usage of the Serial is in warehouse management
/// system, where every item in the stock is labeled with serial number.
/// Once the item is received and before it is put in the stock locations,
/// it receives a unique serial number. If the item is moved between stock
/// locations and even warehouses, its serial value remains the same. Serial
/// number plays a key role in traceability.
/// </remarks>
public interface ISerial : IPrimaryKey<long>
{
/// <summary>
/// The entity which owns the serial number. This could
/// be Product, Semi product, Raw or any other type of
/// entity which needs some kind of labeling.
/// </summary>
string Entity { get; init; }
/// <summary>
/// The primary key of the entity. This points to the exact record of
/// the Entity type.
/// </summary>
string EntityId { get; init; }
/// <summary>
/// The actual Serial number. System can use different middleware techniques
/// to obtain this value because it's very common to be have project specific
/// implementation to calculate this value.
/// </summary>
string Value { get; init; }
/// <summary>
/// The remaining quantity in the stock for this serial. This value can increase
/// or decrease depending on the warehouse implementation. Some systems do reuse
/// the same serial between different receives.
/// </summary>
float Quantity { get; init; }
/// <summary>
/// The date serial was created.
/// </summary>
DateTimeOffset Created { get; init; }
/// <summary>
/// If the item has limited shelf life, this value should hold the date when
/// the shelf life expires.
/// </summary>
DateTimeOffset? BestBefore { get; init; }
/// <summary>
/// The status of the serial number. If the status is <see cref="Status.Disabled"/> the
/// processes using the serial number should not allow the entity to be used in documents.
/// </summary>
Status Status { get; init; }
}

@ -0,0 +1,71 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Logistics.Types.Serials;
/// <summary>
/// The service for manipulating with serials. A <see cref="ISerial"/> is a fundamental
/// entity used by labeling and traceability systems.
/// </summary>
[Service]
[ServiceUrl(LogisticsUrls.Serials)]
public interface ISerialService : IServiceNotifications<long>
{
/// <summary>
/// Queries all serial numbers.
/// </summary>
/// <param name="args">The optional arguments specifiying the
/// behavior of the result set.</param>
/// <returns>A list of <see cref="ISerial"/> entities.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<ISerial>> Query(QueryArgs? args);
/// <summary>
/// Performs a lookup on the serials for the specified set of ids.
/// </summary>
/// <param name="args">The arguments containing the list of ids for
/// which the entities will be returned.</param>
/// <returns>A list of entities that matches the specified ids.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<ISerial>> Query(PrimaryKeyListArgs<long> args);
/// <summary>
/// Returns the first serial that matches the specified id.
/// </summary>
/// <param name="args">The arguments containing the id of the entity.</param>
/// <returns>The <see cref="ISerial"/> if found, <c>null</c> otherwise.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISerial?> Select(PrimaryKeyArgs<long> args);
/// <summary>
/// Returns the first serial with the specified value.
/// </summary>
/// <param name="args">The arguments containing the value for which serial
/// entity will be returned.</param>
/// <returns>The <see cref="ISerial"/> if found, <c>null</c> otherwise.</returns>
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ISerial?> Select(SelectSerialArgs args);
/// <summary>
/// Inserts a new serial number.
/// </summary>
/// <param name="args">The arguments containing the properties of the new serial.</param>
/// <returns>The id of the newly inserted serial.</returns>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<long> Insert(InsertSerialArgs args);
/// <summary>
/// Updates an existing serial.
/// </summary>
/// <param name="args">The arguments containing properties which will change the entity.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdateSerialArgs args);
/// <summary>
/// Performs a partial update on the serial.
/// </summary>
/// <param name="args">The arguments containing properties which has to be updated.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<long> args);
/// <summary>
/// Peranently deletes the serial from the storage.
/// </summary>
/// <param name="args">The arguments containing the id of the entity to be deleted.</param>
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Delete)]
Task Delete(PrimaryKeyArgs<long> args);
}

@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Logistics.Types.Serials;
/// <summary>
/// Arguments used when inserting a new serial number.
/// </summary>
public sealed class InsertSerialArgs : Dto
{
/// <inheritdoc cref="ISerial.Entity"/>
[Required, MaxLength(128)]
public string Entity { get; set; } = default!;
/// <inheritdoc cref="ISerial.EntityId"/>
[Required, MaxLength(128)]
public string EntityId { get; set; } = default!;
/// <inheritdoc cref="ISerial.Quantity"/>
[MinValue(0)]
public float Quantity { get; set; }
/// <inheritdoc cref="ISerial.Created"/>
/// <remarks>
/// If this property is null, the process will most likely
/// set the value of the current date (DateTime.UtcNow).
/// </remarks>
public DateTimeOffset? Created { get; set; }
/// <inheritdoc cref="ISerial.BestBefore"/>
public DateTimeOffset? BestBefore { get; set; }
/// <inheritdoc cref="ISerial.Status"/>
public Status Status { get; set; } = Status.Disabled;
/// <inheritdoc cref="ISerial.Value"/>
[Required, MaxLength(128)]
public string Value { get; set; } = default!;
}
/// <summary>
/// The arguments used when updating the existing serial entity.
/// </summary>
public sealed class UpdateSerialArgs : PrimaryKeyArgs<long>
{
/// <summary>
/// The new quantity. This is an absolute value, do not provide
/// delta values.
/// </summary>
[MinValue(0)]
public float Quantity { get; set; }
/// <inheritdoc cref="ISerial.BestBefore"/>
public DateTimeOffset? BestBefore { get; set; }
/// <inheritdoc cref="ISerial.Status"/>
public Status Status { get; set; } = Status.Disabled;
}
/// <summary>
/// The arguments used for selecting serial by its value.
/// </summary>
public sealed class SelectSerialArgs : Dto
{
/// <inheritdoc cref="ISerial.Value"/>
[Required, MaxLength(128)]
public string Value { get; set; } = default!;
}

@ -0,0 +1,27 @@
using Connected.Data;
using Logistics.Types.Warehouses;
namespace Logistics.Types.WarehouseLocations;
/// <summary>
/// Represents a physical or logical location inside a <see cref="IWarehouse"/>.
/// </summary>
/// <remarks>
/// Each <see cref="IWarehouse"/> contains zero or more <see cref="IWarehouseLocation">locations</see>.
/// Location can be a container, which means it contains child locations, or leaf, which doesn't contain
/// child locations. Items can be put only in leaf locations, whereas containers acts only as aggregators
/// which means they provide calculated values for items contained in the child locations.
/// </remarks>
public interface IWarehouseLocation : IPrimaryKey<int>
{
int? Parent { get; init; }
int Warehouse { get; init; }
string Name { get; init; }
string Code { get; init; }
Status Status { get; init; }
/// <summary>
/// The number of direct child items that belong to this
/// location. If this value is 0 it means the location
/// is leaf. If not, it's a container.
/// </summary>
int ItemCount { get; init; }
}

@ -0,0 +1,29 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
using Logistics.Types.Warehouses;
namespace Logistics.Types.WarehouseLocations;
[Service]
[ServiceUrl(LogisticsUrls.WarehouseLocations)]
public interface IWarehouseLocationService : IServiceNotifications<int>
{
Task<ImmutableList<IWarehouseLocation>> Query(QueryArgs? args);
/// <summary>
/// Queries warehouse locations for the specified <see cref="IWarehouse"/>.
/// </summary>
/// <param name="args">The arguments containing the id of the warehouse.</param>
/// <returns>The list of warehouse locations that belong to the specified warehouse.</returns>
Task<ImmutableList<IWarehouseLocation>> Query(QueryWarehouseLocationArgs args);
Task<ImmutableList<IWarehouseLocation>> Query(PrimaryKeyListArgs<int> args);
Task<ImmutableList<IWarehouseLocation>> QueryChildren(QueryWarehouseLocationChildrenArgs args);
Task<IWarehouseLocation?> Select(PrimaryKeyArgs<int> args);
Task<IWarehouseLocation?> Select(SelectWarehouseLocationArgs args);
Task<int> Insert(InsertWarehouseLocationArgs args);
Task Update(UpdateWarehouseLocationArgs args);
Task Patch(PatchArgs<int> args);
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.ServiceModel;
namespace Logistics.Types.WarehouseLocations;
public sealed class InsertWarehouseLocationArgs : Dto
{
public int? Parent { get; set; }
[MinValue(1)]
public int Warehouse { get; set; }
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public sealed class UpdateWarehouseLocationArgs : PrimaryKeyArgs<int>
{
public int? Parent { get; set; }
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public sealed class SelectWarehouseLocationArgs : Dto
{
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
}
public sealed class QueryWarehouseLocationArgs : QueryArgs
{
[MinValue(1)]
public int Warehouse { get; set; }
}
public sealed class QueryWarehouseLocationChildrenArgs : QueryArgs
{
[MinValue(1)]
public int Warehouse { get; set; }
public int? Parent { get; set; }
}

@ -0,0 +1,10 @@
using Connected.Data;
namespace Logistics.Types.Warehouses;
public interface IWarehouse : IPrimaryKey<int>
{
string Name { get; init; }
string Code { get; init; }
Status Status { get; init; }
}

@ -0,0 +1,32 @@
using System.Collections.Immutable;
using Connected.Annotations;
using Connected.Notifications;
using Connected.ServiceModel;
namespace Logistics.Types.Warehouses;
[Service]
[ServiceUrl(LogisticsUrls.Warehouses)]
public interface IWarehouseService : IServiceNotifications<int>
{
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IWarehouse>> Query(QueryArgs? args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<ImmutableList<IWarehouse>> Query(PrimaryKeyListArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Get | ServiceMethodVerbs.Post)]
Task<IWarehouse?> Select(PrimaryKeyArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Put)]
Task<int> Insert(InsertWarehouseArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Update(UpdateWarehouseArgs args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Patch)]
Task Patch(PatchArgs<int> args);
[ServiceMethod(ServiceMethodVerbs.Post | ServiceMethodVerbs.Delete)]
Task Delete(PrimaryKeyArgs<int> args);
}

@ -0,0 +1,26 @@
using Connected.Data;
using Connected.ServiceModel;
using System.ComponentModel.DataAnnotations;
namespace Logistics.Types.Warehouses;
public class InsertWarehouseArgs : Dto
{
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}
public class UpdateWarehouseArgs : PrimaryKeyArgs<int>
{
[Required, MaxLength(128)]
public string Name { get; set; } = default!;
[Required, MaxLength(32)]
public string Code { get; set; } = default!;
public Status Status { get; set; } = Status.Disabled;
}

@ -0,0 +1,9 @@
using Connected;
using Connected.Annotations;
[assembly: MicroService(MicroServiceType.Service)]
namespace Logistics.Types;
internal sealed class Bootstrapper : Startup
{
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Connected.Entities\Connected.Entities.csproj" />
<ProjectReference Include="..\..\Framework\Connected.Services\Connected.Services.csproj" />
<ProjectReference Include="..\Logistics.Types.Model\Logistics.Types.Model.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,46 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Logistics.Types.Packaging;
[Table(Schema = Domain.Code)]
internal sealed record Packing : ConsistentEntity<int>, IPacking
{
public const string EntityKey = $"{Domain.Code}.{nameof(Packing)}";
[Ordinal(0), Length(32)]
[Index(Unique = true)]
public string Ean { get; init; } = default!;
[Ordinal(1), Length(128)]
public string Entity { get; init; } = default!;
[Ordinal(2), Length(128)]
public string EntityId { get; init; } = default!;
[Ordinal(3)]
public float? Quantity { get; init; }
[Ordinal(4)]
public float? NetWeight { get; init; }
[Ordinal(5)]
public float? GrossWeight { get; init; }
[Ordinal(6)]
public int? Width { get; init; }
[Ordinal(7)]
public int? Height { get; init; }
[Ordinal(8)]
public int? Depth { get; init; }
[Ordinal(9)]
public int? ItemCount { get; init; }
[Ordinal(10)]
public Status Status { get; init; } = Status.Disabled;
}

@ -0,0 +1,177 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Types.Packaging;
internal sealed class PackingOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IPackingService packingService, IEventService events, ICachingService cache)
{
Storage = storage;
PackingService = packingService;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IPackingService PackingService { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await PackingService.Select(Arguments.Id) is not IPacking packing)
return;
SetState(packing);
await Storage.Open<Packing>().Update(Arguments.AsEntity<Packing>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IPackingService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertPackingArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<Packing>().Update(Arguments.AsEntity<Packing>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IPackingService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IPacking>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IPacking>?> OnInvoke()
{
return await (from e in Storage.Open<Packing>()
select e).WithArguments(Arguments).AsEntities<IPacking>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IPacking>>
{
public Lookup(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<IPacking>> OnInvoke()
{
return await (from e in Storage.Open<Packing>()
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IPacking>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IPacking>
{
public Select(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<IPacking?> OnInvoke()
{
return await Cache.Get<IPacking>(Packing.EntityKey, Arguments.Id, async (f) =>
{
return await (from e in Storage.Open<Packing>()
where e.Id == Arguments.Id
select e).AsEntity();
});
}
}
public sealed class SelectByEan : NullableServiceFunction<SelectPackingArgs, IPacking>
{
public SelectByEan(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<IPacking?> OnInvoke()
{
return await Cache.Get<IPacking>(Packing.EntityKey, f => string.Equals(f.Ean, Arguments.Ean, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<Packing>()
where string.Equals(e.Ean, Arguments.Ean, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
public sealed class Update : ServiceAction<UpdatePackingArgs>
{
public Update(IStorageProvider storage, ICachingService cache, IPackingService packingService, IEventService events)
{
Storage = storage;
Cache = cache;
PackingService = packingService;
Events = events;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
private IPackingService PackingService { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await PackingService.Select(Arguments.Id)) is not Packing entity)
return;
await Storage.Open<Packing>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
return SetState(await PackingService.Select(Arguments.Id)) as Packing;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(Packing.EntityKey, Arguments.Id);
await Events.Enqueue(this, PackingService, nameof(PackingService.Updated), Arguments);
}
}
}

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Logistics.Types.Packaging.PackingOps;
namespace Logistics.Types.Packaging;
internal sealed class PackingService : EntityService<int>, IPackingService
{
public PackingService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public async Task<int> Insert(InsertPackingArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not Packing entity)
return;
await Update(args.Patch<UpdatePackingArgs, Packing>(entity));
}
public async Task<ImmutableList<IPacking>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
public async Task<ImmutableList<IPacking>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
public async Task<IPacking?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IPacking?> Select(SelectPackingArgs args)
{
return await Invoke(GetOperation<Ops.SelectByEan>(), args);
}
public async Task Update(UpdatePackingArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
}

@ -0,0 +1,45 @@
using Connected.Middleware;
using Connected.Validation;
namespace Logistics.Types.Packaging;
internal sealed class InsertPackingValidator : MiddlewareComponent, IValidator<InsertPackingArgs>
{
public InsertPackingValidator(IPackingService packingService)
{
PackingService = packingService;
}
public IPackingService PackingService { get; }
public async Task Validate(InsertPackingArgs args)
{
if (await PackingService.Select(new SelectPackingArgs
{
Ean = args.Ean
}) is not null)
{
throw ValidationExceptions.ValueExists(nameof(args.Ean), args.Ean);
}
}
}
internal sealed class UpdatePackingValidator : MiddlewareComponent, IValidator<UpdatePackingArgs>
{
public UpdatePackingValidator(IPackingService packingService)
{
PackingService = packingService;
}
public IPackingService PackingService { get; }
public async Task Validate(UpdatePackingArgs args)
{
if (await PackingService.Select(new SelectPackingArgs
{
Ean = args.Ean
}) is Packing entity && entity.Id != args.Id)
{
throw ValidationExceptions.ValueExists(nameof(args.Ean), args.Ean);
}
}
}

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Logistics.Types.Serials;
/// <inheritdoc cref="ISerial"/>
[Table(Schema = Domain.Code)]
internal sealed record Serial : ConsistentEntity<long>, ISerial
{
/// <summary>
/// The entity identifier which can be used in caching keys for example.
/// </summary>
public const string EntityKey = $"{Domain.Code}.{nameof(Serial)}";
/// <inheritdoc cref="ISerial.Entity"/>
[Ordinal(0), MaxLength(128)]
public string Entity { get; init; } = default!;
/// <inheritdoc cref="ISerial.EntityId"/>
[Ordinal(1), MaxLength(128)]
public string EntityId { get; init; } = default!;
/// <inheritdoc cref="ISerial.Value"/>
[Ordinal(2), MaxLength(128), Index(Unique = true)]
public string Value { get; init; } = default!;
/// <inheritdoc cref="ISerial.Quantity"/>
[Ordinal(3)]
public float Quantity { get; init; }
/// <inheritdoc cref="ISerial.Created"/>
[Ordinal(4)]
public DateTimeOffset Created { get; init; }
/// <inheritdoc cref="ISerial.BestBefore"/>
[Ordinal(5)]
public DateTimeOffset? BestBefore { get; init; }
/// <inheritdoc cref="ISerial.Status"/>
[Ordinal(6)]
public Status Status { get; init; } = Status.Disabled;
}

@ -0,0 +1,220 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Types.Serials;
internal sealed class SerialOps
{
/// <inheritdoc cref="ISerialService.Delete(PrimaryKeyArgs{long})"/>
public sealed class Delete : ServiceAction<PrimaryKeyArgs<long>>
{
public Delete(IStorageProvider storage, ISerialService serials, IEventService events, ICachingService cache)
{
Storage = storage;
Serials = serials;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ISerialService Serials { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Serials.Select(Arguments.Id) is not ISerial serial)
return;
/*
* Setting state of the entity enable other middleware to use the entity even after it
* is deleted. For example, IEventListener will receive the state of this operation.
*/
SetState(serial);
/*
* Perform delete.
*/
await Storage.Open<Serial>().Update(Arguments.AsEntity<Serial>(State.Deleted));
}
protected override async Task OnCommitted()
{
/*
* Remove entity from the cache.
*/
await Cache.Remove(Serial.EntityKey, Arguments.Id);
/*
* Enqueue event so event listeners can respond to the transaction.
*/
await Events.Enqueue(this, Events, nameof(ISerialService.Deleted), Arguments);
}
}
/// <inheritdoc cref=" ISerialService.Insert(InsertSerialArgs)"/>
public sealed class Insert : ServiceFunction<InsertSerialArgs, long>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<long> OnInvoke()
{
/*
* Perform insert and return the newly inserted id.
*/
return (await Storage.Open<Serial>().Update(Arguments.AsEntity<Serial>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(ISerialService.Inserted), Arguments);
}
}
/// <inheritdoc cref="ISerialService.Query(QueryArgs?)"/>
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<ISerial>>
{
public Query(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<ISerial>> OnInvoke()
{
/*
* For non cached entities query always hits the storage.
*/
return await (from e in Storage.Open<Serial>()
select e).WithArguments(Arguments).AsEntities<ISerial>();
}
}
/// <inheritdoc cref="ISerialService.Query(PrimaryKeyListArgs{long})"/>
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<long>, ImmutableList<ISerial>>
{
public Lookup(IStorageProvider storage)
{
Storage = storage;
}
private IStorageProvider Storage { get; }
protected override async Task<ImmutableList<ISerial>> OnInvoke()
{
return await (from e in Storage.Open<Serial>()
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<ISerial>();
}
}
/// <inheritdoc cref="ISerialService.Select(PrimaryKeyArgs{long})"/>
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<long>, ISerial>
{
public Select(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<ISerial?> OnInvoke()
{
/*
* First, try to receive entity from the cache. If it doesn't exist in the cache
* load it from storage. If storage returns non null value, store it in the cache
* for subsequent calls. The entity gets remove either because of inactivity or when
* updating or deleting it.
*/
return await Cache.Get<ISerial>(Serial.EntityKey, Arguments.Id, async (f) =>
{
/*
* Doesn't exist in the cache. Let's do the storage action.
*/
return await (from e in Storage.Open<Serial>()
where e.Id == Arguments.Id
select e).AsEntity();
});
}
}
/// <inheritdoc cref="ISerialService.Select(SelectSerialArgs)"/>
public sealed class SelectByValue : NullableServiceFunction<SelectSerialArgs, ISerial>
{
public SelectByValue(IStorageProvider storage, ICachingService cache)
{
Storage = storage;
Cache = cache;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
protected override async Task<ISerial?> OnInvoke()
{
return await Cache.Get<ISerial>(Serial.EntityKey, f => string.Equals(f.Value, Arguments.Value, StringComparison.OrdinalIgnoreCase), async (f) =>
{
return await (from e in Storage.Open<Serial>()
where string.Equals(e.Value, Arguments.Value, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
});
}
}
/// <inheritdoc cref="ISerialService.Update(UpdateSerialArgs)"/>
public sealed class Update : ServiceAction<UpdateSerialArgs>
{
public Update(IStorageProvider storage, ICachingService cache, ISerialService packingService, IEventService events)
{
Storage = storage;
Cache = cache;
Serials = packingService;
Events = events;
}
private IStorageProvider Storage { get; }
private ICachingService Cache { get; }
private ISerialService Serials { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
/*
* Set the state of the unchanged entity. This enable other middleware to
* calculate quantity delta, for example because they receive the state of
* this operation. Not all middleware supports this, the primary example of
* such state client is IEventListener.
*/
if (SetState(await Serials.Select(Arguments.Id)) is not Serial entity)
return;
/*
* Sinc ethis is concurrent entity we must perform retry if the concurrency fails.
*/
await Storage.Open<Serial>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
/*
* The update failed because of concurrency. Remove the entity from the cache to ensure
* it gets loaded from the storage next time with fresh values and try again.
*/
await Cache.Remove(Serial.EntityKey, Arguments.Id);
/*
* Since the entity reloaded we must overwrite its state.
*/
return SetState(await Serials.Select(Arguments.Id)) as Serial;
});
}
protected override async Task OnCommitted()
{
await Cache.Remove(Serial.EntityKey, Arguments.Id);
await Events.Enqueue(this, Serials, nameof(Serials.Updated), Arguments);
}
}
}

@ -0,0 +1,61 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Logistics.Types.Serials.SerialOps;
namespace Logistics.Types.Serials;
/// <inheritdoc cref="ISerialService"/>
internal sealed class SerialService : EntityService<long>, ISerialService
{
/// <summary>
/// Create a new <see cref="SerialService"/> service.
/// </summary>
/// <param name="context">The context which serves as a DI scope.</param>
public SerialService(IContext context) : base(context)
{
}
/// <inheritdoc cref="ISerialService.Delete(PrimaryKeyArgs{long})"/>
public async Task Delete(PrimaryKeyArgs<long> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="ISerialService.Insert(InsertSerialArgs)"/>
public async Task<long> Insert(InsertSerialArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="ISerialService.Query(QueryArgs?)"/>
public async Task<ImmutableList<ISerial>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="ISerialService.Query(PrimaryKeyListArgs{long})"/>
public async Task<ImmutableList<ISerial>> Query(PrimaryKeyListArgs<long> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
/// <inheritdoc cref="ISerialService.Select(PrimaryKeyArgs{long})"/>
public async Task<ISerial?> Select(PrimaryKeyArgs<long> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="ISerialService.Select(SelectSerialArgs)"/>
public async Task<ISerial?> Select(SelectSerialArgs args)
{
return await Invoke(GetOperation<Ops.SelectByValue>(), args);
}
/// <inheritdoc cref="ISerialService.Update(UpdateSerialArgs)"/>
public async Task Update(UpdateSerialArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="ISerialService.Patch(PatchArgs{long})"/>
public async Task Patch(PatchArgs<long> args)
{
if (await Select(args.Id) is not Serial entity)
return;
await Update(args.Patch<UpdateSerialArgs, Serial>(entity));
}
}

@ -0,0 +1,24 @@
using Connected.Middleware;
using Connected.Validation;
namespace Logistics.Types.Serials;
internal sealed class InsertSerialValidator : MiddlewareComponent, IValidator<InsertSerialArgs>
{
public InsertSerialValidator(ISerialService serials)
{
Serials = serials;
}
public ISerialService Serials { get; }
public async Task Validate(InsertSerialArgs args)
{
if (await Serials.Select(new SelectSerialArgs
{
Value = args.Value
}) is not null)
{
throw ValidationExceptions.ValueExists(nameof(args.Value), args.Value);
}
}
}

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Logistics.Types.WarehouseLocations;
/// <inheritdoc cref="IWarehouseLocation"/>
internal sealed record WarehouseLocation : ConsistentEntity<int>, IWarehouseLocation
{
public const string EntityKey = $"{Domain.Code}.{nameof(WarehouseLocation)}";
/// <inheritdoc cref="IWarehouseLocation.Parent"/>
[Ordinal(0)]
public int? Parent { get; init; }
/// <inheritdoc cref="IWarehouseLocation.Warehouse"/>
[Ordinal(1)]
public int Warehouse { get; init; }
/// <inheritdoc cref="IWarehouseLocation.Name"/>
[Ordinal(2), MaxLength(128)]
public string Name { get; init; } = default!;
/// <inheritdoc cref="IWarehouseLocation.Code"/>
[Ordinal(3), MaxLength(32), Index(Unique = true)]
public string Code { get; init; } = default!;
/// <inheritdoc cref="IWarehouseLocation.Status"/>
[Ordinal(4)]
public Status Status { get; init; }
/// <inheritdoc cref="IWarehouseLocation.ItemCount"/>
[Ordinal(5)]
public int ItemCount { get; init; }
}

@ -0,0 +1,14 @@
using Connected.Entities.Caching;
using Logistics.Types.Warehouses;
namespace Logistics.Types.WarehouseLocations;
internal interface IWarehouseLocationCache : IEntityCacheClient<WarehouseLocation, int> { }
/// <summary>
/// Cache for the <see cref="WarehouseLocation"/> entity.
/// </summary>
internal sealed class WarehouseLocationCache : EntityCacheClient<WarehouseLocation, int>, IWarehouseLocationCache
{
public WarehouseLocationCache(IEntityCacheContext context) : base(context, Warehouse.EntityKey)
{
}
}

@ -0,0 +1,202 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Types.WarehouseLocations;
internal sealed class WarehouseLocationOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IWarehouseLocationService locations, IEventService events, ICachingService cache)
{
Storage = storage;
Locations = locations;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IWarehouseLocationService Locations { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Locations.Select(Arguments.Id) is not IWarehouseLocation entity)
return;
SetState(entity);
await Storage.Open<WarehouseLocation>().Update(Arguments.AsEntity<WarehouseLocation>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(WarehouseLocation.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IWarehouseLocationService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertWarehouseLocationArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<WarehouseLocation>().Update(Arguments.AsEntity<WarehouseLocation>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IWarehouseLocationService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IWarehouseLocation>>
{
public Query(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class QueryByWarehouse : ServiceFunction<QueryWarehouseLocationArgs, ImmutableList<IWarehouseLocation>>
{
public QueryByWarehouse(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where e.Warehouse == Arguments.Warehouse
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class QueryChildren : ServiceFunction<QueryWarehouseLocationChildrenArgs, ImmutableList<IWarehouseLocation>>
{
public QueryChildren(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where e.Warehouse == Arguments.Warehouse
&& (Arguments.Parent is null || e.Parent == Arguments.Parent)
select e).WithArguments(Arguments).AsEntities<IWarehouseLocation>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IWarehouseLocation>>
{
public Lookup(IWarehouseLocationCache locations)
{
Locations = locations;
}
public IWarehouseLocationCache Locations { get; }
protected override async Task<ImmutableList<IWarehouseLocation>> OnInvoke()
{
return await (from e in Locations
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IWarehouseLocation>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IWarehouseLocation>
{
public Select(IWarehouseLocationCache locations)
{
Locations = locations;
}
private IWarehouseLocationCache Locations { get; }
protected override async Task<IWarehouseLocation?> OnInvoke()
{
return await (from e in Locations
where e.Id == Arguments.Id
select e).AsEntity();
}
}
public sealed class SelectByCode : NullableServiceFunction<SelectWarehouseLocationArgs, IWarehouseLocation>
{
public SelectByCode(IWarehouseLocationCache locations)
{
Locations = locations;
}
private IWarehouseLocationCache Locations { get; }
protected override async Task<IWarehouseLocation?> OnInvoke()
{
return await (from e in Locations
where string.Equals(e.Code, Arguments.Code, StringComparison.OrdinalIgnoreCase)
select e).AsEntity();
}
}
public sealed class Update : ServiceAction<UpdateWarehouseLocationArgs>
{
public Update(IStorageProvider storage, IWarehouseLocationCache cache, IWarehouseLocationService locations, IEventService events)
{
Storage = storage;
Cache = cache;
Locations = locations;
Events = events;
}
private IStorageProvider Storage { get; }
private IWarehouseLocationCache Cache { get; }
private IWarehouseLocationService Locations { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Locations.Select(Arguments.Id)) is not WarehouseLocation entity)
return;
await Storage.Open<WarehouseLocation>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return SetState(await Locations.Select(Arguments.Id)) as WarehouseLocation;
});
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, Locations, nameof(Locations.Updated), Arguments);
}
}
}

@ -0,0 +1,29 @@
using Connected.Data.DataProtection;
using Connected.Data.EntityProtection;
using Connected.Entities;
using Connected.Middleware;
using Connected.Validation;
namespace Logistics.Types.WarehouseLocations;
internal class WarehouseLocationProtection : MiddlewareComponent, IEntityProtector<IWarehouseLocation>
{
public WarehouseLocationProtection(IWarehouseLocationCache cache)
{
Cache = cache;
}
public IWarehouseLocationCache Cache { get; }
public async Task Invoke(EntityProtectionArgs<IWarehouseLocation> args)
{
if (args.State != State.Deleted)
return;
/*
* We are protecting the children because warehouse locations support nesting entities.
*/
if (args.Entity.ItemCount > 0)
throw ValidationExceptions.ReferenceExists(args.Entity.GetType(), args.Entity.Id);
await Task.CompletedTask;
}
}

@ -0,0 +1,66 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Logistics.Types.WarehouseLocations.WarehouseLocationOps;
namespace Logistics.Types.WarehouseLocations;
internal sealed class WarehouseLocationService : EntityService<int>, IWarehouseLocationService
{
public WarehouseLocationService(IContext context) : base(context)
{
}
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
public Task<int> Insert(InsertWarehouseLocationArgs args)
{
return Invoke(GetOperation<Ops.Insert>(), args);
}
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not WarehouseLocation entity)
return;
await Update(args.Patch<UpdateWarehouseLocationArgs, WarehouseLocation>(entity));
}
public async Task<ImmutableList<IWarehouseLocation>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
public async Task<ImmutableList<IWarehouseLocation>> Query(QueryWarehouseLocationArgs args)
{
return await Invoke(GetOperation<Ops.QueryByWarehouse>(), args);
}
public async Task<ImmutableList<IWarehouseLocation>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
public async Task<ImmutableList<IWarehouseLocation>> QueryChildren(QueryWarehouseLocationChildrenArgs args)
{
return await Invoke(GetOperation<Ops.QueryChildren>(), args);
}
public async Task<IWarehouseLocation?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
public async Task<IWarehouseLocation?> Select(SelectWarehouseLocationArgs args)
{
return await Invoke(GetOperation<Ops.SelectByCode>(), args);
}
public async Task Update(UpdateWarehouseLocationArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
}

@ -0,0 +1,98 @@
using Connected.Entities;
using Connected.Middleware;
using Connected.Validation;
using Logistics.Types.Warehouses;
namespace Logistics.Types.WarehouseLocations;
internal sealed class InsertWarehouseLocationValidation : MiddlewareComponent, IValidator<InsertWarehouseLocationArgs>
{
public InsertWarehouseLocationValidation(IWarehouseLocationCache cache, IWarehouseService warehouses)
{
Cache = cache;
Warehouses = warehouses;
}
private IWarehouseLocationCache Cache { get; }
private IWarehouseService Warehouses { get; }
public async Task Validate(InsertWarehouseLocationArgs args)
{
/*
* Warehouse existence
*/
if (await Warehouses.Select(args.Warehouse) is null)
throw ValidationExceptions.NotFound(nameof(args.Warehouse), args.Warehouse);
/*
* Code is unique
*/
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
/*
* Check parent
*/
if (args.Parent is not null)
{
var parent = await (from e in Cache where e.Id == args.Parent select e).AsEntity();
if (parent is null)
throw ValidationExceptions.NotFound(nameof(args.Parent), args.Parent);
/*
* Parent and entity must be in the same warehouse.
*/
if (parent.Warehouse != args.Warehouse)
throw ValidationExceptions.Mismatch(nameof(args.Warehouse), args.Warehouse);
}
}
}
internal sealed class UpdateWarehouseLocationValidation : MiddlewareComponent, IValidator<UpdateWarehouseLocationArgs>
{
public UpdateWarehouseLocationValidation(IWarehouseLocationCache cache)
{
Cache = cache;
}
private IWarehouseLocationCache Cache { get; }
public async Task Validate(UpdateWarehouseLocationArgs args)
{
if (await (from e in Cache where e.Id == args.Id select e).AsEntity() is not WarehouseLocation entity)
return;
/*
* Code is unique
*/
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) && e.Id != args.Id select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
/*
* Check parent
*/
if (args.Parent is not null)
{
var parent = await (from e in Cache where e.Id == args.Parent select e).AsEntity();
if (parent is null)
throw ValidationExceptions.NotFound(nameof(args.Parent), args.Parent);
/*
* Parent and entity must be in the same warehouse.
*/
if (parent.Warehouse != entity.Warehouse)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
/*
* nesting under self
*/
if (parent.Id == args.Id)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
var currentParent = await (from e in Cache where e.Id == parent.Parent select e).AsEntity();
while (currentParent is not null)
{
if (currentParent.Id == args.Id)
throw ValidationExceptions.Mismatch(nameof(args.Parent), args.Parent);
currentParent = await (from e in Cache where e.Id == currentParent.Parent select e).AsEntity();
}
}
}
}

@ -0,0 +1,22 @@
using Connected.Annotations;
using Connected.Data;
using Connected.Entities.Annotations;
using Connected.Entities.Consistency;
namespace Logistics.Types.Warehouses;
/// <inheritdoc cref="IWarehouse"/>
[Table(Schema = Domain.Code)]
internal sealed record Warehouse : ConsistentEntity<int>, IWarehouse
{
public const string EntityKey = $"{Domain.Code}.{nameof(Warehouse)}";
/// <inheritdoc cref="IWarehouse.Name"/>
[Ordinal(0), Length(128)]
public string Name { get; init; } = default!;
/// <inheritdoc cref="IWarehouse.Code"/>
[Ordinal(1), Length(32), Index(Unique = true)]
public string Code { get; init; } = default!;
/// <inheritdoc cref="IWarehouse.Status"/>
[Ordinal(2)]
public Status Status { get; init; }
}

@ -0,0 +1,14 @@
using Connected.Entities.Caching;
namespace Logistics.Types.Warehouses;
internal interface IWarehouseCache : IEntityCacheClient<Warehouse, int> { }
/// <summary>
/// Cache for the <see cref="Warehouse"/> entity.
/// </summary>
internal sealed class WarehouseCache : EntityCacheClient<Warehouse, int>, IWarehouseCache
{
public WarehouseCache(IEntityCacheContext context) : base(context, Warehouse.EntityKey)
{
}
}

@ -0,0 +1,150 @@
using System.Collections.Immutable;
using Connected.Caching;
using Connected.Entities;
using Connected.Entities.Storage;
using Connected.Notifications.Events;
using Connected.ServiceModel;
using Connected.Services;
namespace Logistics.Types.Warehouses;
internal sealed class WarehouseOps
{
public sealed class Delete : ServiceAction<PrimaryKeyArgs<int>>
{
public Delete(IStorageProvider storage, IWarehouseService warehouses, IEventService events, ICachingService cache)
{
Storage = storage;
Warehouses = warehouses;
Events = events;
Cache = cache;
}
private IStorageProvider Storage { get; }
private IWarehouseService Warehouses { get; }
private IEventService Events { get; }
private ICachingService Cache { get; }
protected override async Task OnInvoke()
{
if (await Warehouses.Select(Arguments.Id) is not IWarehouse warehouse)
return;
SetState(warehouse);
await Storage.Open<Warehouse>().Update(Arguments.AsEntity<Warehouse>(State.Deleted));
}
protected override async Task OnCommitted()
{
await Cache.Remove(Warehouse.EntityKey, Arguments.Id);
await Events.Enqueue(this, Events, nameof(IWarehouseService.Deleted), Arguments);
}
}
public sealed class Insert : ServiceFunction<InsertWarehouseArgs, int>
{
public Insert(IStorageProvider storage, IEventService events)
{
Storage = storage;
Events = events;
}
private IStorageProvider Storage { get; }
private IEventService Events { get; }
protected override async Task<int> OnInvoke()
{
return (await Storage.Open<Warehouse>().Update(Arguments.AsEntity<Warehouse>(State.New))).Id;
}
protected override async Task OnCommitted()
{
await Events.Enqueue(this, Events, nameof(IWarehouseService.Inserted), Arguments);
}
}
public sealed class Query : ServiceFunction<QueryArgs, ImmutableList<IWarehouse>>
{
public Query(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
public IWarehouseCache Warehouses { get; }
protected override async Task<ImmutableList<IWarehouse>> OnInvoke()
{
return await (from e in Warehouses
select e).WithArguments(Arguments).AsEntities<IWarehouse>();
}
}
public sealed class Lookup : ServiceFunction<PrimaryKeyListArgs<int>, ImmutableList<IWarehouse>>
{
public Lookup(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
public IWarehouseCache Warehouses { get; }
protected override async Task<ImmutableList<IWarehouse>> OnInvoke()
{
return await (from e in Warehouses
where Arguments.IdList.Any(f => f == e.Id)
select e).AsEntities<IWarehouse>();
}
}
public sealed class Select : NullableServiceFunction<PrimaryKeyArgs<int>, IWarehouse>
{
public Select(IWarehouseCache warehouses)
{
Warehouses = warehouses;
}
private IWarehouseCache Warehouses { get; }
protected override async Task<IWarehouse?> OnInvoke()
{
return await (from e in Warehouses
where e.Id == Arguments.Id
select e).AsEntity();
}
}
public sealed class Update : ServiceAction<UpdateWarehouseArgs>
{
public Update(IStorageProvider storage, IWarehouseCache cache, IWarehouseService warehouses, IEventService events)
{
Storage = storage;
Cache = cache;
Warehouses = warehouses;
Events = events;
}
private IStorageProvider Storage { get; }
private IWarehouseCache Cache { get; }
private IWarehouseService Warehouses { get; }
private IEventService Events { get; }
protected override async Task OnInvoke()
{
if (SetState(await Warehouses.Select(Arguments.Id)) is not Warehouse entity)
return;
await Storage.Open<Warehouse>().Update(entity.Merge(Arguments, State.Default), Arguments, async () =>
{
await Cache.Refresh(Arguments.Id);
return SetState(await Warehouses.Select(Arguments.Id)) as Warehouse;
});
}
protected override async Task OnCommitted()
{
await Cache.Refresh(Arguments.Id);
await Events.Enqueue(this, Warehouses, nameof(Warehouses.Updated), Arguments);
}
}
}

@ -0,0 +1,56 @@
using System.Collections.Immutable;
using Connected.Entities;
using Connected.ServiceModel;
using Connected.Services;
using Ops = Logistics.Types.Warehouses.WarehouseOps;
namespace Logistics.Types.Warehouses;
/// <inheritdoc cref="IWarehouseService"/>
internal sealed class WarehouseService : EntityService<int>, IWarehouseService
{
/// <summary>
/// Creates a new <see cref="WarehouseService"/> instance.
/// </summary>
/// <param name="context">The context acting as a DI scope.</param>
public WarehouseService(IContext context) : base(context)
{
}
/// <inheritdoc cref="IWarehouseService.Delete(PrimaryKeyArgs{int})"/>
public async Task Delete(PrimaryKeyArgs<int> args)
{
await Invoke(GetOperation<Ops.Delete>(), args);
}
/// <inheritdoc cref="IWarehouseService.Insert(InsertWarehouseArgs)"/>
public async Task<int> Insert(InsertWarehouseArgs args)
{
return await Invoke(GetOperation<Ops.Insert>(), args);
}
/// <inheritdoc cref="IWarehouseService.Query(QueryArgs?)"/>
public async Task<ImmutableList<IWarehouse>> Query(QueryArgs? args)
{
return await Invoke(GetOperation<Ops.Query>(), args ?? QueryArgs.Default);
}
/// <inheritdoc cref="IWarehouseService.Query(PrimaryKeyListArgs{int})"/>
public async Task<ImmutableList<IWarehouse>> Query(PrimaryKeyListArgs<int> args)
{
return await Invoke(GetOperation<Ops.Lookup>(), args);
}
/// <inheritdoc cref="IWarehouseService.Select(PrimaryKeyArgs{int})"/>
public async Task<IWarehouse?> Select(PrimaryKeyArgs<int> args)
{
return await Invoke(GetOperation<Ops.Select>(), args);
}
/// <inheritdoc cref="IWarehouseService.Update(UpdateWarehouseArgs)"/>
public async Task Update(UpdateWarehouseArgs args)
{
await Invoke(GetOperation<Ops.Update>(), args);
}
/// <inheritdoc cref="IWarehouseService.Patch(PatchArgs{int})"/>
public async Task Patch(PatchArgs<int> args)
{
if (await Select(args.Id) is not Warehouse entity)
return;
await Update(args.Patch<UpdateWarehouseArgs, Warehouse>(entity));
}
}

@ -0,0 +1,36 @@
using Connected.Entities;
using Connected.Middleware;
using Connected.Validation;
namespace Logistics.Types.Warehouses;
internal sealed class InsertWarehouseValidator : MiddlewareComponent, IValidator<InsertWarehouseArgs>
{
public InsertWarehouseValidator(IWarehouseCache cache)
{
Cache = cache;
}
private IWarehouseCache Cache { get; }
public async Task Validate(InsertWarehouseArgs args)
{
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
}
}
internal sealed class UpdateWarehouseValidator : MiddlewareComponent, IValidator<UpdateWarehouseArgs>
{
public UpdateWarehouseValidator(IWarehouseCache cache)
{
Cache = cache;
}
private IWarehouseCache Cache { get; }
public async Task Validate(UpdateWarehouseArgs args)
{
if (await (from e in Cache where string.Equals(e.Code, args.Code, StringComparison.OrdinalIgnoreCase) && e.Id == args.Id select e).AsEntity() is not null)
throw ValidationExceptions.ValueExists(nameof(args.Code), args.Code);
}
}

@ -0,0 +1,149 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.32916.344
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logistics.Types.Model", "Logistics.Types.Model\Logistics.Types.Model.csproj", "{A673CACA-8A88-4AE1-B6C4-E31CED477981}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logistics.Documents.Model", "Logistics.Documents.Model\Logistics.Documents.Model.csproj", "{BCB56C1C-253F-4BB8-88A2-06653ED3E232}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{04C7CB0E-A6E1-4CCC-AF76-B199137278B7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logistics.Processes.Receive", "Logistics.Processes.Receive\Logistics.Processes.Receive.csproj", "{9EB45FF3-4910-4FEA-9553-97410C350AB9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common\Common.csproj", "{A4BF05CA-F790-4296-8647-18CEB1801637}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Model", "..\Common\Common.Model\Common.Model.csproj", "{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected", "..\Connected\Connected\Connected.csproj", "{6D40EEF9-9DB8-4755-B307-485523B1E3EF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logistics.Documents", "Logistics.Documents\Logistics.Documents.csproj", "{D808FC0D-355C-41D9-8560-DF99A963CA6A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Hosting", "..\Framework\Connected.Hosting\Connected.Hosting.csproj", "{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Runtime", "..\Framework\Connected.Runtime\Connected.Runtime.csproj", "{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Data", "..\Framework\Connected.Data\Connected.Data.csproj", "{BF03B6D3-849F-4095-9AB1-3D493075A8AC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Services", "..\Framework\Connected.Services\Connected.Services.csproj", "{F0E8C988-991B-4E2C-B1E1-0917685B7F70}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Middleware", "..\Framework\Connected.Middleware\Connected.Middleware.csproj", "{B8008838-C74C-4CD9-9248-B04005037EC9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Validation", "..\Framework\Connected.Validation\Connected.Validation.csproj", "{7BECE4B3-14A8-4CC1-BD96-E830EB453941}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contacts.Types.Model", "..\Customers\Contacts.Types.Model\Contacts.Types.Model.csproj", "{67EF282C-7CDE-4D85-A628-001306629762}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel", "..\Framework.ServiceModel\Connected.ServiceModel\Connected.ServiceModel.csproj", "{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logistics.Stock.Model", "Logistics.Stock.Model\Logistics.Stock.Model.csproj", "{415704FB-3DCA-41F9-A2D1-0FB72D346532}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Entities", "..\Framework\Connected.Entities\Connected.Entities.csproj", "{0EA01172-88E4-4B29-B553-B71F4EAEF844}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logistics.Stock", "Logistics.Stock\Logistics.Stock.csproj", "{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logistics.Types", "Logistics.Types\Logistics.Types.csproj", "{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A673CACA-8A88-4AE1-B6C4-E31CED477981}.Release|Any CPU.Build.0 = Release|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BCB56C1C-253F-4BB8-88A2-06653ED3E232}.Release|Any CPU.Build.0 = Release|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EB45FF3-4910-4FEA-9553-97410C350AB9}.Release|Any CPU.Build.0 = Release|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4BF05CA-F790-4296-8647-18CEB1801637}.Release|Any CPU.Build.0 = Release|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8}.Release|Any CPU.Build.0 = Release|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D40EEF9-9DB8-4755-B307-485523B1E3EF}.Release|Any CPU.Build.0 = Release|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D808FC0D-355C-41D9-8560-DF99A963CA6A}.Release|Any CPU.Build.0 = Release|Any CPU
{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5}.Release|Any CPU.Build.0 = Release|Any CPU
{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4}.Release|Any CPU.Build.0 = Release|Any CPU
{BF03B6D3-849F-4095-9AB1-3D493075A8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF03B6D3-849F-4095-9AB1-3D493075A8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF03B6D3-849F-4095-9AB1-3D493075A8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF03B6D3-849F-4095-9AB1-3D493075A8AC}.Release|Any CPU.Build.0 = Release|Any CPU
{F0E8C988-991B-4E2C-B1E1-0917685B7F70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0E8C988-991B-4E2C-B1E1-0917685B7F70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0E8C988-991B-4E2C-B1E1-0917685B7F70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0E8C988-991B-4E2C-B1E1-0917685B7F70}.Release|Any CPU.Build.0 = Release|Any CPU
{B8008838-C74C-4CD9-9248-B04005037EC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8008838-C74C-4CD9-9248-B04005037EC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8008838-C74C-4CD9-9248-B04005037EC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8008838-C74C-4CD9-9248-B04005037EC9}.Release|Any CPU.Build.0 = Release|Any CPU
{7BECE4B3-14A8-4CC1-BD96-E830EB453941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BECE4B3-14A8-4CC1-BD96-E830EB453941}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BECE4B3-14A8-4CC1-BD96-E830EB453941}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BECE4B3-14A8-4CC1-BD96-E830EB453941}.Release|Any CPU.Build.0 = Release|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67EF282C-7CDE-4D85-A628-001306629762}.Release|Any CPU.Build.0 = Release|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26}.Release|Any CPU.Build.0 = Release|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415704FB-3DCA-41F9-A2D1-0FB72D346532}.Release|Any CPU.Build.0 = Release|Any CPU
{0EA01172-88E4-4B29-B553-B71F4EAEF844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EA01172-88E4-4B29-B553-B71F4EAEF844}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EA01172-88E4-4B29-B553-B71F4EAEF844}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EA01172-88E4-4B29-B553-B71F4EAEF844}.Release|Any CPU.Build.0 = Release|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C4B2009-1DF6-4358-ABED-64E1F6076CCD}.Release|Any CPU.Build.0 = Release|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7B4FE4C-9D04-41FC-B036-78FAB6B15899}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A4BF05CA-F790-4296-8647-18CEB1801637} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{652D8B33-1485-43DD-9BDA-EE8103C2E0C8} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{6D40EEF9-9DB8-4755-B307-485523B1E3EF} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{1F47C7C9-45F7-469B-9B96-442DE2B3B0C5} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{AB5E17FA-335D-4495-B7C7-4E4F1D6E2AE4} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{BF03B6D3-849F-4095-9AB1-3D493075A8AC} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{F0E8C988-991B-4E2C-B1E1-0917685B7F70} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{B8008838-C74C-4CD9-9248-B04005037EC9} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{7BECE4B3-14A8-4CC1-BD96-E830EB453941} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{67EF282C-7CDE-4D85-A628-001306629762} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{6332E9A7-9EE8-4979-94AF-D297E4BBEA26} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
{0EA01172-88E4-4B29-B553-B71F4EAEF844} = {04C7CB0E-A6E1-4CCC-AF76-B199137278B7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FC558A1B-D7D9-4869-9589-1877A5E64720}
EndGlobalSection
EndGlobal
Loading…
Cancel
Save