123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- <?php
- // Copyright 2019-2022 Hackware SpA <human@hackware.cl>
- // This file is part of "Hackware Web Services Payment" and licensed under
- // the terms of the GNU Affero General Public License version 3, or (at your
- // option) a later version. You should have received a copy of this license
- // along with the software. If not, see <https://www.gnu.org/licenses/>.
- namespace Hawese\Payment;
- use Hawese\Core\TableModel;
- use Hawese\Core\User;
- use Hawese\Payment\Support\Http as HttpSupport;
- use Hawese\Payment\Exceptions\UnexpectedResponseException;
- use Buzz\Client\FileGetContents as HttpClient;
- use Brick\Money\Money;
- use Nyholm\Psr7\Factory\Psr17Factory; // Request, Response & Stream factory
- use Psr\Http\Message\{ResponseInterface, StreamInterface};
- use Illuminate\Database\Query\Builder;
- use UnexpectedValueException;
- class Payment extends TableModel
- {
- public const STATUS_PENDING = 'pending';
- public const STATUS_COMPLETED = 'completed';
- public const STATUS_ABORTED = 'aborted';
- public static $table = 'payments';
- public static $attributes = [
- 'uuid' => ['required', 'uuid'],
- 'origin' => ['required', 'string', 'max:30'],
- 'user_uid' => ['required', 'string', 'min:3', 'max:100'],
- 'gateway' => ['required', 'string', 'min:3', 'max:100'],
- 'currency' => ['required', 'string', 'size:3'],
- 'amount' => ['required', 'integer'],
- 'description' => ['nullable', 'string', 'max:255'],
- 'detail' => ['nullable', 'max:65535'], // JSON
- 'status' => ['required', 'in:pending,completed,aborted'],
- 'created_at' => ['nullable', 'date'],
- 'updated_at' => ['nullable', 'date'],
- ];
- public static $primary_key = 'uuid';
- protected static $incrementing = false;
- public function getDetail()
- {
- return json_decode($this->data['detail']);
- }
- public function setDetail($value)
- {
- $this->data['detail'] = json_encode($value);
- }
- public function setAmount($value)
- {
- if ($value instanceof Money) {
- $this->data['amount'] = $value->getMinorAmount()->toInt();
- $this->data['currency'] = $value->getCurrency()->getCurrencyCode();
- } else {
- $this->data['amount'] = Money::of($value, $this->currency)
- ->getMinorAmount()->toInt();
- }
- }
- public function getAmountAsMoney()
- {
- return Money::ofMinor($this->data['amount'], $this->data['currency']);
- }
- public function getAmount()
- {
- return $this->amountAsMoney->getAmount();
- }
- /**
- * Switches payment status.
- *
- * To pending, completed or aborted depending on $compareWith value.
- *
- * @param bool $validComplete can be used for other validations
- * that confirm complete state is valid, if false will not update
- * to complete, but will update to aborted or pending.
- */
- public function validateAndUpdateStatus(
- $compareWith,
- array $completeWith,
- array $pendingWith = null,
- bool $validComplete = null
- ): void {
- if (in_array($compareWith, $completeWith)) {
- $this->status = Payment::STATUS_COMPLETED;
- } elseif ($pendingWith && in_array($compareWith, $pendingWith)) {
- $this->status = Payment::STATUS_PENDING;
- } else {
- $this->status = Payment::STATUS_ABORTED;
- }
- $notify_status = config("payment.origins.$this->origin.notify.status");
- if (in_array($this->status, $notify_status)) {
- if (
- $this->status === static::STATUS_COMPLETED &&
- $validComplete === false
- ) {
- return;
- }
- $this->update(['status']);
- $this->notify();
- } else {
- $this->update(['status']);
- }
- }
- /**
- * Internal instant payment notification (IPN)
- *
- * Maybe you're wondering why implement a request and response from
- * the ground up, it's because we process the configuration params
- * only, we don't want gateway implementations fiddling with it.
- */
- public function notify(): void
- {
- $config = config("payment.origins.$this->origin.notify");
- $response = self::notifySendRequest(
- $config['method'],
- $config['uri'],
- $this->notifyParams($config['params']),
- $config['headers']
- );
- if ($response->getStatusCode() != 200) {
- throw new UnexpectedResponseException($response);
- }
- }
- public function notifySendRequest(
- string $method,
- string $uri,
- array $params,
- array $headers
- ): ResponseInterface {
- $psr17Factory = new Psr17Factory();
- if ($method == 'GET') {
- $uri .= '?' . HttpSupport::buildUriQuery($params);
- $params = [];
- } elseif ($method == 'POST') {
- if (
- array_key_exists('Content-Type', $headers)
- and $headers['Content-Type'] == 'application/json'
- ) {
- $params = $psr17Factory->createStream(json_encode($params));
- } else {
- $params = $psr17Factory->createStream(
- HttpSupport::buildFormQuery($params)
- );
- }
- } else {
- throw new UnexpectedValueException(
- 'Request method ' . $method . ' is unsupported.',
- 2002
- );
- }
- $request = $psr17Factory
- ->createRequest($method, $uri)
- ->withBody($params);
- foreach ($headers as $key => $value) {
- $request = $request->withHeader($key, $value);
- }
- return (new HttpClient($psr17Factory))->sendRequest($request);
- }
- /**
- * Fills payment notification params with proper data
- */
- public function notifyParams(array &$params): array
- {
- foreach ($params as $key => $value) {
- if (is_array($value)) {
- $params[$key] = $this->notifyParams($value);
- } else {
- $params[$key] = preg_replace_callback(
- '/payment\.(.*)/',
- function ($matches) {
- return $this->{$matches[1]};
- },
- $value
- );
- }
- }
- return $params;
- }
- public function isOwner(User $user): bool
- {
- return $user->uid === $this->user_uid;
- }
- }
|