where('provider', $data['provider']) ->where('oid', $data['oid']) ->where('pay_method', $data['pay_method']) ->lockForUpdate(); $row = $q->first(); if (!$row) { $row = new GcPaymentAttempt(); $row->provider = $data['provider']; $row->oid = $data['oid']; $row->mem_no = $data['mem_no']; $row->order_id = $data['order_id'] ?? null; $row->pay_method = $data['pay_method']; $row->amount = $data['amount']; $row->currency = $data['currency'] ?? 'KRW'; $row->ready_at = now(); } $row->status = 'ready'; $row->token_hash = $data['token_hash']; $row->card_kind = $data['card_kind'] ?? null; $row->vact_kind = $data['vact_kind'] ?? null; $row->user_agent = $data['user_agent'] ?? null; $row->user_ip = $data['user_ip'] ?? null; $row->save(); return $row; } public function findByTokenForUpdate(string $method, string $token): ?GcPaymentAttempt { $hash = hash('sha256', $token); return GcPaymentAttempt::query() ->where('provider', 'danal') ->where('pay_method', $method) ->where('token_hash', $hash) ->lockForUpdate() ->first(); } public function markRedirected(GcPaymentAttempt $a, array $req, array $res): void { $a->status = 'redirected'; $a->redirected_at = now(); $a->request_payload = $this->jsonSafe($req); $a->response_payload = $this->jsonSafe($res); $a->save(); } public function markReturned(GcPaymentAttempt $a, array $payload, ?string $tid, string $code, string $msg, string $status): void { // status: auth_ok/issued/paid/failed/cancelled $a->status = $status; $a->returned_at = now(); $a->pg_tid = $tid ?: $a->pg_tid; $a->return_code = $code ?: $a->return_code; $a->return_msg = $msg ?: $a->return_msg; $a->return_payload = $this->jsonSafe($payload); $a->save(); } public function markNotiPaid(GcPaymentAttempt $a, array $payload, string $tid, int $amount): void { if ($a->status === 'paid') return; $a->status = 'paid'; $a->pg_tid = $tid ?: $a->pg_tid; $a->amount = $amount ?: $a->amount; $a->noti_payload = $this->jsonSafe($payload); $a->noti_at = now(); $a->save(); } public function markCancelled(GcPaymentAttempt $a, array $payload = []): void { if ($a->status === 'paid') return; $a->status = 'cancelled'; $a->returned_at = now(); if ($payload) $a->return_payload = $this->jsonSafe($payload); $a->save(); } public function markFailed(GcPaymentAttempt $a, string $code, string $msg, array $payload = []): void { if ($a->status === 'paid') return; $a->status = 'failed'; $a->returned_at = now(); $a->return_code = $code; $a->return_msg = $msg; if ($payload) $a->return_payload = $this->jsonSafe($payload); $a->save(); } private function jsonSafe(mixed $v): mixed { if (is_array($v)) { foreach ($v as $k => $vv) $v[$k] = $this->jsonSafe($vv); return $v; } if (is_object($v)) { return $this->jsonSafe((array)$v); } if (is_string($v)) { // 이미 UTF-8이면 그대로 if (function_exists('mb_check_encoding') && mb_check_encoding($v, 'UTF-8')) return $v; // EUC-KR로 가정하고 UTF-8로 변환(실패 시 깨진 바이트 제거) $out = @iconv('EUC-KR', 'UTF-8//IGNORE', $v); if ($out === false) $out = ''; if (function_exists('mb_check_encoding') && !mb_check_encoding($out, 'UTF-8')) { $out = @iconv('UTF-8', 'UTF-8//IGNORE', $out) ?: ''; } return $out; } return $v; } public function findAnyByTokenForUpdate(string $token): ?GcPaymentAttempt { $hash = hash('sha256', $token); return GcPaymentAttempt::query() ->where('provider', 'danal') ->where('token_hash', $hash) ->lockForUpdate() ->first(); } }