PayPay Open Payment API(OPA)の突合ファイルをOpenCSVで読み込んでみた。

PayPay OPAでは、前日の取引データなどが、突合ファイルとして生成され、HTTP GETで取得することができます。 こちらのPayPayのディベロッパサイトに、sampleの突合ファイルがありますので、取得して読み込んでみました。

Web Cashier - PayPay Open Payment API Documentation

読み込みに使用した突合ファイル: transaction_000000000000008181_20200130000000_20200130235959.csv

決済番号,加盟店ID,屋号,店舗ID,店舗名,端末番号/PosID,取引ステータス,取引日時,取引金額,レシート番号,支払い方法,マーチャント決済ID
00000000000000000001,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:58:30",150,000-0001,PayPay残高,0001-001
00000000000000000002,000000000000008181,テスト加盟店,test01,テスト01,00001,返金完了,"2020-01-30 23:55:14",-300,000-0002,PayPay残高,0001-002
00000000000000000003,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:49:54",100,000-0003,PayPay残高,0001-003
00000000000000000004,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:47:09",100,000-0004,PayPay残高,0001-004
00000000000000000005,000000000000008181,テスト加盟店,test01,テスト01,00001,取引完了,"2020-01-30 23:45:11",200,000-0005,PayPay残高,0001-005

突合ファイルは、CSV形式だということで、OSSライブラリで読み込みたいと思います。

opencsv –

CSVのライブラリは、いくつかありますが、多くの導入実績があり、 今後も開発が継続していく可能性が高そうな、OpenCSVをチョイスしました。

OpenCSVは、直接、CSVファイルのレコードとJava Beanクラスをバインディングする機能があります。 クラスのフィールド変数にアノテーションを付与して、カラムとの対応付けを定義します。

    /**
     * 突合ファイルのレコードクラス.
     */
    public class ReconciliationRecord implements Serializable {

        private static final long serialVersionUID = 1L;
        /**
         * 決済番号(paymentId).
         */
        @CsvBindByName(column = "決済番号")
        private String orderId;
        /**
         * 加盟店ID.
         */
        @CsvBindByName(column = "加盟店ID")
        private String merchantId;

        /**
         * 屋号.
         */
        @CsvBindByName(column = "屋号")
        private String brandName;
        /**
         * 店舗ID.
         */
        @CsvBindByName(column = "店舗ID")
        private String storeId;
        /**
         * 店舗名.
         */
        @CsvBindByName(column = "店舗名")
        private String storeName;
        /**
         * 端末番号/PosID.
         */
        @CsvBindByName(column = "端末番号/PosID")
        private String terminalId;
        /**
         * 取引ステータス.
         */
        @CsvCustomBindByName(column = "取引ステータス", converter = StatusConverter.class)
        private Status transactionStatus;
        /**
         * 取引日時.
         */
        @CsvBindByName(column = "取引日時")
        @CsvDate("yyyy-MM-dd HH:mm:ss")
        private Date acceptedAt;
        /**
         * 取引金額.
         */
        @CsvBindByName(column = "取引金額")
        private Long amount;
        /**
         * レシート番号.
         */
        @CsvBindByName(column = "レシート番号")
        private String orderReceiptNumber;
        /**
         * 支払い方法.
         */
        @CsvBindByName(column = "支払い方法")
        private String methodOfPayment;
        /**
         * マーチャント決済ID.
         */
        @CsvBindByName(column = "マーチャント決済ID")
        private String merchantPaymentId;

        public ReconciliationRecord() {
        }

        @Override
        public String toString() {
            return new StringJoiner(", ", ReconciliationRecord.class.getSimpleName() + "[", "]")
                    .add("orderId='" + orderId + "'")
                    .add("merchantId='" + merchantId + "'")
                    .add("brandName='" + brandName + "'")
                    .add("storeId='" + storeId + "'")
                    .add("storeName='" + storeName + "'")
                    .add("terminalId='" + terminalId + "'")
                    .add("transactionStatus=" + transactionStatus)
                    .add("acceptedAt=" + acceptedAt)
                    .add("amount=" + amount)
                    .add("orderReceiptNumber='" + orderReceiptNumber + "'")
                    .add("methodOfPayment='" + methodOfPayment + "'")
                    .add("merchantPaymentId='" + merchantPaymentId + "'")
                    .toString();
        }

        public String getOrderId() {
            return orderId;
        }

        public void setOrderId(String orderId) {
            this.orderId = orderId;
        }

        public String getMerchantId() {
            return merchantId;
        }

        public void setMerchantId(String merchantId) {
            this.merchantId = merchantId;
        }

        public String getBrandName() {
            return brandName;
        }

        public void setBrandName(String brandName) {
            this.brandName = brandName;
        }

        public String getStoreId() {
            return storeId;
        }

        public void setStoreId(String storeId) {
            this.storeId = storeId;
        }

        public String getStoreName() {
            return storeName;
        }

        public void setStoreName(String storeName) {
            this.storeName = storeName;
        }

        public String getTerminalId() {
            return terminalId;
        }

        public void setTerminalId(String terminalId) {
            this.terminalId = terminalId;
        }

        public Status getTransactionStatus() {
            return transactionStatus;
        }

        public void setTransactionStatus(Status transactionStatus) {
            this.transactionStatus = transactionStatus;
        }

        public Date getAcceptedAt() {
            return acceptedAt;
        }

        public void setAcceptedAt(Date acceptedAt) {
            this.acceptedAt = acceptedAt;
        }

        public Long getAmount() {
            return amount;
        }

        public void setAmount(Long amount) {
            this.amount = amount;
        }

        public String getOrderReceiptNumber() {
            return orderReceiptNumber;
        }

        public void setOrderReceiptNumber(String orderReceiptNumber) {
            this.orderReceiptNumber = orderReceiptNumber;
        }

        public String getMethodOfPayment() {
            return methodOfPayment;
        }

        public void setMethodOfPayment(String methodOfPayment) {
            this.methodOfPayment = methodOfPayment;
        }

        public String getMerchantPaymentId() {
            return merchantPaymentId;
        }

        public void setMerchantPaymentId(String merchantPaymentId) {
            this.merchantPaymentId = merchantPaymentId;
        }

        enum Status {

            COMPLETED, FAILED, REFUND_COMPLETED, REFUND_FAILED, UNKNOWN;

            private static final String STRING_COMPLETED = "取引完了";
            private static final String STRING_FAILED = "取引失敗";
            private static final String STRING_REFUND_COMPLETED = "返金完了";
            private static final String STRING_REFUND_FAILED = "返金失敗";

            static Status statusOf(final String value) {
                Status status = null;
                switch (value) {
                    case STRING_COMPLETED:
                        status = COMPLETED;
                        break;
                    case STRING_FAILED:
                        status = FAILED;
                        break;
                    case STRING_REFUND_COMPLETED:
                        status = REFUND_COMPLETED;
                        break;
                    case STRING_REFUND_FAILED:
                        status = REFUND_FAILED;
                        break;
                    default:
                        status = UNKNOWN;
                        break;

                }
                return status;
            }
        }
    }

固定値をenum型に変換したかったので、OpenCSVのAbstractBeanFieldクラスを継承して変換処理を実装しています。 そして、この変換処理を適用したいフィールド変数に対して、 @CsvCustomBindByName(column = "取引ステータス", converter = StatusConverter.class)というように指定します。

    /**
     * 取引ステータスをString<->enumに変換します.
     */
    public class StatusConverter extends AbstractBeanField {

        public StatusConverter() {
            super();
        }

        @Override
        protected ReconciliationRecord.Status convert(String value)
                throws CsvDataTypeMismatchException, CsvConstraintViolationException {
            return ReconciliationRecord.Status.statusOf(value);
        }
    }

特に、PayPayのサイトには、記載されてないが、ファイルのエンコーディングは、SHIFT_JISっぽいですね。 csvToBeanクラスを生成して、突合ファイルを読み込みしてみます。

File file = new File("/PATH/TO/transaction_000000000000008181_20200130000000_20200130235959.csv");

try (Reader reader = new InputStreamReader(new FileInputStream(file), "SHIFT_JIS")) {

            CsvToBean<ReconciliationRecord> csvToBean = new CsvToBeanBuilder(reader).withType(ReconciliationRecord.class).build();

            csvToBean.stream().parallel().forEach(new Consumer<ReconciliationRecord>() {

                @Override
                public void accept(ReconciliationRecord reconciliationRecord) {
                    log.debug(reconciliationRecord);
                }

            });
        }

結果

ReconciliationRecord[orderId='00000000000000000001', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:58:30 JST 2020, amount=150, orderReceiptNumber='000-0001', methodOfPayment='PayPay残高', merchantPaymentId='0001-001']
ReconciliationRecord[orderId='00000000000000000002', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=REFUND_COMPLETED, acceptedAt=Thu Jan 30 23:55:14 JST 2020, amount=-300, orderReceiptNumber='000-0002', methodOfPayment='PayPay残高', merchantPaymentId='0001-002']
ReconciliationRecord[orderId='00000000000000000003', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:49:54 JST 2020, amount=100, orderReceiptNumber='000-0003', methodOfPayment='PayPay残高', merchantPaymentId='0001-003']
ReconciliationRecord[orderId='00000000000000000004', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:47:09 JST 2020, amount=100, orderReceiptNumber='000-0004', methodOfPayment='PayPay残高', merchantPaymentId='0001-004']
ReconciliationRecord[orderId='00000000000000000005', merchantId='000000000000008181', brandName='テスト加盟店', storeId='test01', storeName='テスト01', terminalId='00001', transactionStatus=COMPLETED, acceptedAt=Thu Jan 30 23:45:11 JST 2020, amount=200, orderReceiptNumber='000-0005', methodOfPayment='PayPay残高', merchantPaymentId='0001-005']

ふと、これを見て、acceptedAtは、タイムゾーンまで含んでいない。 JSTであってるだろうかという疑問がでてきました。

他の日時パラメタは、エポックタイムスタンプだったり、協定世界時 (UTC)なのに何故でしょうか。UTCかもしれないと思いつつ、 ファイルのエンコーディングSHIFT_JISっぽいし、カラム名が日本語だし、JSTの可能性も否めない。

そこで、別の種類の突合ファイルをPayPayのディベロッパサイトからダウンロードして見てみたところ。

preauth_transaction_000000000000008181_20200130000000_20200130235959.csv

"orderId","merchantId","brandName","storeId","storeName","terminalId","transactionStatus","acceptedAt","amount","orderReceiptNumber","methodOfPayment","merchantPaymentId"
"3456789012345678901","234567890123456789","〇〇加盟店","01234567","","0000","COMPLETED","2020-09-23T00:00:26+09:00","480","","wallet","1447142183_202009230000250894_2"
"3456789012345678902","234567890123456789","〇〇加盟店","01234567","","0000","CANCELED","2020-09-23T07:45:03+09:00","1848","","wallet","1480206255_202009230745030768_6"
"3455979365068570624","234567890123456789","〇〇加盟店","01234567","","0000","EXPIRED","2020-09-23T13:00:00+09:00","1738","","wallet","1000014012_202009302301040213_0"
"3456423834054230016","234567890123456789","〇〇加盟店","01234567","","0000","FAILED","2020-09-23T13:24:54+09:00","20","","wallet","pp_0cb694dc-5340-44ef-a173-6034de590bc5"
"3456789012345678903","234567890123456789","〇〇加盟店","01234567","","0000","REFUNDED","2020-09-23T16:53:25+09:00","1325","","wallet","1000191023_202008261653230283_4"
"3456789012345678904","234567890123456789","〇〇加盟店","01234567","","0000","AUTHORIZED","2020-09-23T23:31:43+09:00","2924","","wallet","1489917170_202009232331310185_0"

こっちは、+09:00がついています。 ファイル名からみると、1/30のデータっぽいけど、レコードは、9/23です。 レコードが、1/30なら確定的だったけど、これは参考にならないかもしれない。

しかしながら、単純に、+09:00を落としただけなんじゃないかという気が、、、確認する必要がありそうです。