From fdcba5d37444e1d4d0d51e4ecfa599c7d7556ace Mon Sep 17 00:00:00 2001 From: Tess Stoddard Date: Mon, 8 Dec 2025 11:05:04 -0700 Subject: [PATCH] feat: add recurring p2p transfers endpoints --- .../p2p_transfer/P2PTransferBaseAccessor.java | 38 ++---- .../RecurringP2PTransferBaseAccessor.java | 127 ++++++++++++++++++ .../mx/path/model/mdx/model/Resources.java | 5 + .../p2p_transfer/RecurringP2PTransfer.java | 28 ++++ .../P2PTransferDurationsController.java | 4 +- .../P2PTransferFrequenciesController.java | 4 +- .../RecurringP2PTransfersController.java | 47 +++++++ .../P2PTransferDurationsControllerTest.groovy | 6 +- ...2PTransferFrequenciesControllerTest.groovy | 6 +- ...RecurringP2PTransfersControllerTest.groovy | 116 ++++++++++++++++ 10 files changed, 345 insertions(+), 36 deletions(-) create mode 100644 mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/RecurringP2PTransferBaseAccessor.java create mode 100644 mdx-models/src/main/java/com/mx/path/model/mdx/model/p2p_transfer/RecurringP2PTransfer.java create mode 100644 mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersController.java create mode 100644 mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersControllerTest.groovy diff --git a/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/P2PTransferBaseAccessor.java b/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/P2PTransferBaseAccessor.java index 7e79c972..a7efeda7 100644 --- a/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/P2PTransferBaseAccessor.java +++ b/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/P2PTransferBaseAccessor.java @@ -25,11 +25,7 @@ public class P2PTransferBaseAccessor extends Accessor { @GatewayAPI @Getter(AccessLevel.PROTECTED) - private DurationBaseAccessor durations; - - @GatewayAPI - @Getter(AccessLevel.PROTECTED) - private FrequencyBaseAccessor frequencies; + private RecurringP2PTransferBaseAccessor recurring; @GatewayAPI @Getter(AccessLevel.PROTECTED) @@ -117,39 +113,21 @@ public void setAccounts(AccountBaseAccessor accounts) { } /** - * Accessor for duration operations - * - * @return accessor - */ - @API - public DurationBaseAccessor durations() { - return durations; - } - - /** - * Sets duration accessor - * @param durations - */ - public void setDurations(DurationBaseAccessor durations) { - this.durations = durations; - } - - /** - * Accessor for frequency operations + * Accessor for recurring transfer operations * * @return accessor */ @API - public FrequencyBaseAccessor frequencies() { - return frequencies; + public RecurringP2PTransferBaseAccessor recurring() { + return recurring; } /** - * Sets frequency accessor - * @param frequencies + * Sets recurring transfer accessor + * @param recurring */ - public void setFrequencies(FrequencyBaseAccessor frequencies) { - this.frequencies = frequencies; + public void setRecurring(RecurringP2PTransferBaseAccessor recurring) { + this.recurring = recurring; } /** diff --git a/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/RecurringP2PTransferBaseAccessor.java b/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/RecurringP2PTransferBaseAccessor.java new file mode 100644 index 00000000..4f79a64f --- /dev/null +++ b/mdx-models/src/main/java/com/mx/path/model/mdx/accessor/p2p_transfer/RecurringP2PTransferBaseAccessor.java @@ -0,0 +1,127 @@ +package com.mx.path.model.mdx.accessor.p2p_transfer; + +import lombok.AccessLevel; +import lombok.Getter; + +import com.mx.path.core.common.accessor.API; +import com.mx.path.core.common.accessor.AccessorMethodNotImplementedException; +import com.mx.path.core.common.gateway.GatewayAPI; +import com.mx.path.core.common.gateway.GatewayClass; +import com.mx.path.gateway.accessor.Accessor; +import com.mx.path.gateway.accessor.AccessorResponse; +import com.mx.path.model.mdx.model.MdxList; +import com.mx.path.model.mdx.model.p2p_transfer.RecurringP2PTransfer; + +/** + * Accessor base for recurring P2P transfer operations + */ +@GatewayClass +@API(specificationUrl = "https://developer.mx.com/drafts/mdx/p2p_transfer/index.html#recurring-p2p-transfers") +public class RecurringP2PTransferBaseAccessor extends Accessor { + @GatewayAPI + @Getter(AccessLevel.PROTECTED) + private DurationBaseAccessor durations; + + @GatewayAPI + @Getter(AccessLevel.PROTECTED) + private FrequencyBaseAccessor frequencies; + + public RecurringP2PTransferBaseAccessor() { + } + + /** + * Create a recurring P2P transfer + * + * @param p2pTransfer + * @return + */ + @GatewayAPI + @API(description = "Create a recurring P2P transfer") + public AccessorResponse create(RecurringP2PTransfer p2pTransfer) { + throw new AccessorMethodNotImplementedException(); + } + + /** + * Delete a recurring P2P transfer + * + * @param id + * @return + */ + @GatewayAPI + @API(description = "Delete a recurring P2P transfer") + public AccessorResponse delete(String id) { + throw new AccessorMethodNotImplementedException(); + } + + /** + * Get a recurring P2P Transfer + * + * @param id + * @return + */ + @GatewayAPI + @API(description = "Get a recurring P2P transfer") + public AccessorResponse get(String id) { + throw new AccessorMethodNotImplementedException(); + } + + /** + * List all recurring P2P transfers + * + * @return + */ + @GatewayAPI + @API(description = "List all recurring P2P transfers") + public AccessorResponse> list() { + throw new AccessorMethodNotImplementedException(); + } + + /** + * Update a recurring P2P transfer + * + * @param id + * @param p2pTransfer + * @return + */ + @GatewayAPI + @API(description = "Update a recurring P2P transfer") + public AccessorResponse update(String id, RecurringP2PTransfer p2pTransfer) { + throw new AccessorMethodNotImplementedException(); + } + + /** + * Accessor for duration operations + * + * @return accessor + */ + @API + public DurationBaseAccessor durations() { + return durations; + } + + /** + * Sets duration accessor + * @param durations + */ + public void setDurations(DurationBaseAccessor durations) { + this.durations = durations; + } + + /** + * Accessor for frequency operations + * + * @return accessor + */ + @API + public FrequencyBaseAccessor frequencies() { + return frequencies; + } + + /** + * Sets frequency accessor + * @param frequencies + */ + public void setFrequencies(FrequencyBaseAccessor frequencies) { + this.frequencies = frequencies; + } +} diff --git a/mdx-models/src/main/java/com/mx/path/model/mdx/model/Resources.java b/mdx-models/src/main/java/com/mx/path/model/mdx/model/Resources.java index 25565518..f7960d4e 100644 --- a/mdx-models/src/main/java/com/mx/path/model/mdx/model/Resources.java +++ b/mdx-models/src/main/java/com/mx/path/model/mdx/model/Resources.java @@ -63,6 +63,7 @@ import com.mx.path.model.mdx.model.origination.Origination; import com.mx.path.model.mdx.model.p2p_transfer.Duration; import com.mx.path.model.mdx.model.p2p_transfer.P2PTransfer; +import com.mx.path.model.mdx.model.p2p_transfer.RecurringP2PTransfer; import com.mx.path.model.mdx.model.payment.Bill; import com.mx.path.model.mdx.model.payment.Enrollment; import com.mx.path.model.mdx.model.payment.Merchant; @@ -397,6 +398,10 @@ private static void registerP2PTransferModels(GsonBuilder builder) { builder.registerTypeAdapter(P2PTransfer.class, new ModelWrappableSerializer("p2p_transfer")); builder.registerTypeAdapter(new TypeToken>() { }.getType(), new ModelWrappableSerializer("p2p_transfers")); + // Recurring P2P Transfers + builder.registerTypeAdapter(RecurringP2PTransfer.class, new ModelWrappableSerializer("recurring_p2p_transfer")); + builder.registerTypeAdapter(new TypeToken>() { + }.getType(), new ModelWrappableSerializer("recurring_p2p_transfers")); } private static void registerPaymentsModels(GsonBuilder builder) { diff --git a/mdx-models/src/main/java/com/mx/path/model/mdx/model/p2p_transfer/RecurringP2PTransfer.java b/mdx-models/src/main/java/com/mx/path/model/mdx/model/p2p_transfer/RecurringP2PTransfer.java new file mode 100644 index 00000000..f1a34af9 --- /dev/null +++ b/mdx-models/src/main/java/com/mx/path/model/mdx/model/p2p_transfer/RecurringP2PTransfer.java @@ -0,0 +1,28 @@ +package com.mx.path.model.mdx.model.p2p_transfer; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import com.mx.path.model.mdx.model.MdxBase; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RecurringP2PTransfer extends MdxBase { + private String id; + private String accountId; + private BigDecimal amount; + private String confirmationId; + private String deliveryMethod; + private String durationType; + private String durationValue; + private String frequencyId; + private String memo; + private String recipientId; + private String recipientVerificationAnswer; + private String recipientVerificationQuestion; + private LocalDate startOn; + private String status; +} diff --git a/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferDurationsController.java b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferDurationsController.java index b3f2c6f9..b755c3f2 100644 --- a/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferDurationsController.java +++ b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferDurationsController.java @@ -13,9 +13,9 @@ @RestController @RequestMapping(value = "{clientId}", produces = BaseController.MDX_MEDIA) public class P2PTransferDurationsController extends BaseController { - @RequestMapping(value = "/users/{userId}/p2p_transfers/durations", method = RequestMethod.GET) + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers/durations", method = RequestMethod.GET) public final ResponseEntity> list() { - AccessorResponse> response = gateway().p2pTransfers().durations().list(); + AccessorResponse> response = gateway().p2pTransfers().recurring().durations().list(); return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); } } diff --git a/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesController.java b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesController.java index 4fc8a64f..cb9df6e1 100644 --- a/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesController.java +++ b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesController.java @@ -13,9 +13,9 @@ @RestController @RequestMapping(value = "{clientId}", produces = BaseController.MDX_MEDIA) public class P2PTransferFrequenciesController extends BaseController { - @RequestMapping(value = "/users/{userId}/p2p_transfers/frequencies", method = RequestMethod.GET) + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers/frequencies", method = RequestMethod.GET) public final ResponseEntity> list() { - AccessorResponse> response = gateway().p2pTransfers().frequencies().list(); + AccessorResponse> response = gateway().p2pTransfers().recurring().frequencies().list(); return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); } } diff --git a/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersController.java b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersController.java new file mode 100644 index 00000000..a2692463 --- /dev/null +++ b/mdx-web/src/main/java/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersController.java @@ -0,0 +1,47 @@ +package com.mx.path.model.mdx.web.controller; + +import com.mx.path.gateway.accessor.AccessorResponse; +import com.mx.path.model.mdx.model.MdxList; +import com.mx.path.model.mdx.model.p2p_transfer.RecurringP2PTransfer; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "{clientId}", produces = BaseController.MDX_MEDIA) +public class RecurringP2PTransfersController extends BaseController { + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers", method = RequestMethod.POST, consumes = BaseController.MDX_MEDIA) + public final ResponseEntity create(@RequestBody RecurringP2PTransfer p2pTransferRequest) { + AccessorResponse response = gateway().p2pTransfers().recurring().create(p2pTransferRequest); + return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); + } + + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers/{id}", method = RequestMethod.DELETE) + public final ResponseEntity delete(@PathVariable("id") String p2pTransferId) { + AccessorResponse response = gateway().p2pTransfers().recurring().delete(p2pTransferId); + return new ResponseEntity<>(createMultiMapForResponse(response.getHeaders()), HttpStatus.NO_CONTENT); + } + + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers/{id}", method = RequestMethod.GET) + public final ResponseEntity get(@PathVariable("id") String p2pTransferId) { + AccessorResponse response = gateway().p2pTransfers().recurring().get(p2pTransferId); + return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); + } + + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers", method = RequestMethod.GET) + public final ResponseEntity> list() { + AccessorResponse> response = gateway().p2pTransfers().recurring().list(); + return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); + } + + @RequestMapping(value = "/users/{userId}/recurring_p2p_transfers/{id}", method = RequestMethod.PUT, consumes = BaseController.MDX_MEDIA) + public final ResponseEntity update(@PathVariable("id") String p2pTransferId, @RequestBody RecurringP2PTransfer p2pTransferRequest) { + AccessorResponse response = gateway().p2pTransfers().recurring().update(p2pTransferId, p2pTransferRequest); + return new ResponseEntity<>(response.getResult().wrapped(), createMultiMapForResponse(response.getHeaders()), HttpStatus.OK); + } +} diff --git a/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferDurationsControllerTest.groovy b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferDurationsControllerTest.groovy index 03fa8700..dfb61b10 100644 --- a/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferDurationsControllerTest.groovy +++ b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferDurationsControllerTest.groovy @@ -9,6 +9,7 @@ import com.mx.path.gateway.accessor.AccessorResponse import com.mx.path.gateway.api.Gateway import com.mx.path.gateway.api.p2p_transfer.DurationGateway import com.mx.path.gateway.api.p2p_transfer.P2PTransferGateway +import com.mx.path.gateway.api.p2p_transfer.RecurringP2PTransferGateway import com.mx.path.model.mdx.model.MdxList import com.mx.path.model.mdx.model.p2p_transfer.Duration @@ -20,14 +21,17 @@ class P2PTransferDurationsControllerTest extends Specification { P2PTransferDurationsController subject Gateway gateway P2PTransferGateway p2pTransferGateway + RecurringP2PTransferGateway recurringP2PTransferGateway DurationGateway durationGateway def setup() { subject = new P2PTransferDurationsController() p2pTransferGateway = mock(P2PTransferGateway) + recurringP2PTransferGateway = mock(RecurringP2PTransferGateway) durationGateway = mock(DurationGateway) - doReturn(durationGateway).when(p2pTransferGateway).durations() + doReturn(recurringP2PTransferGateway).when(p2pTransferGateway).recurring() + doReturn(durationGateway).when(recurringP2PTransferGateway).durations() gateway = spy(Gateway.builder().clientId("client-1234").p2pTransfers(p2pTransferGateway).build()) } diff --git a/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesControllerTest.groovy b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesControllerTest.groovy index c762ba75..5597c9f1 100644 --- a/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesControllerTest.groovy +++ b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/P2PTransferFrequenciesControllerTest.groovy @@ -9,6 +9,7 @@ import com.mx.path.gateway.accessor.AccessorResponse import com.mx.path.gateway.api.Gateway import com.mx.path.gateway.api.p2p_transfer.FrequencyGateway import com.mx.path.gateway.api.p2p_transfer.P2PTransferGateway +import com.mx.path.gateway.api.p2p_transfer.RecurringP2PTransferGateway import com.mx.path.model.mdx.model.Frequency import com.mx.path.model.mdx.model.MdxList @@ -20,14 +21,17 @@ class P2PTransferFrequenciesControllerTest extends Specification { P2PTransferFrequenciesController subject Gateway gateway P2PTransferGateway p2pTransferGateway + RecurringP2PTransferGateway recurringP2PTransferGateway FrequencyGateway frequencyGateway def setup() { subject = new P2PTransferFrequenciesController() p2pTransferGateway = mock(P2PTransferGateway) + recurringP2PTransferGateway = mock(RecurringP2PTransferGateway) frequencyGateway = mock(FrequencyGateway) - doReturn(frequencyGateway).when(p2pTransferGateway).frequencies() + doReturn(recurringP2PTransferGateway).when(p2pTransferGateway).recurring() + doReturn(frequencyGateway).when(recurringP2PTransferGateway).frequencies() gateway = spy(Gateway.builder().clientId("client-1234").p2pTransfers(p2pTransferGateway).build()) } diff --git a/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersControllerTest.groovy b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersControllerTest.groovy new file mode 100644 index 00000000..d4b29539 --- /dev/null +++ b/mdx-web/src/test/groovy/com/mx/path/model/mdx/web/controller/RecurringP2PTransfersControllerTest.groovy @@ -0,0 +1,116 @@ +package com.mx.path.model.mdx.web.controller + + +import static org.mockito.Mockito.doReturn +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.spy +import static org.mockito.Mockito.verify + +import com.mx.path.gateway.accessor.AccessorResponse +import com.mx.path.gateway.api.Gateway +import com.mx.path.gateway.api.p2p_transfer.P2PTransferGateway +import com.mx.path.gateway.api.p2p_transfer.RecurringP2PTransferGateway +import com.mx.path.model.mdx.model.MdxList +import com.mx.path.model.mdx.model.p2p_transfer.RecurringP2PTransfer + +import org.springframework.http.HttpStatus + +import spock.lang.Specification + +class RecurringP2PTransfersControllerTest extends Specification { + RecurringP2PTransfersController subject + Gateway gateway + P2PTransferGateway p2pTransferGateway + RecurringP2PTransferGateway recurringP2PTransferGateway + + def setup() { + subject = new RecurringP2PTransfersController() + p2pTransferGateway = mock(P2PTransferGateway) + recurringP2PTransferGateway = mock(RecurringP2PTransferGateway) + + doReturn(recurringP2PTransferGateway).when(p2pTransferGateway).recurring() + gateway = spy(Gateway.builder().clientId("client-1234").p2pTransfers(p2pTransferGateway).build()) + } + + def cleanup() { + BaseController.clearGateway() + } + + def "create interacts with gateway"() { + given: + BaseController.setGateway(gateway) + def p2pTransfer = new RecurringP2PTransfer() + doReturn(new AccessorResponse().withResult(p2pTransfer)).when(recurringP2PTransferGateway).create(p2pTransfer) + + when: + def result = subject.create(p2pTransfer) + + then: + HttpStatus.OK == result.statusCode + result.body == p2pTransfer + verify(recurringP2PTransferGateway).create(p2pTransfer) || true + } + + def "delete interacts with gateway"() { + given: + BaseController.setGateway(gateway) + def id = "transfer-1234" + doReturn(new AccessorResponse()).when(recurringP2PTransferGateway).delete(id) + + when: + def result = subject.delete(id) + + then: + HttpStatus.NO_CONTENT == result.statusCode + verify(recurringP2PTransferGateway).delete(id) || true + } + + def "get interacts with gateway"() { + given: + BaseController.setGateway(gateway) + def id = "transfer-1234" + def p2pTransfer = new RecurringP2PTransfer() + doReturn(new AccessorResponse().withResult(p2pTransfer)).when(recurringP2PTransferGateway).get(id) + + when: + def result = subject.get(id) + + then: + HttpStatus.OK == result.statusCode + result.body == p2pTransfer + verify(recurringP2PTransferGateway).get(id) || true + } + + def "list interacts with gateway"() { + given: + BaseController.setGateway(gateway) + def p2pTransfers = new MdxList().tap { + add(new RecurringP2PTransfer()) + } + doReturn(new AccessorResponse>().withResult(p2pTransfers)).when(recurringP2PTransferGateway).list() + + when: + def result = subject.list() + + then: + HttpStatus.OK == result.statusCode + result.body == p2pTransfers + verify(recurringP2PTransferGateway).list() || true + } + + def "update interacts with gateway"() { + given: + BaseController.setGateway(gateway) + def id = "transfer-1234" + def p2pTransfer = new RecurringP2PTransfer() + doReturn(new AccessorResponse().withResult(p2pTransfer)).when(recurringP2PTransferGateway).update(id, p2pTransfer) + + when: + def result = subject.update(id, p2pTransfer) + + then: + HttpStatus.OK == result.statusCode + result.body == p2pTransfer + verify(recurringP2PTransferGateway).update(id, p2pTransfer) || true + } +}