PayPay決済では、PayPay Open Payment API(OPA)という、決済操作をするAPIを提供しています。
その一つに、PayPay側からイベント通知を行うWebhookを提供しており、以下ようなJSON形式のデータがPostされるようです。
そのJSON形式のデータをjacksonで、デシリアライズしてみました。
https://www.paypay.ne.jp/opa/doc/jp/v1.0/webcashier#section/Webhook
{
"notification_type": "Transaction",
"merchant_id": "123456789",
"store_id": "123456",
"pos_id": "123",
"order_id": "123456789123456",
"merchant_order_id": "A12345",
"authorized_at": "2020-03-13T13:35:30Z",
"expires_at": "2020-04-13T13:35:29Z",
"paid_at": "2020-04-13T13:35:29Z",
"order_amount": 12345,
"state": "COMPLETED"
}
クラス名は、Notificationにしました。
また、今後、PayPay側でJSONの仕様変更があるかもしれません。
たとえば、パラメタの追加は、想定しなければいけませんので、
@JsonIgnoreProperties(ignoreUnknown = true)
を指定し、新たなパラメタは無視しています。
package paypay.handling;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import java.io.Serializable;
import java.util.Date;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Notification implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("authorized_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private Date authorizedAt;
@JsonProperty("confirmation_expires_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private Date confirmationExpiresAt;
@JsonProperty("expires_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private Date expiresAt;
@JsonProperty("merchant_id")
private String merchantId;
@JsonProperty("notification_id")
private String notificationId;
@JsonProperty("fileType")
private FileType fileType;
@JsonProperty("path")
private String path;
@JsonProperty("requestedAt")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private Date requestedAt;
@JsonProperty("merchant_order_id")
private String merchantOrderId;
@JsonProperty("notification_type")
private NotificationType notificationType;
@JsonProperty("order_amount")
private Integer orderAmount;
@JsonProperty("order_id")
private String orderId;
@JsonProperty("paid_at")
private String paidAt;
@JsonProperty("pos_id")
private String posId;
@JsonProperty("state")
private State state;
@JsonProperty("store_id")
private String storeId;
public Notification() {
}
public String getNotificationId() {
return notificationId;
}
public void setNotificationId(String notificationId) {
this.notificationId = notificationId;
}
public FileType getFileType() {
return fileType;
}
public void setFileType(FileType fileType) {
this.fileType = fileType;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public Date getRequestedAt() {
return requestedAt;
}
public void setRequestedAt(Date requestedAt) {
this.requestedAt = requestedAt;
}
public Date getAuthorizedAt() {
return authorizedAt;
}
public void setAuthorizedAt(Date authorizedAt) {
this.authorizedAt = authorizedAt;
}
public Date getConfirmationExpiresAt() {
return confirmationExpiresAt;
}
public void setConfirmationExpiresAt(Date confirmationExpiresAt) {
this.confirmationExpiresAt = confirmationExpiresAt;
}
public Date getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Date expiresAt) {
this.expiresAt = expiresAt;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public String getMerchantOrderId() {
return merchantOrderId;
}
public void setMerchantOrderId(String merchantOrderId) {
this.merchantOrderId = merchantOrderId;
}
public NotificationType getNotificationType() {
return notificationType;
}
public void setNotificationType(NotificationType notificationType) {
this.notificationType = notificationType;
}
public Integer getOrderAmount() {
return orderAmount;
}
public void setOrderAmount(Integer orderAmount) {
this.orderAmount = orderAmount;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getPaidAt() {
return paidAt;
}
public void setPaidAt(String paidAt) {
this.paidAt = paidAt;
}
public String getPosId() {
return posId;
}
public void setPosId(String posId) {
this.posId = posId;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public String getStoreId() {
return storeId;
}
public void setStoreId(String storeId) {
this.storeId = storeId;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
public enum State {
COMPLETED("COMPLETED"),
CANCELED("CANCELED"),
EXPIRED("EXPIRED"),
AUTHORIZED("AUTHORIZED"),
FAILED("FAILED"),
UNKNOWN("UNKNOWN");
private final String name;
State(final String name) {
this.name = name;
}
public static State enumOf(final String name) {
for (State value : values()) {
if (StringUtils.equals(value.getName(), name)) {
return value;
}
}
return UNKNOWN;
}
@JsonCreator // This is the factory method and must be static
public static State fromString(String string) {
return State.enumOf(string);
}
private String getName() {
return this.name;
}
}
public enum FileType {
TRANSACTION_RECON("transaction_recon"),
UNKNOWN("UNKNOWN");
private final String name;
FileType(final String name) {
this.name = name;
}
public static FileType enumOf(final String name) {
for (FileType value : values()) {
if (StringUtils.equals(value.getName(), name)) {
return value;
}
}
return UNKNOWN;
}
@JsonCreator // This is the factory method and must be static
public static FileType fromString(String string) {
return FileType.enumOf(string);
}
private String getName() {
return this.name;
}
}
public enum NotificationType {
TRANSACTION("Transaction"),
FILE_CREATED("file.created"),
UNKNOWN("UNKNOWN");
private final String name;
NotificationType(final String name) {
this.name = name;
}
public static NotificationType enumOf(final String name) {
for (NotificationType value : values()) {
if (StringUtils.equals(value.getName(), name)) {
return value;
}
}
return UNKNOWN;
}
@JsonCreator // This is the factory method and must be static
public static NotificationType fromString(String string) {
return NotificationType.enumOf(string);
}
private String getName() {
return this.name;
}
}
}
変数名が、 Java側はローワーキャメルケースに対してJSON側はスネークケースと異なるため、@JsonProperty
で、明示的にJSON側の変数名を指定しています。
余談ですが、JSON側のrequestedAt
とfileType
だけが、ローワーキャメルケースになっていました。ドキュメント上の誤りなのか、実際、そうなのか不明です。
ついでに言うと、突合ファイルの仕様も、項目名や値が、残高ブロックありが英語で、残高ブロックなしは、日本語になってました。
PayPayの決済システムの初期実装は、アメリカとかインドとかでやって、途中から、日本で引き取った感じでしょうかね。
なんというか、一貫性の無さに一抹の不安を感じざるをえません。
さて、デシリアライズできたか確認します。
SpringMVCなので、以下のような、handlerメソッドを作成して、引数で、Notificationを受けとるようにします。
@RequestMapping(value = "/webhook.api", method = RequestMethod.POST)
public void webhookHandler(@RequestBody final Notification notification, HttpServletRequest request, HttpServletResponse response) {
log.debug("デバッグ: " + notification);
}
テスト
結果
Notification@4312a609[authorizedAt=Fri Mar 13 22:35:30 JST 2020,confirmationExpiresAt=<null>,expiresAt=Mon Apr 13 22:35:29 JST 2020,merchantId=123456789,notificationId=<null>,fileType=<null>,path=<null>,requestedAt=<null>,merchantOrderId=A12345,notificationType=TRANSACTION,orderAmount=12345,orderId=123456789123456,paidAt=<null>,posId=123,state=AUTHORIZED,storeId=123456]