-
Notifications
You must be signed in to change notification settings - Fork 0
feat(s1): application controllers — REST endpoints + CommandHandler (STA-114) #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8b7b204
52ba109
e02b81b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.stablecoin.payments.orchestrator.application.controller; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| /** | ||
| * Request DTO for cancelling a payment. | ||
| */ | ||
| public record CancelPaymentRequest( | ||
| @NotBlank(message = "reason is required") | ||
| String reason | ||
| ) {} | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| package com.stablecoin.payments.orchestrator.application.controller; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.DecimalMin; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.NotBlank; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import jakarta.validation.constraints.NotNull; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import java.math.BigDecimal; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.UUID; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Request DTO for initiating a cross-border payment. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| public record InitiatePaymentRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||
| @NotNull(message = "senderId is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| UUID senderId, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotNull(message = "recipientId is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| UUID recipientId, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotNull(message = "sourceAmount is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| @DecimalMin(value = "0.01", message = "sourceAmount must be positive") | ||||||||||||||||||||||||||||||||||||||||||||||||
| BigDecimal sourceAmount, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding an upper bound on Without 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotBlank(message = "sourceCurrency is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| String sourceCurrency, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotBlank(message = "targetCurrency is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| String targetCurrency, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotBlank(message = "sourceCountry is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| String sourceCountry, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @NotBlank(message = "targetCountry is required") | ||||||||||||||||||||||||||||||||||||||||||||||||
| String targetCountry | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider ISO validation for currency and country codes.
💡 Example: Pattern validation for currency codes- `@NotBlank`(message = "sourceCurrency is required")
+ `@NotBlank`(message = "sourceCurrency is required")
+ `@Pattern`(regexp = "^[A-Z]{3}$", message = "sourceCurrency must be a valid 3-letter currency code")
String sourceCurrency,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| ) {} | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.stablecoin.payments.orchestrator.application.controller; | ||
|
|
||
| import com.stablecoin.payments.orchestrator.domain.service.PaymentCommandHandler; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * REST controller for payment lifecycle management. | ||
| * <p> | ||
| * Thin HTTP handler that delegates all business logic to {@link PaymentCommandHandler}. | ||
| */ | ||
| @Slf4j | ||
| @RestController | ||
| @RequestMapping("/v1/payments") | ||
| @RequiredArgsConstructor | ||
| public class PaymentController { | ||
|
|
||
| private final PaymentCommandHandler commandHandler; | ||
|
|
||
| /** | ||
| * Initiates a new cross-border payment. | ||
| * <p> | ||
| * Idempotent: returns 200 with existing payment if the same Idempotency-Key is reused. | ||
| * Returns 201 on first creation. | ||
| */ | ||
| @PostMapping | ||
| public ResponseEntity<PaymentResponse> initiatePayment( | ||
| @NotBlank(message = "Idempotency-Key header is required") | ||
| @RequestHeader("Idempotency-Key") String idempotencyKey, | ||
| @Valid @RequestBody InitiatePaymentRequest request) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| log.info("POST /v1/payments idempotencyKey={}, senderId={}", idempotencyKey, request.senderId()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stop logging raw idempotency keys and cancel reasons. Both values are user-controlled. Also applies to: 79-79 🤖 Prompt for AI Agents |
||
|
|
||
| var result = commandHandler.initiatePayment( | ||
| idempotencyKey, | ||
| UUID.randomUUID(), | ||
| request.senderId(), | ||
| request.recipientId(), | ||
| request.sourceAmount(), | ||
| request.sourceCurrency(), | ||
| request.targetCurrency(), | ||
| request.sourceCountry(), | ||
| request.targetCountry() | ||
| ); | ||
|
|
||
| var response = PaymentResponse.from(result.payment()); | ||
| var status = result.replay() ? HttpStatus.OK : HttpStatus.CREATED; | ||
|
|
||
| return ResponseEntity.status(status).body(response); | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves a payment by its ID. | ||
| */ | ||
| @GetMapping("/{paymentId}") | ||
| public PaymentResponse getPayment(@PathVariable UUID paymentId) { | ||
| log.info("GET /v1/payments/{}", paymentId); | ||
| return PaymentResponse.from(commandHandler.getPayment(paymentId)); | ||
| } | ||
|
|
||
| /** | ||
| * Cancels a payment by sending a cancel signal to the workflow. | ||
| * Returns 200 if accepted, 409 if the payment is in a terminal state. | ||
| */ | ||
| @PostMapping("/{paymentId}/cancel") | ||
| public PaymentResponse cancelPayment( | ||
| @PathVariable UUID paymentId, | ||
| @Valid @RequestBody CancelPaymentRequest request) { | ||
| log.info("POST /v1/payments/{}/cancel reason={}", paymentId, request.reason()); | ||
| return PaymentResponse.from(commandHandler.cancelPayment(paymentId, request.reason())); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.stablecoin.payments.orchestrator.application.controller; | ||
|
|
||
| import com.stablecoin.payments.orchestrator.domain.model.Payment; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Response DTO representing a payment in the API layer. | ||
| */ | ||
| public record PaymentResponse( | ||
| UUID paymentId, | ||
| String idempotencyKey, | ||
| UUID correlationId, | ||
| String state, | ||
| UUID senderId, | ||
| UUID recipientId, | ||
| BigDecimal sourceAmount, | ||
| String sourceCurrency, | ||
| String targetCurrency, | ||
| BigDecimal targetAmount, | ||
| String sourceCountry, | ||
| String targetCountry, | ||
| String failureReason, | ||
| Instant createdAt, | ||
| Instant updatedAt, | ||
| Instant expiresAt | ||
| ) { | ||
|
|
||
| /** | ||
| * Maps a domain {@link Payment} aggregate to the API response DTO. | ||
| */ | ||
| public static PaymentResponse from(Payment payment) { | ||
| return new PaymentResponse( | ||
| payment.paymentId(), | ||
| payment.idempotencyKey(), | ||
| payment.correlationId(), | ||
| payment.state().name(), | ||
| payment.senderId(), | ||
| payment.recipientId(), | ||
| payment.sourceAmount().amount(), | ||
| payment.sourceCurrency(), | ||
| payment.targetCurrency(), | ||
| payment.targetAmount() != null ? payment.targetAmount().amount() : null, | ||
| payment.corridor().sourceCountry(), | ||
| payment.corridor().targetCountry(), | ||
| payment.failureReason(), | ||
| payment.createdAt(), | ||
| payment.updatedAt(), | ||
| payment.expiresAt() | ||
| ); | ||
| } | ||
|
Comment on lines
+34
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if corridor can be null in Payment domain
ast-grep --pattern $'class Payment {
$$$
Corridor corridor$$$
$$$
}'
# Also check record definition
rg -A5 'record Payment\(' --type=javaRepository: Puneethkumarck/stablebridge-platform Length of output: 950 🏁 Script executed: cd payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/domain/model
# First, check the file size
wc -l Payment.java
# Then read the complete record definition
cat -n Payment.java | head -80Repository: Puneethkumarck/stablebridge-platform Length of output: 5089 🏁 Script executed: # Read the rest of Payment.java to check for compact constructor or validation
cat -n payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/domain/model/Payment.java | sed -n '66,150p'Repository: Puneethkumarck/stablebridge-platform Length of output: 5541 🏁 Script executed: # Check for compact constructor in Payment record
cat -n payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/domain/model/Payment.java | grep -A10 "public Payment"
# Also check how PaymentResponse.from() is called
rg -B3 -A3 'PaymentResponse\.from\(' --type=javaRepository: Puneethkumarck/stablebridge-platform Length of output: 14180 Add null-safety for
Guard both corridor accesses: payment.corridor() != null ? payment.corridor().sourceCountry() : null,
payment.corridor() != null ? payment.corridor().targetCountry() : null,Alternatively, consider adding a compact constructor to Payment to enforce corridor non-nullability at the record level. 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.stablecoin.payments.orchestrator.domain.model; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Thrown when attempting to cancel a payment that is in a terminal state | ||
| * and cannot be cancelled. | ||
| */ | ||
| public class PaymentNotCancellableException extends RuntimeException { | ||
|
|
||
| public PaymentNotCancellableException(UUID paymentId, PaymentState state) { | ||
| super("Payment %s cannot be cancelled in state %s".formatted(paymentId, state)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.stablecoin.payments.orchestrator.domain.model; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| /** | ||
| * Thrown when a payment cannot be found by its ID. | ||
| */ | ||
| public class PaymentNotFoundException extends RuntimeException { | ||
|
|
||
| public PaymentNotFoundException(UUID paymentId) { | ||
| super("Payment not found: " + paymentId); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding a length constraint on
reason.Without a
@Size(max = ...)limit, clients could submit arbitrarily large cancellation reasons, potentially causing DB storage issues or log bloat downstream.🛡️ Optional: Add max length constraint
🤖 Prompt for AI Agents