PayPay Open Payment API(OPA)のWebhookをjacksonしてみた
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]