Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
117022a
Add CrateTransmissionCommand, refactor common transmission plumbing t…
elsand Dec 16, 2025
9507105
Merge branch 'main' into fix/add-create-transmission-command
elsand Dec 16, 2025
63332f9
Remove dead code
elsand Dec 16, 2025
633a04a
Add checked to avoid wraparound
elsand Dec 16, 2025
e539754
Add checked to avoid wraparound
elsand Dec 16, 2025
11e5327
Fix compilation error
elsand Dec 16, 2025
f865735
Merge branch 'main' into fix/add-create-transmission-command
elsand Dec 16, 2025
945def7
Remove propertyname as parameter
elsand Dec 16, 2025
04f84f3
Add repository and bespoke query to validate hiearchy
elsand Dec 18, 2025
ecaa415
Simplify query
elsand Dec 19, 2025
be5e46b
Merge branch 'main' into fix/add-create-transmission-command
elsand Dec 19, 2025
102c43b
Merge branch 'main' into fix/add-create-transmission-command
elsand Jan 5, 2026
b968d38
Merge branch 'main' into fix/add-create-transmission-command
MagnusSandgren Jan 6, 2026
5ce5235
PR suggestions (#3208)
MagnusSandgren Jan 6, 2026
ea2c387
Separating dto/validator/mapping in vertical
elsand Jan 7, 2026
1cd5438
Remove dataloader
elsand Jan 7, 2026
a190803
Fix typo
elsand Jan 7, 2026
438837e
Fix typo in swagger
elsand Jan 8, 2026
610b482
Merge branch 'main' into fix/add-create-transmission-command
elsand Jan 8, 2026
9dbc469
Merge branch 'main' into fix/add-create-transmission-command
elsand Jan 9, 2026
9b65d65
Fix errors after merge
elsand Jan 9, 2026
de0cfc5
Fix docstrings
elsand Jan 9, 2026
0c47d29
Fix swagger
elsand Jan 9, 2026
4c7655a
Merge branch 'main' into fix/add-create-transmission-command
elsand Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ This file describes how AI coding agents should interact with the repository.
- **Run K6 functional tests**: `./tests/k6/run.sh -e localdev -a v1 -u "$TOKENGENERATOR_USERNAME" -p "$TOKENGENERATOR_PASSWORD" suites/all-single-pass.js`
- Do **not** run performance test suites. All K6 tests requires internet connectivity.

Always run `dotnet build` and `dotnet test` after making changes. Running integration tests require Docker, so in environments without a running Docker engine (ie. Codex), use `dotnet test Digdir.Domain.Dialogporten.sln --filter 'FullyQualifiedName!~Integration'` to skip them. All code must compile with `TreatWarningsAsErrors=true` and pass the .NET analyzers.
Always run `dotnet build` and `dotnet test` after making changes. Running integration tests require Docker, so run tests outside any sandbox.

Changes that affect Swagger/GraphQL spec must be reflected in the `docs/schema/v1/*verified*.json` files. Use the corresponding `*received*.json` files, which are generated upon build, for synchronization. The SwaggerSnapshot test will fail if these files are not identical. The SwaggerSnapshot test will fail if running the in debug configuration (must use release).
If a sandbox is absolutely needed, use `dotnet test Digdir.Domain.Dialogporten.sln --filter 'FullyQualifiedName!~Integration'` to skip them.

All code must compile with `TreatWarningsAsErrors=true` and pass the .NET analyzers.

Changes that affect Swagger/GraphQL spec must be reflected in the `docs/schema/v1/*verified*.json` files. Use the corresponding `*received*.json` files, which are generated upon build, for synchronization.
The SwaggerSnapshot test will fail if these files are not identical. The SwaggerSnapshot test will fail if running the in debug configuration (must use release).

## Code Style Guidelines
- Use file-scoped namespaces with `using` directives outside the namespace.
Expand Down
104 changes: 98 additions & 6 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -3081,7 +3081,7 @@
"type": "string"
},
"id": {
"description": "The UUDIv7 of the action may be provided to support idempotent additions to the list of activities.\nIf not supplied, a new UUIDv7 will be generated.",
"description": "A UUIDv7 may be provided to support idempotent additions to the list of activities.\nIf not supplied, a new UUIDv7 will be generated.",
"example": "01913cd5-784f-7d3b-abef-4c77b1f0972d",
"format": "guid",
"nullable": true,
Expand Down Expand Up @@ -3112,13 +3112,105 @@
},
"type": "object"
},
"V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionAttachment": {
"additionalProperties": false,
"properties": {
"displayName": {
"description": "The display name of the attachment that should be used in GUIs.",
"items": {
"$ref": "#/components/schemas/V1CommonLocalizations_Localization"
},
"nullable": true,
"type": "array"
},
"expiresAt": {
"description": "The UTC timestamp when the attachment expires and is no longer available.",
"format": "date-time",
"nullable": true,
"type": "string"
},
"id": {
"description": "A self-defined UUIDv7 may be provided to support idempotent additions of transmission attachments. If not provided, a new UUIDv7 will be generated.",
"example": "01913cd5-784f-7d3b-abef-4c77b1f0972d",
"format": "guid",
"nullable": true,
"type": "string"
},
"urls": {
"description": "The URLs associated with the attachment, each referring to a different representation of the attachment.",
"items": {
"$ref": "#/components/schemas/V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionAttachmentUrl"
},
"nullable": true,
"type": "array"
}
},
"type": "object"
},
"V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionAttachmentUrl": {
"additionalProperties": false,
"properties": {
"consumerType": {
"description": "The type of consumer the URL is intended for.",
"oneOf": [
{
"$ref": "#/components/schemas/Attachments_AttachmentUrlConsumerType"
}
]
},
"mediaType": {
"description": "The media type of the attachment.",
"example": "application/pdf\napplication/zip",
"nullable": true,
"type": "string"
},
"url": {
"description": "The fully qualified URL of the attachment.",
"format": "uri",
"type": "string"
}
},
"type": "object"
},
"V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionContent": {
"additionalProperties": false,
"properties": {
"contentReference": {
"description": "Front-channel embedded content. Used to dynamically embed content in the frontend from an external URL. Must be HTTPS.",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"summary": {
"description": "The transmission summary.",
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
},
"title": {
"description": "The transmission title. Must be text/plain.",
"oneOf": [
{
"$ref": "#/components/schemas/V1CommonContent_ContentValue"
}
]
}
},
"type": "object"
},
"V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionRequest": {
"additionalProperties": false,
"properties": {
"attachments": {
"description": "The transmission-level attachments.",
"items": {
"$ref": "#/components/schemas/V1ServiceOwnerDialogsCommandsUpdate_TransmissionAttachment"
"$ref": "#/components/schemas/V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionAttachment"
},
"nullable": true,
"type": "array"
Expand All @@ -3134,7 +3226,7 @@
"nullable": true,
"oneOf": [
{
"$ref": "#/components/schemas/V1ServiceOwnerDialogsCommandsUpdate_TransmissionContent"
"$ref": "#/components/schemas/V1ServiceOwnerDialogsCommandsCreateTransmission_TransmissionContent"
}
]
},
Expand All @@ -3155,7 +3247,7 @@
"type": "string"
},
"id": {
"description": "The UUDIv7 of the action may be provided to support idempotent additions to the list of transmissions.\nIf not supplied, a new UUIDv7 will be generated.",
"description": "A UUIDv7 may be provided to support idempotent additions to the list of transmissions.\nIf not supplied, a new UUIDv7 will be generated.",
"example": "01913cd5-784f-7d3b-abef-4c77b1f0972d",
"format": "guid",
"nullable": true,
Expand Down Expand Up @@ -3210,7 +3302,7 @@
"type": "string"
},
"id": {
"description": "The UUDIv7 of the action may be provided to support idempotent additions to the list of activities.\nIf not supplied, a new UUIDv7 will be generated.",
"description": "A UUIDv7 may be provided to support idempotent additions to the list of activities.\nIf not supplied, a new UUIDv7 will be generated.",
"example": "01913cd5-784f-7d3b-abef-4c77b1f0972d",
"format": "guid",
"nullable": true,
Expand Down Expand Up @@ -3725,7 +3817,7 @@
"type": "string"
},
"id": {
"description": "The UUDIv7 of the action may be provided to support idempotent additions to the list of transmissions.\nIf not supplied, a new UUIDv7 will be generated.",
"description": "A UUIDv7 may be provided to support idempotent additions to the list of transmissions.\nIf not supplied, a new UUIDv7 will be generated.",
"example": "01913cd5-784f-7d3b-abef-4c77b1f0972d",
"format": "guid",
"nullable": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
using Digdir.Domain.Dialogporten.Application.Common.Context;
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.SearchNew;
using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.SearchNew;
using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Common;
using Digdir.Domain.Dialogporten.Application.Externals;
using MediatR.NotificationPublishers;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SearchDialogQueryEu = Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search.SearchDialogQuery;
Expand Down Expand Up @@ -72,6 +74,8 @@ public static IServiceCollection AddApplication(this IServiceCollection services
.AddTransient<IUserRegistry, UserRegistry>()
.AddTransient<IUserParties, UserParties>()
.AddTransient<IClock, Clock>()
.AddTransient<IDialogTransmissionAppender, DialogTransmissionAppender>()
.AddTransient<ITransmissionHierarchyValidator, TransmissionHierarchyValidator>()
.AddTransient<IApplicationFeatureToggle<SearchDialogQueryEu, SearchDialogResultEu>, OptimizedEndUserDialogSearchFeatureToggle>()
.AddTransient<IApplicationFeatureToggle<SearchDialogQuerySo, SearchDialogResultSo>, OptimizedServiceOwnerDialogSearchFeatureToggle>()
.AddDataLoaders()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<ItemGroup>
<InternalsVisibleTo Include="Digdir.Domain.Dialogporten.Application.Integration.Tests"/>
<InternalsVisibleTo Include="Digdir.Domain.Dialogporten.Application.Unit.Tests"/>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Transmissions;

namespace Digdir.Domain.Dialogporten.Application.Externals;

public interface ITransmissionHierarchyRepository
{
Task<IReadOnlyCollection<TransmissionHierarchyNode>> GetHierarchyNodes(
Guid dialogId,
IReadOnlyCollection<Guid> startIds,
CancellationToken cancellationToken);
}

public sealed record TransmissionHierarchyNode(Guid Id, Guid? ParentId);
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,29 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.Common.Extensions;

internal static class TransmissionEntityExtensions
{
internal static bool ContainsTransmissionByEndUser(this List<DialogTransmission> transmissions) =>
transmissions.Any(x => x.TypeId
is DialogTransmissionType.Values.Submission
or DialogTransmissionType.Values.Correction);
internal static bool ContainsTransmissionByEndUser(this IEnumerable<DialogTransmission> transmissions) =>
transmissions.Any(x => IsEndUserTransmissionType(x.TypeId));

internal static (int FromParty, int FromServiceOwner) GetTransmissionCounts(this IEnumerable<DialogTransmission> transmissions)
{
var fromParty = 0;
var fromServiceOwner = 0;

foreach (var transmission in transmissions)
{
if (IsEndUserTransmissionType(transmission.TypeId))
{
fromParty++;
}
else
{
fromServiceOwner++;
}
}

return (fromParty, fromServiceOwner);
}

private static bool IsEndUserTransmissionType(DialogTransmissionType.Values typeId)
=> typeId is DialogTransmissionType.Values.Submission or DialogTransmissionType.Values.Correction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Actors;
using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Common;
using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Common;
using Digdir.Domain.Dialogporten.Domain.Actors;
using Digdir.Domain.Dialogporten.Domain.Common;
using Digdir.Domain.Dialogporten.Domain.DialogEndUserContexts.Entities;
Expand Down Expand Up @@ -46,6 +47,7 @@ internal sealed class CreateDialogCommandHandler : IRequestHandler<CreateDialogC
private readonly IDomainContext _domainContext;
private readonly IResourceRegistry _resourceRegistry;
private readonly IServiceResourceAuthorizer _serviceResourceAuthorizer;
private readonly ITransmissionHierarchyValidator _transmissionHierarchyValidator;
private readonly IUser _user;

public CreateDialogCommandHandler(
Expand All @@ -55,7 +57,8 @@ public CreateDialogCommandHandler(
IUnitOfWork unitOfWork,
IDomainContext domainContext,
IResourceRegistry resourceRegistry,
IServiceResourceAuthorizer serviceResourceAuthorizer)
IServiceResourceAuthorizer serviceResourceAuthorizer,
ITransmissionHierarchyValidator transmissionHierarchyValidator)
{
_user = user ?? throw new ArgumentNullException(nameof(user));
_db = db ?? throw new ArgumentNullException(nameof(db));
Expand All @@ -64,6 +67,7 @@ public CreateDialogCommandHandler(
_domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext));
_resourceRegistry = resourceRegistry ?? throw new ArgumentNullException(nameof(resourceRegistry));
_serviceResourceAuthorizer = serviceResourceAuthorizer ?? throw new ArgumentNullException(nameof(serviceResourceAuthorizer));
_transmissionHierarchyValidator = transmissionHierarchyValidator ?? throw new ArgumentNullException(nameof(transmissionHierarchyValidator));
}

public async Task<CreateDialogResult> Handle(CreateDialogCommand request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -113,23 +117,11 @@ public async Task<CreateDialogResult> Handle(CreateDialogCommand request, Cancel
}

dialog.HasUnopenedContent = DialogUnopenedContent.HasUnopenedContent(dialog, serviceResourceInformation);
_transmissionHierarchyValidator.ValidateWholeAggregate(dialog);

_domainContext.AddErrors(dialog.Transmissions.ValidateReferenceHierarchy(
keySelector: x => x.Id,
parentKeySelector: x => x.RelatedTransmissionId,
propertyName: nameof(CreateDialogDto.Transmissions),
maxDepth: 20,
maxWidth: 20));

dialog.FromPartyTransmissionsCount = (short)dialog.Transmissions
.Count(x => x.TypeId
is DialogTransmissionType.Values.Submission
or DialogTransmissionType.Values.Correction);

dialog.FromServiceOwnerTransmissionsCount = (short)dialog.Transmissions
.Count(x => x.TypeId is not
(DialogTransmissionType.Values.Submission
or DialogTransmissionType.Values.Correction));
var (fromParty, fromServiceOwner) = dialog.Transmissions.GetTransmissionCounts();
dialog.FromPartyTransmissionsCount = checked((short)fromParty);
dialog.FromServiceOwnerTransmissionsCount = checked((short)fromServiceOwner);

if (dialog.Transmissions.ContainsTransmissionByEndUser())
{
Expand Down
Loading