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ライブラリで読み込みたいと思います。
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
を落としただけなんじゃないかという気が、、、確認する必要がありそうです。