Payment.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <?php
  2. // Copyright 2019-2022 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 Brick\Money\Money;
  14. use Nyholm\Psr7\Factory\Psr17Factory; // Request, Response & Stream factory
  15. use Psr\Http\Message\{ResponseInterface, StreamInterface};
  16. use Illuminate\Database\Query\Builder;
  17. use UnexpectedValueException;
  18. class Payment extends TableModel
  19. {
  20. public const STATUS_PENDING = 'pending';
  21. public const STATUS_COMPLETED = 'completed';
  22. public const STATUS_ABORTED = 'aborted';
  23. public static $table = 'payments';
  24. public static $attributes = [
  25. 'uuid' => ['required', 'uuid'],
  26. 'origin' => ['required', 'string', 'max:30'],
  27. 'user_uid' => ['required', 'string', 'min:3', 'max:100'],
  28. 'gateway' => ['required', 'string', 'min:3', 'max:100'],
  29. 'currency' => ['required', 'string', 'size:3'],
  30. 'amount' => ['required', 'integer'],
  31. 'description' => ['nullable', 'string', 'max:255'],
  32. 'detail' => ['nullable', 'max:65535'], // JSON
  33. 'status' => ['required', 'in:pending,completed,aborted'],
  34. 'created_at' => ['nullable', 'date'],
  35. 'updated_at' => ['nullable', 'date'],
  36. ];
  37. public static $primary_key = 'uuid';
  38. protected static $incrementing = false;
  39. public function getDetail()
  40. {
  41. return json_decode($this->data['detail']);
  42. }
  43. public function setDetail($value)
  44. {
  45. $this->data['detail'] = json_encode($value);
  46. }
  47. public function setAmount($value)
  48. {
  49. if ($value instanceof Money) {
  50. $this->data['amount'] = $value->getMinorAmount()->toInt();
  51. $this->data['currency'] = $value->getCurrency()->getCurrencyCode();
  52. } else {
  53. $this->data['amount'] = Money::of($value, $this->currency)
  54. ->getMinorAmount()->toInt();
  55. }
  56. }
  57. public function getAmountAsMoney()
  58. {
  59. return Money::ofMinor($this->data['amount'], $this->data['currency']);
  60. }
  61. public function getAmount()
  62. {
  63. return $this->amountAsMoney->getAmount();
  64. }
  65. /**
  66. * Switches payment status.
  67. *
  68. * To pending, completed or aborted depending on $compareWith value.
  69. *
  70. * @param bool $validComplete can be used for other validations
  71. * that confirm complete state is valid, if false will not update
  72. * to complete, but will update to aborted or pending.
  73. */
  74. public function validateAndUpdateStatus(
  75. $compareWith,
  76. array $completeWith,
  77. array $pendingWith = null,
  78. bool $validComplete = null
  79. ): void {
  80. if (in_array($compareWith, $completeWith)) {
  81. $this->status = Payment::STATUS_COMPLETED;
  82. } elseif ($pendingWith && in_array($compareWith, $pendingWith)) {
  83. $this->status = Payment::STATUS_PENDING;
  84. } else {
  85. $this->status = Payment::STATUS_ABORTED;
  86. }
  87. $notify_status = config("payment.origins.$this->origin.notify.status");
  88. if (in_array($this->status, $notify_status)) {
  89. if (
  90. $this->status === static::STATUS_COMPLETED &&
  91. $validComplete === false
  92. ) {
  93. return;
  94. }
  95. $this->update(['status']);
  96. $this->notify();
  97. } else {
  98. $this->update(['status']);
  99. }
  100. }
  101. /**
  102. * Internal instant payment notification (IPN)
  103. *
  104. * Maybe you're wondering why implement a request and response from
  105. * the ground up, it's because we process the configuration params
  106. * only, we don't want gateway implementations fiddling with it.
  107. */
  108. public function notify(): void
  109. {
  110. $config = config("payment.origins.$this->origin.notify");
  111. $response = self::notifySendRequest(
  112. $config['method'],
  113. $config['uri'],
  114. $this->notifyParams($config['params']),
  115. $config['headers']
  116. );
  117. if ($response->getStatusCode() != 200) {
  118. throw new UnexpectedResponseException($response);
  119. }
  120. }
  121. public function notifySendRequest(
  122. string $method,
  123. string $uri,
  124. array $params,
  125. array $headers
  126. ): ResponseInterface {
  127. $psr17Factory = new Psr17Factory();
  128. if ($method == 'GET') {
  129. $uri .= '?' . HttpSupport::buildUriQuery($params);
  130. $params = [];
  131. } elseif ($method == 'POST') {
  132. if (
  133. array_key_exists('Content-Type', $headers)
  134. and $headers['Content-Type'] == 'application/json'
  135. ) {
  136. $params = $psr17Factory->createStream(json_encode($params));
  137. } else {
  138. $params = $psr17Factory->createStream(
  139. HttpSupport::buildFormQuery($params)
  140. );
  141. }
  142. } else {
  143. throw new UnexpectedValueException(
  144. 'Request method ' . $method . ' is unsupported.',
  145. 2002
  146. );
  147. }
  148. $request = $psr17Factory
  149. ->createRequest($method, $uri)
  150. ->withBody($params);
  151. foreach ($headers as $key => $value) {
  152. $request = $request->withHeader($key, $value);
  153. }
  154. return (new HttpClient($psr17Factory))->sendRequest($request);
  155. }
  156. /**
  157. * Fills payment notification params with proper data
  158. */
  159. public function notifyParams(array &$params): array
  160. {
  161. foreach ($params as $key => $value) {
  162. if (is_array($value)) {
  163. $params[$key] = $this->notifyParams($value);
  164. } else {
  165. $params[$key] = preg_replace_callback(
  166. '/payment\.(.*)/',
  167. function ($matches) {
  168. return $this->{$matches[1]};
  169. },
  170. $value
  171. );
  172. }
  173. }
  174. return $params;
  175. }
  176. public function isOwner(User $user): bool
  177. {
  178. return $user->uid === $this->user_uid;
  179. }
  180. }