diff --git a/collections/Appliances/Add appliance Rate.bru b/collections/Appliances/Add appliance Rate.bru new file mode 100644 index 000000000..9703379ba --- /dev/null +++ b/collections/Appliances/Add appliance Rate.bru @@ -0,0 +1,20 @@ +meta { + name: Add appliance Rate + type: http + seq: 1 +} + +post { + url: {{mpm_backend_url}}/api/appliances/payment/41 + body: json + auth: inherit +} + +body:json { + { + "person_id": 261, + "admin_id": null, + "amount": "500", + "payment_provider": 19 + } +} diff --git a/collections/Appliances/folder.bru b/collections/Appliances/folder.bru new file mode 100644 index 000000000..c01a8fa9c --- /dev/null +++ b/collections/Appliances/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Appliances + seq: 43 +} + +auth { + mode: inherit +} diff --git a/docs/integrations-guide/images/vodacom-mz-banner-logo.svg b/docs/integrations-guide/images/vodacom-mz-banner-logo.svg new file mode 100644 index 000000000..fa6bc6e16 --- /dev/null +++ b/docs/integrations-guide/images/vodacom-mz-banner-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/docs/integrations-guide/vodacom-mz.md b/docs/integrations-guide/vodacom-mz.md new file mode 100644 index 000000000..539c7c556 --- /dev/null +++ b/docs/integrations-guide/vodacom-mz.md @@ -0,0 +1,47 @@ +--- +order: 38 +--- + +

+ + Vodacom MZ + +

+ +# Vodacom MZ + +Vodacom MZ M-Pesa suppors two fundamentally different way to programmatically interact with transactions. + +MPM-initiated transactions + +M-Pesa Generic C2B API Integration (customer-initiated transactions) + +## M-Pesa OpenAPI Integration (MPM-initiated transactions) + +### Onboarding (OpenAPI) + +- Create an [Developer Portal](https://developer.mpesa.vm.co.mz/) account (this immediately gives access to Test environment) +- API Keys +- + +Finally, click `Request Validation` to initiate the process on generating live API keys. + +> [!INFO] +> This is a manual process on Vodacom side and can take varying amount of time. + +### Usage (OpenAPI) + +Because ... + +## M-Pesa Generic C2B API Integration (customer-initiated transactions) + +### Onboarding (Generic C2B API) + +Onboarding to use the Vodacom Generic C2B API is a custom process with certain security implications. +A full implementation might require the establishment of a VPN tunnel and certificate exchange. + +The exact steps are T.B.D. diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Console/Commands/InstallPackage.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Console/Commands/InstallPackage.php index cfa9faac2..60c0e5f67 100644 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/Console/Commands/InstallPackage.php +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Console/Commands/InstallPackage.php @@ -2,19 +2,24 @@ namespace App\Plugins\VodacomMzPaymentProvider\Console\Commands; +use App\Plugins\VodacomMzPaymentProvider\Services\VodacomMzCredentialService; use Illuminate\Console\Command; class InstallPackage extends Command { protected $signature = 'vodacom-mz-payment-provider:install'; protected $description = 'Install VodacomMzPaymentProvider Package'; - public function __construct() { + public function __construct( + private VodacomMzCredentialService $credentialService, + ) { parent::__construct(); } public function handle(): void { $this->info('Installing VodacomMzPaymentProvider Integration Package\n'); + $this->credentialService->getCredentials(); + $this->info('Package installed successfully..'); } } diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Exceptions/VodacomMzApiResponseException.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Exceptions/VodacomMzApiResponseException.php new file mode 100644 index 000000000..18059fc1b --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Exceptions/VodacomMzApiResponseException.php @@ -0,0 +1,5 @@ + + */ + public function c2bPayment( + string $transactionReference, + string $customerMsisdn, + float $amount, + string $thirdPartyReference, + ): array { + $credential = $this->credentialService->getCredentials(); + + return $this->send($credential, 'POST', 18352, '/ipg/v1x/c2bPayment/singleStage/', [ + 'input_TransactionReference' => $transactionReference, + 'input_CustomerMSISDN' => $customerMsisdn, + 'input_Amount' => (string) $amount, + 'input_ThirdPartyReference' => $thirdPartyReference, + 'input_ServiceProviderCode' => $credential->service_provider_code, + ]); + } + + /** + * @return array + */ + public function queryTransactionStatus(string $queryReference, string $thirdPartyReference): array { + $credential = $this->credentialService->getCredentials(); + + return $this->send($credential, 'GET', 18353, '/ipg/v1x/queryTransactionStatus/', [ + 'input_QueryReference' => $queryReference, + 'input_ThirdPartyReference' => $thirdPartyReference, + 'input_ServiceProviderCode' => $credential->service_provider_code, + ]); + } + + /** + * @param array $params + * + * @return array + */ + private function send(VodacomMzCredential $credential, string $method, int $port, string $path, array $params): array { + $url = $credential->buildUri($port, $path); + $payloadKey = $method === 'GET' ? 'query' : 'json'; + + try { + // IPG returns business failures (e.g. INS-2006 insufficient balance) as a 4xx + // with the detail in the body, so http_errors stays off and the decoded body + // is returned as-is for the caller to inspect via `output_ResponseCode`. + $response = $this->httpClient->request($method, $url, [ + $payloadKey => $params, + 'headers' => $this->headers($credential), + 'http_errors' => false, + ]); + } catch (GuzzleException $exception) { + Log::critical('Vodacom MZ API request failed', [ + 'url' => $url, + 'message' => $exception->getMessage(), + ]); + throw new VodacomMzApiResponseException($exception->getMessage()); + } + + return json_decode((string) $response->getBody(), true) ?? []; + } + + /** + * @return array + */ + private function headers(VodacomMzCredential $credential): array { + return [ + 'Authorization' => 'Bearer '.$credential->getBearerToken(), + 'Origin' => '*', + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzCredentialController.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzCredentialController.php new file mode 100644 index 000000000..f2efa0df3 --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzCredentialController.php @@ -0,0 +1,29 @@ +credentialService->getCredentials()); + } + + public function update(VodacomMzCredentialRequest $request): VodacomMzCredentialResource { + $credentials = $this->credentialService->updateCredentials([ + 'api_key' => (string) $request->string('api_key'), + 'public_key' => (string) $request->string('public_key'), + 'service_provider_code' => (string) $request->string('service_provider_code'), + 'live' => $request->boolean('live'), + ]); + + return VodacomMzCredentialResource::make($credentials); + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomTransactionController.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzTransactionController.php similarity index 76% rename from src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomTransactionController.php rename to src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzTransactionController.php index b0d698b94..adaede33d 100644 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomTransactionController.php +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Controllers/VodacomMzTransactionController.php @@ -7,7 +7,7 @@ use App\Plugins\VodacomMzPaymentProvider\Http\Requests\VodacomTransactionProcessRequest; use App\Plugins\VodacomMzPaymentProvider\Http\Requests\VodacomTransactionValidationRequest; use App\Plugins\VodacomMzPaymentProvider\Http\Resources\VodacomTransactionResource; -use App\Plugins\VodacomMzPaymentProvider\Services\VodacomTransactionService; +use App\Plugins\VodacomMzPaymentProvider\Services\VodacomMzTransactionService; use Dedoc\Scramble\Attributes\Group; /** @@ -16,37 +16,16 @@ * API endpoints for integrating with Vodacom's M-Pesa payment services */ #[Group('Plugins / Vodacom Mz')] -class VodacomTransactionController extends Controller { - public function __construct(private VodacomTransactionService $vodacomService) {} +class VodacomMzTransactionController extends Controller { + public function __construct( + private VodacomMzTransactionService $vodacomService, + ) {} /** * Validate Transaction. * * Validates a transaction before processing. Use this endpoint to verify if a transaction * can proceed based on the provided information. This is typically the first step in the payment flow. - * - * @bodyParam serialNumber string required Unique identifier for the product/service being purchased pattern: ^[A-Z0-9]{8,12}$ Example: ABC123456789 - * @bodyParam amount number required Transaction amount in the local currency Example: 15000 - * @bodyParam payerPhoneNumber string required Customer's phone number in international format pattern: ^258[0-9]{9}$ Example: 258712345678 - * @bodyParam referenceId string required Unique reference identifier for this transaction pattern: ^[A-Za-z0-9\-]{5,20}$ Example: ORD-12345-ABC - * - * @response scenario="Success" { - * "data": { - * "transactionId": "VOD-TXN-123456", - * "status": "validated", - * "details": { - * "product": "Internet Bundle", - * "validAmount": true - * }, - * "success": true - * } - * } - * @response 400 scenario="Validation Error" { - * "data": { - * "message": "Invalid amount specified for this product", - * "success": false - * } - * } */ public function validateTransaction(VodacomTransactionValidationRequest $request): VodacomTransactionResource { $validatedData = $request->validated(); diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomMzCredentialRequest.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomMzCredentialRequest.php new file mode 100644 index 000000000..3b431a0d0 --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomMzCredentialRequest.php @@ -0,0 +1,21 @@ + + */ + public function rules(): array { + return [ + 'api_key' => ['required', 'string'], + 'public_key' => ['required', 'string'], + 'service_provider_code' => ['required', 'string'], + 'live' => ['required', 'boolean'], + ]; + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomTransactionValidationRequest.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomTransactionValidationRequest.php index 180d37774..9c82ef876 100644 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomTransactionValidationRequest.php +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Requests/VodacomTransactionValidationRequest.php @@ -14,10 +14,11 @@ public function authorize(): bool { */ public function rules(): array { return [ - 'serialNumber' => ['required', 'string', 'regex:/^[A-Z0-9]{8,12}$/'], - 'amount' => ['required', 'numeric', 'min:100', 'max:5000000'], - 'payerPhoneNumber' => ['required', 'string', 'regex:/^258[0-9]{9}$/'], - 'referenceId' => ['required', 'string', 'regex:/^[A-Za-z0-9\-]{5,20}$/'], + // `reference` has to match an existing Device's serial number else the transaction gets rejected. + 'reference' => ['required', 'string', 'regex:/^[A-Z0-9]{8,12}$/'], + // Transaction `amount` in MZN (Mozambican metical) + 'amount' => ['required', 'numeric', 'min:1', 'max:5000000'], + 'request_id' => ['required', 'uuid'], ]; } } diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Resources/VodacomMzCredentialResource.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Resources/VodacomMzCredentialResource.php new file mode 100644 index 000000000..e59ae865a --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Http/Resources/VodacomMzCredentialResource.php @@ -0,0 +1,7 @@ + */ public function toArray(Request $request): array { + // return [ + // 'data' => array_merge(parent::toArray($request), ['success' => true]), + // ]; + return [ - 'data' => array_merge(parent::toArray($request), ['success' => true]), + 'response_code' => 'asdf', + 'response_desc' => $this->title, + 'simulation_id' => $this->id, + 'payment_details' => [ + 'beneficiary' => 'user_id', + 'chargedType' => 'user_id', + 'chargedAmount' => 'user_id', + ], + + // 'author' => UserResource::make($this->whenLoaded('author')), ]; } diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzCredential.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzCredential.php new file mode 100644 index 000000000..01e2d3cd9 --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzCredential.php @@ -0,0 +1,62 @@ + */ + protected $casts = [ + 'live' => 'boolean', + ]; + + /** + * Build a full IPG endpoint URL. Only the host changes between environments; the + * ports observed so far are identical for sandbox and live. + */ + public function buildUri(int $port, string $path): string { + $host = $this->live ? self::HOST_LIVE : self::HOST_SANDBOX; + + return 'https://'.$host.':'.$port.$path; + } + + /** + * Build the IPG Authorization bearer: the API key RSA-encrypted with the provider + * public key (PKCS#1 v1.5) and base64-encoded. The public key is stored as bare + * base64, so it is wrapped in a PEM envelope before use. + */ + public function getBearerToken(): string { + $pemPublicKey = "-----BEGIN PUBLIC KEY-----\n" + .chunk_split((string) $this->public_key, 64, "\n") + ."-----END PUBLIC KEY-----\n"; + + $publicKeyResource = openssl_pkey_get_public($pemPublicKey); + + if ($publicKeyResource === false) { + throw new VodacomMzApiResponseException('Invalid Vodacom MZ public key'); + } + + $encrypted = ''; + if (!openssl_public_encrypt((string) $this->api_key, $encrypted, $publicKeyResource, OPENSSL_PKCS1_PADDING)) { + throw new VodacomMzApiResponseException('Failed to encrypt Vodacom MZ API key'); + } + + return base64_encode($encrypted); + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzTransaction.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzTransaction.php index 512b28f0e..7a2180680 100644 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzTransaction.php +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Models/VodacomMzTransaction.php @@ -17,4 +17,10 @@ */ class VodacomMzTransaction extends BaseModel { protected $table = 'vodacom_mz_transactions'; + + public const STATUS_REQUESTED = 0; + public const STATUS_FAILED = -1; + public const STATUS_SUCCESS = 1; + public const STATUS_COMPLETED = 2; + public const STATUS_ABANDONED = 3; } diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzCredentialService.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzCredentialService.php new file mode 100644 index 000000000..25952a7de --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzCredentialService.php @@ -0,0 +1,49 @@ +credential->newQuery()->firstOrCreate([], [ + 'api_key' => null, + 'public_key' => null, + 'service_provider_code' => null, + 'live' => false, + ]); + + return $this->decryptCredentialFields($credential, self::ENCRYPTED_FIELDS); + } + + /** + * @param array{ + * api_key?: string|null, + * public_key?: string|null, + * service_provider_code?: string|null, + * live?: bool + * } $data + */ + public function updateCredentials(array $data): VodacomMzCredential { + $credential = $this->credential->newQuery()->firstOrCreate([]); + + $encryptedData = $this->encryptCredentialFields($data, self::ENCRYPTED_FIELDS); + + $credential->update($encryptedData); + $credential->save(); + + $credential->fresh(); + + return $this->decryptCredentialFields($credential, self::ENCRYPTED_FIELDS); + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzTransactionService.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzTransactionService.php new file mode 100644 index 000000000..519166c9e --- /dev/null +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomMzTransactionService.php @@ -0,0 +1,83 @@ +meter, + $this->address, + $this->transaction, + $this->vodacomMzTransaction + ); + } + + /** + * Validate a transaction request. + * + * @param array $data + * + * @return array + */ + public function validateTransaction(array $data): array { + return []; + } + + /** + * Process a transaction. + * + * @param array $data + * + * @return array + */ + public function processTransaction(array $data): array { + return []; + } + + /** + * Check the status of a transaction. + * + * @param array $data + * + * @return array + */ + public function transactionEnquiryStatus(array $data): array { + // $referenceId = $data['referenceId']; + $statuses = ['Failed', 'Succeed', 'Pending']; + $randomStatus = $statuses[array_rand($statuses)]; + + return [ + 'status' => [$randomStatus], + ]; + } + + public function initializePayment( + float $amount, + string $sender, + string $message, + string $type, + int $customerId, + ?string $serialId = null, + ): array { + // dd('Here we are!'); + return [ + // 'transaction' => $transaction, + // 'provider_data' => [ + // 'redirect_url' => $result['redirectionUrl'], + // 'reference' => $result['reference'], + // ], + ]; + } +} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomTransactionService.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomTransactionService.php deleted file mode 100644 index 9d70f3f2a..000000000 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/Services/VodacomTransactionService.php +++ /dev/null @@ -1,44 +0,0 @@ - $data - * - * @return array - */ - public function validateTransaction(array $data): array { - return []; - } - - /** - * Process a transaction. - * - * @param array $data - * - * @return array - */ - public function processTransaction(array $data): array { - return []; - } - - /** - * Check the status of a transaction. - * - * @param array $data - * - * @return array - */ - public function transactionEnquiryStatus(array $data): array { - // $referenceId = $data['referenceId']; - $statuses = ['Failed', 'Succeed', 'Pending']; - $randomStatus = $statuses[array_rand($statuses)]; - - return [ - 'status' => [$randomStatus], - ]; - } -} diff --git a/src/backend/app/Plugins/VodacomMzPaymentProvider/routes/api.php b/src/backend/app/Plugins/VodacomMzPaymentProvider/routes/api.php index 3fd49fe56..b481bf5b9 100644 --- a/src/backend/app/Plugins/VodacomMzPaymentProvider/routes/api.php +++ b/src/backend/app/Plugins/VodacomMzPaymentProvider/routes/api.php @@ -1,14 +1,20 @@ group(function () { + Route::prefix('credentials')->group(function () { + Route::get('/', [VodacomMzCredentialController::class, 'show']); + Route::put('/', [VodacomMzCredentialController::class, 'update']); + }); + Route::prefix('transactions') ->middleware('auth:api-key') ->group(function () { - Route::post('/validate', [VodacomTransactionController::class, 'validateTransaction']); - Route::post('/process', [VodacomTransactionController::class, 'processTransaction']); - Route::post('/query', [VodacomTransactionController::class, 'queryTransactionStatus']); + Route::post('/validate', [VodacomMzTransactionController::class, 'validateTransaction']); + Route::post('/process', [VodacomMzTransactionController::class, 'processTransaction']); + Route::post('/query', [VodacomMzTransactionController::class, 'queryTransactionStatus']); }); }); diff --git a/src/backend/app/Services/AbstractPaymentAggregatorTransactionService.php b/src/backend/app/Services/AbstractPaymentAggregatorTransactionService.php index 3570c7060..4ee87258d 100644 --- a/src/backend/app/Services/AbstractPaymentAggregatorTransactionService.php +++ b/src/backend/app/Services/AbstractPaymentAggregatorTransactionService.php @@ -14,6 +14,7 @@ use App\Plugins\SmsTransactionParser\Models\SmsTransaction; use App\Plugins\SteamaMeter\Exceptions\ModelNotFoundException; use App\Plugins\SwiftaPaymentProvider\Models\SwiftaTransaction; +use App\Plugins\VodacomMzPaymentProvider\Models\VodacomMzTransaction; use App\Plugins\WavecomPaymentProvider\Models\WaveComTransaction; use App\Plugins\WaveMoneyPaymentProvider\Models\WaveMoneyTransaction; use App\Utils\MinimumPurchaseAmountValidator; @@ -30,7 +31,7 @@ public function __construct( private Meter $meter, private Address $address, private Transaction $transaction, - private SwiftaTransaction|WaveMoneyTransaction|WaveComTransaction|PaystackTransaction|PesapalTransaction|SmsTransaction $paymentAggregatorTransaction, + private SwiftaTransaction|WaveMoneyTransaction|WaveComTransaction|PaystackTransaction|PesapalTransaction|SmsTransaction|VodacomMzTransaction $paymentAggregatorTransaction, ) {} public function validatePaymentOwner(string $meterSerialNumber, float $amount): void { diff --git a/src/backend/app/Services/ApiResolvers/Data/ApiResolverMap.php b/src/backend/app/Services/ApiResolvers/Data/ApiResolverMap.php index 7a31bf246..2c0a27b2b 100644 --- a/src/backend/app/Services/ApiResolvers/Data/ApiResolverMap.php +++ b/src/backend/app/Services/ApiResolvers/Data/ApiResolverMap.php @@ -29,7 +29,7 @@ class ApiResolverMap { public const DATA_EXPORTING_API = 'api/export'; public const ODYSSEY_PAYMENTS_API = 'api/odyssey'; public const AFRICAS_TALKING_API = 'api/africas-talking/callback'; - public const VODACOM_MZ_PAYMENT_PROVIDER = 'api/vodacom_mz/'; + public const VODACOM_MZ_PAYMENT_PROVIDER = 'api/vodacom_mz/transactions/'; public const PAYSTACK_API = 'api/paystack/'; public const PESAPAL_API = 'api/pesapal/'; public const ECREEE_METER_DATA_API = 'api/ecreee-e-tender/ecreee-meter-data'; diff --git a/src/backend/app/Services/PaymentInitializationService.php b/src/backend/app/Services/PaymentInitializationService.php index 8a43dcdb2..a3ecbff6f 100644 --- a/src/backend/app/Services/PaymentInitializationService.php +++ b/src/backend/app/Services/PaymentInitializationService.php @@ -9,6 +9,7 @@ use App\Models\Transaction\Transaction; use App\Plugins\PaystackPaymentProvider\Services\PaystackTransactionService; use App\Plugins\PesapalPaymentProvider\Services\PesapalTransactionService; +use App\Plugins\VodacomMzPaymentProvider\Services\VodacomMzTransactionService; use App\Services\Interfaces\PaymentInitializer; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Collection; @@ -22,6 +23,7 @@ class PaymentInitializationService { */ private const PROVIDER_MAP = [ 0 => CashTransactionService::class, + MpmPlugin::VODACOM_MZ_PAYMENT_PROVIDER => VodacomMzTransactionService::class, MpmPlugin::PAYSTACK_PAYMENT_PROVIDER => PaystackTransactionService::class, MpmPlugin::PESAPAL_PAYMENT_PROVIDER => PesapalTransactionService::class, ]; diff --git a/src/backend/database/migrations/tenant/2026_06_09_000000_create_vodacom_mz_credentials.php b/src/backend/database/migrations/tenant/2026_06_09_000000_create_vodacom_mz_credentials.php new file mode 100644 index 000000000..7c045a70e --- /dev/null +++ b/src/backend/database/migrations/tenant/2026_06_09_000000_create_vodacom_mz_credentials.php @@ -0,0 +1,28 @@ +create('vodacom_mz_credentials', function (Blueprint $table) { + $table->id(); + $table->text('api_key')->nullable(); + $table->text('public_key')->nullable(); + $table->string('service_provider_code')->nullable(); + $table->boolean('live')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::connection('tenant')->dropIfExists('vodacom_mz_credentials'); + } +}; diff --git a/src/frontend/src/ExportedRoutes.js b/src/frontend/src/ExportedRoutes.js index ada4c0530..04c0fa539 100644 --- a/src/frontend/src/ExportedRoutes.js +++ b/src/frontend/src/ExportedRoutes.js @@ -92,6 +92,7 @@ import SparkMeterSiteList from "@/plugins/spark-meter/modules/Site/SiteList.vue" import SparkMeterTariffDetail from "@/plugins/spark-meter/modules/Tariff/TariffDetail.vue" import SparkMeterTariffList from "@/plugins/spark-meter/modules/Tariff/TariffList.vue" import SparkShsOverview from "@/plugins/spark-shs/modules/Overview/Overview.vue" +import VodacomMzOverview from "@/plugins/vodacom-mz-payment-provider/modules/Overview/Overview.vue" import SteamaCoAgentList from "@/plugins/steama-meter/modules/Agent/AgentList.vue" import SteamaCoCustomerList from "@/plugins/steama-meter/modules/Customer/CustomerList.vue" import SteamaCoCustomerDetail from "@/plugins/steama-meter/modules/Customer/CustomerMovements.vue" @@ -1801,6 +1802,30 @@ export const exportedRoutes = [ }, ], }, + { + path: "/vodacom-mz", + component: ChildRouteWrapper, + meta: { + sidebar: { + enabled_by_mpm_plugin_id: 19, + name: "Vodacom MZ", + icon: "payments", + }, + }, + children: [ + { + path: "overview", + component: VodacomMzOverview, + meta: { + layout: "default", + sidebar: { + enabled: true, + name: "Overview", + }, + }, + }, + ], + }, { path: "/sms-transaction-parser", component: ChildRouteWrapper, diff --git a/src/frontend/src/main.js b/src/frontend/src/main.js index 61ba8f399..501f3dca6 100644 --- a/src/frontend/src/main.js +++ b/src/frontend/src/main.js @@ -45,6 +45,7 @@ import Prospect from "@/plugins/prospect/modules/Overview/Credential.vue" import SmsTransactionParserSetup from "@/plugins/sms-transaction-parser/modules/Overview/Setup.vue" import Spark from "@/plugins/spark-meter/modules/Overview/Credential.vue" import SparkShs from "@/plugins/spark-shs/modules/Overview/Credential.vue" +import VodacomMzPaymentProvider from "@/plugins/vodacom-mz-payment-provider/modules/Overview/Credential.vue" import Steamaco from "@/plugins/steama-meter/modules/Overview/Credential.vue" import Stron from "@/plugins/stron-meter/modules/Overview/Credential.vue" import SunKing from "@/plugins/sun-king-shs/modules/Overview/Credential.vue" @@ -92,6 +93,7 @@ Vue.component("Pesapal", Pesapal) Vue.component("TextbeeSmsGateway", TextbeeSmsGateway) Vue.component("SparkShs", SparkShs) Vue.component("SmsTransactionParser", SmsTransactionParserSetup) +Vue.component("VodacomMzPaymentProvider", VodacomMzPaymentProvider) // NEW PLUGIN PLACEHOLDER (DO NOT REMOVE THIS LINE) const toArray = (value) => { diff --git a/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Credential.vue b/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Credential.vue new file mode 100644 index 000000000..8f7fdcbce --- /dev/null +++ b/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Credential.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Overview.vue b/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Overview.vue new file mode 100644 index 000000000..6318e016b --- /dev/null +++ b/src/frontend/src/plugins/vodacom-mz-payment-provider/modules/Overview/Overview.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/src/frontend/src/plugins/vodacom-mz-payment-provider/repositories/CredentialRepository.js b/src/frontend/src/plugins/vodacom-mz-payment-provider/repositories/CredentialRepository.js new file mode 100644 index 000000000..12ef7d448 --- /dev/null +++ b/src/frontend/src/plugins/vodacom-mz-payment-provider/repositories/CredentialRepository.js @@ -0,0 +1,12 @@ +import Client from "@/repositories/Client/AxiosClient.js" + +const resource = `/api/vodacom_mz/credentials` + +export default { + get() { + return Client.get(`${resource}`) + }, + put(credentials) { + return Client.put(`${resource}`, credentials) + }, +} diff --git a/src/frontend/src/plugins/vodacom-mz-payment-provider/services/CredentialService.js b/src/frontend/src/plugins/vodacom-mz-payment-provider/services/CredentialService.js new file mode 100644 index 000000000..e009a3fa3 --- /dev/null +++ b/src/frontend/src/plugins/vodacom-mz-payment-provider/services/CredentialService.js @@ -0,0 +1,56 @@ +import CredentialRepository from "../repositories/CredentialRepository.js" + +import { ErrorHandler } from "@/Helpers/ErrorHandler.js" + +export class CredentialService { + constructor() { + this.repository = CredentialRepository + this.credential = { + apiKey: null, + publicKey: null, + serviceProviderCode: null, + live: false, + } + } + fromJson(credentialData) { + this.credential = { + apiKey: credentialData.api_key, + publicKey: credentialData.public_key, + serviceProviderCode: credentialData.service_provider_code, + live: credentialData.live, + } + return this.credential + } + async getCredential() { + try { + let response = await this.repository.get() + if (response.status === 200) { + return this.fromJson(response.data.data) + } else { + return new ErrorHandler(response.error, "http", response.status) + } + } catch (e) { + let errorMessage = e.response.data.message + return new ErrorHandler(errorMessage, "http") + } + } + async updateCredential() { + try { + let credentialPayload = { + api_key: this.credential.apiKey, + public_key: this.credential.publicKey, + service_provider_code: this.credential.serviceProviderCode, + live: this.credential.live, + } + let response = await this.repository.put(credentialPayload) + if (response.status === 200 || response.status === 201) { + return this.fromJson(response.data.data) + } else { + return new ErrorHandler(response.error, "http", response.status) + } + } catch (e) { + let errorMessage = e.response.data.message + return new ErrorHandler(errorMessage, "http") + } + } +}