Payment.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. // Copyright 2019 Hackware SpA <human@hackware.cl>
  3. // This file is part of "Hackware Web Services Payment" and licensed under
  4. // the terms of the GNU Affero General Public License version 3, or (at your
  5. // option) a later version. You should have received a copy of this license
  6. // along with the software. If not, see <https://www.gnu.org/licenses/>.
  7. namespace Hawese\Payment;
  8. use Hawese\Core\TableModel;
  9. use Hawese\Core\User;
  10. use Hawese\Payment\Support\Http as HttpSupport;
  11. use Hawese\Payment\Exceptions\UnexpectedResponseException;
  12. use Buzz\Client\FileGetContents as HttpClient;
  13. use Nyholm\Psr7\Factory\Psr17Factory; // Request, Response & Stream factory
  14. use Psr\Http\Message\{ResponseInterface, StreamInterface};
  15. use Illuminate\Database\Query\Builder;
  16. use UnexpectedValueException;
  17. class Payment extends TableModel
  18. {
  19. public const STATUS_PENDING = 'pending';
  20. public const STATUS_COMPLETED = 'completed';
  21. public const STATUS_ABORTED = 'aborted';
  22. public static $table = 'payments';
  23. public static $attributes = [
  24. 'uuid' => ['required', 'uuid'],
  25. 'user_uid' => ['required', 'string', 'min:3', 'max:100'],
  26. 'gateway' => ['required', 'string', 'min:3', 'max:100'],
  27. 'currency' => ['required', 'string', 'size:3'], // MoneyPHP format
  28. 'amount' => ['required', 'integer'], // MoneyPHP format
  29. 'description' => ['nullable', 'string', 'max:255'],
  30. 'detail' => ['nullable', 'string', 'max:65535'],
  31. 'status' => ['required', 'in:pending,completed,aborted'],
  32. 'created_at' => ['nullable', 'date'],
  33. 'updated_at' => ['nullable', 'date'],
  34. ];
  35. public static $primary_key = 'uuid';
  36. protected static $incrementing = false;
  37. /**
  38. * Switches payment status.
  39. *
  40. * To pending, completed or aborted depending on $compareWith value.
  41. *
  42. * @param bool $validComplete can be used for other validations
  43. * that confirm complete state is valid, if false will not update
  44. * to complete, but will update to aborted or pending.
  45. */
  46. public function validateAndUpdateStatus(
  47. $compareWith,
  48. array $completeWith,
  49. array $pendingWith = null,
  50. bool $validComplete = null
  51. ): void {
  52. if (in_array($compareWith, $completeWith)) {
  53. $this->status = Payment::STATUS_COMPLETED;
  54. } elseif ($pendingWith && in_array($compareWith, $pendingWith)) {
  55. $this->status = Payment::STATUS_PENDING;
  56. } else {
  57. $this->status = Payment::STATUS_ABORTED;
  58. }
  59. if ($this->status === static::STATUS_COMPLETED) {
  60. if ($validComplete === false) {
  61. return;
  62. }
  63. $this->update(['status']);
  64. $this->notify();
  65. } else {
  66. $this->update(['status']);
  67. }
  68. }
  69. /**
  70. * Internal instant payment notification (IPN)
  71. *
  72. * Maybe you're wondering why implement a request and response from
  73. * the ground up, it's because we process the configuration params
  74. * only, we don't want gateway implementations fiddling with it.
  75. */
  76. public function notify(): void
  77. {
  78. $config = config('payment.notify_to');
  79. $response = self::notifySendRequest(
  80. $config['method'],
  81. $config['uri'],
  82. $this->notifyParams($config['params']),
  83. $config['headers']
  84. );
  85. if ($response->getStatusCode() != 200) {
  86. throw new UnexpectedResponseException($response);
  87. }
  88. }
  89. public function notifySendRequest(
  90. string $method,
  91. string $uri,
  92. array $params,
  93. array $headers
  94. ): ResponseInterface {
  95. $psr17Factory = new Psr17Factory();
  96. if ($method == 'GET') {
  97. $uri .= '?' . HttpSupport::buildUriQuery($params);
  98. $params = [];
  99. } elseif ($method == 'POST') {
  100. $params = $psr17Factory->createStream(
  101. HttpSupport::buildFormQuery($params)
  102. );
  103. } else {
  104. throw new UnexpectedValueException(
  105. 'Request method ' . $method . ' is unsupported.',
  106. 2002
  107. );
  108. }
  109. $request = $psr17Factory
  110. ->createRequest($method, $uri)
  111. ->withBody($params);
  112. foreach ($headers as $key => $value) {
  113. $request = $request->withHeader($key, $value);
  114. }
  115. return (new HttpClient($psr17Factory))->sendRequest($request);
  116. }
  117. /**
  118. * Fills payment notification params with proper data
  119. */
  120. public function notifyParams(array &$params): array
  121. {
  122. foreach ($params as $key => $value) {
  123. if (is_array($value)) {
  124. $params[$key] = json_encode( // 1 level of depth!
  125. $this->notifyParams($value, $this)
  126. );
  127. } else {
  128. $params[$key] = preg_replace_callback(
  129. '/payment\.(.*)/',
  130. function ($matches) {
  131. return $this->{$matches[1]};
  132. },
  133. $value
  134. );
  135. }
  136. }
  137. return $params;
  138. }
  139. public function isOwner(User $user): bool
  140. {
  141. return $user->uid === $this->user_uid;
  142. }
  143. }