diff --git a/app/Codes/EnvironmentName.php b/app/Codes/EnvironmentName.php index bf52242..94c5cf5 100644 --- a/app/Codes/EnvironmentName.php +++ b/app/Codes/EnvironmentName.php @@ -4,6 +4,7 @@ namespace App\Codes; enum EnvironmentName: string { + case TEST = 'testing'; case LOCAL = 'local'; case STAGING = 'staging'; case PRODUCTOIN = 'production'; diff --git a/app/Console/Commands/SMBCPoll.php b/app/Console/Commands/SMBCPoll.php new file mode 100644 index 0000000..681f276 --- /dev/null +++ b/app/Console/Commands/SMBCPoll.php @@ -0,0 +1,186 @@ + + */ + private Collection $applications; + + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + $this->applications = 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->applications as $app) { + $this->outputInfo(sprintf("申請登録 顧客コード:%s 申請番号:%s", $app->customerCode, $app->applicationNo)); + $app->save(); + } + $this->outputInfo(sprintf("申請登録件数:%d件", $this->applications->count())); + + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + throw $e; + } + + return self::RESULTCODE_SUCCESS; + } + + /** + * @return Carbon[] + */ + private function getFromTo() + { + + $status = SmbcPollStatus::all()->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) + { + if ($data->status === SMBCStatus::SUCCESS) { + $customer = Customer::findByCustomerCode($data->getCustomerCode()); + + $application = new BankAccountUpdateApplication(); + $manager = new GeneralApplicationManager($application); + $manager + ->setCustomer($customer) + ->makeApplication(); + + $application->bankBranchIdBefore = $customer->bankBranchId; + + $application->bankBranchIdAfter = $data->bankBranchCode; + $application->bankCodeAfter = $data->bankCode; + $application->bankNameAfter = $data->bankName; + $application->branchCodeAfter = $data->branchCode; + $application->branchNameAfter = $data->branchName; + $application->accountTypeAfter = $data->accountType; + $application->accountNameKanaAfter = $data->accountName; + $application->accountNoAfter = $data->accountNo; + $application->accountYuchoSignAfter = $data->yuchoSign; + $application->accountYuchoNoAfter = $data->yuchoAccountNo; + $application->smbcApplicationDatetime = $data->applicationDatetime; + $application->smbcAcceptNo = $data->acceptNo; + $application->smbcResult = $data->all; + + $this->applications->push($application); + return; + } + if ($data->status === SMBCStatus::ERROR || $data->status === SMBCStatus::CANCEL) { + // TODOエラーメール送信 + + return; + } + if ($data->status === SMBCStatus::PROCESSING) { + $this->outputInfo(sprintf("処理中のためスキップ 受付番号%s", $data->acceptNo)); + return; + } + if ($data->address5 !== SMBC::CONDITION_ADDR5_FROM_MY_PAGE) { + $this->outputInfo(sprintf("MyPage以外からの申請のためスキップ 受付番号%s", $data->acceptNo)); + return; + } + } + + private function saveFromTo(Carbon $from, Carbon $to) + { + DB::table(SmbcPollStatus::getTableName())->delete(); + + $status = new SmbcPollStatus(); + $status->condition_datetime_to = $to; + $status->save(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 108c7b5..fc3232b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { Schedules\HeartBeat::register($schedule); + Schedules\SMBCPoll::register($schedule); } /** diff --git a/app/Console/Schedules/SMBCPoll.php b/app/Console/Schedules/SMBCPoll.php new file mode 100644 index 0000000..04a4020 --- /dev/null +++ b/app/Console/Schedules/SMBCPoll.php @@ -0,0 +1,17 @@ +command(CommandsSMBCPoll::class) + ->everyThreeMinutes() + ->description("SMBC口座振替申請結果取得"); + } +} diff --git a/app/Http/API/SMBC/PollResult.php b/app/Http/API/SMBC/PollResult.php new file mode 100644 index 0000000..a6bb5fb --- /dev/null +++ b/app/Http/API/SMBC/PollResult.php @@ -0,0 +1,147 @@ +> + */ + private Collection $lines; + + /** + * @var Collection + */ + 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; + } +} diff --git a/app/Http/API/SMBC/PollResultRecord.php b/app/Http/API/SMBC/PollResultRecord.php new file mode 100644 index 0000000..90008cc --- /dev/null +++ b/app/Http/API/SMBC/PollResultRecord.php @@ -0,0 +1,98 @@ + $data + */ + public function __construct(Collection $data) + { + $this->acceptNo = $data[self::IDX_ACCEPT_NO]; + $this->applicationDatetime = DateUtil::parse($data[self::IDX_APPLICATION_DATE] . $data[self::IDX_APPLICATION_TIME]); + $this->customerNo = $data[self::IDX_CUSTOMER_NO]; + $this->address5 = $data[self::IDX_ADDRESS5]; + $this->status = SMBCStatus::from($data[self::IDX_STATUS]); + $this->bankCode = $data[self::IDX_BANK_CODE]; + $this->bankName = $data[self::IDX_BANK_NAME]; + $this->branchCode = $data[self::IDX_BRANCH_CODE]; + $this->branchName = $data[self::IDX_BRANCH_NAME]; + $this->accountType = $data[self::IDX_ACCOUNT_TYPE]; + $this->accountNo = $data[self::IDX_ACCOUNT_NO]; + $this->accountName = $data[self::IDX_ACCOUNT_NAME]; + + // ゆうちょの場合、 + // 支店コードに記号5桁のうちの3桁 + // 口座番号に記号8桁のうちの7桁が入っているので + // 調整する + if ($this->bankCode === self::BANK_CODE_YUCHO) { + + $branchCode = $this->branchCode; + $accountNo = $this->accountNo; + $this->branchCode = ""; + $this->accountNo = ""; + + // 記号には前後に1,0を付与する + $this->yuchoSign = sprintf("1%s0", $branchCode); + + // 口座番号には末尾に1を付与する + $this->yuchoAccountNo = sprintf("%s1", $accountNo); + + // 支店コードは記号の前2桁に8を付与する + $this->branchCode = sprintf("%s8", Str::of($branchCode)->substr(2)); + } + + $this->bankBranchCode = sprintf("%d%s", intval($this->bankCode), $this->branchCode); + + $this->all = $data->implode(","); + } + + public function getCustomerCode(): int + { + return intval($this->customerNo); + } +} diff --git a/app/Http/API/SMBC/SMBC.php b/app/Http/API/SMBC/SMBC.php new file mode 100644 index 0000000..5f0735f --- /dev/null +++ b/app/Http/API/SMBC/SMBC.php @@ -0,0 +1,100 @@ + "213", + 'shori_kbn' => "2001", + 'shop_cd' => "7694156", + 'syuno_co_cd' => "58763", + 'shop_pwd' => $password, + + // 口座振替受付ステータス更新日時のFROM-TO検索条件 + 'kfr_utk_status_upd_date_from' => $from->format('Ymd'), + 'kfr_utk_status_upd_time_from' => $from->format('His'), + 'kfr_utk_status_upd_date_to' => $to->format('Ymd'), + 'kfr_utk_status_upd_time_to' => $to->format('His'), + + // ソート指定 + 'sort_list' => "11", // 口座振替受付ステータス更新日時 + 'sort_jun' => "2" // 降順 + ]; + + $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.systemPassword'); + if (!$password) { + throw new ConfigException('smbc.systemPassword', $password); + } + + $url = config('smbc.registerUrl'); + if (!$url) { + throw new ConfigException('smbc.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; + } +} diff --git a/app/Http/API/SMBC/SMBCStatus.php b/app/Http/API/SMBC/SMBCStatus.php new file mode 100644 index 0000000..3c337ca --- /dev/null +++ b/app/Http/API/SMBC/SMBCStatus.php @@ -0,0 +1,11 @@ +middleware('auth:sanctum'); + } + + protected function run(Request $request): JsonResponse + { + $customer = Customer::getSelf(); + + $data = SMBC::getRegisterStartParam($customer); + + return $this->successResponse($data); + } +} diff --git a/app/Http/Controllers/Web/Customer/BankAccountRegisterStartParam.php b/app/Http/Controllers/Web/Customer/BankAccountRegisterStartParam.php new file mode 100644 index 0000000..aacea93 --- /dev/null +++ b/app/Http/Controllers/Web/Customer/BankAccountRegisterStartParam.php @@ -0,0 +1,15 @@ +json($ret) ->withHeaders($this->makeHeader()); } else { - abort(500); + + if (app()->environment([EnvironmentName::PRODUCTOIN->value])) { + abort(500); + } + return response() + ->json($ret) + ->withHeaders($this->makeHeader()); } } diff --git a/app/Kintone/KintoneRecordQuery.php b/app/Kintone/KintoneRecordQuery.php index 682a3fe..ae3a65c 100644 --- a/app/Kintone/KintoneRecordQuery.php +++ b/app/Kintone/KintoneRecordQuery.php @@ -28,7 +28,7 @@ class KintoneRecordQuery $ret .= " "; } $ret .= $this->order; - logger(sprintf("QUERY[%s]:%s", $this->appName, $ret)); + // logger(sprintf("QUERY[%s]:%s", $this->appName, $ret)); return $ret; } diff --git a/app/Kintone/Models/Bank.php b/app/Kintone/Models/Bank.php new file mode 100644 index 0000000..f8db5b1 --- /dev/null +++ b/app/Kintone/Models/Bank.php @@ -0,0 +1,13 @@ + FieldType::SINGLE_LINE_TEXT, + self::FIELD_BANK_CODE_BEFORE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BANK_NAME_BEFORE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BRANCH_CODE_BEFORE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BRANCH_NAME_BEFORE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_ACCOUNT_TYPE_BEFORE => FieldType::NUMBER, + self::FIELD_ACCOUNT_NAME_KANA_BEFORE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_ACCOUNT_NO_BEFORE => FieldType::NUMBER, + self::FIELD_ACCOUNT_YUCHO_SIGN_BEFORE => FieldType::NUMBER, + self::FIELD_ACCOUNT_YUCHO_NO_BEFORE => FieldType::NUMBER, + self::FIELD_BANK_BRANCH_ID_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BANK_CODE_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BANK_NAME_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BRANCH_CODE_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BRANCH_NAME_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_ACCOUNT_TYPE_AFTER => FieldType::NUMBER, + self::FIELD_ACCOUNT_NAME_KANA_AFTER => FieldType::SINGLE_LINE_TEXT, + self::FIELD_ACCOUNT_NO_AFTER => FieldType::NUMBER, + self::FIELD_ACCOUNT_YUCHO_SIGN_AFTER => FieldType::NUMBER, + self::FIELD_ACCOUNT_YUCHO_NO_AFTER => FieldType::NUMBER, + self::FIELD_APPLICATION_CUSTOMER_CODE => FieldType::SINGLE_LINE_TEXT, + self::FIELD_SMBC_APPLICATION_DATETIME => FieldType::DATETIME, + self::FIELD_SMBC_ACCEPT_NO => FieldType::SINGLE_LINE_TEXT, + self::FIELD_SMBC_RESULT => FieldType::SINGLE_LINE_TEXT, + + ]; + + protected const FIELD_NAMES = [ + ...parent::FIELD_NAMES, + ]; +} diff --git a/app/Kintone/Models/Customer.php b/app/Kintone/Models/Customer.php index 0659b77..202f7fb 100644 --- a/app/Kintone/Models/Customer.php +++ b/app/Kintone/Models/Customer.php @@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Auth; * @property string phoneNumber * @property string zipCode * @property string address + * @property string bankBranchId */ class Customer extends KintoneModel { @@ -25,6 +26,7 @@ class Customer extends KintoneModel const FIELD_PHONE_NUMBER = "電話番号"; const FIELD_ZIP_CODE = "契約者_郵便番号"; const FIELD_ADDRESS = "住所"; + const FIELD_BANK_BRANCH_ID = "ChargedBankBranchCode"; protected const FIELDS = [ ...parent::FIELDS, @@ -35,6 +37,7 @@ class Customer extends KintoneModel self::FIELD_PHONE_NUMBER => FieldType::LINK, self::FIELD_ZIP_CODE => FieldType::SINGLE_LINE_TEXT, self::FIELD_ADDRESS => FieldType::SINGLE_LINE_TEXT, + self::FIELD_BANK_BRANCH_ID => FieldType::SINGLE_LINE_TEXT, ]; protected const FIELD_NAMES = [ diff --git a/app/Kintone/Models/GeneralApplication.php b/app/Kintone/Models/GeneralApplication.php index b7806ae..df95e7e 100644 --- a/app/Kintone/Models/GeneralApplication.php +++ b/app/Kintone/Models/GeneralApplication.php @@ -47,6 +47,7 @@ abstract class GeneralApplication extends KintoneModel protected const RELATIONS = [ SeasonTicketContract::class, Customer::class, + Bank::class, ]; public static function findByApplicationNo(string $applicationNo): static diff --git a/app/Logic/GeneralApplicationManager.php b/app/Logic/GeneralApplicationManager.php index 00468d4..ef3ad67 100644 --- a/app/Logic/GeneralApplicationManager.php +++ b/app/Logic/GeneralApplicationManager.php @@ -3,6 +3,7 @@ namespace App\Logic; use App\Exceptions\AppCommonException; +use App\Kintone\Models\BankAccountUpdateApplication; use App\Kintone\Models\Customer; use App\Kintone\Models\GeneralApplication; use App\Kintone\Models\Parking; @@ -61,6 +62,10 @@ class GeneralApplicationManager $this->setType("振替頻度変更"); return; } + if ($model instanceof BankAccountUpdateApplication) { + $this->setType("口座変更申請"); + return; + } } public function setCustomer(Customer $customer): static @@ -102,6 +107,10 @@ class GeneralApplicationManager $this->model->parkingName = $this->parking->parkingName; } + if ($this->model instanceof BankAccountUpdateApplication) { + $this->model->applicationCustomerCode = $this->customer->customerCode; + } + return $this->model; } diff --git a/app/Models/SmbcPollStatus.php b/app/Models/SmbcPollStatus.php new file mode 100644 index 0000000..c3a8def --- /dev/null +++ b/app/Models/SmbcPollStatus.php @@ -0,0 +1,22 @@ + 'datetime', + ]; + + public function getHistory(): ?HistoryModel + { + return null; + } + + public function getModelName(): string + { + return "SMBC口座振替登録依頼確認ステータス"; + } +} diff --git a/app/Util/EncodingUtil.php b/app/Util/EncodingUtil.php new file mode 100644 index 0000000..7606cd8 --- /dev/null +++ b/app/Util/EncodingUtil.php @@ -0,0 +1,16 @@ + [ ...App\Kintone\Models\Customer::setConfig(), + ...App\Kintone\Models\Bank::setConfig(), ...App\Kintone\Models\Parking::setConfig(), ...App\Kintone\Models\SeasonTicketContract::setConfig(), ...App\Kintone\Models\SeasonTicketContractEntry::setConfig(), diff --git a/config/smbc.php b/config/smbc.php new file mode 100644 index 0000000..845e2c1 --- /dev/null +++ b/config/smbc.php @@ -0,0 +1,20 @@ + env("SMBC_SYSTEM_PASSWORD"), + 'searchPassword' => env("SMBC_SEARCH_PASSWORD"), + + 'registerUrl' => env("SMBC_URL_REGISTER"), + 'searchUrl' => env("SMBC_URL_SEARCH"), + +]; diff --git a/database/migrations/2023_10_06_182600_create_smbc_poll_statuses_table.php b/database/migrations/2023_10_06_182600_create_smbc_poll_statuses_table.php new file mode 100644 index 0000000..93a5f24 --- /dev/null +++ b/database/migrations/2023_10_06_182600_create_smbc_poll_statuses_table.php @@ -0,0 +1,30 @@ +baseColumn(); + + $table->dateTime('condition_datetime_to')->comment('前回検索条件_TO時刻'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('smbc_poll_statuses'); + } +}; diff --git a/routes/api.php b/routes/api.php index 46e97f1..c56a3c5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -37,6 +37,7 @@ RouteHelper::post('/ask', App\Http\Controllers\Web\FAQ\AskController::class); RouteHelper::post('/email/change/start', App\Http\Controllers\Web\Customer\ChangeEmailStartController::class); RouteHelper::post('/email/change/verify', App\Http\Controllers\Web\Customer\ChangeEmailVerifyController::class); RouteHelper::post('/customer/update-info-order', App\Http\Controllers\Web\Customer\UpdateUserInfoOrderController::class); +RouteHelper::get('/customer/bank-account-register/start', App\Http\Controllers\Web\Customer\BankAccountRegisterStartController::class); RouteHelper::post('/password/setting/start', App\Http\Controllers\Web\Auth\PasswordSettingStartController::class); RouteHelper::post('/password/setting/verify', App\Http\Controllers\Web\Auth\PasswordSettingVerifyController::class); diff --git a/tests/Feature/Http/API/SMBC/SMBCTest.php b/tests/Feature/Http/API/SMBC/SMBCTest.php new file mode 100644 index 0000000..78ad899 --- /dev/null +++ b/tests/Feature/Http/API/SMBC/SMBCTest.php @@ -0,0 +1,21 @@ +setDate(2023, 10, 5); + $to = DateUtil::now()->setDate(2023, 10, 7); + $result = SMBC::poll($from, $to); + + $this->assertEquals("", $result->getMessage()); + $this->assertTrue($result->ok()); + } +} diff --git a/tests/Feature/Kintone/BankAccountUpdateApplicationTest.php b/tests/Feature/Kintone/BankAccountUpdateApplicationTest.php new file mode 100644 index 0000000..449bd4d --- /dev/null +++ b/tests/Feature/Kintone/BankAccountUpdateApplicationTest.php @@ -0,0 +1,41 @@ +setCustomer($customer) + ->makeApplication(); + + $application->bankBranchIdBefore = $customer->bankBranchId; + + $application->bankBranchIdAfter = "1004"; + $application->bankCodeAfter = "1"; + $application->bankNameAfter = "みずほー"; + $application->branchCodeAfter = "4"; + $application->branchNameAfter = "まるのうち"; + $application->accountTypeAfter = "1"; + $application->accountNameKanaAfter = "テストタロウ"; + $application->accountNoAfter = "1234567"; + $application->accountYuchoSignAfter = "01234"; + $application->accountYuchoNoAfter = "12345678"; + + $application->save(); + + $this->assertTrue(true); + } +}