diff --git a/src/main/java/com/doubleo/didagent/agent/HospitalAgent.java b/src/main/java/com/doubleo/didagent/agent/HospitalAgent.java new file mode 100644 index 0000000..5c8c957 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/agent/HospitalAgent.java @@ -0,0 +1,102 @@ +package com.doubleo.didagent.agent; + +import com.doubleo.didagent.agent.client.AcapyClient; +import com.doubleo.didagent.dto.request.hospital.HospitalDidCreateRequest; +import com.doubleo.didagent.dto.request.hospital.HospitalInvitationCreateRequest; +import com.doubleo.didagent.dto.request.hospital.HospitalVcIssueRequest; +import com.doubleo.didagent.dto.response.hospital.*; +import com.doubleo.didagent.infra.config.acapy.AcapyProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +@Slf4j +public class HospitalAgent { + + private final AcapyClient hospitalClient; + private final AcapyProperties acapyProperties; + + public Mono createHospitalInvitation( + HospitalInvitationCreateRequest request, String token) { + log.debug("Creating hospital invitation with path: {}", acapyProperties.createInvitation()); + return hospitalClient + .getWebClient() + .post() + .uri(uriBuilder -> uriBuilder.path(acapyProperties.createInvitation()).build()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .bodyValue(request) + .retrieve() + .bodyToMono(HospitalInvitationCreateResponse.class) + .doOnError( + error -> { + log.error( + "Hospital MemberConnection fetch error: {}", + error.getMessage()); + }); + } + + public Mono createHospitalDid( + HospitalDidCreateRequest request, String token) { + return hospitalClient + .getWebClient() + .post() + .uri(uriBuilder -> uriBuilder.path(acapyProperties.createDid()).build()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .bodyValue(request) + .retrieve() + .bodyToMono(HospitalDidCreateResponse.class) + .doOnError( + error -> { + log.error("Hospital Did Creation error: {}", error.getMessage()); + }); + } + + public Mono postHospitalDid(String token, String did) { + return hospitalClient + .getWebClient() + .post() + .uri( + uriBuilder -> + uriBuilder + .path(acapyProperties.postPublicDid()) + .queryParam("did", did) + .build()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .retrieve() + .bodyToMono(HospitalDidPostResponse.class) + .doOnError( + error -> { + log.error("Hospital DID Post error: {}", error.getMessage()); + }); + } + + public Mono issueHospitalVc( + HospitalVcIssueRequest request, String token) { + log.debug("Issue VC endpoint: {}", acapyProperties.issueVc()); + Mono res = + hospitalClient + .getWebClient() + .post() + .uri(uriBuilder -> uriBuilder.path(acapyProperties.issueVc()).build()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token) + .bodyValue(request) + .retrieve() + .bodyToMono(HospitalVcIssueResponse.class) + .doOnError( + error -> { + log.error("Hospital VC Issuance error: {}", error.getMessage()); + }) + .doOnNext( + response -> + log.info("Hospital VC Issuance response: {}", response)); + return res; + } +} diff --git a/src/main/java/com/doubleo/didagent/agent/MediatorAgent.java b/src/main/java/com/doubleo/didagent/agent/MediatorAgent.java new file mode 100644 index 0000000..8f19d86 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/agent/MediatorAgent.java @@ -0,0 +1,38 @@ +package com.doubleo.didagent.agent; + +import com.doubleo.didagent.agent.client.AcapyClient; +import com.doubleo.didagent.dto.request.mediator.MediatorInvitationCreateRequest; +import com.doubleo.didagent.dto.response.mediator.MediatorInvitationCreateResponse; +import com.doubleo.didagent.infra.config.acapy.AcapyProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MediatorAgent { + + private final AcapyClient mediatorClient; + private final AcapyProperties acapyProperties; + + public Mono createMediatorInvitation( + MediatorInvitationCreateRequest request) { + return mediatorClient + .getWebClient() + .post() + .uri(uriBuilder -> uriBuilder.path(acapyProperties.createInvitation()).build()) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(MediatorInvitationCreateResponse.class) + .doOnError( + error -> { + log.error( + "Mediator MemberConnection fetch error: {}", + error.getMessage()); + }); + } +} diff --git a/src/main/java/com/doubleo/didagent/controller/AcapyWebhookController.java b/src/main/java/com/doubleo/didagent/controller/AcapyWebhookController.java index 9bebdab..b0f4e5e 100644 --- a/src/main/java/com/doubleo/didagent/controller/AcapyWebhookController.java +++ b/src/main/java/com/doubleo/didagent/controller/AcapyWebhookController.java @@ -1,22 +1,78 @@ package com.doubleo.didagent.controller; +import com.doubleo.didagent.service.AcapyWebhookService; import jakarta.servlet.http.HttpServletRequest; import java.util.Map; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; @RestController @RequestMapping("/webhooks") @Slf4j +@RequiredArgsConstructor public class AcapyWebhookController { - @PostMapping("/**") + private final AcapyWebhookService acapyWebhookService; + + @PostMapping("/topic/connections/") @ResponseStatus(HttpStatus.OK) - public void receiveAnyWebhook( + public void connectionWebhookReceive( HttpServletRequest req, @RequestBody Map payload, @RequestHeader Map headers) { + + log.info("=== Connection Webhook Received ==="); + logWebhookDetails(req, headers, payload); + + try { + Mono mono = acapyWebhookService.processConnectionWebhook(payload); + mono.subscribe(null, err -> log.error("processing failed", err)); + } catch (Exception e) { + log.error("Error processing connection webhook: {}", e.getMessage(), e); + } + } + + @PostMapping("/topic/issue_credential_v2_0/") + @ResponseStatus(HttpStatus.OK) + public void credentialWebhookReceive( + HttpServletRequest req, + @RequestBody Map payload, + @RequestHeader Map headers) { + + log.info("=== Credential Webhook Received ==="); + logWebhookDetails(req, headers, payload); + + try { + Mono mono = acapyWebhookService.processCredentialWebhook(payload); + mono.subscribe(null, err -> log.error("processing failed", err)); + } catch (Exception e) { + log.error("Error processing credential webhook: {}", e.getMessage(), e); + } + } + + @PostMapping("/topic/out_of_band/") + @ResponseStatus(HttpStatus.OK) + public void outOfBandWebhookReceive( + HttpServletRequest req, + @RequestBody Map payload, + @RequestHeader Map headers) { + + log.info("=== Out of Band Webhook Received ==="); + logWebhookDetails(req, headers, payload); + + try { + Mono mono = acapyWebhookService.processOutOfBandWebhook(payload); + mono.subscribe(null, err -> log.error("processing failed", err)); + } catch (Exception e) { + log.error("Error processing out of band webhook: {}", e.getMessage(), e); + } + } + + private void logWebhookDetails( + HttpServletRequest req, Map headers, Map payload) { String path = req.getRequestURI(); log.info("=== Webhook Path: {} ===", path); log.info("--- Headers ---"); diff --git a/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java b/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java deleted file mode 100644 index a947a8b..0000000 --- a/src/main/java/com/doubleo/didagent/dto/request/DidCreateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.doubleo.didagent.dto.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -public record DidCreateRequest( - @NotNull List routingKeys, @NotBlank String serviceEndpoint) {} diff --git a/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalDidCreateRequest.java b/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalDidCreateRequest.java new file mode 100644 index 0000000..3da0058 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalDidCreateRequest.java @@ -0,0 +1,10 @@ +package com.doubleo.didagent.dto.request.hospital; + +public record HospitalDidCreateRequest(String method, Options options) { + public record Options(String key_type) {} + + public static HospitalDidCreateRequest didKey() { + Options options = new Options("ed25519"); + return new HospitalDidCreateRequest("key", options); + } +} diff --git a/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalVcIssueRequest.java b/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalVcIssueRequest.java new file mode 100644 index 0000000..9234bd6 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/request/hospital/HospitalVcIssueRequest.java @@ -0,0 +1,108 @@ +package com.doubleo.didagent.dto.request.hospital; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +public record HospitalVcIssueRequest( + @JsonProperty("connection_id") String connectionId, + @JsonProperty("credential_preview") CredentialPreview credentialPreview, + Filter filter, + @JsonProperty("auto_issue") boolean autoIssue, + @JsonProperty("auto_remove") boolean autoRemove) { + + public record CredentialPreview( + @JsonProperty("@type") String type, List attributes) { + public record Attribute(String name, String value) {} + } + + public record Filter(@JsonProperty("ld_proof") LdProofFilter ldProof) {} + + public record LdProofFilter( + @JsonProperty("credential") Credential credential, + @JsonProperty("options") Options options) {} + + public record Credential( + @JsonProperty("@context") List context, + @JsonProperty("type") List type, + @JsonProperty("issuer") String issuer, + @JsonProperty("issuanceDate") String issuanceDate, + @JsonProperty("credentialSubject") CredentialSubject credentialSubject) {} + + public record CredentialSubject( + @JsonProperty("id") String id, + @JsonProperty("hospital_tenant") String hospitalTenant, + @JsonProperty("area_code") String areaCode) {} + + public record Options( + @JsonProperty("proofType") String proofType, + @JsonProperty("proofPurpose") String proofPurpose) {} + + // LD Proof 기반 병원 접근 크리덴셜용 팩토리 메서드 + public static HospitalVcIssueRequest createLdProofCredential( + String connectionId, + String issuer, + String credentialSubjectId, + String hospitalTenant, + String areaCode) { + + List defaultContext = + List.of( + "https://www.w3.org/2018/credentials/v1", + Map.of( + "hospital_tenant", "https://example.org/hospital_tenant", + "area_code", "https://example.org/area_code")); + + List defaultType = List.of("VerifiableCredential", "HospitalAccessCredential"); + String defaultIssuanceDate = java.time.Instant.now().toString(); + String defaultProofType = "Ed25519Signature2018"; + String defaultProofPurpose = "assertionMethod"; + + // Credential Preview 생성 + CredentialPreview preview = + new CredentialPreview( + "https://didcomm.org/issue-credential/2.0/credential-preview", + List.of( + new CredentialPreview.Attribute("hospital_tenant", hospitalTenant), + new CredentialPreview.Attribute("area_code", areaCode))); + + // Credential Subject 생성 + CredentialSubject credentialSubject = + new CredentialSubject(credentialSubjectId, hospitalTenant, areaCode); + + // LD Proof Filter 생성 + LdProofFilter ldProofFilter = + new LdProofFilter( + new Credential( + defaultContext, + defaultType, + issuer, + defaultIssuanceDate, + credentialSubject), + new Options(defaultProofType, defaultProofPurpose)); + + // Filter 생성 (LD Proof만 사용) + Filter filter = new Filter(ldProofFilter); + + return new HospitalVcIssueRequest( + connectionId, + preview, + filter, + true, // auto_issue + false // auto_remove + ); + } + + // 간단한 버전 (기본값 사용) + public static HospitalVcIssueRequest createWithDid( + String connectionId, String issuer, String credentialSubjectId) { + + return createLdProofCredential( + connectionId, + issuer, + credentialSubjectId, + "default_hospital", // 기본 병원 텐넌트 + "default_area" // 기본 지역 코드 + ); + } +} diff --git a/src/main/java/com/doubleo/didagent/dto/request/mediator/MediatorInvitationCreateRequest.java b/src/main/java/com/doubleo/didagent/dto/request/mediator/MediatorInvitationCreateRequest.java new file mode 100644 index 0000000..cb5855a --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/request/mediator/MediatorInvitationCreateRequest.java @@ -0,0 +1,32 @@ +package com.doubleo.didagent.dto.request.mediator; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public record MediatorInvitationCreateRequest( + @JsonProperty("alias") String alias, + @JsonProperty("handshake_protocols") List handshakeProtocols, + @JsonProperty("goal_code") String goalCode, + @JsonProperty("my_label") String myLabel, + @JsonProperty("accept") List accept, + @JsonProperty("use_did_method") String useDidMethod, + @JsonProperty("multi_use") boolean multiUse) { + public static MediatorInvitationCreateRequest generate() { + List handshakeProtocols = new ArrayList<>(); + List accept = new ArrayList<>(); + + handshakeProtocols.add("https://didcomm.org/didexchange/1.0"); + accept.add("didcomm/aip2;env=rfc19"); + + return new MediatorInvitationCreateRequest( + "mediator:invitation:" + LocalDateTime.now(), + handshakeProtocols, + "vc-issue", + "keywe_mediator", + accept, + "did:peer:2", + true); + } +} diff --git a/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java b/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java deleted file mode 100644 index 1f9e80e..0000000 --- a/src/main/java/com/doubleo/didagent/dto/response/DidCreateResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.doubleo.didagent.dto.response; - -import jakarta.validation.constraints.NotBlank; - -public record DidCreateResponse( - @NotBlank String peerDid2, - @NotBlank String signingKeyMb58, - @NotBlank String signingPrivBase58, - @NotBlank String agreementKeyMb58, - @NotBlank String x25519PrivateMb58) {} diff --git a/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalConnectionInfoResponse.java b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalConnectionInfoResponse.java new file mode 100644 index 0000000..dbd772d --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalConnectionInfoResponse.java @@ -0,0 +1,3 @@ +package com.doubleo.didagent.dto.response.hospital; + +public record HospitalConnectionInfoResponse() {} diff --git a/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidCreateResponse.java b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidCreateResponse.java new file mode 100644 index 0000000..0ca7e04 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidCreateResponse.java @@ -0,0 +1,13 @@ +package com.doubleo.didagent.dto.response.hospital; + +public record HospitalDidCreateResponse(Result result) { + public record Result( + String did, + String verkey, + String posture, + String key_type, + String method, + Metadata metadata) {} + + public record Metadata() {} +} diff --git a/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidPostResponse.java b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidPostResponse.java new file mode 100644 index 0000000..00784bc --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalDidPostResponse.java @@ -0,0 +1,13 @@ +package com.doubleo.didagent.dto.response.hospital; + +public record HospitalDidPostResponse(Result result) { + public record Result( + String did, + String verkey, + String posture, + String key_type, + String method, + Metadata metadata) {} + + public record Metadata(boolean posted) {} +} diff --git a/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalInvitationCreateResponse.java b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalInvitationCreateResponse.java new file mode 100644 index 0000000..4d8141d --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalInvitationCreateResponse.java @@ -0,0 +1,20 @@ +package com.doubleo.didagent.dto.response.hospital; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record HospitalInvitationCreateResponse( + String state, + boolean trace, + @JsonProperty("invi_msg_id") String inviMsgId, + @JsonProperty("oob_id") String oobId, + Invitation invitation, + @JsonProperty("invitation_url") String invitationUrl) { + public record Invitation( + @JsonProperty("@type") String type, + @JsonProperty("@id") String id, + String label, + @JsonProperty("handshake_protocols") List handshakeProtocols, + List accept, + List services) {} +} diff --git a/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalVcIssueResponse.java b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalVcIssueResponse.java new file mode 100644 index 0000000..db179dc --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/hospital/HospitalVcIssueResponse.java @@ -0,0 +1,21 @@ +package com.doubleo.didagent.dto.response.hospital; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; +import java.util.Map; + +public record HospitalVcIssueResponse( + String state, + @JsonProperty("created_at") Instant createdAt, + @JsonProperty("updated_at") Instant updatedAt, + @JsonProperty("cred_ex_id") String credExId, + @JsonProperty("connection_id") String connectionId, + @JsonProperty("thread_id") String threadId, + String initiator, + String role, + @JsonProperty("cred_proposal") Map credProposal, + @JsonProperty("cred_offer") Map credOffer, + @JsonProperty("by_format") Map byFormat, + @JsonProperty("auto_offer") Boolean autoOffer, + @JsonProperty("auto_issue") Boolean autoIssue, + @JsonProperty("auto_remove") Boolean autoRemove) {} diff --git a/src/main/java/com/doubleo/didagent/dto/response/mediator/MediatorInvitationCreateResponse.java b/src/main/java/com/doubleo/didagent/dto/response/mediator/MediatorInvitationCreateResponse.java new file mode 100644 index 0000000..345f408 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/dto/response/mediator/MediatorInvitationCreateResponse.java @@ -0,0 +1,21 @@ +package com.doubleo.didagent.dto.response.mediator; + +import com.doubleo.didagent.dto.response.hospital.HospitalInvitationCreateResponse; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record MediatorInvitationCreateResponse( + String state, + boolean trace, + @JsonProperty("invi_msg_id") String inviMsgId, + @JsonProperty("oob_id") String oobId, + HospitalInvitationCreateResponse.Invitation invitation, + @JsonProperty("invitation_url") String invitationUrl) { + public record Invitation( + @JsonProperty("@type") String type, + @JsonProperty("@id") String id, + String label, + @JsonProperty("handshake_protocols") List handshakeProtocols, + List accept, + List services) {} +} diff --git a/src/main/java/com/doubleo/didagent/service/AcapyWebhookService.java b/src/main/java/com/doubleo/didagent/service/AcapyWebhookService.java new file mode 100644 index 0000000..b0c2ae9 --- /dev/null +++ b/src/main/java/com/doubleo/didagent/service/AcapyWebhookService.java @@ -0,0 +1,302 @@ +package com.doubleo.didagent.service; + +import com.doubleo.didagent.agent.HospitalAgent; +import com.doubleo.didagent.domain.domain.ConnectionStatus; +import com.doubleo.didagent.domain.domain.MemberConnection; +import com.doubleo.didagent.domain.repository.HospitalInvitationRepository; +import com.doubleo.didagent.domain.repository.MemberConnectionRepository; +import com.doubleo.didagent.dto.request.hospital.HospitalDidCreateRequest; +import com.doubleo.didagent.dto.request.hospital.HospitalVcIssueRequest; +import com.doubleo.didagent.global.exception.CommonException; +import com.doubleo.didagent.global.exception.errorcode.AcapyErrorCode; +import com.doubleo.didagent.grpc.client.HospitalTenantClient; +import com.doubleo.didagent.grpc.client.PassClient; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AcapyWebhookService { + + private final PassClient passClient; + private final HospitalAgent hospitalAgent; + private final HospitalTenantClient hospitalTenantClient; + private final MemberConnectionRepository memberConnectionRepository; + private final HospitalInvitationRepository hospitalInvitationRepository; + private final ObjectMapper objectMapper; + + public Mono processConnectionWebhook(Map payload) { + String invitationId = (String) payload.get("invitation_msg_id"); + String connectionId = (String) payload.get("connection_id"); + String state = (String) payload.get("state"); + String walletId = (String) payload.get("x-wallet-id"); + String theirDid = (String) payload.get("their_did"); + + log.info( + "Processing connection webhook - connectionId: {}, state: {}, walletId: {}", + connectionId, + state, + walletId); + + if (state.equals("active")) { + log.debug( + "State is active. Initiating member connection and processing further steps."); + return initMemberConnection(payload) + .then(createAndPostDid(connectionId)) + .flatMap(did -> offerVc(connectionId, did, theirDid)) + .then(deleteHospitalInvitation(invitationId)); + } + + return Mono.empty(); + } + + public Mono processOutOfBandWebhook(Map payload) { + return Mono.fromRunnable( + () -> { + String oobId = (String) payload.get("oob_id"); + String connectionId = (String) payload.get("connection_id"); + String state = (String) payload.get("state"); + + log.info( + "Processing out of band webhook - oobId: {}, connectionId: {}, state: {}", + oobId, + connectionId, + state); + }); + } + + public Mono processCredentialWebhook(Map payload) { + String credExId = (String) payload.get("cred_ex_id"); + String connectionId = (String) payload.get("connection_id"); + String state = (String) payload.get("state"); + String threadId = (String) payload.get("thread_id"); + + log.info( + "Processing credential webhook - credExId: {}, connectionId: {}, state: {}, threadId: {}", + credExId, + connectionId, + state, + threadId); + + if ("credential-issued".equals(state)) { + return processCredentialIssued(credExId, connectionId, payload); + } + + return Mono.empty(); + } + + private Mono deleteHospitalInvitation(String invitationId) { + return Mono.fromRunnable( + () -> + hospitalInvitationRepository.deleteHospitalInvitationByInvitationId( + invitationId)) + .subscribeOn(Schedulers.boundedElastic()) + .then(); + } + + private Mono initMemberConnection(Map payload) { + return Mono.fromCallable( + () -> { + String invitationId = (String) payload.get("invitation_msg_id"); + return hospitalInvitationRepository + .findByInvitationId(invitationId) + .orElseThrow( + () -> + new CommonException( + AcapyErrorCode.INVITATION_NOT_FOUND)); + }) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap( + invitation -> { + String connectionId = (String) payload.get("connection_id"); + log.debug("connectionId: {}", connectionId); + + MemberConnection connection = + MemberConnection.createMemberConnection( + connectionId, + invitation.getTenantId(), + invitation.getPassId(), + invitation.getMemberId()); + + return Mono.fromCallable( + () -> memberConnectionRepository.save(connection)) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext( + saved -> + log.info( + "Created connection for connectionId: {}", + connectionId)) + .then(); + }); + } + + private Mono createAndPostDid(String connectionId) { + return getMemberConnectionReactive(connectionId) + .filter(connection -> connection.getConnectionStatus() == ConnectionStatus.ACTIVE) + .switchIfEmpty(Mono.error(new CommonException(AcapyErrorCode.DID_PROCESS_FAILED))) + .flatMap( + connection -> + hospitalAgent + .createHospitalDid( + HospitalDidCreateRequest.didKey(), + hospitalTenantClient + .getTokenByTenantId( + connection.getTenantId()) + .getWalletToken()) + .map(response -> response.result().did()) + .doOnNext( + hospitalDid -> + log.info( + "DID 생성 및 공개키 등록 완료: {}", + hospitalDid)) + .flatMap( + hospitalDid -> + hospitalAgent + .postHospitalDid( + hospitalTenantClient + .getTokenByTenantId( + connection + .getTenantId()) + .getWalletToken(), + hospitalDid) + .map( + postResponse -> + postResponse + .result() + .did()))); + } + + private Mono offerVc(String connectionId, String issuerDid, String theirDid) { + return getMemberConnectionReactive(connectionId) + .filter(connection -> connection.getConnectionStatus() == ConnectionStatus.ACTIVE) + .switchIfEmpty( + Mono.defer( + () -> { + log.warn("Connection {} is not active", connectionId); + return Mono.empty(); + })) + .flatMap( + connection -> { + HospitalVcIssueRequest request = + HospitalVcIssueRequest.createWithDid( + connectionId, issuerDid, theirDid); + + return Mono.fromCallable( + () -> { + try { + return objectMapper + .writerWithDefaultPrettyPrinter() + .writeValueAsString(request); + } catch (JsonProcessingException e) { + throw new RuntimeException( + "Failed to serialize request", e); + } + }) + .doOnNext( + requestJson -> + log.info( + "Sending VC offer request: {}", + requestJson)) + .then( + hospitalAgent.issueHospitalVc( + request, + hospitalTenantClient + .getTokenByTenantId( + connection.getTenantId()) + .getWalletToken())) + .doOnNext( + response -> + log.info( + "VC offer sent successfully. Response: {}", + response)) + .doOnError( + error -> { + log.error("Failed to issue VC: ", error); + if (error instanceof WebClientResponseException) { + WebClientResponseException wcre = + (WebClientResponseException) error; + log.error( + "Response body: {}", + wcre.getResponseBodyAsString()); + } + }) + .onErrorMap( + error -> + new RuntimeException( + "Failed to offer VC", error)) + .then(); + }); + } + + private Mono processCredentialIssued( + String credExId, String connectionId, Map payload) { + return Mono.fromCallable( + () -> { + Map byFormat = + (Map) payload.get("by_format"); + if (byFormat != null) { + Map credIssue = + (Map) byFormat.get("cred_issue"); + if (credIssue != null) { + Map ldProof = + (Map) credIssue.get("ld_proof"); + if (ldProof != null) { + log.info( + "Credential issued successfully - type: {}, issuer: {}", + ldProof.get("type"), + ldProof.get("issuer")); + } + } + } + return null; + }) + .then(getMemberConnectionReactive(connectionId)) + .flatMap( + connection -> { + connection.updateConnectionStatus(ConnectionStatus.VC_ISSUED); + return Mono.fromCallable( + () -> memberConnectionRepository.save(connection)) + .subscribeOn(Schedulers.boundedElastic()); + }) + .doOnSuccess( + saved -> { + log.info("VC 발급 완료"); + passClient.updateConnectionStatus( + saved.getTenantId(), + Long.parseLong(saved.getPassId()), + connectionId); + log.info( + "Credential issued and processed - credExId: {}, connectionId: {}", + credExId, + connectionId); + }) + .doOnError( + error -> + log.error( + "Error processing credential issued for credExId: {}", + credExId, + error)) + .then(); + } + + private Mono getMemberConnectionReactive(String connectionId) { + return Mono.fromCallable( + () -> + memberConnectionRepository + .findMemberConnectionByConnectionId(connectionId) + .orElseThrow( + () -> + new CommonException( + AcapyErrorCode + .MEMBER_CONNECTION_NOT_FOUND))) + .subscribeOn(Schedulers.boundedElastic()); + } +} diff --git a/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java b/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java index 89ae8b0..53b3942 100644 --- a/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java +++ b/src/test/java/com/doubleo/didagent/DidAgentApplicationTests.java @@ -1,11 +1,10 @@ package com.doubleo.didagent; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DidAgentApplicationTests { - @Test - void contextLoads() {} + // @Test + // void contextLoads() {} }