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\Builders\RequestBodyBuilder;
17: use IPay\Builders\TransactionBuilder;
18: use IPay\Captcha\CaptchaSolver;
19: use IPay\Contracts\AbstractApi;
20: use IPay\Http\Plugins\ExceptionThrower;
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 RequestBodyBuilder
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', get_defined_vars() + $this->bypassCaptcha());
84:
85: return new self($this->client, ['sessionId' => $response['sessionId']]);
86: }
87:
88: private function populateLazyProperties(): void
89: {
90: $this->customer = $this->objectMapper->hydrateObject(
91: Customer::class,
92: $this->post('getCustomerDetails')['customerInfo'],
93: );
94: $this->accounts = $this->objectMapper->hydrateObjects(
95: Account::class,
96: $this->post('getEntitiesAndAccounts')['accounts'],
97: )->toArray();
98: }
99:
100: /**
101: * @suppress 1416
102: */
103: public function transactions(?string $accountNumber = null): TransactionBuilder
104: {
105: $that = $this;
106:
107: return (\Closure::bind(function () use ($that, $accountNumber): TransactionBuilder {
108: $builder = new TransactionBuilder();
109: $builder->resolver = \Closure::bind(function (array $parameters): \Traversable {
110: return $this->getTransactions($parameters);
111: }, $that, $that::class);
112: $builder->parameters['accountNumber'] = $accountNumber ?? $that->customer->accountNumber;
113:
114: return $builder;
115: }, null, TransactionBuilder::class))();
116: }
117:
118: /**
119: * @param ParametersType $parameters
120: *
121: * @return \Traversable<int, Transaction>
122: *
123: * @throws Exceptions\SessionException
124: */
125: private function getTransactions(array $parameters): \Traversable
126: {
127: $parameters['pageNumber'] = 0;
128: do {
129: $transactions = $this->post(
130: 'getHistTransactions',
131: $parameters
132: )['transactions'];
133: foreach ($this->objectMapper->hydrateObjects(
134: Transaction::class,
135: $transactions
136: )->getIterator() as $transaction) {
137: yield $transaction;
138: }
139: ++$parameters['pageNumber'];
140: } while (count($transactions) > 0);
141: }
142:
143: /**
144: * @param ParametersType $parameters
145: *
146: * @return mixed[]
147: */
148: private function post(string $uri, array $parameters = []): array
149: {
150: $response = $this->client->post(
151: sprintf('ipay/wa/%s', $uri),
152: [],
153: RequestBodyBuilder::new()
154: ->enhance($this->getRequiredParameters())
155: ->build($parameters)
156: ->encrypt(),
157: );
158:
159: return Json::decode((string) $response->getBody(), true);
160: }
161:
162: /**
163: * @return ParametersType
164: */
165: private function getRequiredParameters(): array
166: {
167: return array_merge([
168: 'lang' => 'en',
169: 'requestId' => Random::generate(12, '0-9A-Z').'|'.time(),
170: ], $this->authenticatedParameters);
171: }
172:
173: /**
174: * @return array{captchaId:string,captchaCode:string}
175: */
176: private function bypassCaptcha(): array
177: {
178: $captchaId = Random::generate(9, '0-9a-zA-Z');
179: $svg = (string) $this->client
180: ->get(sprintf('api/get-captcha/%s', $captchaId))
181: ->getBody();
182: $captchaCode = CaptchaSolver::solve($svg);
183:
184: return compact('captchaId', 'captchaCode');
185: }
186: }
187: