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 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")
+ }
+ }
+}