diff --git a/database/migrations/2022_01_06_140726_create_tanda_requests_table.php b/database/migrations/2022_01_06_140726_create_tanda_requests_table.php index cc98539..aa3f806 100644 --- a/database/migrations/2022_01_06_140726_create_tanda_requests_table.php +++ b/database/migrations/2022_01_06_140726_create_tanda_requests_table.php @@ -21,7 +21,7 @@ function (Blueprint $table) { $table->string('request_id')->unique(); $table->string('status'); $table->string('message'); -// $table->string('receipt_number')->nullable(); + $table->string('receipt_number')->nullable(); $table->string('command_id'); $table->string('provider'); $table->string('destination'); diff --git a/src/Console/RequestStatusCommand.php b/src/Console/RequestStatusCommand.php new file mode 100644 index 0000000..f72037a --- /dev/null +++ b/src/Console/RequestStatusCommand.php @@ -0,0 +1,61 @@ +tanda->queryRequestStatus(); + + if (count($results['successful'])) { + $this->info("Successful queries: "); + + foreach ($results['successful'] as $reference => $message) { + $this->comment(" * $reference ---> $message"); + } + } + + if (count($results['errors'])) { + $this->info("Failed queries: "); + + foreach ($results['errors'] as $reference => $message) { + $this->comment(" * $reference ---> $message"); + } + } + + if (empty($results['successful']) && empty($results['errors'])) { + $this->comment("Nothing to query... all transactions seem to be ok."); + } + } +} diff --git a/src/Console/TransactionStatusCommand.php b/src/Console/TransactionStatusCommand.php deleted file mode 100644 index 9fc5ab1..0000000 --- a/src/Console/TransactionStatusCommand.php +++ /dev/null @@ -1,63 +0,0 @@ -tanda = $repository; - } - - /** - * Execute the console command. - * - */ - public function handle() - { -// $results = $this->tanda->queryTransactionStatus(); -// -// if (count($results['successful'])) { -// $this->info("Successful queries: "); -// -// foreach ($results['successful'] as $reference => $message) { -// $this->comment(" * $reference ---> $message"); -// } -// } -// -// if (count($results['errors'])) { -// $this->info("Failed queries: "); -// -// foreach ($results['errors'] as $reference => $message) { -// $this->comment(" * $reference ---> $message"); -// } -// } -// -// if (empty($results['successful']) && empty($results['errors'])) { -// $this->comment("Nothing to query... all transactions seem to be ok."); -// } - } -} diff --git a/src/Facades/Utility.php b/src/Facades/Utility.php index 7902e94..7612509 100644 --- a/src/Facades/Utility.php +++ b/src/Facades/Utility.php @@ -8,6 +8,7 @@ /** * @method static TR airtimePurchase(int $phone, int $amount, int $relationId = null) * @method static array requestStatus(string $reference) + * @method static TR billPayment(int $accountNo, int $amount, string $provider, int $relationId = null) * * @see \DrH\Tanda\Library\Utility */ diff --git a/src/Library/Authenticator.php b/src/Library/Authenticator.php index 80641b2..f68c19d 100644 --- a/src/Library/Authenticator.php +++ b/src/Library/Authenticator.php @@ -10,24 +10,17 @@ class Authenticator { - protected string $endpoint; - - protected BaseClient $client; - - protected static Authenticator $instance; + private string $endpoint; private ?string $credentials = null; /** * Authenticator constructor. * - * @param BaseClient $baseClient + * @param BaseClient $client */ - public function __construct(BaseClient $baseClient) + public function __construct(private BaseClient $client) { - $this->client = $baseClient; - $this->endpoint = Endpoints::build(Endpoints::AUTH); - self::$instance = $this; } @@ -89,6 +82,8 @@ private function makeRequest(): ResponseInterface $clientId = config('tanda.client_id', false); $clientSecret = config('tanda.client_secret', false); + $this->endpoint = Endpoints::build(Endpoints::AUTH); + return $this->client->clientInterface->request( 'POST', $this->endpoint, diff --git a/src/Library/Core.php b/src/Library/Core.php index 4a453d4..2562de1 100644 --- a/src/Library/Core.php +++ b/src/Library/Core.php @@ -66,7 +66,7 @@ public function sendRequest(string $method, string $url, array $body): ResponseI public function getBody(array $body): array { -// Added these to reduce redundancy in child classes + // Added these to reduce redundancy in child classes $body += [ 'referenceParameters' => $this->getReferenceParameters() ]; @@ -82,29 +82,16 @@ public function getTelcoFromPhone(int $phone): string $safReg = '/^(?:254|\+254|0)?((?:7(?:[0129][0-9]|4[0123568]|5[789]|6[89])|(1([1][0-5])))[0-9]{6})$/'; $airReg = '/^(?:254|\+254|0)?((?:(7(?:(3[0-9])|(5[0-6])|(6[27])|(8[0-9])))|(1([0][0-6])))[0-9]{6})$/'; $telReg = '/^(?:254|\+254|0)?(7(7[0-9])[0-9]{6})$/'; - $equReg = '/^(?:254|\+254|0)?(7(6[3-6])[0-9]{6})$/'; + // $equReg = '/^(?:254|\+254|0)?(7(6[3-6])[0-9]{6})$/'; $faibaReg = '/^(?:254|\+254|0)?(747[0-9]{6})$/'; - switch (1) { - case preg_match($safReg, $phone): - $result = Providers::SAFARICOM; - break; - case preg_match($airReg, $phone): - $result = Providers::AIRTEL; - break; - case preg_match($telReg, $phone): - $result = Providers::TELKOM; - break; -// case preg_match($equReg, $phone): -// $result = Providers::EQUITEL; -// break; - case preg_match($faibaReg, $phone): - $result = Providers::FAIBA; - break; - default: - $result = null; - break; - } + $result = match (1) { + preg_match($safReg, $phone) => Providers::SAFARICOM, + preg_match($airReg, $phone) => Providers::AIRTEL, + preg_match($telReg, $phone) => Providers::TELKOM, + preg_match($faibaReg, $phone) => Providers::FAIBA, + default => null, + }; if (!$result) { throw new TandaException("Phone does not seem to be valid or supported"); @@ -172,6 +159,7 @@ protected function fireTandaEvent(TandaRequest $request): void if ($request->status == 000001) { return; } + if ($request->status == 000000) { event(new TandaRequestSuccessEvent($request)); } else { diff --git a/src/Library/Utility.php b/src/Library/Utility.php index 8de0b38..92add82 100644 --- a/src/Library/Utility.php +++ b/src/Library/Utility.php @@ -2,6 +2,7 @@ namespace DrH\Tanda\Library; +use Carbon\Carbon; use DrH\Tanda\Exceptions\TandaException; use DrH\Tanda\Models\TandaRequest; use GuzzleHttp\Exception\GuzzleException; @@ -70,7 +71,6 @@ public function airtimePurchase( * @param int $accountNo * @param int $amount * @param string $provider - * @param int $phone * @param int|null $relationId * @param bool $save * @return array|TandaRequest @@ -81,10 +81,11 @@ public function billPayment( int $accountNo, int $amount, string $provider, - int $phone, int $relationId = null, bool $save = true ): array | TandaRequest { + $provider = strtoupper($provider); + $allowedProviders = [ Providers::KPLC_PREPAID, Providers::KPLC_POSTPAID, @@ -97,16 +98,20 @@ public function billPayment( $this->validate($provider, $amount); - if (!in_array(strtoupper($provider), $allowedProviders)) { + if (!in_array($provider, $allowedProviders)) { throw new TandaException("Provider does not seem to be valid or supported"); } $this->provider = $provider; $this->amount = $amount; - $this->destination = $phone; + $this->destination = $accountNo; $this->setCommand($this->provider); + if (in_array($provider, [Providers::KPLC_PREPAID, Providers::KPLC_POSTPAID])) { + $this->provider = 'KPLC'; + } + // TODO: Check whether customerContact is necessary or what it is used for. // $phone = $this->formatPhoneNumber($phone); $requestParameters = [ @@ -146,7 +151,23 @@ public function billPayment( */ public function requestStatus(string $reference): array { - return $this->request(Endpoints::STATUS, [], [':requestId' => $reference]); + $response = $this->request(Endpoints::STATUS, [], [':requestId' => $reference]); + + $request = TandaRequest::whereRequestId($response['id'])->first(); + + if ($request && $request->status !== $response['status']) { + $request->update([ + 'status' => $response['status'], + 'message' => $response['message'], + 'receipt_number' => $response['receiptNumber'], + 'result' => $response['resultParameters'], + 'last_modified' => Carbon::parse($response['datetimeLastModified'])->utc(), + ]); + + $this->fireTandaEvent($request); + } + + return $response; } private function setCommand(string $provider) diff --git a/src/Repositories/Tanda.php b/src/Repositories/Tanda.php new file mode 100644 index 0000000..2b7dec2 --- /dev/null +++ b/src/Repositories/Tanda.php @@ -0,0 +1,73 @@ +utility = new Utility($baseClient); + } + + #[ArrayShape(['successful' => "array", 'errors' => "array"])] + public function queryRequestStatus(): array + { + /** @var TandaRequest[] $tandaRequests */ + $tandaRequests = TandaRequest::whereStatus(000001)->get(); + $success = $errors = []; + + foreach ($tandaRequests as $request) { + try { + $result = $this->utility->requestStatus($request->request_id); + + $success[$request->request_id] = $result['message']; + + $data = [ + 'status' => $result['status'], + 'message' => $result['message'], + 'receipt_number' => $result['receiptNumber'], + 'result' => $result['resultParameters'], + 'last_modified' => Carbon::parse($result['datetimeLastModified'])->utc(), + ]; + + $transaction = TandaRequest::updateOrCreate( + ['request_id' => $result['id']], + $data + ); + + $this->fireTandaEvent($transaction); + } catch (TandaException | GuzzleException $e) { + $errors[$request->request_id] = $e->getMessage(); + } + } + + return ['successful' => $success, 'errors' => $errors]; + } + + /** + * @param TandaRequest $request + * @return void + */ + private static function fireTandaEvent(TandaRequest $request): void + { + if ($request->status == 000001) { + return; + } + + $request->status == 000000 + ? TandaRequestSuccessEvent::dispatch($request) + : TandaRequestFailedEvent::dispatch($request); + } +} diff --git a/src/TandaServiceProvider.php b/src/TandaServiceProvider.php index 0bb7808..f26ac43 100644 --- a/src/TandaServiceProvider.php +++ b/src/TandaServiceProvider.php @@ -2,6 +2,8 @@ namespace DrH\Tanda; +use DrH\Tanda\Console\InstallCommand; +use DrH\Tanda\Console\RequestStatusCommand; use DrH\Tanda\Library\BaseClient; use DrH\Tanda\Library\Utility; use GuzzleHttp\Client; @@ -91,8 +93,8 @@ protected function registerCommands() { if ($this->app->runningInConsole()) { $this->commands([ - Console\InstallCommand::class, - Console\TransactionStatusCommand::class + InstallCommand::class, + RequestStatusCommand::class ]); } } diff --git a/tests/Library/EndpointsTest.php b/tests/Library/EndpointsTest.php index 35fbc7c..ac81495 100644 --- a/tests/Library/EndpointsTest.php +++ b/tests/Library/EndpointsTest.php @@ -19,46 +19,46 @@ function get_url_from_valid_endpoint() } /** @test */ - function throw_error_on_invalid_endpoint() + function replaces_organization_id_correctly() { Config::set('tanda.organization_id', 'org'); - $this->expectException(TandaException::class); + $testUrl = Endpoints::build(Endpoints::REQUEST); + $actualUrl = Config::get('tanda.urls.base') . '/io/v1/organizations/org/requests'; - Endpoints::build("test_invalid"); + $this->assertSame($actualUrl, $testUrl); } /** @test */ - function replaces_organization_id_correctly() + function adds_sandbox_to_url_correctly() { Config::set('tanda.organization_id', 'org'); + Config::set('tanda.sandbox', true); $testUrl = Endpoints::build(Endpoints::REQUEST); - $actualUrl = Config::get('tanda.urls.base') . '/io/v1/organizations/org/requests'; + $actualUrl = Config::get('tanda.urls.base') . '/sandbox/io/v1/organizations/org/requests'; $this->assertSame($actualUrl, $testUrl); } /** @test */ - function throw_error_on_unset_organization_id() + function throws_on_invalid_endpoint() { - Config::set('tanda.organization_id'); + Config::set('tanda.organization_id', 'org'); $this->expectException(TandaException::class); - Endpoints::build(Endpoints::REQUEST); + Endpoints::build("test_invalid"); } /** @test */ - function adds_sandbox_to_url_correctly() + function throws_on_unset_organization_id() { - Config::set('tanda.organization_id', 'org'); - Config::set('tanda.sandbox', true); + Config::set('tanda.organization_id'); - $testUrl = Endpoints::build(Endpoints::REQUEST); - $actualUrl = Config::get('tanda.urls.base') . '/sandbox/io/v1/organizations/org/requests'; + $this->expectException(TandaException::class); - $this->assertSame($actualUrl, $testUrl); + Endpoints::build(Endpoints::REQUEST); } } diff --git a/tests/Library/UtilityTest.php b/tests/Library/UtilityTest.php index 09b7fed..9988ab9 100644 --- a/tests/Library/UtilityTest.php +++ b/tests/Library/UtilityTest.php @@ -63,7 +63,7 @@ function bill_payment_is_successful() new Response(200, ['Content_type' => 'application/json'], json_encode($this->mockResponses['request_pending']))); - $res = (new Utility($this->_client))->billPayment(765432100, 100, Providers::KPLC_POSTPAID, 765432100, null, false); + $res = (new Utility($this->_client))->billPayment(765432100, 100, Providers::KPLC_POSTPAID, null, false); $this->assertIsArray($res); $this->assertEquals('000001', $res['status']); @@ -80,7 +80,7 @@ function bill_payment_request_is_saved_to_db() json_encode($this->mockResponses['request_failed']))); // TODO: Make tests cover all providers and commands - $res = (new Utility($this->_client))->billPayment(765432100, 10, Providers::DSTV, 765432100); + $res = (new Utility($this->_client))->billPayment(765432100, 10, Providers::DSTV); $this->assertInstanceOf(TandaRequest::class, $res); $this->assertEquals(000002, $res->status); @@ -92,7 +92,7 @@ function bill_payment_fails_on_invalid_provider() { $this->expectException(TandaException::class); - (new Utility($this->_client))->billPayment(765432100, 10, Providers::SAFARICOM, 765432100); + (new Utility($this->_client))->billPayment(765432100, 10, Providers::SAFARICOM); } /** @test */ @@ -100,12 +100,12 @@ function bill_payment_fails_on_invalid_amount() { $this->expectException(TandaException::class); - (new Utility($this->_client))->billPayment(765432100, 10, Providers::KPLC_POSTPAID, 765432100, null, false); + (new Utility($this->_client))->billPayment(765432100, 10, Providers::KPLC_POSTPAID, null, false); } /** @test */ - function request_status() + function request_status_is_successful() { $this->mock->append( new Response(200, ['Content_type' => 'application/json'], diff --git a/tests/Unit/RequestStatusCommandTest.php b/tests/Unit/RequestStatusCommandTest.php new file mode 100644 index 0000000..a9dab33 --- /dev/null +++ b/tests/Unit/RequestStatusCommandTest.php @@ -0,0 +1,43 @@ +artisan('tanda:query_status') + ->expectsOutput('Nothing to query... all transactions seem to be ok.') + ->assertExitCode(0); + } + + /** @test */ + function the_command_echoes_failed_queries() + { + TandaRequest::create([ + 'request_id' => 'd33d079c-6bf2-430f-a1c9-d3cf45f8671a', + 'status' => 000001, + 'message' => 'Request received successfully.', + 'command_id' => Commands::AIRTIME_COMMAND, + 'provider' => Providers::SAFARICOM, + 'destination' => '234123', + 'amount' => 10, + 'last_modified' => Carbon::now(), + ]); + + $this->artisan('tanda:query_status') + ->expectsOutput('Failed queries: ') + ->assertExitCode(0); + } +} diff --git a/tests/Unit/TransactionStatusCommandTest.php b/tests/Unit/TransactionStatusCommandTest.php deleted file mode 100644 index dca2747..0000000 --- a/tests/Unit/TransactionStatusCommandTest.php +++ /dev/null @@ -1,58 +0,0 @@ -artisan('tanda:query_status') -// ->expectsOutput('Nothing to query... all transactions seem to be ok.') -// ->assertExitCode(0); -// } - -// TODO: Would require us to inject a modded Kyanda repo to runtime -// /** @test */ -// function the_command_echoes_successful_queries() -// { -// KyandaRequest::create([ -// 'status_code' => '0000', -// 'status' => 'Success', -// 'merchant_reference' => 'KYAAPI677834', -// 'message' => 'Your request has been posted successfully!' -// ]); -// -// $this->mock->append( -// new Response(200, ['Content_type' => 'application/json'], -// json_encode($this->mockResponses['query_transaction_status']))); -// -// $this->artisan('kyanda:query_status') -// ->expectsOutput('Successful queries: ') -// ->assertExitCode(0); -// } - -// /** @test */ -// function the_command_echoes_failed_queries() -// { -// TandaRequest::create([ -// 'status_code' => '0000', -// 'status' => 'Success', -// 'merchant_reference' => 'KYAAPI677834', -// 'provider' => Providers::SAFARICOM, -// 'message' => 'Your request has been posted successfully!' -// ]); -// -// $this->artisan('tanda:query_status') -// ->expectsOutput('Failed queries: ') -// ->assertExitCode(0); -// } -//}