From b9e1ff67f317fc42e7f34963f6230edc5dbfde02 Mon Sep 17 00:00:00 2001
From: Obinna Ikeh
Date: Wed, 18 Mar 2026 14:07:57 +0100
Subject: [PATCH 1/5] Refactor: Generialize payment initiation
This changes attempts to consolidate Payment initiation in on central location PaymentInitialization. It also introduces another payment method with sold appliance.
---
docs/usage-guide/images/payment-flow.svg | 4 +
.../AppliancePaymentController.php | 53 +++--
.../Controllers/AppliancePersonController.php | 57 ++++--
src/backend/app/Models/Plugins.php | 6 +
.../Http/Controllers/PaystackController.php | 27 ++-
.../Controllers/PaystackPublicController.php | 41 ++--
.../Services/PaystackTransactionService.php | 67 +++++++
.../Services/PaystackWebhookService.php | 19 +-
.../Tests/Unit/PaystackTransactionTest.php | 3 -
.../PaystackPaymentProvider/routes/api.php | 2 +-
.../app/Services/CashTransactionService.php | 21 ++
.../Services/PaymentInitializationService.php | 97 +++++++++
src/backend/app/Services/PluginsService.php | 19 ++
.../Services/TransactionPaymentProcessor.php | 6 +
src/backend/routes/api.php | 1 +
.../AppliancePaymentControllerTest.php | 187 ++++++++++++++++++
...pliancePersonControllerDownPaymentTest.php | 127 ++++++++++++
.../Unit/PaymentInitializationServiceTest.php | 169 ++++++++++++++++
...ransactionServiceInitializePaymentTest.php | 98 +++++++++
.../Client/Appliances/SellApplianceModal.vue | 38 +++-
.../Client/Appliances/SoldApplianceDetail.vue | 33 +++-
.../modules/Overview/Credential.vue | 78 --------
.../modules/Payment/PublicPaymentForm.vue | 2 +-
.../modules/Payment/PublicPaymentResult.vue | 34 +++-
.../AppliancePaymentRepository.js | 3 +
.../src/services/AppliancePaymentService.js | 12 ++
26 files changed, 1036 insertions(+), 168 deletions(-)
create mode 100644 docs/usage-guide/images/payment-flow.svg
create mode 100644 src/backend/app/Services/PaymentInitializationService.php
create mode 100644 src/backend/tests/Feature/AppliancePaymentControllerTest.php
create mode 100644 src/backend/tests/Feature/AppliancePersonControllerDownPaymentTest.php
create mode 100644 src/backend/tests/Unit/PaymentInitializationServiceTest.php
create mode 100644 src/backend/tests/Unit/PaystackTransactionServiceInitializePaymentTest.php
diff --git a/docs/usage-guide/images/payment-flow.svg b/docs/usage-guide/images/payment-flow.svg
new file mode 100644
index 000000000..42035812b
--- /dev/null
+++ b/docs/usage-guide/images/payment-flow.svg
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/src/backend/app/Http/Controllers/AppliancePaymentController.php b/src/backend/app/Http/Controllers/AppliancePaymentController.php
index ba183ca28..984cf47a7 100644
--- a/src/backend/app/Http/Controllers/AppliancePaymentController.php
+++ b/src/backend/app/Http/Controllers/AppliancePaymentController.php
@@ -7,15 +7,17 @@
use App\Models\AppliancePerson;
use App\Services\AppliancePaymentService;
use App\Services\AppliancePersonService;
-use App\Services\CashTransactionService;
+use App\Services\PaymentInitializationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AppliancePaymentController extends Controller {
+ public const CASH_TRANSACTION_PROVIVER = 0;
+
public function __construct(
private AppliancePaymentService $appliancePaymentService,
private AppliancePersonService $appliancePersonService,
- private CashTransactionService $cashTransactionService,
+ private PaymentInitializationService $paymentInitializationService,
) {}
public function store(AppliancePerson $appliancePerson, Request $request): ApiResource {
@@ -24,10 +26,13 @@ public function store(AppliancePerson $appliancePerson, Request $request): ApiRe
$result = $this->getPaymentForAppliance($request, $appliancePerson);
DB::connection('tenant')->commit();
- return ApiResource::make([
- 'appliance_person' => $result['appliance_person'],
- 'transaction_id' => $result['transaction_id'],
- ]);
+ return ApiResource::make(array_merge(
+ [
+ 'appliance_person' => $result['appliance_person'],
+ 'transaction_id' => $result['transaction_id'],
+ ],
+ $result['provider_data'],
+ ));
} catch (\Exception $e) {
DB::connection('tenant')->rollBack();
throw new \Exception($e->getMessage(), $e->getCode(), $e);
@@ -40,32 +45,54 @@ public function checkStatus(int $transactionId): ApiResource {
return ApiResource::make($status);
}
+ public function paymentProviders(): ApiResource {
+ $providers = $this->paymentInitializationService->paymentProviders();
+
+ return ApiResource::make($providers);
+ }
+
/**
* @return array
*/
- public function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): array {
+ private function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): array {
$creatorId = auth('api')->user()->id;
$amount = (float) $request->input('amount');
+ $providerId = (int) $request->input('payment_provider', 0);
+ $companyId = $request->attributes->get('companyId');
+
$applianceDetail = $this->appliancePersonService->getApplianceDetails($appliancePerson->id);
$this->appliancePaymentService->validateAmount($applianceDetail, $amount);
$deviceSerial = $applianceDetail->device_serial;
$applianceOwner = $appliancePerson->person;
- $companyId = $request->attributes->get('companyId');
if (!$applianceOwner) {
throw new \InvalidArgumentException('Appliance owner not found');
}
$ownerAddress = $applianceOwner->addresses()->where('is_primary', 1)->first();
- $sender = $ownerAddress == null ? '-' : $ownerAddress->phone;
- $transaction =
- $this->cashTransactionService->createCashTransaction($creatorId, $amount, $sender, $deviceSerial, $appliancePerson->id);
+ $sender = $ownerAddress === null ? '-' : $ownerAddress->phone;
+
+ $message = $deviceSerial ?? (string) $appliancePerson->id;
- dispatch(new ProcessPayment($companyId, $transaction->id));
+ $result = $this->paymentInitializationService->initialize(
+ providerId: $providerId,
+ amount: $amount,
+ sender: $sender,
+ message: $message,
+ type: 'deferred_payment',
+ customerId: $applianceOwner->id,
+ creatorId: $creatorId,
+ serialId: $deviceSerial,
+ );
+
+ if ($providerId === $this::CASH_TRANSACTION_PROVIVER) {
+ dispatch(new ProcessPayment($companyId, $result['transaction']->id));
+ }
return [
'appliance_person' => $appliancePerson,
- 'transaction_id' => $transaction->id,
+ 'transaction_id' => $result['transaction']->id,
+ 'provider_data' => $result['provider_data'],
];
}
}
diff --git a/src/backend/app/Http/Controllers/AppliancePersonController.php b/src/backend/app/Http/Controllers/AppliancePersonController.php
index 97a537d8c..23f56359d 100644
--- a/src/backend/app/Http/Controllers/AppliancePersonController.php
+++ b/src/backend/app/Http/Controllers/AppliancePersonController.php
@@ -5,6 +5,7 @@
use App\Events\PaymentSuccessEvent;
use App\Events\TransactionSuccessfulEvent;
use App\Http\Resources\ApiResource;
+use App\Jobs\ProcessPayment;
use App\Models\Appliance;
use App\Models\AppliancePerson;
use App\Models\Person\Person;
@@ -13,15 +14,17 @@
use App\Services\AppliancePersonService;
use App\Services\ApplianceRateService;
use App\Services\ApplianceService;
-use App\Services\CashTransactionService;
use App\Services\DeviceService;
use App\Services\GeographicalInformationService;
+use App\Services\PaymentInitializationService;
use App\Services\UserAppliancePersonService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AppliancePersonController extends Controller {
+ public const CASH_TRANSACTION_PROVIVER = 0;
+
public function __construct(
private AppliancePerson $appliancePerson,
private AppliancePersonService $appliancePersonService,
@@ -31,7 +34,7 @@ public function __construct(
private AddressesService $addressesService,
private GeographicalInformationService $geographicalInformationService,
private AddressGeographicalInformationService $addressGeographicalInformationService,
- private CashTransactionService $cashTransactionService,
+ private PaymentInitializationService $paymentInitializationService,
private ApplianceService $applianceService,
private ApplianceRateService $applianceRateService,
) {}
@@ -98,29 +101,45 @@ public function store(
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geographicalInformation);
}
+ $downPaymentInitData = [];
if ($downPayment > 0) {
+ $paymentProviderId = (int) $request->input('payment_provider', 0);
+ $companyId = $request->attributes->get('companyId');
$sender = isset($addressData) ? $addressData['phone'] : '-';
- $transaction = $this->cashTransactionService->createCashTransaction(
- $user->id,
- $downPayment,
- $sender,
- $deviceSerial
+ $message = $deviceSerial ?? (string) $appliancePerson->id;
+ $person = $appliancePerson->person;
+
+ $result = $this->paymentInitializationService->initialize(
+ providerId: $paymentProviderId,
+ amount: $downPayment,
+ sender: $sender,
+ message: $message,
+ type: 'deferred_payment',
+ customerId: $person->id,
+ creatorId: $user->id,
+ serialId: $deviceSerial ?? null,
);
- $applianceRate = $this->applianceRateService->getDownPaymentAsApplianceRate($appliancePerson);
- event(new PaymentSuccessEvent(
- amount: (int) $transaction->amount,
- paymentService: 'web',
- paymentType: 'down payment',
- sender: $transaction->sender,
- paidFor: $applianceRate,
- payer: $appliancePerson->person,
- transaction: $transaction,
- ));
- event(new TransactionSuccessfulEvent($transaction));
+
+ if ($paymentProviderId === $this::CASH_TRANSACTION_PROVIVER) {
+ $applianceRate = $this->applianceRateService->getDownPaymentAsApplianceRate($appliancePerson);
+ event(new PaymentSuccessEvent(
+ amount: (int) $result['transaction']->amount,
+ paymentService: 'web',
+ paymentType: 'down payment',
+ sender: $result['transaction']->sender,
+ paidFor: $applianceRate,
+ payer: $appliancePerson->person,
+ transaction: $result['transaction'],
+ ));
+ event(new TransactionSuccessfulEvent($result['transaction']));
+ } else {
+ dispatch(new ProcessPayment($companyId, $result['transaction']->id));
+ $downPaymentInitData = $result['provider_data'];
+ }
}
DB::connection('tenant')->commit();
- return ApiResource::make($appliancePerson);
+ return ApiResource::make(array_merge(['appliance_person' => $appliancePerson], $downPaymentInitData));
} catch (\Exception $e) {
DB::connection('tenant')->rollBack();
throw new \Exception($e->getMessage(), (int) $e->getCode(), $e);
diff --git a/src/backend/app/Models/Plugins.php b/src/backend/app/Models/Plugins.php
index 4e96895b7..fb7598741 100644
--- a/src/backend/app/Models/Plugins.php
+++ b/src/backend/app/Models/Plugins.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Models\Base\BaseModel;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
@@ -15,4 +16,9 @@
class Plugins extends BaseModel {
public const ACTIVE = 1;
public const INACTIVE = 0;
+
+ /** @return BelongsTo */
+ public function mpmPlugin(): BelongsTo {
+ return $this->belongsTo(MpmPlugin::class);
+ }
}
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackController.php b/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackController.php
index e8c1b369a..41941b3d0 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackController.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackController.php
@@ -5,7 +5,6 @@
namespace App\Plugins\PaystackPaymentProvider\Http\Controllers;
use App\Plugins\PaystackPaymentProvider\Http\Requests\TransactionInitializeRequest;
-use App\Plugins\PaystackPaymentProvider\Http\Resources\PaystackResource;
use App\Plugins\PaystackPaymentProvider\Http\Resources\PaystackTransactionResource;
use App\Plugins\PaystackPaymentProvider\Models\PaystackTransaction;
use App\Plugins\PaystackPaymentProvider\Modules\Api\PaystackApiService;
@@ -23,10 +22,28 @@ public function __construct(
private PaystackWebhookService $webhookService,
) {}
- public function startTransaction(TransactionInitializeRequest $request): PaystackResource {
- $transaction = $request->getPaystackTransaction();
-
- return PaystackResource::make($this->apiService->initializeTransaction($transaction));
+ public function initializeTransaction(TransactionInitializeRequest $request): JsonResponse {
+ $customerId = (int) $request->input('customer_id');
+ $serialId = $request->input('device_serial');
+ $amount = (float) $request->input('amount');
+ $sender = $this->transactionService->getCustomerPhoneByCustomerId($customerId) ?? '';
+
+ $result = $this->transactionService->initializePayment(
+ amount: $amount,
+ sender: $sender,
+ message: $serialId,
+ type: 'energy',
+ customerId: $customerId,
+ serialId: $serialId,
+ );
+
+ return response()->json([
+ 'data' => [
+ 'redirectionUrl' => $result['provider_data']['redirect_url'],
+ 'reference' => $result['provider_data']['reference'],
+ 'error' => null,
+ ],
+ ]);
}
/**
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackPublicController.php b/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackPublicController.php
index 69041f8bb..18dc948a9 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackPublicController.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Http/Controllers/PaystackPublicController.php
@@ -15,7 +15,6 @@
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Log;
-use Ramsey\Uuid\Uuid;
class PaystackPublicController extends Controller {
public function __construct(
@@ -66,12 +65,7 @@ public function initiatePayment(PublicPaymentRequest $request, string $companyHa
return response()->json(['error' => 'Invalid company identifier'], 400);
}
- // Validate device serial and amount
$validatedData = $request->validated();
-
- // Get agent_id from query parameters if present
- $agentId = $request->query('agent');
-
$deviceType = $validatedData['device_type'] ?? 'meter';
$deviceSerial = $validatedData['device_serial'];
@@ -82,30 +76,26 @@ public function initiatePayment(PublicPaymentRequest $request, string $companyHa
$customerId = $this->transactionService->getCustomerIdByMeterSerial($deviceSerial);
}
- // Create Paystack transaction
- $transaction = $this->transactionService->createPublicTransaction([
- 'amount' => $validatedData['amount'],
- 'currency' => $validatedData['currency'],
- 'serial_id' => $deviceSerial,
- 'device_type' => $deviceType,
- 'customer_id' => $customerId,
- 'order_id' => Uuid::uuid4()->toString(),
- 'reference_id' => Uuid::uuid4()->toString(),
- 'agent_id' => $agentId ? (int) $agentId : null,
- ]);
+ if (!$customerId) {
+ return response()->json(['error' => 'Customer not found for device'], 400);
+ }
- // Initialize Paystack transaction with company ID for callback URL
- $result = $this->apiService->initializeTransaction($transaction, $companyId);
+ $sender = $this->transactionService->getCustomerPhoneByCustomerId($customerId) ?? '';
- if ($result['error']) {
- return response()->json(['error' => $result['error']], 400);
- }
+ $result = $this->transactionService->initializePayment(
+ amount: (float) $validatedData['amount'],
+ sender: $sender,
+ message: $deviceSerial,
+ type: 'energy',
+ customerId: $customerId,
+ serialId: $deviceSerial,
+ );
return response()->json([
'success' => true,
- 'redirection_url' => $result['redirectionUrl'],
- 'reference' => $result['reference'],
- 'transaction_id' => $transaction->getId(),
+ 'redirect_url' => $result['provider_data']['redirect_url'],
+ 'reference' => $result['provider_data']['reference'],
+ 'transaction_id' => $result['transaction']->id,
]);
} catch (\Exception $e) {
Log::error('PaystackPublicController: Failed to initiate payment', [
@@ -167,6 +157,7 @@ public function showResult(Request $request, string $companyHash, ?int $companyI
'currency' => $transaction->getCurrency(),
'serial_id' => $transaction->getDeviceSerial(),
'device_type' => $transaction->getDeviceType(),
+ 'payment_type' => $mainTransaction?->type,
'status' => $transaction->getStatus(),
'created_at' => $transaction->getAttribute('created_at'),
],
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
index 1b8c94bf1..f48441e13 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
@@ -12,7 +12,9 @@
use App\Models\SolarHomeSystem;
use App\Models\Transaction\Transaction;
use App\Plugins\PaystackPaymentProvider\Models\PaystackTransaction;
+use App\Plugins\PaystackPaymentProvider\Modules\Api\PaystackApiService;
use App\Services\AbstractPaymentAggregatorTransactionService;
+use App\Services\DeviceService;
use App\Services\Interfaces\IBaseService;
use App\Services\PersonService;
use Illuminate\Database\Eloquent\Collection;
@@ -28,6 +30,7 @@ public function __construct(
private Address $address,
private Transaction $transaction,
private PaystackTransaction $paystackTransaction,
+ private PaystackApiService $paystackApiService,
) {
parent::__construct(
$this->meter,
@@ -136,6 +139,70 @@ public function processSuccessfulPayment(int $companyId, PaystackTransaction $tr
$transaction->save();
}
+ public function processFailedPayment(PaystackTransaction $transaction): void {
+ $transaction->setStatus(PaystackTransaction::STATUS_FAILED);
+ $transaction->save();
+
+ $relatedTransaction = $transaction->transaction;
+ if ($relatedTransaction) {
+ $relatedTransaction->update(['status' => PaystackTransaction::STATUS_FAILED]);
+ }
+ }
+
+ /**
+ * Create a PaystackTransaction + Transaction and initialize via the Paystack API.
+ * The caller supplies message and type, keeping routing knowledge outside this service.
+ *
+ * @return array{transaction: Transaction, provider_data: array}
+ */
+ public function initializePayment(
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ int $customerId,
+ ?string $serialId = null,
+ ): array {
+ $deviceType = null;
+ if ($serialId !== null) {
+ $device = app(DeviceService::class)->getBySerialNumber($serialId);
+ $deviceType = $device?->device_type;
+ }
+
+ $paystackTxn = $this->paystackTransaction->newQuery()->create([
+ 'amount' => $amount,
+ 'currency' => config('paystack-payment-provider.currency.default', 'NGN'),
+ 'order_id' => Uuid::uuid4()->toString(),
+ 'reference_id' => Uuid::uuid4()->toString(),
+ 'status' => PaystackTransaction::STATUS_REQUESTED,
+ 'customer_id' => $customerId,
+ 'serial_id' => $serialId,
+ 'device_type' => $deviceType,
+ 'metadata' => ['customer_id' => $customerId, 'serial_id' => $serialId, 'transaction_type' => $type],
+ ]);
+
+ /** @var Transaction $transaction */
+ $transaction = $paystackTxn->transaction()->create([
+ 'amount' => $amount,
+ 'sender' => $sender,
+ 'message' => $message,
+ 'type' => $type,
+ ]);
+
+ $result = $this->paystackApiService->initializeTransaction($paystackTxn);
+ if ($result['error']) {
+ throw new \RuntimeException('Paystack initialization failed: '.$result['error']);
+ }
+
+ return [
+ 'transaction' => $transaction,
+ 'provider_data' => [
+ 'redirect_url' => $result['redirectionUrl'],
+ 'reference' => $result['reference'],
+ ],
+ ];
+ }
+
/**
* @param array $transactionData
*/
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackWebhookService.php b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackWebhookService.php
index af97d976e..b67ca3112 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackWebhookService.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackWebhookService.php
@@ -58,20 +58,9 @@ private function handleSuccessfulPayment(array $data, int $companyId): void {
return;
}
- $paystackTransaction->setExternalTransactionId($data['id'] ?? '');
-
- // Get customer's phone number for sender field
- $customerPhone = $this->transactionService->getCustomerPhoneByCustomerId($paystackTransaction->getCustomerId());
- $sender = $customerPhone ?: '';
-
- $paystackTransaction->transaction()->create([
- 'amount' => $paystackTransaction->getAmount(),
- 'sender' => $sender,
- 'message' => $paystackTransaction->getDeviceSerial(),
- 'type' => 'energy',
- ]);
+ $paystackTransaction->setExternalTransactionId((string) ($data['id'] ?? ''));
$paystackTransaction->save();
- // Process the successful payment in MPM
+
$this->transactionService->processSuccessfulPayment($companyId, $paystackTransaction);
} catch (\Exception $e) {
Log::error('PaystackWebhookService: Failed to process payment', [
@@ -94,7 +83,9 @@ private function handleFailedPayment(array $data): void {
return;
}
- $paystackTransaction->setStatus(PaystackTransaction::STATUS_FAILED);
+ $paystackTransaction->setExternalTransactionId((string) ($data['id'] ?? ''));
$paystackTransaction->save();
+
+ $this->transactionService->processFailedPayment($paystackTransaction);
}
}
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Tests/Unit/PaystackTransactionTest.php b/src/backend/app/Plugins/PaystackPaymentProvider/Tests/Unit/PaystackTransactionTest.php
index 1129404b4..426aac226 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Tests/Unit/PaystackTransactionTest.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Tests/Unit/PaystackTransactionTest.php
@@ -6,12 +6,10 @@
use PHPUnit\Framework\TestCase as BaseTestCase;
class PaystackTransactionTest extends BaseTestCase {
- /** @test */
public function itReturnsCorrectTransactionName(): void {
$this->assertEquals('paystack_transaction', PaystackTransaction::getTransactionName());
}
- /** @test */
public function itHasCorrectStatusConstants(): void {
$this->assertEquals(0, PaystackTransaction::STATUS_REQUESTED);
$this->assertEquals(1, PaystackTransaction::STATUS_SUCCESS);
@@ -21,7 +19,6 @@ public function itHasCorrectStatusConstants(): void {
$this->assertEquals(5, PaystackTransaction::MAX_ATTEMPTS);
}
- /** @test */
public function itHasCorrectRelationName(): void {
$this->assertEquals('paystack_transaction', PaystackTransaction::RELATION_NAME);
}
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/routes/api.php b/src/backend/app/Plugins/PaystackPaymentProvider/routes/api.php
index 021c95a8d..1944b5cc6 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/routes/api.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/routes/api.php
@@ -13,7 +13,7 @@
Route::post('/credential/agent-payment-url', [PaystackCredentialController::class, 'generateAgentPaymentUrl']);
// Transaction management
- Route::post('/transaction/initialize', [PaystackController::class, 'startTransaction']);
+ Route::post('/transaction/initialize', [PaystackController::class, 'initializeTransaction']);
Route::get('/transaction/verify/{reference}', [PaystackController::class, 'verifyTransaction']);
Route::get('/transactions', [PaystackController::class, 'getTransactions']);
Route::get('/transactions/{id}', [PaystackController::class, 'getTransaction']);
diff --git a/src/backend/app/Services/CashTransactionService.php b/src/backend/app/Services/CashTransactionService.php
index 57f815e25..b935257b0 100644
--- a/src/backend/app/Services/CashTransactionService.php
+++ b/src/backend/app/Services/CashTransactionService.php
@@ -9,6 +9,27 @@
class CashTransactionService {
public function __construct(private CashTransaction $cashTransaction, private Transaction $transaction) {}
+ public function createTransaction(int $creatorId, float $amount, string $sender, string $message, string $type): Transaction {
+ return DB::transaction(function () use ($creatorId, $amount, $sender, $message, $type) {
+ $cashTransaction = $this->cashTransaction->newQuery()->create([
+ 'user_id' => $creatorId,
+ 'status' => 1,
+ ]);
+
+ $transaction = $this->transaction->newQuery()->make([
+ 'amount' => $amount,
+ 'sender' => $sender,
+ 'message' => $message,
+ 'type' => $type,
+ ]);
+
+ $transaction->originalTransaction()->associate($cashTransaction);
+ $transaction->save();
+
+ return $transaction;
+ });
+ }
+
public function createCashTransaction(int $creatorId, float $amount, string $sender, ?string $deviceSerial = null, ?int $applianceId = null): Transaction {
return DB::transaction(function () use ($creatorId, $amount, $sender, $deviceSerial, $applianceId) {
$cashTransaction = $this->cashTransaction->newQuery()->create([
diff --git a/src/backend/app/Services/PaymentInitializationService.php b/src/backend/app/Services/PaymentInitializationService.php
new file mode 100644
index 000000000..5d8430165
--- /dev/null
+++ b/src/backend/app/Services/PaymentInitializationService.php
@@ -0,0 +1,97 @@
+}
+ */
+ public function initialize(
+ int $providerId,
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ int $customerId,
+ int $creatorId,
+ ?string $serialId = null,
+ ): array {
+ $validProviderIds = [
+ 0, // cash
+ MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ ];
+
+ if (!in_array($providerId, $validProviderIds, true)) {
+ throw new \InvalidArgumentException("Unsupported payment provider ID: {$providerId}");
+ }
+
+ return match ($providerId) {
+ MpmPlugin::PAYSTACK_PAYMENT_PROVIDER => $this->paystackTransactionService->initializePayment(
+ $amount,
+ $sender,
+ $message,
+ $type,
+ $customerId,
+ $serialId,
+ ),
+ default => $this->initializeCash($creatorId, $amount, $sender, $message, $type),
+ };
+ }
+
+ /**
+ * @return array{transaction: Transaction, provider_data: array}
+ */
+ private function initializeCash(
+ int $creatorId,
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ ): array {
+ $transaction = $this->cashTransactionService->createTransaction(
+ $creatorId,
+ $amount,
+ $sender,
+ $message,
+ $type,
+ );
+
+ return ['transaction' => $transaction, 'provider_data' => []];
+ }
+
+ /** @return Collection */
+ public function paymentProviders(): Collection {
+ $activePlugins = $this->pluginsService->getActivePaymentProviders();
+
+ return $activePlugins->map(function (Plugins $plugin): array {
+ $mpmPlugin = $this->mpmPluginService->getById($plugin->mpm_plugin_id);
+
+ return ['id' => $plugin->mpm_plugin_id, 'name' => $mpmPlugin instanceof MpmPlugin ? $mpmPlugin->name : 'Unknown'];
+ });
+ }
+}
diff --git a/src/backend/app/Services/PluginsService.php b/src/backend/app/Services/PluginsService.php
index 9cf8fc49d..b5d50ba61 100644
--- a/src/backend/app/Services/PluginsService.php
+++ b/src/backend/app/Services/PluginsService.php
@@ -50,6 +50,25 @@ public function getByMpmPluginId(int $mpmPluginId): ?Plugins {
->first();
}
+ /**
+ * @return Collection
+ */
+ public function getActivePaymentProviders(): Collection {
+ $paymentProviderIds = [
+ MpmPlugin::SWIFTA_PAYMENT_PROVIDER,
+ MpmPlugin::MESOMB_PAYMENT_PROVIDER,
+ MpmPlugin::WAVE_MONEY_PAYMENT_PROVIDER,
+ MpmPlugin::WAVECOM_PAYMENT_PROVIDER,
+ MpmPlugin::VODACOM_MOBILE_MONEY,
+ MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ ];
+
+ return $this->plugin->newQuery()
+ ->where('status', Plugins::ACTIVE)
+ ->whereIn('mpm_plugin_id', $paymentProviderIds)
+ ->get();
+ }
+
public function isPluginActive(int $pluginId): bool {
return $this->plugin->newQuery()
->where('mpm_plugin_id', '=', $pluginId)
diff --git a/src/backend/app/Services/TransactionPaymentProcessor.php b/src/backend/app/Services/TransactionPaymentProcessor.php
index f48864acd..54934985a 100644
--- a/src/backend/app/Services/TransactionPaymentProcessor.php
+++ b/src/backend/app/Services/TransactionPaymentProcessor.php
@@ -34,6 +34,12 @@ public static function process(int $companyId, int $transactionId): void {
return;
}
+ if ($transaction->paygoAppliance()->exists()) {
+ dispatch(new ApplianceTransactionProcessor($companyId, $transactionId));
+
+ return;
+ }
+
if ($transaction->nonPaygoAppliance()->exists()) {
dispatch(new ApplianceTransactionProcessor($companyId, $transactionId));
diff --git a/src/backend/routes/api.php b/src/backend/routes/api.php
index be163ba11..4b6f1a9ee 100644
--- a/src/backend/routes/api.php
+++ b/src/backend/routes/api.php
@@ -138,6 +138,7 @@
});
Route::group(['prefix' => 'payment'], static function () {
+ Route::get('/providers', [AppliancePaymentController::class, 'paymentProviders'])->middleware('permission:payments');
Route::post('/{appliance_person}', [AppliancePaymentController::class, 'store'])->middleware('permission:payments');
Route::get('/status/{transaction_id}', [AppliancePaymentController::class, 'checkStatus'])->where('transaction_id', '[0-9]+')->middleware('permission:payments');
});
diff --git a/src/backend/tests/Feature/AppliancePaymentControllerTest.php b/src/backend/tests/Feature/AppliancePaymentControllerTest.php
new file mode 100644
index 000000000..b69ee8650
--- /dev/null
+++ b/src/backend/tests/Feature/AppliancePaymentControllerTest.php
@@ -0,0 +1,187 @@
+createTestData();
+
+ Plugins::query()->create([
+ 'mpm_plugin_id' => MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ 'status' => Plugins::ACTIVE,
+ ]);
+
+ Plugins::query()->create([
+ 'mpm_plugin_id' => MpmPlugin::WAVE_MONEY_PAYMENT_PROVIDER,
+ 'status' => Plugins::INACTIVE,
+ ]);
+
+ $response = $this->actingAs($this->user)->get('/api/appliances/payment/providers');
+
+ $response->assertStatus(200);
+ $this->assertCount(1, $response['data']);
+ $this->assertEquals(MpmPlugin::PAYSTACK_PAYMENT_PROVIDER, $response['data'][0]['id']);
+ }
+
+ public function testReturnsEmptyListWhenNoActivePaymentPlugins(): void {
+ $this->createTestData();
+
+ $response = $this->actingAs($this->user)->get('/api/appliances/payment/providers');
+
+ $response->assertStatus(200);
+ $this->assertCount(0, $response['data']);
+ }
+
+ public function testCreatesCashTransactionAndDispatchesProcessPaymentJob(): void {
+ $this->createTestData();
+ Queue::fake();
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Appliance',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+
+ /** @var AppliancePerson $appliancePerson */
+ $appliancePerson = AppliancePersonFactory::new()->create([
+ 'appliance_id' => $appliance->id,
+ 'person_id' => $person->id,
+ 'total_cost' => 500,
+ 'rate_count' => 5,
+ 'down_payment' => 0,
+ ]);
+
+ ApplianceRate::query()->create([
+ 'appliance_person_id' => $appliancePerson->id,
+ 'rate_cost' => 100,
+ 'remaining' => 100,
+ 'remind' => 0,
+ 'due_date' => now()->addMonth(),
+ ]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/payment/{$appliancePerson->id}",
+ [
+ 'amount' => 100,
+ 'payment_provider' => 0,
+ ]
+ );
+
+ $response->assertStatus(200);
+ $this->assertEquals($appliancePerson->id, $response['data']['appliance_person']['id']);
+ $this->assertNotNull($response['data']['transaction_id']);
+
+ Queue::assertPushed(ProcessPayment::class);
+ }
+
+ public function testReturnsRedirectUrlForPaystackPayment(): void {
+ $this->createTestData();
+ Queue::fake();
+
+ $apiService = $this->createMock(PaystackApiService::class);
+ $apiService->method('initializeTransaction')->willReturn([
+ 'error' => null,
+ 'redirectionUrl' => 'https://paystack.com/pay/test123',
+ 'reference' => 'ref_test123',
+ ]);
+ $this->app->instance(PaystackApiService::class, $apiService);
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Appliance',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+
+ /** @var AppliancePerson $appliancePerson */
+ $appliancePerson = AppliancePersonFactory::new()->create([
+ 'appliance_id' => $appliance->id,
+ 'person_id' => $person->id,
+ 'total_cost' => 500,
+ 'rate_count' => 5,
+ 'down_payment' => 0,
+ ]);
+
+ ApplianceRate::query()->create([
+ 'appliance_person_id' => $appliancePerson->id,
+ 'rate_cost' => 100,
+ 'remaining' => 100,
+ 'remind' => 0,
+ 'due_date' => now()->addMonth(),
+ ]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/payment/{$appliancePerson->id}",
+ [
+ 'amount' => 100,
+ 'payment_provider' => MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ ]
+ );
+
+ $response->assertStatus(200);
+ $this->assertEquals('https://paystack.com/pay/test123', $response['data']['redirect_url']);
+ $this->assertEquals('ref_test123', $response['data']['reference']);
+
+ Queue::assertNotPushed(ProcessPayment::class);
+ }
+
+ public function testRejectsUnknownPaymentProviderId(): void {
+ $this->createTestData();
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Appliance',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+
+ /** @var AppliancePerson $appliancePerson */
+ $appliancePerson = AppliancePersonFactory::new()->create([
+ 'appliance_id' => $appliance->id,
+ 'person_id' => $person->id,
+ 'total_cost' => 500,
+ 'rate_count' => 5,
+ 'down_payment' => 0,
+ ]);
+
+ ApplianceRate::query()->create([
+ 'appliance_person_id' => $appliancePerson->id,
+ 'rate_cost' => 100,
+ 'remaining' => 100,
+ 'remind' => 0,
+ 'due_date' => now()->addMonth(),
+ ]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/payment/{$appliancePerson->id}",
+ [
+ 'amount' => 100,
+ 'payment_provider' => 999,
+ ]
+ );
+
+ $response->assertStatus(500);
+ }
+}
diff --git a/src/backend/tests/Feature/AppliancePersonControllerDownPaymentTest.php b/src/backend/tests/Feature/AppliancePersonControllerDownPaymentTest.php
new file mode 100644
index 000000000..106610dfd
--- /dev/null
+++ b/src/backend/tests/Feature/AppliancePersonControllerDownPaymentTest.php
@@ -0,0 +1,127 @@
+
+ */
+ private function buildSaleRequest(int $applianceId, int $personId, int $userId, float $downPayment): array {
+ return [
+ 'id' => $applianceId,
+ 'person_id' => $personId,
+ 'user_id' => $userId,
+ 'cost' => 1000,
+ 'rate' => 5,
+ 'rate_type' => 'monthly',
+ 'down_payment' => $downPayment,
+ 'points' => '0,0',
+ ];
+ }
+
+ public function testItFiresPaymentSuccessEventForCashDownPayment(): void {
+ $this->createTestData();
+ Event::fake([PaymentSuccessEvent::class, TransactionSuccessfulEvent::class]);
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Solar Panel',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+ $seller = UserFactory::new()->create(['company_id' => $this->companyId]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/person/{$appliance->id}/people/{$person->id}",
+ array_merge($this->buildSaleRequest($appliance->id, $person->id, $seller->id, 200), [
+ 'payment_provider' => 0,
+ ])
+ );
+
+ $response->assertStatus(200);
+
+ Event::assertDispatched(PaymentSuccessEvent::class);
+ Event::assertDispatched(TransactionSuccessfulEvent::class);
+
+ $this->assertNotNull($response['data']['appliance_person']);
+ }
+
+ public function testItDispatchesProcessPaymentJobForPaystackDownPayment(): void {
+ $this->createTestData();
+ Queue::fake();
+
+ $apiService = $this->createMock(PaystackApiService::class);
+ $apiService->method('initializeTransaction')->willReturn([
+ 'error' => null,
+ 'redirectionUrl' => 'https://paystack.com/pay/dp_test',
+ 'reference' => 'ref_dp_test',
+ ]);
+ $this->app->instance(PaystackApiService::class, $apiService);
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Solar Panel',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+ $seller = UserFactory::new()->create(['company_id' => $this->companyId]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/person/{$appliance->id}/people/{$person->id}",
+ array_merge($this->buildSaleRequest($appliance->id, $person->id, $seller->id, 200), [
+ 'payment_provider' => MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ ])
+ );
+
+ $response->assertStatus(200);
+
+ Queue::assertPushed(ProcessPayment::class);
+
+ $this->assertNotNull($response['data']['appliance_person']);
+ $this->assertEquals('https://paystack.com/pay/dp_test', $response['data']['redirect_url']);
+ }
+
+ public function testItCreatesAppliancePersonWithoutDownPayment(): void {
+ $this->createTestData();
+ Event::fake();
+
+ $person = PersonFactory::new()->create();
+ $applianceType = ApplianceTypeFactory::new()->create();
+ $appliance = Appliance::query()->create([
+ 'name' => 'Test Solar Panel',
+ 'price' => 1000,
+ 'appliance_type_id' => $applianceType->id,
+ ]);
+ $seller = UserFactory::new()->create(['company_id' => $this->companyId]);
+
+ $response = $this->actingAs($this->user)->post(
+ "/api/appliances/person/{$appliance->id}/people/{$person->id}",
+ $this->buildSaleRequest($appliance->id, $person->id, $seller->id, 0)
+ );
+
+ $response->assertStatus(200);
+
+ Event::assertNotDispatched(PaymentSuccessEvent::class);
+ $this->assertNotNull($response['data']['appliance_person']);
+ }
+}
diff --git a/src/backend/tests/Unit/PaymentInitializationServiceTest.php b/src/backend/tests/Unit/PaymentInitializationServiceTest.php
new file mode 100644
index 000000000..f600e4c20
--- /dev/null
+++ b/src/backend/tests/Unit/PaymentInitializationServiceTest.php
@@ -0,0 +1,169 @@
+cashService = $this->createMock(CashTransactionService::class);
+ $this->paystackService = $this->createMock(PaystackTransactionService::class);
+ $pluginsService = $this->createMock(PluginsService::class);
+ $mpmPluginService = $this->createMock(MpmPluginService::class);
+
+ $this->service = new PaymentInitializationService(
+ $this->cashService,
+ $this->paystackService,
+ $pluginsService,
+ $mpmPluginService,
+ );
+ }
+
+ public function testThrowsForUnknownProviderId(): void {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Unsupported payment provider ID: 999');
+
+ $this->service->initialize(
+ providerId: 999,
+ amount: 100.0,
+ sender: '+2340000',
+ message: 'DEVICE-001',
+ type: 'deferred_payment',
+ customerId: 1,
+ creatorId: 1,
+ );
+ }
+
+ public function testDelegatesToCashTransactionServiceForProviderZero(): void {
+ $transaction = new Transaction();
+
+ $this->cashService
+ ->expects($this->once())
+ ->method('createTransaction')
+ ->with(1, 100.0, '+2340000', '42', 'deferred_payment')
+ ->willReturn($transaction);
+
+ $result = $this->service->initialize(
+ providerId: 0,
+ amount: 100.0,
+ sender: '+2340000',
+ message: '42',
+ type: 'deferred_payment',
+ customerId: 5,
+ creatorId: 1,
+ );
+
+ $this->assertSame($transaction, $result['transaction']);
+ $this->assertSame([], $result['provider_data']);
+ }
+
+ public function testDelegatesToPaystackServiceForPaystackProvider(): void {
+ $transaction = new Transaction();
+
+ $this->paystackService
+ ->expects($this->once())
+ ->method('initializePayment')
+ ->with(100.0, '+2340000', '42', 'deferred_payment', 5, null)
+ ->willReturn([
+ 'transaction' => $transaction,
+ 'provider_data' => [
+ 'redirect_url' => 'https://paystack.com/pay/abc',
+ 'reference' => 'ref_abc',
+ ],
+ ]);
+
+ $result = $this->service->initialize(
+ providerId: MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ amount: 100.0,
+ sender: '+2340000',
+ message: '42',
+ type: 'deferred_payment',
+ customerId: 5,
+ creatorId: 1,
+ );
+
+ $this->assertSame($transaction, $result['transaction']);
+ $this->assertSame('https://paystack.com/pay/abc', $result['provider_data']['redirect_url']);
+ }
+
+ public function testDoesNotCallPaystackServiceForCashProvider(): void {
+ $transaction = new Transaction();
+
+ $this->cashService->method('createTransaction')->willReturn($transaction);
+ $this->paystackService->expects($this->never())->method('initializePayment');
+
+ $this->service->initialize(
+ providerId: 0,
+ amount: 50.0,
+ sender: '-',
+ message: '1',
+ type: 'deferred_payment',
+ customerId: 1,
+ creatorId: 1,
+ );
+ }
+
+ public function testDoesNotCallCashServiceForPaystackProvider(): void {
+ $transaction = new Transaction();
+
+ $this->paystackService->method('initializePayment')->willReturn([
+ 'transaction' => $transaction,
+ 'provider_data' => ['redirect_url' => 'https://paystack.com/pay/x', 'reference' => 'ref_x'],
+ ]);
+ $this->cashService->expects($this->never())->method('createTransaction');
+
+ $this->service->initialize(
+ providerId: MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ amount: 50.0,
+ sender: '-',
+ message: '1',
+ type: 'deferred_payment',
+ customerId: 1,
+ creatorId: 1,
+ );
+ }
+
+ public function testPassesSerialIdToPaystackServiceWhenProvided(): void {
+ $transaction = new Transaction();
+
+ $this->paystackService
+ ->expects($this->once())
+ ->method('initializePayment')
+ ->with(200.0, '+2340000', 'SERIAL-001', 'deferred_payment', 5, 'SERIAL-001')
+ ->willReturn([
+ 'transaction' => $transaction,
+ 'provider_data' => ['redirect_url' => 'https://paystack.com/pay/y', 'reference' => 'ref_y'],
+ ]);
+
+ $this->service->initialize(
+ providerId: MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
+ amount: 200.0,
+ sender: '+2340000',
+ message: 'SERIAL-001',
+ type: 'deferred_payment',
+ customerId: 5,
+ creatorId: 1,
+ serialId: 'SERIAL-001',
+ );
+ }
+}
diff --git a/src/backend/tests/Unit/PaystackTransactionServiceInitializePaymentTest.php b/src/backend/tests/Unit/PaystackTransactionServiceInitializePaymentTest.php
new file mode 100644
index 000000000..763df8127
--- /dev/null
+++ b/src/backend/tests/Unit/PaystackTransactionServiceInitializePaymentTest.php
@@ -0,0 +1,98 @@
+createMock(PaystackApiService::class);
+ $apiService->method('initializeTransaction')->willReturn($apiResponse);
+
+ $this->app->instance(PaystackApiService::class, $apiService);
+
+ /** @var PaystackTransactionService $service */
+ $service = $this->app->make(PaystackTransactionService::class);
+
+ return $service;
+ }
+
+ public function testCreatesPaystackTransactionAndTransactionRecords(): void {
+ $service = $this->makeServiceWithMockedApi([
+ 'error' => null,
+ 'redirectionUrl' => 'https://paystack.com/pay/test',
+ 'reference' => 'ref_test_123',
+ ]);
+
+ $result = $service->initializePayment(
+ amount: 200.0,
+ sender: '+2340000',
+ message: '42',
+ type: 'deferred_payment',
+ customerId: 1,
+ );
+
+ $this->assertInstanceOf(Transaction::class, $result['transaction']);
+ $this->assertSame('42', $result['transaction']->message);
+ $this->assertSame('deferred_payment', $result['transaction']->type);
+ $this->assertSame('https://paystack.com/pay/test', $result['provider_data']['redirect_url']);
+ $this->assertSame('ref_test_123', $result['provider_data']['reference']);
+
+ $paystackTxn = PaystackTransaction::query()->where('customer_id', 1)->where('amount', 200.0)->first();
+ $this->assertNotNull($paystackTxn);
+
+ $transaction = Transaction::query()->where('message', '42')->where('type', 'deferred_payment')->first();
+ $this->assertNotNull($transaction);
+ }
+
+ public function testSetsSerialIdOnPaystackTransactionWhenProvided(): void {
+ $service = $this->makeServiceWithMockedApi([
+ 'error' => null,
+ 'redirectionUrl' => 'https://paystack.com/pay/serial',
+ 'reference' => 'ref_serial',
+ ]);
+
+ $service->initializePayment(
+ amount: 150.0,
+ sender: '+2340001',
+ message: 'SERIAL-XYZ',
+ type: 'deferred_payment',
+ customerId: 2,
+ serialId: 'SERIAL-XYZ',
+ );
+
+ $paystackTxn = PaystackTransaction::query()->where('serial_id', 'SERIAL-XYZ')->first();
+ $this->assertNotNull($paystackTxn);
+
+ $transaction = Transaction::query()->where('message', 'SERIAL-XYZ')->first();
+ $this->assertNotNull($transaction);
+ }
+
+ public function testThrowsWhenPaystackApiReturnsError(): void {
+ $service = $this->makeServiceWithMockedApi([
+ 'error' => 'API error: invalid key',
+ 'redirectionUrl' => null,
+ 'reference' => null,
+ ]);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Paystack initialization failed: API error: invalid key');
+
+ $service->initializePayment(
+ amount: 100.0,
+ sender: '-',
+ message: '1',
+ type: 'deferred_payment',
+ customerId: 1,
+ );
+ }
+}
diff --git a/src/frontend/src/modules/Client/Appliances/SellApplianceModal.vue b/src/frontend/src/modules/Client/Appliances/SellApplianceModal.vue
index 8dfe62b13..ecf1bddad 100644
--- a/src/frontend/src/modules/Client/Appliances/SellApplianceModal.vue
+++ b/src/frontend/src/modules/Client/Appliances/SellApplianceModal.vue
@@ -389,6 +389,24 @@
+
+
+
+
+ Cash
+
+ {{ provider.name }}
+
+
+
+
How Much Do You Want to Pay?
+
+
+
+ Cash
+
+ {{ provider.name }}
+
+
+
payments
@@ -368,6 +381,9 @@ export default {
detailedDeviceInfo: null,
editRow: null,
tempCost: null,
+ paymentProviders: [],
+ selectedProviderId: 0,
+ redirectUrl: null,
}
},
computed: {
@@ -454,10 +470,19 @@ export default {
this.tempCost = cost
this.editRow = "rate_" + id
},
+ async openPaymentDialog() {
+ this.getPayment = true
+ const providers = await this.appliancePayment.getPaymentProviders()
+ if (!(providers instanceof ErrorHandler)) {
+ this.paymentProviders = providers
+ }
+ },
closeGetPayment() {
this.getPayment = false
this.payment = null
this.errorLabel = false
+ this.selectedProviderId = 0
+ this.redirectUrl = null
},
async editRate(data) {
this.progress = true
@@ -557,6 +582,7 @@ export default {
adminId: this.adminId,
rates: this.soldAppliance.rates,
amount: this.payment,
+ paymentProvider: this.selectedProviderId,
}
const result = await this.appliancePayment.getPaymentForAppliance(
@@ -568,6 +594,11 @@ export default {
throw result
}
+ if (result.redirect_url) {
+ this.redirectUrl = result.redirect_url
+ window.open(result.redirect_url, "_blank")
+ }
+
// Check if transaction_id is returned (async processing)
if (result.transaction_id) {
// Poll for payment processing status
diff --git a/src/frontend/src/plugins/paystack-payment-provider/modules/Overview/Credential.vue b/src/frontend/src/plugins/paystack-payment-provider/modules/Overview/Credential.vue
index d309240b9..7d36af0f5 100644
--- a/src/frontend/src/plugins/paystack-payment-provider/modules/Overview/Credential.vue
+++ b/src/frontend/src/plugins/paystack-payment-provider/modules/Overview/Credential.vue
@@ -240,55 +240,6 @@
-
-
-
-
-
-
-
-
-
- content_copy
-
-
-
-
-
-
-
-
-
- content_copy
-
-
-
-
-
- warning
- These URLs expire in 24 hours. Use for temporary or
- agent-generated links.
-
-
@@ -336,8 +287,6 @@ export default {
loadingUrls: false,
publicUrls: {
permanent_payment_url: "",
- time_based_payment_url: "",
- time_based_result_url: "",
},
callbackUrl: "",
}
@@ -382,13 +331,6 @@ export default {
permanent_payment_url: this.addFrontendPrefix(
response.permanent_payment_url,
),
- time_based_payment_url: this.addFrontendPrefix(
- response.time_based_payment_url,
- ),
- time_based_result_url: this.addFrontendPrefix(
- response.time_based_result_url,
- ),
- company_id: response.company_id,
}
this.callbackUrl = this.addFrontendPrefix(
@@ -486,31 +428,11 @@ export default {
margin-bottom: 1.5rem;
}
-.time-based-url {
- border-left: 4px solid #ff9800;
- padding-left: 1rem;
- background-color: #fff3e0;
- border-radius: 4px;
- margin-bottom: 1.5rem;
-}
-
.url-icon {
margin-right: 8px;
vertical-align: middle;
}
-.url-sublabel {
- display: block;
- font-weight: 500;
- margin-bottom: 0.5rem;
- color: #666;
- font-size: 14px;
-}
-
-.url-sub-item {
- margin-bottom: 1rem;
-}
-
.url-description {
display: flex;
align-items: center;
diff --git a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
index ffb9f45a6..45bef9082 100644
--- a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
+++ b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
@@ -290,7 +290,7 @@ export default {
timer: 2000,
timerProgressBar: true,
}).then(() => {
- window.location = data.redirection_url
+ window.location = data.redirect_url
})
} catch (error) {
this.$swal({
diff --git a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
index e203c7be9..fc9badf32 100644
--- a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
+++ b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
@@ -28,16 +28,20 @@
Transaction ID:
{{ paymentResult.transaction.id }}
-
+
{{ deviceLabel }}:
{{ paymentResult.transaction.serial_id }}
-
+
Device Type:
{{ deviceTypeName }}
+
+ Payment Type:
+ Installment
+
Amount Paid:
@@ -58,7 +62,7 @@
-
+
diff --git a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
index 45bef9082..a6fb89cb0 100644
--- a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
+++ b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentForm.vue
@@ -290,7 +290,7 @@ export default {
timer: 2000,
timerProgressBar: true,
}).then(() => {
- window.location = data.redirect_url
+ window.location = data.redirect_url
})
} catch (error) {
this.$swal({
diff --git a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
index fc9badf32..ce1ba6911 100644
--- a/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
+++ b/src/frontend/src/plugins/paystack-payment-provider/modules/Payment/PublicPaymentResult.vue
@@ -283,13 +283,15 @@ export default {
},
// Non-paygo installments have no device_serial — no device or token to show
isNonPaygoInstallment() {
- return this.isInstallment && !this.paymentResult?.transaction?.serial_id
+ return this.isInstallment && !this.paymentResult?.transaction?.serial_id
},
deviceType() {
return this.paymentResult?.transaction?.device_type || "meter"
},
isSHS() {
- return this.deviceType === "solar_home_system" || this.deviceType === "shs"
+ return (
+ this.deviceType === "solar_home_system" || this.deviceType === "shs"
+ )
},
deviceTypeName() {
return this.isSHS ? "Solar Home System" : "Meter"
From cf885d9a338e5022ff7977d69ceac5050c2f6796 Mon Sep 17 00:00:00 2001
From: Obinna Ikeh
Date: Wed, 1 Apr 2026 00:38:12 +0100
Subject: [PATCH 4/5] Refactor: apply review improvements
---
docs/development/plugins.md | 48 ++++++++++++++++
...t-flow.svg => payment-flow.excalidraw.svg} | 0
.../AppliancePaymentController.php | 2 -
.../Controllers/AppliancePersonController.php | 1 -
.../Services/PaystackTransactionService.php | 3 +-
.../app/Services/CashTransactionService.php | 39 +++++++------
.../Interfaces/PaymentInitializer.php | 36 ++++++++++++
.../Services/PaymentInitializationService.php | 55 +++++++------------
.../Services/TransactionPaymentProcessor.php | 6 --
.../Unit/PaymentInitializationServiceTest.php | 32 ++++++-----
10 files changed, 142 insertions(+), 80 deletions(-)
rename docs/usage-guide/images/{payment-flow.svg => payment-flow.excalidraw.svg} (100%)
create mode 100644 src/backend/app/Services/Interfaces/PaymentInitializer.php
diff --git a/docs/development/plugins.md b/docs/development/plugins.md
index 7644bb8ad..c7bcb6dc4 100644
--- a/docs/development/plugins.md
+++ b/docs/development/plugins.md
@@ -150,6 +150,54 @@ class InstallPackage extends Command
}
```
+#### Payment Provider Plugins
+
+If your plugin is a **payment provider** (processes transactions from an external payment gateway), there are additional integration steps required:
+
+1. **Implement the `PaymentInitializer` interface**
+
+ Your plugin's transaction service must implement `App\Services\Interfaces\PaymentInitializer`. This enforces a consistent `initializePayment()` method that creates the provider-specific record and an associated `Transaction` entry.
+
+ ```php
+ use App\Services\Interfaces\PaymentInitializer;
+
+ class YourProviderTransactionService implements PaymentInitializer {
+ public function initializePayment(
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ int $customerId,
+ ?string $serialId = null,
+ ): array {
+ // 1. Create your provider-specific transaction record
+ // 2. Create the associated Transaction record
+ // 3. Call your provider's API if needed (e.g. to get a redirect URL)
+ // 4. Return ['transaction' => $transaction, 'provider_data' => [...]]
+ }
+ }
+ ```
+
+2. **Register in the provider map**
+
+ Add your plugin's MpmPlugin ID and service class to the `PROVIDER_MAP` constant in `App\Services\PaymentInitializationService`:
+
+ ```php
+ private const PROVIDER_MAP = [
+ 0 => CashTransactionService::class,
+ MpmPlugin::PAYSTACK_PAYMENT_PROVIDER => PaystackTransactionService::class,
+ MpmPlugin::YOUR_PAYMENT_PROVIDER => YourProviderTransactionService::class,
+ ];
+ ```
+
+3. **Register in the active providers list**
+
+ Add your plugin's MpmPlugin ID to the `$paymentProviderIds` array in `App\Services\PluginsService::getActivePaymentProviders()`. This is required for your provider to appear as an available payment option in the UI.
+
+4. **Provider validation**
+
+ Payment validation (device ownership, minimum purchase amounts) is handled via the `ITransactionProvider::validateRequest()` interface and `AbstractPaymentAggregatorTransactionService::validatePaymentOwner()`. If your provider receives incoming payment callbacks, implement `ITransactionProvider` in your provider class to plug into the existing validation pipeline.
+
### Frontend Integration
This section describe how to integrate your plugin with the MicroPowerManager frontend.
diff --git a/docs/usage-guide/images/payment-flow.svg b/docs/usage-guide/images/payment-flow.excalidraw.svg
similarity index 100%
rename from docs/usage-guide/images/payment-flow.svg
rename to docs/usage-guide/images/payment-flow.excalidraw.svg
diff --git a/src/backend/app/Http/Controllers/AppliancePaymentController.php b/src/backend/app/Http/Controllers/AppliancePaymentController.php
index 527396e2c..be789546b 100644
--- a/src/backend/app/Http/Controllers/AppliancePaymentController.php
+++ b/src/backend/app/Http/Controllers/AppliancePaymentController.php
@@ -60,7 +60,6 @@ public function paymentProviders(): ApiResource {
* @return array
*/
private function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): array {
- $creatorId = auth('api')->user()->id;
$amount = (float) $request->input('amount');
$providerId = (int) $request->input('payment_provider', 0);
$companyId = $request->attributes->get('companyId');
@@ -86,7 +85,6 @@ private function getPaymentForAppliance(Request $request, AppliancePerson $appli
message: $message,
type: 'deferred_payment',
customerId: $applianceOwner->id,
- creatorId: $creatorId,
serialId: $deviceSerial,
);
diff --git a/src/backend/app/Http/Controllers/AppliancePersonController.php b/src/backend/app/Http/Controllers/AppliancePersonController.php
index 23f56359d..710324aa8 100644
--- a/src/backend/app/Http/Controllers/AppliancePersonController.php
+++ b/src/backend/app/Http/Controllers/AppliancePersonController.php
@@ -116,7 +116,6 @@ public function store(
message: $message,
type: 'deferred_payment',
customerId: $person->id,
- creatorId: $user->id,
serialId: $deviceSerial ?? null,
);
diff --git a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
index ab520e088..56e4e7cb7 100644
--- a/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
+++ b/src/backend/app/Plugins/PaystackPaymentProvider/Services/PaystackTransactionService.php
@@ -14,6 +14,7 @@
use App\Services\AbstractPaymentAggregatorTransactionService;
use App\Services\DeviceService;
use App\Services\Interfaces\IBaseService;
+use App\Services\Interfaces\PaymentInitializer;
use App\Services\PersonService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -22,7 +23,7 @@
/**
* @implements IBaseService
*/
-class PaystackTransactionService extends AbstractPaymentAggregatorTransactionService implements IBaseService {
+class PaystackTransactionService extends AbstractPaymentAggregatorTransactionService implements IBaseService, PaymentInitializer {
public function __construct(
private Meter $meter,
private Address $address,
diff --git a/src/backend/app/Services/CashTransactionService.php b/src/backend/app/Services/CashTransactionService.php
index b935257b0..7787f3b54 100644
--- a/src/backend/app/Services/CashTransactionService.php
+++ b/src/backend/app/Services/CashTransactionService.php
@@ -4,9 +4,11 @@
use App\Models\Transaction\CashTransaction;
use App\Models\Transaction\Transaction;
+use App\Services\Interfaces\PaymentInitializer;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
-class CashTransactionService {
+class CashTransactionService implements PaymentInitializer {
public function __construct(private CashTransaction $cashTransaction, private Transaction $transaction) {}
public function createTransaction(int $creatorId, float $amount, string $sender, string $message, string $type): Transaction {
@@ -30,24 +32,21 @@ public function createTransaction(int $creatorId, float $amount, string $sender,
});
}
- public function createCashTransaction(int $creatorId, float $amount, string $sender, ?string $deviceSerial = null, ?int $applianceId = null): Transaction {
- return DB::transaction(function () use ($creatorId, $amount, $sender, $deviceSerial, $applianceId) {
- $cashTransaction = $this->cashTransaction->newQuery()->create([
- 'user_id' => $creatorId,
- 'status' => 1,
- ]);
-
- $transaction = $this->transaction->newQuery()->make([
- 'amount' => $amount,
- 'sender' => $sender,
- 'message' => $deviceSerial ?? strval($applianceId ?? '-'),
- 'type' => 'deferred_payment',
- ]);
-
- $transaction->originalTransaction()->associate($cashTransaction);
- $transaction->save();
-
- return $transaction;
- });
+ /**
+ * @return array{transaction: Transaction, provider_data: array}
+ */
+ public function initializePayment(
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ int $customerId,
+ ?string $serialId = null,
+ ): array {
+ $creatorId = Auth::id();
+
+ $transaction = $this->createTransaction($creatorId, $amount, $sender, $message, $type);
+
+ return ['transaction' => $transaction, 'provider_data' => []];
}
}
diff --git a/src/backend/app/Services/Interfaces/PaymentInitializer.php b/src/backend/app/Services/Interfaces/PaymentInitializer.php
new file mode 100644
index 000000000..cf427eb59
--- /dev/null
+++ b/src/backend/app/Services/Interfaces/PaymentInitializer.php
@@ -0,0 +1,36 @@
+}
+ */
+ public function initializePayment(
+ float $amount,
+ string $sender,
+ string $message,
+ string $type,
+ int $customerId,
+ ?string $serialId = null,
+ ): array;
+}
diff --git a/src/backend/app/Services/PaymentInitializationService.php b/src/backend/app/Services/PaymentInitializationService.php
index 5d8430165..3c2b7f36c 100644
--- a/src/backend/app/Services/PaymentInitializationService.php
+++ b/src/backend/app/Services/PaymentInitializationService.php
@@ -8,14 +8,26 @@
use App\Models\Plugins;
use App\Models\Transaction\Transaction;
use App\Plugins\PaystackPaymentProvider\Services\PaystackTransactionService;
+use App\Services\Interfaces\PaymentInitializer;
+use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Collection;
class PaymentInitializationService {
+ /**
+ * Maps provider IDs to their PaymentInitializer implementation class.
+ * When adding a new payment provider plugin, register it here.
+ *
+ * @var array>
+ */
+ private const PROVIDER_MAP = [
+ 0 => CashTransactionService::class,
+ MpmPlugin::PAYSTACK_PAYMENT_PROVIDER => PaystackTransactionService::class,
+ ];
+
public function __construct(
- private CashTransactionService $cashTransactionService,
- private PaystackTransactionService $paystackTransactionService,
private PluginsService $pluginsService,
private MpmPluginService $mpmPluginService,
+ private Container $container,
) {}
/**
@@ -26,7 +38,6 @@ public function __construct(
* @param string $message Transaction routing key (device serial or entity ID)
* @param string $type Transaction type (e.g. 'deferred_payment', 'energy')
* @param int $customerId Person ID of the customer
- * @param int $creatorId Admin user ID (used for cash transactions)
* @param string|null $serialId Device serial, if applicable
*
* @return array{transaction: Transaction, provider_data: array}
@@ -38,50 +49,22 @@ public function initialize(
string $message,
string $type,
int $customerId,
- int $creatorId,
?string $serialId = null,
): array {
- $validProviderIds = [
- 0, // cash
- MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
- ];
-
- if (!in_array($providerId, $validProviderIds, true)) {
+ if (!isset(self::PROVIDER_MAP[$providerId])) {
throw new \InvalidArgumentException("Unsupported payment provider ID: {$providerId}");
}
- return match ($providerId) {
- MpmPlugin::PAYSTACK_PAYMENT_PROVIDER => $this->paystackTransactionService->initializePayment(
- $amount,
- $sender,
- $message,
- $type,
- $customerId,
- $serialId,
- ),
- default => $this->initializeCash($creatorId, $amount, $sender, $message, $type),
- };
- }
+ $initializer = $this->container->make(self::PROVIDER_MAP[$providerId]);
- /**
- * @return array{transaction: Transaction, provider_data: array}
- */
- private function initializeCash(
- int $creatorId,
- float $amount,
- string $sender,
- string $message,
- string $type,
- ): array {
- $transaction = $this->cashTransactionService->createTransaction(
- $creatorId,
+ return $initializer->initializePayment(
$amount,
$sender,
$message,
$type,
+ $customerId,
+ $serialId,
);
-
- return ['transaction' => $transaction, 'provider_data' => []];
}
/** @return Collection */
diff --git a/src/backend/app/Services/TransactionPaymentProcessor.php b/src/backend/app/Services/TransactionPaymentProcessor.php
index 54934985a..f48864acd 100644
--- a/src/backend/app/Services/TransactionPaymentProcessor.php
+++ b/src/backend/app/Services/TransactionPaymentProcessor.php
@@ -34,12 +34,6 @@ public static function process(int $companyId, int $transactionId): void {
return;
}
- if ($transaction->paygoAppliance()->exists()) {
- dispatch(new ApplianceTransactionProcessor($companyId, $transactionId));
-
- return;
- }
-
if ($transaction->nonPaygoAppliance()->exists()) {
dispatch(new ApplianceTransactionProcessor($companyId, $transactionId));
diff --git a/src/backend/tests/Unit/PaymentInitializationServiceTest.php b/src/backend/tests/Unit/PaymentInitializationServiceTest.php
index f600e4c20..7cd43c84e 100644
--- a/src/backend/tests/Unit/PaymentInitializationServiceTest.php
+++ b/src/backend/tests/Unit/PaymentInitializationServiceTest.php
@@ -11,12 +11,16 @@
use App\Services\MpmPluginService;
use App\Services\PaymentInitializationService;
use App\Services\PluginsService;
+use Illuminate\Contracts\Container\Container;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class PaymentInitializationServiceTest extends TestCase {
private PaymentInitializationService $service;
+ /** @var Container&MockObject */
+ private MockObject $container;
+
/** @var CashTransactionService&MockObject */
private MockObject $cashService;
@@ -31,11 +35,16 @@ protected function setUp(): void {
$pluginsService = $this->createMock(PluginsService::class);
$mpmPluginService = $this->createMock(MpmPluginService::class);
+ $this->container = $this->createMock(Container::class);
+ $this->container->method('make')->willReturnMap([
+ [CashTransactionService::class, [], $this->cashService],
+ [PaystackTransactionService::class, [], $this->paystackService],
+ ]);
+
$this->service = new PaymentInitializationService(
- $this->cashService,
- $this->paystackService,
$pluginsService,
$mpmPluginService,
+ $this->container,
);
}
@@ -50,18 +59,17 @@ public function testThrowsForUnknownProviderId(): void {
message: 'DEVICE-001',
type: 'deferred_payment',
customerId: 1,
- creatorId: 1,
);
}
- public function testDelegatesToCashTransactionServiceForProviderZero(): void {
+ public function testDelegatesToCashServiceForProviderZero(): void {
$transaction = new Transaction();
$this->cashService
->expects($this->once())
- ->method('createTransaction')
- ->with(1, 100.0, '+2340000', '42', 'deferred_payment')
- ->willReturn($transaction);
+ ->method('initializePayment')
+ ->with(100.0, '+2340000', '42', 'deferred_payment', 5, null)
+ ->willReturn(['transaction' => $transaction, 'provider_data' => []]);
$result = $this->service->initialize(
providerId: 0,
@@ -70,7 +78,6 @@ public function testDelegatesToCashTransactionServiceForProviderZero(): void {
message: '42',
type: 'deferred_payment',
customerId: 5,
- creatorId: 1,
);
$this->assertSame($transaction, $result['transaction']);
@@ -99,7 +106,6 @@ public function testDelegatesToPaystackServiceForPaystackProvider(): void {
message: '42',
type: 'deferred_payment',
customerId: 5,
- creatorId: 1,
);
$this->assertSame($transaction, $result['transaction']);
@@ -109,7 +115,8 @@ public function testDelegatesToPaystackServiceForPaystackProvider(): void {
public function testDoesNotCallPaystackServiceForCashProvider(): void {
$transaction = new Transaction();
- $this->cashService->method('createTransaction')->willReturn($transaction);
+ $this->cashService->method('initializePayment')
+ ->willReturn(['transaction' => $transaction, 'provider_data' => []]);
$this->paystackService->expects($this->never())->method('initializePayment');
$this->service->initialize(
@@ -119,7 +126,6 @@ public function testDoesNotCallPaystackServiceForCashProvider(): void {
message: '1',
type: 'deferred_payment',
customerId: 1,
- creatorId: 1,
);
}
@@ -130,7 +136,7 @@ public function testDoesNotCallCashServiceForPaystackProvider(): void {
'transaction' => $transaction,
'provider_data' => ['redirect_url' => 'https://paystack.com/pay/x', 'reference' => 'ref_x'],
]);
- $this->cashService->expects($this->never())->method('createTransaction');
+ $this->cashService->expects($this->never())->method('initializePayment');
$this->service->initialize(
providerId: MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
@@ -139,7 +145,6 @@ public function testDoesNotCallCashServiceForPaystackProvider(): void {
message: '1',
type: 'deferred_payment',
customerId: 1,
- creatorId: 1,
);
}
@@ -162,7 +167,6 @@ public function testPassesSerialIdToPaystackServiceWhenProvided(): void {
message: 'SERIAL-001',
type: 'deferred_payment',
customerId: 5,
- creatorId: 1,
serialId: 'SERIAL-001',
);
}
From e170a391a5ee40fb55babc071199b8131575f7a7 Mon Sep 17 00:00:00 2001
From: Obinna Ikeh
Date: Wed, 1 Apr 2026 00:52:26 +0100
Subject: [PATCH 5/5] Fix broken tests
---
src/backend/app/Services/PluginsService.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/app/Services/PluginsService.php b/src/backend/app/Services/PluginsService.php
index b5d50ba61..6c0d43b98 100644
--- a/src/backend/app/Services/PluginsService.php
+++ b/src/backend/app/Services/PluginsService.php
@@ -59,7 +59,7 @@ public function getActivePaymentProviders(): Collection {
MpmPlugin::MESOMB_PAYMENT_PROVIDER,
MpmPlugin::WAVE_MONEY_PAYMENT_PROVIDER,
MpmPlugin::WAVECOM_PAYMENT_PROVIDER,
- MpmPlugin::VODACOM_MOBILE_MONEY,
+ MpmPlugin::VODACOM_MZ_PAYMENT_PROVIDER,
MpmPlugin::PAYSTACK_PAYMENT_PROVIDER,
];