diff --git a/.gitignore b/.gitignore index 2328e65..90f34a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea vendor tools test.php diff --git a/composer.json b/composer.json index 5c55c6c..81bf33b 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "type": "library", "license": "MIT", "autoload": { - "classmap": [ - "src" - ] + "psr-4": { + "IPay\\": "src/" + } }, "authors": [ { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index aab4991..4ca93a8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,2 +1,6 @@ parameters: - ignoreErrors: [] + ignoreErrors: + - + message: "#^Property IPay\\\\Builders\\\\TransactionBuilder\\:\\:\\$resolver is never written, only read\\.$#" + count: 1 + path: src/Builders/TransactionBuilder.php diff --git a/src/Api/AbstractApi.php b/src/Api/AbstractApi.php deleted file mode 100644 index c4b04a9..0000000 --- a/src/Api/AbstractApi.php +++ /dev/null @@ -1,63 +0,0 @@ -objectMapper = new ObjectMapperUsingReflection( - new DefinitionProvider( - keyFormatter: new KeyFormatterWithoutConversion(), - ), - ); - } - - /** - * @param ParametersType $parameters - * - * @return mixed[] - */ - protected function post(string $uri, array $parameters = []): array - { - $response = $this->iPayClient->getClient()->post( - sprintf('ipay/wa/%s', $uri), - [], - BodyBuilder::from($parameters) - ->enhance($this->getSession()->getRequestParameters()) - ->build() - ->encrypt() - ); - - return Json::decode((string) $response->getBody(), true); - } - - /** - * @return T - */ - public function getSession(): SessionInterface - { - return $this->session; - } -} diff --git a/src/Api/AuthenticatedApi.php b/src/Api/AuthenticatedApi.php deleted file mode 100644 index ad9c919..0000000 --- a/src/Api/AuthenticatedApi.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ -final class AuthenticatedApi extends AbstractApi -{ - use LazyGhostTrait { - createLazyGhost as private; - initializeLazyObject as private; - isLazyObjectInitialized as private; - resetLazyObject as private; - } - - public Customer $customer; - - /** @var list */ - public array $accounts; - - public function __construct( - IPayClient $iPayClient, - AuthenticatedSession $session, - ) { - parent::__construct($iPayClient, $session); - self::createLazyGhost( - initializer: $this->populateLazyProperties(...), - instance: $this, - ); - } - - private function populateLazyProperties(): void - { - $this->customer = $this->objectMapper->hydrateObject( - Customer::class, - $this->post('getCustomerDetails')['customerInfo'], - ); - $this->accounts = $this->objectMapper->hydrateObjects( - Account::class, - $this->post('getEntitiesAndAccounts')['accounts'], - )->toArray(); - } - - public function transactions(?string $accountNumber = null): TransactionBuilder - { - return TransactionBuilder::from( - $this->getTransactions(...), - ['accountNumber' => $accountNumber ?? $this->customer->accountNumber], - ); - } - - /** - * @param ParametersType $parameters - * - * @return \Traversable - * - * @throws \IPay\Exception\SessionException - */ - private function getTransactions(array $parameters): \Traversable - { - $parameters['pageNumber'] = 0; - do { - $transactions = $this->post( - 'getHistTransactions', - $parameters - )['transactions']; - foreach ($this->objectMapper->hydrateObjects( - Transaction::class, - $transactions - )->getIterator() as $transaction) { - yield $transaction; - } - ++$parameters['pageNumber']; - } while (count($transactions) > 0); - } -} diff --git a/src/Api/UnauthenticatedApi.php b/src/Api/UnauthenticatedApi.php deleted file mode 100644 index 317491b..0000000 --- a/src/Api/UnauthenticatedApi.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -final class UnauthenticatedApi extends AbstractApi -{ - public function login(string $userName, string $accessCode): AuthenticatedApi - { - $parameters = get_defined_vars() + $this->bypassCaptcha(); - - /** @var array{sessionId: string, ...} */ - $result = $this->post('signIn', $parameters); - - return new AuthenticatedApi( - $this->iPayClient, - new AuthenticatedSession($result['sessionId']) - ); - } - - /** - * @return array{captchaId:string,captchaCode:string} - */ - private function bypassCaptcha(): array - { - $captchaId = Random::generate(9, '0-9a-zA-Z'); - $svg = (string) $this->iPayClient->getClient() - ->get(sprintf('api/get-captcha/%s', $captchaId)) - ->getBody(); - $captchaCode = CaptchaSolver::solve($svg); - - return compact('captchaId', 'captchaCode'); - } -} diff --git a/src/Builder/BodyBuilder.php b/src/Builder/BodyBuilder.php deleted file mode 100644 index e72ed90..0000000 --- a/src/Builder/BodyBuilder.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * @extends \ArrayObject - */ -final class BodyBuilder extends \ArrayObject implements \Stringable, \JsonSerializable -{ - /** - * @param ParametersType $array - */ - private function __construct(array $array) - { - parent::__construct($array); - } - - /** - * @param ParametersType $parameters - */ - public static function from(array $parameters): static - { - return new static($parameters); - } - - /** - * @param ParametersType $parameters - */ - public function enhance(array $parameters): static - { - return static::from($this->getArrayCopy() + $parameters); - } - - public function build(): self - { - $this->ksort(); - - $this['signature'] = md5(http_build_query($this->getArrayCopy())); - - return $this; - } - - public function encrypt(): string - { - return static::from(['encrypted' => Encrypter::encrypt($this)]); - } - - public function __toString(): string - { - return Json::encode($this); - } - - /** - * @return ParametersType - */ - #[\ReturnTypeWillChange] - public function jsonSerialize(): array - { - return $this->getArrayCopy(); - } -} diff --git a/src/Builders/BodyBuilder.php b/src/Builders/BodyBuilder.php new file mode 100644 index 0000000..66d0fea --- /dev/null +++ b/src/Builders/BodyBuilder.php @@ -0,0 +1,58 @@ + + */ +final class BodyBuilder implements \Stringable, \JsonSerializable +{ + /** + * @param ParametersType $parameters + */ + public function __construct( + private array $parameters = [], + ) { + } + + public function setSessionId(string $value): void + { + $this->parameters['sessionId'] = $value; + } + + /** + * @param ParametersType $parameters + */ + public function build(array $parameters = []): self + { + $data = array_merge([ + 'lang' => 'en', + 'requestId' => Random::generate(12, '0-9A-Z').'|'.time(), + ], $this->parameters, $parameters); + ksort($data); + $data['signature'] = md5(http_build_query($data)); + + return new self($data); + } + + public function encrypt(): string + { + return new self(['encrypted' => Encryptor::encrypt($this)]); + } + + public function __toString(): string + { + return Json::encode($this->parameters); + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->parameters; + } +} diff --git a/src/Builder/TransactionBuilder.php b/src/Builders/TransactionBuilder.php similarity index 53% rename from src/Builder/TransactionBuilder.php rename to src/Builders/TransactionBuilder.php index 8dffe08..751f59f 100644 --- a/src/Builder/TransactionBuilder.php +++ b/src/Builders/TransactionBuilder.php @@ -1,42 +1,22 @@ - * * @implements \IteratorAggregate */ final class TransactionBuilder implements \IteratorAggregate { - /** - * @param GetterType $getter - * @param ParametersType $parameters - */ - private function __construct( - private \Closure $getter, - private array $parameters, - ) { - } + /** @var \Closure(ParametersType):\Traversable */ + private \Closure $resolver; - /** - * @param GetterType $getter - * @param ParametersType $parameters - */ - public static function from( - \Closure $getter, - array $parameters, - ): self { - return new self( - $getter, - $parameters, - ); - } + /** @var ParametersType */ + private array $parameters = []; public function between( \DateTimeInterface $from, @@ -65,6 +45,6 @@ public function type(TransactionType $type): self public function getIterator(): \Traversable { - return ($this->getter)($this->parameters); + return ($this->resolver)($this->parameters); } } diff --git a/src/Contracts/AbstractApi.php b/src/Contracts/AbstractApi.php new file mode 100644 index 0000000..c2ada03 --- /dev/null +++ b/src/Contracts/AbstractApi.php @@ -0,0 +1,17 @@ + */ + public array $accounts; + + abstract public function transactions(?string $accountNumber = null): TransactionBuilder; +} diff --git a/src/Encryption/Encrypter.php b/src/Encryption/Encryptor.php similarity index 96% rename from src/Encryption/Encrypter.php rename to src/Encryption/Encryptor.php index c20a8f1..dd94323 100644 --- a/src/Encryption/Encrypter.php +++ b/src/Encryption/Encryptor.php @@ -4,7 +4,7 @@ use phpseclib\Crypt\RSA; -final class Encrypter +final class Encryptor { private const IPAY_PUBLIC_KEY = <<login($username, $password); } private function __construct( + string $username, + string $password, private HttpMethodsClientInterface $client, + private BodyBuilder $bodyBuilder = new BodyBuilder(), + private ObjectMapper $objectMapper = new ObjectMapperUsingReflection( + new DefinitionProvider( + keyFormatter: new KeyFormatterWithoutConversion(), + ), + ), ) { + /** @var array{sessionId: string, ...} */ + $response = $this->post('signIn', [ + 'userName' => $username, + 'accessCode' => $password, + ] + $this->bypassCaptcha()); + $bodyBuilder->setSessionId($response['sessionId']); + self::createLazyGhost( + initializer: $this->populateLazyProperties(...), + instance: $this, + ); + } + + private function populateLazyProperties(): void + { + $this->customer = $this->objectMapper->hydrateObject( + Customer::class, + $this->post('getCustomerDetails')['customerInfo'], + ); + $this->accounts = $this->objectMapper->hydrateObjects( + Account::class, + $this->post('getEntitiesAndAccounts')['accounts'], + )->toArray(); + } + + /** + * @suppress 1416 + */ + public function transactions(?string $accountNumber = null): TransactionBuilder + { + $that = $this; + + return (\Closure::bind(function () use ($that, $accountNumber): TransactionBuilder { + $builder = new TransactionBuilder(); + $builder->resolver = \Closure::bind(function (array $parameters): \Traversable { + return $this->getTransactions($parameters); + }, $that, $that::class); + $builder->parameters['accountNumber'] = $accountNumber ?? $that->customer->accountNumber; + + return $builder; + }, null, TransactionBuilder::class))(); } - public function getClient(): HttpMethodsClientInterface + /** + * @param ParametersType $parameters + * + * @return \Traversable + * + * @throws Exceptions\SessionException + */ + private function getTransactions(array $parameters): \Traversable + { + $parameters['pageNumber'] = 0; + do { + $transactions = $this->post( + 'getHistTransactions', + $parameters + )['transactions']; + foreach ($this->objectMapper->hydrateObjects( + Transaction::class, + $transactions + )->getIterator() as $transaction) { + yield $transaction; + } + ++$parameters['pageNumber']; + } while (count($transactions) > 0); + } + + /** + * @param ParametersType $parameters + * + * @return mixed[] + */ + private function post(string $uri, array $parameters = []): array { - return $this->client; + $response = $this->client->post( + sprintf('ipay/wa/%s', $uri), + [], + $this->bodyBuilder->build($parameters)->encrypt(), + ); + + return Json::decode((string) $response->getBody(), true); + } + + /** + * @return array{captchaId:string,captchaCode:string} + */ + private function bypassCaptcha(): array + { + $captchaId = Random::generate(9, '0-9a-zA-Z'); + $svg = (string) $this->client + ->get(sprintf('api/get-captcha/%s', $captchaId)) + ->getBody(); + $captchaCode = CaptchaSolver::solve($svg); + + return compact('captchaId', 'captchaCode'); } } diff --git a/src/Session/AuthenticatedSession.php b/src/Session/AuthenticatedSession.php deleted file mode 100644 index 81b2153..0000000 --- a/src/Session/AuthenticatedSession.php +++ /dev/null @@ -1,15 +0,0 @@ - $this->id, ...parent::getRequestParameters()]; - } -} diff --git a/src/Session/SessionInterface.php b/src/Session/SessionInterface.php deleted file mode 100644 index 1cd088c..0000000 --- a/src/Session/SessionInterface.php +++ /dev/null @@ -1,11 +0,0 @@ - 'en', - 'requestId' => Random::generate(12, '0-9A-Z').'|'.time(), - ]; - } -} diff --git a/src/Entity/Account.php b/src/ValueObjects/Account.php similarity index 60% rename from src/Entity/Account.php rename to src/ValueObjects/Account.php index 0c4676a..4e7713e 100644 --- a/src/Entity/Account.php +++ b/src/ValueObjects/Account.php @@ -1,29 +1,9 @@