Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/development/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docs/usage-guide/images/payment-flow.excalidraw.svg
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diagram mentions Provider Validation. Where in the code does this happen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referencing to validations done on initializePayment for example with Paystack provider which goes through a call chain that ends up in InitializeTransactionResource which does validation on the structure of the data.

https://github.com/EnAccess/micropowermanager/pull/1385/changes#diff-d52a331b98749532774430e83f1cbed2cce947a64d05307ad552ece71ff25c4bR156

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 46 additions & 16 deletions src/backend/app/Http/Controllers/AppliancePaymentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,40 @@
use App\Models\AppliancePerson;
use App\Services\AppliancePaymentService;
use App\Services\AppliancePersonService;
use App\Services\CashTransactionService;
use App\Services\PaymentInitializationService;
use Illuminate\Http\JsonResponse;
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 {
public function store(AppliancePerson $appliancePerson, Request $request): ApiResource|JsonResponse {
try {
DB::connection('tenant')->beginTransaction();
$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 (\InvalidArgumentException $e) {
DB::connection('tenant')->rollBack();

return response()->json(['message' => $e->getMessage()], 422);
} catch (\Exception $e) {
DB::connection('tenant')->rollBack();
throw new \Exception($e->getMessage(), $e->getCode(), $e);
throw new \Exception($e->getMessage(), (int) $e->getCode(), $e);
}
}

Expand All @@ -40,32 +50,52 @@ public function checkStatus(int $transactionId): ApiResource {
return ApiResource::make($status);
}

public function paymentProviders(): ApiResource {
$providers = $this->paymentInitializationService->paymentProviders();

return ApiResource::make($providers);
}

/**
* @return array<string, mixed>
*/
public function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): array {
$creatorId = auth('api')->user()->id;
private function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): array {
$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;

dispatch(new ProcessPayment($companyId, $transaction->id));
$message = $deviceSerial ?? (string) $appliancePerson->id;

$result = $this->paymentInitializationService->initialize(
providerId: $providerId,
amount: $amount,
sender: $sender,
message: $message,
type: 'deferred_payment',
customerId: $applianceOwner->id,
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'],
];
}
}
56 changes: 37 additions & 19 deletions src/backend/app/Http/Controllers/AppliancePersonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
) {}
Expand Down Expand Up @@ -98,29 +101,44 @@ 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,
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);
Expand Down
6 changes: 6 additions & 0 deletions src/backend/app/Models/Plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Models;

use App\Models\Base\BaseModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

/**
Expand All @@ -15,4 +16,9 @@
class Plugins extends BaseModel {
public const ACTIVE = 1;
public const INACTIVE = 0;

/** @return BelongsTo<MpmPlugin, $this> */
public function mpmPlugin(): BelongsTo {
return $this->belongsTo(MpmPlugin::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
],
]);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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'];

Expand All @@ -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', [
Expand Down Expand Up @@ -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'),
],
Expand Down
Loading
Loading