Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.metaformsystems.redline.api.dto.request.DataPlaneRegistrationRequest;
import com.metaformsystems.redline.api.dto.request.ParticipantDeployment;
import com.metaformsystems.redline.api.dto.request.PartnerReferenceRequest;
import com.metaformsystems.redline.api.dto.request.ServiceProvider;
import com.metaformsystems.redline.api.dto.request.TenantRegistration;
import com.metaformsystems.redline.api.dto.response.Dataspace;
Expand Down Expand Up @@ -202,6 +203,29 @@ public ResponseEntity<List<PartnerReference>> getPartners(@PathVariable Long pro
return ResponseEntity.ok(references);
}

@PostMapping("service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/partners/{dataspaceId}")
// @PreAuthorize("hasRole('USER')")
@Operation(summary = "Create partner reference", description = "Creates a new partner reference for a participant in a specific dataspace")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Partner reference successfully created",
content = @Content(schema = @Schema(implementation = PartnerReference.class))),
@ApiResponse(responseCode = "400", description = "Invalid partner reference data"),
@ApiResponse(responseCode = "404", description = "Service provider, tenant, participant, or dataspace not found")
})
@Parameter(name = "providerId", description = "Database ID of the service provider", required = true)
@Parameter(name = "tenantId", description = "Database ID of the tenant", required = true)
@Parameter(name = "participantId", description = "Database ID of the participant", required = true)
@Parameter(name = "dataspaceId", description = "Database ID of the dataspace", required = true)
public ResponseEntity<PartnerReference> createPartner(@PathVariable Long providerId,
@PathVariable Long tenantId,
@PathVariable Long participantId,
@PathVariable Long dataspaceId,
@RequestBody PartnerReferenceRequest request) {
var partnerReference = tenantService.createPartnerReference(providerId, tenantId, participantId, dataspaceId, request);
// TODO auth check for provider access
return ResponseEntity.ok(partnerReference);
}

@GetMapping("service-providers/{serviceProviderId}/tenants/{tenantId}/participants/{participantId}/dataspaces")
// @PreAuthorize("hasRole('USER')")
@Operation(summary = "Get participant dataspaces", description = "Retrieves a list of dataspaces associated with a specific participant")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2026 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package com.metaformsystems.redline.api.dto.request;

import java.util.Map;

/**
* Request DTO for creating a partner reference.
*/
public record PartnerReferenceRequest(
String identifier,
String nickname,
Map<String, Object> properties
) {
public PartnerReferenceRequest(String identifier, String nickname) {
this(identifier, nickname, Map.of());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@

package com.metaformsystems.redline.api.dto.response;

public record PartnerReference(String identifier, String nickname) {
import java.util.Map;

public record PartnerReference(String identifier, String nickname, Map<String, Object> properties) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,34 @@

package com.metaformsystems.redline.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Embeddable;

import java.util.HashMap;
import java.util.Map;

/**
* A reference to a partner organization. The identifier is the participant identifier such as a DID.
*/
@Embeddable
public record PartnerReference(String identifier, String nickname) {
public record PartnerReference(
String identifier,
String nickname,

@Column(name = "properties", columnDefinition = "TEXT")
@Convert(converter = HashMapConverter.class)
Map<String, Object> properties
) {
public PartnerReference {
// Canonical constructor - initialize properties if null
if (properties == null) {
properties = new HashMap<>();
}
}

// Convenience constructor for backward compatibility
public PartnerReference(String identifier, String nickname) {
this(identifier, nickname, new HashMap<>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.metaformsystems.redline.api.dto.request.DataPlaneRegistrationRequest;
import com.metaformsystems.redline.api.dto.request.ParticipantDeployment;
import com.metaformsystems.redline.api.dto.request.PartnerReferenceRequest;
import com.metaformsystems.redline.api.dto.request.TenantRegistration;
import com.metaformsystems.redline.api.dto.response.Dataspace;
import com.metaformsystems.redline.api.dto.response.Participant;
Expand Down Expand Up @@ -218,13 +219,49 @@ public Participant getParticipant(Long id) {
return toParticipantResource(profile);
}

@Transactional
public PartnerReference createPartnerReference(Long providerId, Long tenantId, Long participantId, Long dataspaceId, PartnerReferenceRequest request) {
// Find participant first
var participant = participantRepository.findById(participantId)
.orElseThrow(() -> new ObjectNotFoundException("Participant not found with id: " + participantId));

// Verify participant belongs to the specified tenant
if (participant.getTenant() == null || !participant.getTenant().getId().equals(tenantId)) {
throw new ObjectNotFoundException("Participant " + participantId + " does not belong to tenant " + tenantId);
}

// Verify tenant belongs to the specified service provider
var tenant = participant.getTenant();
if (tenant.getServiceProvider() == null || !tenant.getServiceProvider().getId().equals(providerId)) {
throw new ObjectNotFoundException("Tenant " + tenantId + " does not belong to service provider " + providerId);
}

// Find dataspace info
var dataspaceInfo = participant.getDataspaceInfos().stream()
.filter(i -> i.getDataspaceId().equals(dataspaceId))
.findFirst()
.orElseThrow(() -> new ObjectNotFoundException("Dataspace info not found for participant " + participantId + " and dataspace " + dataspaceId));

// Create and add partner reference
var partnerReference = new com.metaformsystems.redline.domain.entity.PartnerReference(
request.identifier(),
request.nickname(),
request.properties() != null ? request.properties() : new java.util.HashMap<>()
);

dataspaceInfo.getPartners().add(partnerReference);
participantRepository.save(participant);

return new PartnerReference(partnerReference.identifier(), partnerReference.nickname(), partnerReference.properties());
}

@Transactional
public List<PartnerReference> getPartnerReferences(Long participantId, Long dataspacesId) {
return participantRepository.findById(participantId).stream()
.flatMap(p -> p.getDataspaceInfos().stream())
.filter(i -> i.getDataspaceId().equals(dataspacesId))
.flatMap(i -> i.getPartners().stream())
.map(r -> new PartnerReference(r.identifier(), r.nickname()))
.map(r -> new PartnerReference(r.identifier(), r.nickname(), r.properties()))
.toList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metaformsystems.redline.api.dto.request.DataspaceInfo;
import com.metaformsystems.redline.api.dto.request.ParticipantDeployment;
import com.metaformsystems.redline.api.dto.request.PartnerReferenceRequest;
import com.metaformsystems.redline.api.dto.request.ServiceProvider;
import com.metaformsystems.redline.api.dto.request.TenantRegistration;
import com.metaformsystems.redline.application.service.TokenProvider;
Expand Down Expand Up @@ -480,6 +481,100 @@ void shouldGetParticipantDataspaces_withMultipleDataspaces() throws Exception {
.andExpect(jsonPath("$[*].name").value(org.hamcrest.Matchers.containsInAnyOrder("Test Dataspace", "Second Dataspace")));
}

@Test
void shouldCreatePartnerReference() throws Exception {
// Create a tenant and participant with dataspace info
var tenant = new Tenant();
tenant.setName("Test Tenant");
tenant.setServiceProvider(serviceProvider);
tenant = tenantRepository.save(tenant);

var participant = new Participant();
participant.setIdentifier("Test Participant");
participant.setTenant(tenant);

// Add dataspace info to participant
var dataspaceInfo = new com.metaformsystems.redline.domain.entity.DataspaceInfo();
dataspaceInfo.setDataspaceId(dataspace.getId());
participant.getDataspaceInfos().add(dataspaceInfo);

tenant.addParticipant(participant);
participant = participantRepository.save(participant);

var request = new PartnerReferenceRequest("did:web:partner.com", "Partner Name", Map.of("key", "value"));

mockMvc.perform(post("/api/ui/service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/partners/{dataspaceId}",
serviceProvider.getId(), tenant.getId(), participant.getId(), dataspace.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.identifier").value("did:web:partner.com"))
.andExpect(jsonPath("$.nickname").value("Partner Name"))
.andExpect(jsonPath("$.properties.key").value("value"));

// Verify partner was saved
var savedParticipant = participantRepository.findById(participant.getId()).orElseThrow();
var savedDataspaceInfo = savedParticipant.getDataspaceInfos().iterator().next();
assertThat(savedDataspaceInfo.getPartners()).hasSize(1);
assertThat(savedDataspaceInfo.getPartners().get(0).identifier()).isEqualTo("did:web:partner.com");
assertThat(savedDataspaceInfo.getPartners().get(0).nickname()).isEqualTo("Partner Name");
}

@Test
void shouldCreatePartnerReference_withProperties() throws Exception {
// Create a tenant and participant with dataspace info
var tenant = new Tenant();
tenant.setName("Test Tenant");
tenant.setServiceProvider(serviceProvider);
tenant = tenantRepository.save(tenant);

var participant = new Participant();
participant.setIdentifier("Test Participant");
participant.setTenant(tenant);

var dataspaceInfo = new com.metaformsystems.redline.domain.entity.DataspaceInfo();
dataspaceInfo.setDataspaceId(dataspace.getId());
participant.getDataspaceInfos().add(dataspaceInfo);

tenant.addParticipant(participant);
participant = participantRepository.save(participant);

var properties = Map.<String, Object>of(
"region", "EU",
"compliance", "GDPR",
"metadata", Map.of("createdBy", "admin", "tags", List.of("partner", "trusted"))
);
var request = new PartnerReferenceRequest("did:web:partner.com", "Partner Name", properties);

mockMvc.perform(post("/api/ui/service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/partners/{dataspaceId}",
serviceProvider.getId(), tenant.getId(), participant.getId(), dataspace.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.identifier").value("did:web:partner.com"))
.andExpect(jsonPath("$.nickname").value("Partner Name"))
.andExpect(jsonPath("$.properties.region").value("EU"))
.andExpect(jsonPath("$.properties.compliance").value("GDPR"));
}

@Test
void shouldNotCreatePartnerReference_whenParticipantNotFound() throws Exception {
var tenant = new Tenant();
tenant.setName("Test Tenant");
tenant.setServiceProvider(serviceProvider);
tenant = tenantRepository.save(tenant);

var request = new PartnerReferenceRequest("did:web:partner.com", "Partner Name");

mockMvc.perform(post("/api/ui/service-providers/{providerId}/tenants/{tenantId}/participants/{participantId}/partners/{dataspaceId}",
serviceProvider.getId(), tenant.getId(), 999L, dataspace.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(404))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("Participant not found")));
}

@Test
void shouldGetParticipantDataspaces_whenParticipantNotFound() throws Exception {
// Create a tenant without participant
Expand Down
Loading
Loading