Fork me on GitHub

PayPal Tutorial

This tutorial demonstrates the ease of integrating PayPal to take payments.

Tutorial Source

Server side PayPal

The latest PayPal can be invoked completely from the client side. This is very convenient, however still requires verifying the funds were appropriately captured on server side.

To take more control and ensure the orders are not manipulated client side, the PayPal interaction can be done server side. The PayPalHttpClientManagedObjectSource provides the server side PayPal client.

The two interactions required are:

  1. Create the order
  2. Capture funds for the order

These interactions are the following:

	@Value
	@HttpObject
	public static class CreateOrder {
		private String currency;
	}

	@Value
	public static class CreatedOrder {
		private String orderId;
		private String status;
	}

	public void createOrder(CreateOrder createOrder, PayPalHttpClient client, ObjectResponse<CreatedOrder> response)
			throws IOException {
		String currency = createOrder.getCurrency();
		HttpResponse<Order> order = client
				.execute(new OrdersCreateRequest().requestBody(new OrderRequest().checkoutPaymentIntent("CAPTURE")
						.purchaseUnits(Arrays.asList(new PurchaseUnitRequest().description("Test create order")
								.amountWithBreakdown(new AmountWithBreakdown().currencyCode(currency).value("5.00")
										.amountBreakdown(new AmountBreakdown()
												.itemTotal(new Money().currencyCode(currency).value("4.50"))
												.taxTotal(new Money().currencyCode(currency).value("0.50"))))
								.items(Arrays.asList(new Item().name("Domain").description("Domain subscription")
										.unitAmount(new Money().currencyCode(currency).value("4.50"))
										.tax(new Money().currencyCode(currency).value("0.50")).quantity("1")))))));
		response.send(new CreatedOrder(order.result().id(), order.result().status()));
	}
	@Value
	@HttpObject
	public static class CaptureOrder {
		private String orderId;
	}

	@Value
	public static class CapturedOrder {
		private String orderId;
		private String status;
	}

	public void captureOrder(CaptureOrder captureOrder, PayPalHttpClient client, ObjectResponse<CapturedOrder> response)
			throws IOException {
		OrdersCaptureRequest request = new OrdersCaptureRequest(captureOrder.orderId);
		request.requestBody(new OrderRequest());
		HttpResponse<Order> order = client.execute(request);
		response.send(new CapturedOrder(order.result().id(), order.result().status()));
	}

See PayPal for further details.

Configure PayPal

The PayPalHttpClientManagedObjectSource depends on a PayPalConfigurationRepository to provide the PayPal configuration. This enables different implementations to pull from configuration, database, etc.

For simplicity (and use your own PayPal accounts), this tutorial uses an in memory implementation configured by REST request. The implementation is as follows:

public class InMemoryPayPalConfigurationRepository implements PayPalConfigurationRepository {

	private static volatile PayPalEnvironment environmet = null;

	public void loadEnvironment(String clientId, String clientSecret) {
		environmet = new PayPalEnvironment.Sandbox(clientId, clientSecret);
	}

	/*
	 * ================= PayPalConfigurationRepository ==================
	 */

	@Override
	public PayPalEnvironment createPayPalEnvironment() {
		return environmet;
	}
}

with REST request to configure as follows:

	@Value
	@HttpObject
	public static class Configuration {
		private String clientId;
		private String clientSecret;
	}

	public void configure(Configuration configuration, InMemoryPayPalConfigurationRepository repository) {
		repository.loadEnvironment(configuration.clientId, configuration.clientSecret);
	}

Testing

The following unit tests demonstrates the ability to mock PayPal for testing:

	@Order(1)
	@RegisterExtension
	public final PayPalExtension payPal = new PayPalExtension();

	@Order(2)
	@RegisterExtension
	public final MockWoofServerExtension server = new MockWoofServerExtension();

	@Test
	public void createOrder() throws Exception {

		// Record create order
		this.payPal.addOrdersCreateResponse(new com.paypal.orders.Order().id("MOCK_ORDER_ID").status("CREATED"))
				.validate((request) -> {
					assertEquals("/v2/checkout/orders?", request.path(), "Incorrect order");
					OrderRequest order = (OrderRequest) request.requestBody();
					assertEquals("CAPTURE", order.checkoutPaymentIntent(), "Incorrect intent");
					assertEquals("5.00", order.purchaseUnits().get(0).amountWithBreakdown().value(),
							"Incorrect amount");
				});

		// Create
		MockWoofResponse response = this.server
				.send(MockWoofServer.mockJsonRequest(HttpMethod.POST, "/create", new CreateOrder("AUD")));
		response.assertJson(200, new CreatedOrder("MOCK_ORDER_ID", "CREATED"));
	}

	@Test
	public void captureOrder() throws Exception {

		// Record capture order
		this.payPal.addOrdersCaptureResponse(new com.paypal.orders.Order().id("MOCK_ORDER_ID").status("COMPLETED"))
				.validate((request) -> {
					assertEquals("/v2/checkout/orders/MOCK_ORDER_ID/capture?", request.path(), "Incorrect order");
				});

		// Create
		MockWoofResponse response = this.server
				.send(MockWoofServer.mockJsonRequest(HttpMethod.POST, "/capture", new CaptureOrder("MOCK_ORDER_ID")));
		response.assertJson(200, new CapturedOrder("MOCK_ORDER_ID", "COMPLETED"));
	}

JUnit 4 example:

	public final PayPalRule payPal = new PayPalRule();

	public final MockWoofServerRule server = new MockWoofServerRule();

	@Rule
	public final RuleChain ordered = RuleChain.outerRule(this.payPal).around(this.server);

	@Test
	public void createOrder() throws Exception {

		// Record create order
		this.payPal.addOrdersCreateResponse(new Order().id("MOCK_ORDER_ID").status("CREATED")).validate((request) -> {
			assertEquals("Incorrect order", "/v2/checkout/orders?", request.path());
			OrderRequest order = (OrderRequest) request.requestBody();
			assertEquals("Incorrect intent", "CAPTURE", order.checkoutPaymentIntent());
			assertEquals("Incorrect amount", "5.00", order.purchaseUnits().get(0).amountWithBreakdown().value());
		});

		// Create
		MockWoofResponse response = this.server
				.send(MockWoofServer.mockJsonRequest(HttpMethod.POST, "/create", new CreateOrder("AUD")));
		response.assertJson(200, new CreatedOrder("MOCK_ORDER_ID", "CREATED"));
	}

	@Test
	public void captureOrder() throws Exception {

		// Record capture order
		this.payPal.addOrdersCaptureResponse(new Order().id("MOCK_ORDER_ID").status("COMPLETED"))
				.validate((request) -> {
					assertEquals("Incorrect order", "/v2/checkout/orders/MOCK_ORDER_ID/capture?", request.path());
				});

		// Create
		MockWoofResponse response = this.server
				.send(MockWoofServer.mockJsonRequest(HttpMethod.POST, "/capture", new CaptureOrder("MOCK_ORDER_ID")));
		response.assertJson(200, new CapturedOrder("MOCK_ORDER_ID", "COMPLETED"));
	}

Next

The next tutorial covers migrating Spring Web MVC to WoOF.