Browse Source

SMBC決済結果取得対応

master
sosuke.iwabuchi 2 years ago
parent
commit
6a412da2f9
16 changed files with 658 additions and 20 deletions
  1. +3
    -5
      app/Console/Commands/SMBCBankAccountRegisterPoll.php
  2. +194
    -0
      app/Console/Commands/SMBCPaymentPoll.php
  3. +1
    -1
      app/Console/Schedules/SMBCBankAccountRegisterPoll.php
  4. +18
    -0
      app/Console/Schedules/SMBCPaymentPoll.php
  5. +8
    -8
      app/Http/API/SMBC/BankAccountRegister/SMBC.php
  6. +19
    -0
      app/Http/API/SMBC/Payment/PaymentStatus.php
  7. +143
    -0
      app/Http/API/SMBC/Payment/PollResult.php
  8. +55
    -0
      app/Http/API/SMBC/Payment/PollResultRecord.php
  9. +100
    -0
      app/Http/API/SMBC/Payment/SMBC.php
  10. +12
    -0
      app/Kintone/Models/DropDown/SmbcPayment/SmbcPaymentStatus.php
  11. +62
    -0
      app/Kintone/Models/SmbcPayment.php
  12. +1
    -1
      app/Models/SmbcPollStatus.php
  13. +1
    -1
      app/Util/DateUtil.php
  14. +1
    -0
      config/kintone.php
  15. +11
    -4
      config/smbc.php
  16. +29
    -0
      database/migrations/2023_11_06_163700_add_column_smbc_type.php

+ 3
- 5
app/Console/Commands/SMBCBankAccountRegisterPoll.php View File

@@ -16,7 +16,6 @@ use App\Util\DBUtil;
use Exception;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class SMBCBankAccountRegisterPoll extends BaseCommand
{
@@ -116,7 +115,7 @@ class SMBCBankAccountRegisterPoll extends BaseCommand
private function getFromTo()
{

$status = SmbcPollStatus::all()->first();
$status = SmbcPollStatus::whereType(self::class)->first();
$now = DateUtil::now();
if ($status === null) {
$this->outputInfo("検索範囲初期化");
@@ -183,9 +182,8 @@ class SMBCBankAccountRegisterPoll extends BaseCommand

private function saveFromTo(Carbon $from, Carbon $to)
{
DB::table(SmbcPollStatus::getTableName())->delete();

$status = new SmbcPollStatus();
$status = SmbcPollStatus::whereType(self::class)->firstOrNew();
$status->type = self::class;
$status->condition_datetime_to = $to;
$status->save();
}


+ 194
- 0
app/Console/Commands/SMBCPaymentPoll.php View File

@@ -0,0 +1,194 @@
<?php

namespace App\Console\Commands;

use App\Http\API\SMBC\Payment\PollResultRecord;
use App\Http\API\SMBC\Payment\SMBC;
use App\Http\API\SMBC\Payment\PaymentStatus;
use App\Kintone\Models\Customer;
use App\Kintone\Models\DropDown\SmbcPayment\SmbcPaymentStatus;
use App\Kintone\Models\SmbcPayment;
use App\Models\SmbcPollStatus;
use App\Util\DateUtil;
use App\Util\DBUtil;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class SMBCPaymentPoll extends BaseCommand
{

const COMMAND = "smbc:payment-poll";


/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = self::COMMAND;

/**
* The console command description.
*
* @var string
*/
protected $description = 'SMBCへ支払結果を取得する';

static public function getCommand()
{
return self::COMMAND;
}


/**
* @var Collection<int, SmbcPayment>
*/
private Collection $results;


/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->applications = collect();
$this->results = collect();
}

/**
* Execute the console command.
*
* @return int
*/
public function service(): int
{
try {
$db = DBUtil::instance();
$db->beginTransaction();

// 検索範囲の取得
[$from, $to] = $this->getFromTo();
$this->outputInfo(sprintf("検索範囲 %s-%s", $from->format('Y/m/d H:i:s'), $to->format('Y/m/d H:i:s')));

// 検索実行
$result = SMBC::poll($from, $to);

// 検索結果の確認
if (!$result->ok()) {
$this->outputError($result->getMessage());
return self::RESULTCODE_FAILED;
}
$this->outputInfo(sprintf("取得対象 %d件", $result->getCount()));

// データハンドリング
foreach ($result->getRecord() as $data) {
$this->handleData($data);
}

// 検索実績の登録
$this->saveFromTo($from, $to);

// キントーンへ登録
foreach ($this->results as $result) {
$result->save();
}
$this->outputInfo(sprintf("登録件数:%d件", $this->results->count()));

$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw $e;
}

return self::RESULTCODE_SUCCESS;
}

/**
* @return Carbon[]
*/
private function getFromTo()
{

$status = SmbcPollStatus::whereType(self::class)->first();
$now = DateUtil::now();
if ($status === null) {
$this->outputInfo("検索範囲初期化");
return [
$now->clone()->addDays(-5),
$now->clone(),
];
}

return [
$status->condition_datetime_to->clone()->addSecond(),
$now->clone(),
];
}

private function handleData(PollResultRecord $data)
{
try {
$payment = SmbcPayment::getAccess()->first(
SmbcPayment::getQuery()
->where(SmbcPayment::FIELD_ORDER_NO, $data->orderNo)
);
} catch (ModelNotFoundException $e) {
$this->outputWarn(sprintf("存在しない支払予定 [%s]", $data->orderNo));
throw $e;
}


// 完了ケース
if ($data->paymentStatus === PaymentStatus::C0200_決済完了) {
$payment->status = SmbcPaymentStatus::S003_決済結果反映済み;

$payment->paymentAmount = $data->paymentAmount;
$payment->paymentDate = $data->paymentDate;
}

// 未完了ケース
else if (in_array(
$data->paymentStatus,
[
PaymentStatus::C0011_請求取消,
PaymentStatus::C0111_依頼取消,
PaymentStatus::C0700_不明入金,
PaymentStatus::C0800_不明通知,
PaymentStatus::C0900_決済申込NG,
],
true
)) {
$payment->status = SmbcPaymentStatus::S003_決済結果反映済み;
}

// 処理中ケース
else {
$payment->status = SmbcPaymentStatus::S002_決済結果待ち;
}

// 共通ケース

$payment->acceptNo = $data->acceptNo;
$payment->paymentStatus = $data->paymentStatusName;
$payment->paymentStatusUpdateDatetime = $data->paymentStatusUpdateDatetime;
$payment->paymentWarningStatus = $data->paymentWarningStatusName;
$payment->paymentWarningStatusUpdateDatetime = $data->paymentWarningStatusUpdateDatetime;
$payment->allResponse = $data->all;


$this->results->push($payment);
}

private function saveFromTo(Carbon $from, Carbon $to)
{
$status = SmbcPollStatus::whereType(self::class)->firstOrNew();
$status->type = self::class;
$status->condition_datetime_to = $to;
$status->save();
}
}

+ 1
- 1
app/Console/Schedules/SMBCBankAccountRegisterPoll.php View File

@@ -11,7 +11,7 @@ class SMBCBankAccountRegisterPoll extends BaseSchedule
static public function register(Schedule $schedule)
{
$schedule->command(Command::class)
->everyThreeMinutes()
->everyOddHour()
->unlessBetween('1:00', '6:00')
->description("SMBC口座振替申請結果取得");
}


+ 18
- 0
app/Console/Schedules/SMBCPaymentPoll.php View File

@@ -0,0 +1,18 @@
<?php

namespace App\Console\Schedules;

use App\Console\Commands\SMBCPaymentPoll as Command;
use Illuminate\Console\Scheduling\Schedule;

class SMBCPaymentPoll extends BaseSchedule
{

static public function register(Schedule $schedule)
{
$schedule->command(Command::class)
->everyTwoHours()
->unlessBetween('1:00', '6:00')
->description("SMBC支払結果取得");
}
}

+ 8
- 8
app/Http/API/SMBC/BankAccountRegister/SMBC.php View File

@@ -14,14 +14,14 @@ class SMBC

public static function poll(Carbon $from, Carbon $to)
{
$url = config('smbc.searchUrl');
$url = config('smbc.bankAccountRegister.searchUrl');
if (!$url) {
throw new ConfigException("smbc.searchUrl", $url);
throw new ConfigException("smbc.bankAccountRegister.searchUrl", $url);
}

$password = config('smbc.searchPassword');
$password = config('smbc.bankAccountRegister.searchPassword');
if (!$password) {
throw new ConfigException("smbc.searchPassword", $password);
throw new ConfigException("smbc.bankAccountRegister.searchPassword", $password);
}

$sendData = [
@@ -60,14 +60,14 @@ class SMBC
public static function getRegisterStartParam(Customer $customer)
{

$password = config('smbc.systemPassword');
$password = config('smbc.bankAccountRegister.systemPassword');
if (!$password) {
throw new ConfigException('smbc.systemPassword', $password);
throw new ConfigException('smbc.bankAccountRegister.systemPassword', $password);
}

$url = config('smbc.registerUrl');
$url = config('smbc.bankAccountRegister.registerUrl');
if (!$url) {
throw new ConfigException('smbc.registerUrl', $url);
throw new ConfigException('smbc.bankAccountRegister.registerUrl', $url);
}

$param = [


+ 19
- 0
app/Http/API/SMBC/Payment/PaymentStatus.php View File

@@ -0,0 +1,19 @@
<?php

namespace App\Http\API\SMBC\Payment;

enum PaymentStatus: string
{
case C0000_決済申込中 = "0000";
case C0010_請求確定待ち = "0010";
case C0011_請求取消 = "0011";
case C0100_請求中 = "0100";
case C0111_依頼取消 = "0111";
case C0200_決済完了 = "0200";
case C0300_再発行済 = "0300";
case C0400_速報済 = "0400";
case C0500_速報取消 = "0500";
case C0700_不明入金 = "0700";
case C0800_不明通知 = "0800";
case C0900_決済申込NG = "0900";
}

+ 143
- 0
app/Http/API/SMBC/Payment/PollResult.php View File

@@ -0,0 +1,143 @@
<?php

namespace App\Http\API\SMBC\Payment;

use App\Util\EncodingUtil;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

class PollResult
{
// 共通
private const IDX_RECORD_CODE = 0;
// ヘッダー
private const IDX_HEADER_RESULT = 1;
private const IDX_HEADER_MESSAGE = 2;
// ボディー
private const IDX_FOOTER_COUNT = 1;
// フッター

private const RECORD_CODE_HEADER = "10";
private const RECORD_CODE_BODY = "20";
private const RECORD_CODE_FOOTER = "80";
private const RESULT_CODE_SUCCESS = ["000000", "943037"];


/**
* @var Collection<int, Collection<int, string>>
*/
private Collection $lines;

/**
* @var Collection<int, PollResultRecord>
*/
private Collection $body;

private bool $success = false;
private string $message = "";
private int $count = 0;

public function __construct(string $data)
{
$this->lines = collect();
$lines = Str::of($data)->replace('"', '')->explode("\r\n");
foreach ($lines as $lineStr) {
if ($lineStr) {
$lineStr = EncodingUtil::toUtf8FromSjis($lineStr);
$this->lines->push(Str::of($lineStr)->explode(','));
}
}

if (!$this->parseHeader()) {
return;
}
if (!$this->parseBody()) {
return;
}
if (!$this->parseFooter()) {
return;
}

$this->success = true;
}

public function ok(): bool
{
return $this->success;
}
public function getMessage(): string
{
return $this->message;
}

public function getRecord()
{
return $this->body;
}

public function getCount(): int
{
return $this->count;
}

private function parseHeader(): bool
{
$header = $this->lines->first();
if (!$header) {
$this->success = false;
$this->message = "ヘッダーなし";
return false;
}

$resultCode = $header->get(self::IDX_HEADER_RESULT);
if (!in_array($resultCode, self::RESULT_CODE_SUCCESS)) {
$this->success = false;
$this->message = sprintf("結果コードNG %s", $resultCode);
$readMessage = EncodingUtil::toUtf8FromSjis($header->get(self::IDX_HEADER_MESSAGE));
return false;
}
return true;
}

private function parseBody(): bool
{
$this->body = collect();

try {
foreach ($this->lines as $line) {
if ($line[self::IDX_RECORD_CODE] === self::RECORD_CODE_BODY) {
$this->body->push(new PollResultRecord($line));
}
}
} catch (Exception $e) {
$this->success = false;
$this->message = sprintf("Bodyパース失敗 %s", $e->getMessage());
return false;
}

return true;
}

private function parseFooter(): bool
{
$footer = $this->lines->last();

if (!$footer) {
$this->success = false;
$this->message = "フッターなし";
return false;
}

$count = intval($footer->get(self::IDX_FOOTER_COUNT));

if ($count !== $this->body->count()) {
$this->success = false;
$this->message = sprintf("読込件数に差異あり %d : %d", $count, $this->body->count());
return false;
}
$this->count = $count;

return true;
}
}

+ 55
- 0
app/Http/API/SMBC/Payment/PollResultRecord.php View File

@@ -0,0 +1,55 @@
<?php

namespace App\Http\API\SMBC\Payment;

use App\Util\DateUtil;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class PollResultRecord
{

private const IDX_ORDER_NO = 10;
private const IDX_ACCEPT_NO = 11;
private const IDX_PAYMENT_STATUS = 12;
private const IDX_PAYMENT_WARNING_STATUS = 13;
private const IDX_PAYMENT_STATUS_NAME = 14;
private const IDX_PAYMENT_WARNING_STATUS_NAME = 15;
private const IDX_PAYMENT_STATUS_UPDATE_DATETIME = 16;
private const IDX_PAYMENT_WARNING_STATUS_UPDATE_DATETIME = 17;
private const IDX_PAYMENT_AMOUNT = 28;
private const IDX_PAYMENT_DATE = 29;

public string $orderNo;
public string $acceptNo;
public PaymentStatus $paymentStatus;
public string $paymentWarningStatus;
public string $paymentStatusName;
public string $paymentWarningStatusName;
public ?Carbon $paymentStatusUpdateDatetime;
public ?Carbon $paymentWarningStatusUpdateDatetime;
public int $paymentAmount;
public ?Carbon $paymentDate;

public string $all = "";

/**
*
* @param Collection<int, string> $data
*/
public function __construct(Collection $data)
{
$this->orderNo = $data[self::IDX_ORDER_NO];
$this->acceptNo = $data[self::IDX_ACCEPT_NO];
$this->paymentStatus = PaymentStatus::from($data[self::IDX_PAYMENT_STATUS]);
$this->paymentWarningStatus = $data[self::IDX_PAYMENT_WARNING_STATUS];
$this->paymentStatusName = $data[self::IDX_PAYMENT_STATUS_NAME];
$this->paymentWarningStatusName = $data[self::IDX_PAYMENT_WARNING_STATUS_NAME];
$this->paymentStatusUpdateDatetime = DateUtil::parse($data[self::IDX_PAYMENT_STATUS_UPDATE_DATETIME]);
$this->paymentWarningStatusUpdateDatetime = DateUtil::parse($data[self::IDX_PAYMENT_WARNING_STATUS_UPDATE_DATETIME]);
$this->paymentAmount = intval($data[self::IDX_PAYMENT_AMOUNT]);
$this->paymentDate = DateUtil::parse($data[self::IDX_PAYMENT_DATE]);

$this->all = $data->implode(",");
}
}

+ 100
- 0
app/Http/API/SMBC/Payment/SMBC.php View File

@@ -0,0 +1,100 @@
<?php

namespace App\Http\API\SMBC\Payment;

use App\Exceptions\ConfigException;
use App\Kintone\Models\Customer;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;

class SMBC
{
const CONDITION_ADDR5_FROM_MY_PAGE = "マイページからの申請";


public static function poll(Carbon $from, Carbon $to)
{
$url = config('smbc.payment.searchUrl');
if (!$url) {
throw new ConfigException("smbc.payment.searchUrl", $url);
}

$password = config('smbc.payment.searchPassword');
if (!$password) {
throw new ConfigException("smbc.payment.searchPassword", $password);
}

$sendData = [
'version' => "213",
'shori_kbn' => "0500",
'shop_cd' => "7068054",
'syuno_co_cd' => "58800",
'shop_pwd' => $password,

// 更新日時のFROM-TO検索条件
'jotai_date_from' => $from->format('Ymd'),
'jotai_time_from' => $from->format('Hi'),
'jotai_date_to' => $to->format('Ymd'),
'jotai_time_to' => $to->format('Hi'),

// ソート指定
'sort_list' => "13", // 処理日時
'sort_jun' => "1" // 昇順
];

$res = Http::withHeaders([
'Content-Type' => 'application/x-www-form-urlencoded',
'Content-Encoding' => 'Shift_JIS'
])->asForm()->post(
$url,
$sendData
);

if ($res->failed()) {
throw $res->toException();
}

return new PollResult($res->body());
}

public static function getRegisterStartParam(Customer $customer)
{

$password = config('smbc.bankAccountRegister.systemPassword');
if (!$password) {
throw new ConfigException('smbc.bankAccountRegister.systemPassword', $password);
}

$url = config('smbc.bankAccountRegister.registerUrl');
if (!$url) {
throw new ConfigException('smbc.bankAccountRegister.registerUrl', $url);
}

$param = [
'bill_no' => sprintf("%012d", $customer->customerCode),
'bill_name' => $customer->customerName,
'bill_kana' => mb_convert_kana($customer->customerNameKana, "ks"),

'version' => "130",
'bill_method' => "01",
'shop_cd' => "7694156",
'syuno_co_cd' => "58763",
'shop_pwd' => $password,
'shoporder_no' => "",
'koushin_kbn' => "1",
'shop_phon_hyoji_kbn' => "1",
'shop_mail_hyoji_kbn' => "1",
'kessai_id' => "0101",

'bill_adr_5' => self::CONDITION_ADDR5_FROM_MY_PAGE,
];
$param['fs'] = hash('sha256', $param['shop_cd'] . $param['syuno_co_cd'] . $param['bill_no'] . $param['shoporder_no'] . $param['shop_pwd']);

$data = [
'url' => $url,
'param' => $param,
];

return $data;
}
}

+ 12
- 0
app/Kintone/Models/DropDown/SmbcPayment/SmbcPaymentStatus.php View File

@@ -0,0 +1,12 @@
<?php

namespace App\Kintone\Models\DropDown\SmbcPayment;

abstract class SmbcPaymentStatus
{
const S001_起票 = "起票";
const S002_決済結果待ち = "決済結果待ち";
const S003_決済結果反映済み = "決済結果反映済み";
const S004_消込済み = "消込済み";
const S005_消込エラー = "消込エラー";
}

+ 62
- 0
app/Kintone/Models/SmbcPayment.php View File

@@ -0,0 +1,62 @@
<?php

namespace App\Kintone\Models;

use App\Http\API\SMBC\Payment\PaymentStatus;
use Illuminate\Support\Carbon;

/**
* アプリ名 コンビニ支払予定・結果
*
* @property string status
* @property string orderNo
* @property int customerCode
* @property int claimAmount
* @property string acceptNo
* @property string paymentStatus
* @property ?Carbon paymentStatusUpdateDatetime
* @property string paymentWarningStatus
* @property ?Carbon paymentWarningStatusUpdateDatetime
* @property ?int paymentAmount
* @property ?Carbon paymentDate
* @property string status
* @property string allResponse
*/
class SmbcPayment extends KintoneModel
{
const CONFIG_KEY = "KINTONE_APP_SMBC_PAYMENT";

const FIELD_STATUS = "状態";
const FIELD_ORDER_NO = "請求_補助_番号";
const FIELD_CUSTOMER_CODE = "顧客コード";
const FIELD_CLAIM_AMOUNT = "請求金額";

const FIELD_ACCEPT_NO = "決済受付番号";
const FIELD_PAYMENT_STATUS = "決済ステータス";
const FIELD_PAYMENT_STATUS_UPDATE_DATETIME = "決済ステータス更新日時";
const FIELD_PAYMENT_WARNING_STATUS = "決済警告ステータス";
const FIELD_PAYMENT_WARNING_STATUS_UPDATE_DATETIME = "決済警告ステータス更新日時";
const FIELD_PAYMENT_AMOUNT = "入金金額";
const FIELD_PAYMENT_DATE = "収納日";
const FIELD_ALL_RESPONSE = "受信電文";

protected const FIELDS = [
...parent::FIELDS,
self::FIELD_STATUS => FieldType::DROP_DOWN,
self::FIELD_ORDER_NO => FieldType::SINGLE_LINE_TEXT,
self::FIELD_CUSTOMER_CODE => FieldType::NUMBER,
self::FIELD_CLAIM_AMOUNT => FieldType::NUMBER,
self::FIELD_ACCEPT_NO => FieldType::SINGLE_LINE_TEXT,
self::FIELD_PAYMENT_STATUS => FieldType::DROP_DOWN,
self::FIELD_PAYMENT_STATUS_UPDATE_DATETIME => FieldType::DATETIME,
self::FIELD_PAYMENT_WARNING_STATUS => FieldType::SINGLE_LINE_TEXT,
self::FIELD_PAYMENT_WARNING_STATUS_UPDATE_DATETIME => FieldType::DATETIME,
self::FIELD_PAYMENT_AMOUNT => FieldType::NUMBER,
self::FIELD_PAYMENT_DATE => FieldType::DATE,
self::FIELD_ALL_RESPONSE => FieldType::SINGLE_LINE_TEXT,
];

protected const FIELD_NAMES = [
...parent::FIELD_NAMES,
];
}

+ 1
- 1
app/Models/SmbcPollStatus.php View File

@@ -17,6 +17,6 @@ class SmbcPollStatus extends AppModel

public function getModelName(): string
{
return "SMBC口座振替登録依頼確認ステータス";
return "SMBC確認ステータス";
}
}

+ 1
- 1
app/Util/DateUtil.php View File

@@ -26,7 +26,7 @@ class DateUtil

public static function parse(?string $source): Carbon|null
{
if ($source === null) {
if (!$source) {
return null;
}
$date = Carbon::parse($source);


+ 1
- 0
config/kintone.php View File

@@ -49,6 +49,7 @@ return [
...App\Kintone\Models\FAQ::setConfig(),
...App\Kintone\Models\Ask::setConfig(),
...App\Kintone\Models\Receipt::setConfig(),
...App\Kintone\Models\SmbcPayment::setConfig(),
],

];

+ 11
- 4
config/smbc.php View File

@@ -10,11 +10,18 @@ return [
|
| キントーンAPIのホストを定義
*/
'bankAccountRegister' => [
'systemPassword' => env("SMBC_SYSTEM_PASSWORD"),
'searchPassword' => env("SMBC_SEARCH_PASSWORD"),

'systemPassword' => env("SMBC_SYSTEM_PASSWORD"),
'searchPassword' => env("SMBC_SEARCH_PASSWORD"),
'registerUrl' => env("SMBC_URL_REGISTER"),
'searchUrl' => env("SMBC_URL_SEARCH"),
],

'registerUrl' => env("SMBC_URL_REGISTER"),
'searchUrl' => env("SMBC_URL_SEARCH"),
'payment' => [
'searchPassword' => env("SMBC_SEARCH_PASSWORD"),

'searchUrl' => env("SMBC_URL_SEARCH"),
],

];

+ 29
- 0
database/migrations/2023_11_06_163700_add_column_smbc_type.php View File

@@ -0,0 +1,29 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('smbc_poll_statuses', function (Blueprint $table) {

$table->string("type")->comment('タイプ')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('smbc_poll_statuses', function (Blueprint $table) {
$table->dropColumn("type");
});
}
};

Loading…
Cancel
Save