1: <?php
2:
3: namespace IPay;
4:
5: use EventSauce\ObjectHydrator\DefinitionProvider;
6: use EventSauce\ObjectHydrator\KeyFormatterWithoutConversion;
7: use EventSauce\ObjectHydrator\ObjectMapper;
8: use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;
9: use Http\Client\Common\HttpMethodsClient;
10: use Http\Client\Common\HttpMethodsClientInterface;
11: use Http\Client\Common\Plugin\BaseUriPlugin;
12: use Http\Client\Common\Plugin\ContentTypePlugin;
13: use Http\Client\Common\PluginClient;
14: use Http\Discovery\Psr17FactoryDiscovery;
15: use Http\Discovery\Psr18ClientDiscovery;
16: use IPay\Captcha\CaptchaSolver;
17: use IPay\Contracts\AbstractApi;
18: use IPay\Http\Plugins\ExceptionThrower;
19: use IPay\Resources\Transactions;
20: use IPay\Utils\RequestBodyBuilder;
21: use IPay\ValueObjects\Account;
22: use IPay\ValueObjects\Customer;
23: use IPay\ValueObjects\Transaction;
24: use Nette\Utils\Json;
25: use Nette\Utils\Random;
26: use Symfony\Component\VarExporter\LazyGhostTrait;
27:
28: /**
29: * @phpstan-import-type ParametersType from AbstractApi
30: */
31: final class IPayClient extends AbstractApi
32: {
33: use LazyGhostTrait {
34: createLazyGhost as private;
35: }
36:
37: /**
38: * @throws Exceptions\LoginException
39: */
40: public static function fromCredentials(string $username, string $password): AbstractApi
41: {
42: $client = (new self(
43: new HttpMethodsClient(
44: new PluginClient(Psr18ClientDiscovery::find(), [
45: new BaseUriPlugin(
46: Psr17FactoryDiscovery::findUriFactory()
47: ->createUri('https://api-ipay.vietinbank.vn')
48: ),
49: new ContentTypePlugin(),
50: new ExceptionThrower(),
51: ]),
52: Psr17FactoryDiscovery::findRequestFactory(),
53: Psr17FactoryDiscovery::findStreamFactory(),
54: ),
55: ))->login($username, $password);
56:
57: return $client;
58: }
59:
60: /**
61: * @param ParametersType $authenticatedParameters
62: */
63: private function __construct(
64: private HttpMethodsClientInterface $client,
65: private array $authenticatedParameters = [],
66: private ObjectMapper $objectMapper = new ObjectMapperUsingReflection(
67: new DefinitionProvider(
68: keyFormatter: new KeyFormatterWithoutConversion(),
69: ),
70: ),
71: ) {
72: if ($authenticatedParameters) {
73: self::createLazyGhost(
74: initializer: $this->populateLazyProperties(...),
75: instance: $this,
76: );
77: }
78: }
79:
80: private function login(string $userName, string $accessCode): self
81: {
82: /** @var array{sessionId: string, ...} */
83: $response = $this->post('signIn', [
84: 'userName' => $userName,
85: 'accessCode' => $accessCode,
86: ...$this->bypassCaptcha(),
87: ]);
88:
89: return new self($this->client, ['sessionId' => $response['sessionId']]);
90: }
91:
92: private function populateLazyProperties(): void
93: {
94: $this->customer = $this->objectMapper->hydrateObject(
95: Customer::class,
96: $this->post('getCustomerDetails')['customerInfo'],
97: );
98: $this->accounts = $this->objectMapper->hydrateObjects(
99: Account::class,
100: $this->post('getEntitiesAndAccounts')['accounts'],
101: )->toArray();
102: }
103:
104: /**
105: * @suppress 1416
106: */
107: public function transactions(?string $accountNumber = null): Transactions
108: {
109: return (\Closure::bind(function (IPayClient $api, ?string $accountNumber): Transactions {
110: $transactions = new Transactions();
111: $transactions->resolver = \Closure::bind(function (array $parameters): \Traversable {
112: return $this->getTransactions($parameters);
113: }, $api, $api::class);
114: $transactions->parameters['accountNumber'] = $accountNumber ?? $api->customer->accountNumber;
115:
116: return $transactions;
117: }, null, Transactions::class))($this, $accountNumber);
118: }
119:
120: /**
121: * @param ParametersType $parameters
122: *
123: * @return \Traversable<int, Transaction>
124: *
125: * @throws Exceptions\SessionException
126: */
127: private function getTransactions(array $parameters): \Traversable
128: {
129: $parameters['pageNumber'] = 0;
130: do {
131: $transactions = $this->post(
132: 'getHistTransactions',
133: $parameters
134: )['transactions'];
135: foreach ($this->objectMapper->hydrateObjects(
136: Transaction::class,
137: $transactions
138: )->getIterator() as $transaction) {
139: yield $transaction;
140: }
141: ++$parameters['pageNumber'];
142: } while (count($transactions) > 0);
143: }
144:
145: /**
146: * @param ParametersType $parameters
147: *
148: * @return mixed[]
149: */
150: private function post(string $uri, array $parameters = []): array
151: {
152: $response = $this->client->post(
153: sprintf('ipay/wa/%s', $uri),
154: [],
155: RequestBodyBuilder::new()
156: ->enhance($this->getRequiredParameters())
157: ->build($parameters)
158: ->encrypt(),
159: );
160:
161: return Json::decode((string) $response->getBody(), true);
162: }
163:
164: /**
165: * @return ParametersType
166: */
167: private function getRequiredParameters(): array
168: {
169: return array_merge([
170: 'lang' => 'en',
171: 'requestId' => Random::generate(12, '0-9A-Z').'|'.time(),
172: ], $this->authenticatedParameters);
173: }
174:
175: /**
176: * @return array{captchaId:string,captchaCode:string}
177: */
178: private function bypassCaptcha(): array
179: {
180: $captchaId = Random::generate(9, '0-9a-zA-Z');
181: $svg = (string) $this->client
182: ->get(sprintf('api/get-captcha/%s', $captchaId))
183: ->getBody();
184: $captchaCode = CaptchaSolver::solve($svg);
185:
186: return compact('captchaId', 'captchaCode');
187: }
188: }
189: