2
votes

I am wanting to extend the functionality of a Spring App to include an HTTP endpoint to receive Paypal Instant Payment Notifications.

Paypal sends these in the HTTP body like so:

mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=windows-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_name=Test+User&notify_version=2.6&custom=&payer_status=verified&address_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&payment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&residence_country=US&test_ipn=1&handling_amount=0.00&transaction_subject=&payment_gross=19.95&shipping=0.00

https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/

Do I need to define a concrete class to define the request body e.g.

Request Class

    public class PaypalIPNRequest {

    private static final long serialVersionUID = 1L;
    private String mc_gross;
    private String protection_eligibility;
    private String address_street;
    ...


    public PaypalIPNRequest() {
    }

    //getters setters
}

Controller

@Override
@Auditable
@RequestMapping(value = "/ipnRequest.do", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void ipnRequest(@RequestBody final PaypalIPNRequest request) {
}

As stated in this SO answer: @RequestBody and @ResponseBody annotations in Spring

What happens though if Paypal change their IPN request in the future, will this break?

Is there a better way to pass the request body without having a specific class?

Could I use HttpServletRequest?

1

1 Answers

3
votes

What I have done in the past is use com.paypal.base.ipn.IPNMessage to validate and retrieve from the request (like you proposed) just the fields that where important to me instead of mapping the entire request body to a concrete class, i.e.:

private final static String PAYPAL_WEB_IPN_TXN_PARAM = "txn_id";
private final static String PAYPAL_WEB_IPN_AMOUNT_PARAM = "mc_gross";
private final static String PAYPAL_WEB_IPN_PAYMENT_STATUS_PARAM = "payment_status";
private final static String PAYPAL_WEB_IPN_PAYMENT_STATUS = "Completed";

@Resource(name = "payPalConfigurationMap")
private Map<String, String> configurationMap;

private OAuthTokenCredential oAuthTokenCredential;

@PostConstruct
public void init() {
    Properties properties = new Properties();
    properties.putAll(configurationMap);
    PayPalResource.initConfig(properties);

    oAuthTokenCredential = new OAuthTokenCredential(
            configurationMap.get(Constants.CLIENT_ID),
            configurationMap.get(Constants.CLIENT_SECRET),
            configurationMap
    );
}


public DonationDTO validateWebIPN(HttpServletRequest request) throws Exception {
    IPNMessage ipnlistener = new IPNMessage(request, configurationMap);
    boolean isIpnVerified = ipnlistener.validate();

    String paymentStatus = ipnlistener.getIpnValue(PAYPAL_WEB_IPN_PAYMENT_STATUS_PARAM);

    if (isIpnVerified && paymentStatus.equalsIgnoreCase(PAYPAL_WEB_IPN_PAYMENT_STATUS)) {
        String amount = ipnlistener.getIpnValue(PAYPAL_WEB_IPN_AMOUNT_PARAM);
        String tx = ipnlistener.getIpnValue(PAYPAL_WEB_IPN_TXN_PARAM);

        // irrelevant code

        return donationDTO;
    }else{
        String exceptionMessage = "Problem when requesting info from PayPal service";
        logger.error(exceptionMessage);
        throw new Exception(exceptionMessage);
    }
}

This way, unless Paypal changes the name of the fields (which shouldn't happen) you shouldn't have any problem.