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
36 changes: 23 additions & 13 deletions src/backend/app/DTO/TransactionDataContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
class TransactionDataContainer {
public int $accessRateDebt;
public Transaction $transaction;
public Device $device;
public ?Device $device = null;
public ?Tariff $tariff = null;
public Manufacturer $manufacturer;
public ?Manufacturer $manufacturer = null;
public Token $token;
/** @var array<int, array<string, float|int>> */
public array $paidRates;
Expand Down Expand Up @@ -48,18 +48,20 @@ public static function initialize(Transaction $transaction): TransactionDataCont
$container->meter = null;

try {
// Get device by serial number
$container->device = $deviceService->getBySerialNumber($transaction->message);
// Get device by serial number (may be null for general appliances like TV, bulbs, etc.)
$device = $deviceService->getBySerialNumber($transaction->message);

// Get the associated device model (Meter or SHS)
$deviceModel = $container->device->device;
$container->manufacturer = $deviceModel->manufacturer ?? null;
if ($device !== null) {
$container->device = $device;

// Handle device type specific logic
if ($deviceModel instanceof Meter) {
$container->handleMeterDevice($deviceModel);
} elseif ($deviceModel instanceof SolarHomeSystem) {
$container->handleSHSDevice($deviceModel);
$deviceModel = $container->device->device;
$container->manufacturer = $deviceModel->manufacturer ?? null;

if ($deviceModel instanceof Meter) {
$container->handleMeterDevice($deviceModel);
} elseif ($deviceModel instanceof SolarHomeSystem) {
$container->handleSHSDevice($deviceModel);
}
}

// Handle appliance payments if any
Expand Down Expand Up @@ -92,7 +94,15 @@ private function handleSHSDevice(SolarHomeSystem $shs): void {
* Handle appliance payment related initialization.
*/
private function handleAppliancePayments(Transaction $transaction): void {
$this->appliancePerson = $transaction->appliance()->first();
if ($transaction->paygoAppliance()->exists()) {
/** @var AppliancePerson|null $appliancePerson */
$appliancePerson = $transaction->paygoAppliance()->first();
$this->appliancePerson = $appliancePerson;
} else {
/** @var AppliancePerson|null $appliancePerson */
$appliancePerson = $transaction->nonPaygoAppliance()->first();
$this->appliancePerson = $appliancePerson;
}

if ($this->appliancePerson) {
$installments = $this->appliancePerson->rates;
Expand Down
5 changes: 5 additions & 0 deletions src/backend/app/Exceptions/TransactionNotMatchedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace App\Exceptions;

class TransactionNotMatchedException extends \Exception {}
51 changes: 48 additions & 3 deletions src/backend/app/Http/Controllers/AppliancePaymentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,69 @@
namespace App\Http\Controllers;

use App\Http\Resources\ApiResource;
use App\Jobs\ProcessPayment;
use App\Models\AppliancePerson;
use App\Services\AppliancePaymentService;
use App\Services\AppliancePersonService;
use App\Services\CashTransactionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class AppliancePaymentController extends Controller {
public function __construct(private AppliancePaymentService $appliancePaymentService) {}
public function __construct(
private AppliancePaymentService $appliancePaymentService,
private AppliancePersonService $appliancePersonService,
private CashTransactionService $cashTransactionService,
) {}

public function store(AppliancePerson $appliancePerson, Request $request): ApiResource {
try {
DB::connection('tenant')->beginTransaction();
$this->appliancePaymentService->getPaymentForAppliance($request, $appliancePerson);
$result = $this->getPaymentForAppliance($request, $appliancePerson);
DB::connection('tenant')->commit();

return ApiResource::make($appliancePerson);
return ApiResource::make([
'appliance_person' => $result['appliance_person'],
'transaction_id' => $result['transaction_id'],
]);
} catch (\Exception $e) {
DB::connection('tenant')->rollBack();
throw new \Exception($e->getMessage(), $e->getCode(), $e);
}
}

public function checkStatus(int $transactionId): ApiResource {
$status = $this->appliancePaymentService->checkPaymentStatus($transactionId);

return ApiResource::make($status);
}

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

dispatch(new ProcessPayment($companyId, $transaction->id));

return [
'appliance_person' => $appliancePerson,
'transaction_id' => $transaction->id,
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public function __construct(
*/
public function store(
Appliance $appliance,
Person $person,
Request $request,
): ApiResource {
try {
Expand Down
30 changes: 26 additions & 4 deletions src/backend/app/Jobs/ApplianceTransactionProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

use App\DTO\TransactionDataContainer;
use App\Events\TransactionFailedEvent;
use App\Events\TransactionSuccessfulEvent;
use App\Exceptions\ApplianceTokenNotProcessedException;
use App\Exceptions\TransactionAmountNotEnoughException;
use App\Exceptions\TransactionNotInitializedException;
use App\Models\Appliance;
use App\Models\Transaction\Transaction;
use App\Services\AppliancePaymentService;
use App\Utils\ApplianceInstallmentPayer;
use Illuminate\Support\Facades\Log;

Expand All @@ -31,17 +34,36 @@ public function executeJob(): void {
$this->initializeTransaction();
$container = $this->initializeTransactionDataContainer();

$originalTransaction = $this->transaction->originalTransaction()->first();
if ($originalTransaction?->conflicts()->exists()) {
return; // skip transaction processing if it has conflicts
}

try {
$this->checkForMinimumPurchaseAmount($container);
$container = $this->payApplianceInstallments($container);
$this->processToken($container);
$appliance = $container->appliancePerson->appliance;
$this->processTransactionPayment($container, $appliance);
} catch (\Exception $e) {
Log::info('Transaction failed.: '.$e->getMessage());
event(new TransactionFailedEvent($this->transaction, $e->getMessage()));
throw new ApplianceTokenNotProcessedException($e->getMessage(), $e->getCode(), $e);
}
}

private function processTransactionPayment(TransactionDataContainer $container, Appliance $appliance): void {
$isPaygo = $appliance->applianceType->paygo_enabled;

$this->checkForMinimumPurchaseAmount($container);
$container = $this->payApplianceInstallments($container);
if ($isPaygo) {
$this->processToken($container);
} else {
$appliancePaymentService = resolve(AppliancePaymentService::class);
$creatorId = $this->transaction->originalTransaction()->first()->user_id ?? 0;
$appliancePaymentService->createPaymentLog($container->appliancePerson, $container->amount, $creatorId);
event(new TransactionSuccessfulEvent($this->transaction));
}
}

private function initializeTransaction(): void {
$this->transaction = Transaction::query()->find($this->transactionId);
$this->transaction->type = 'deferred_payment';
Expand All @@ -60,7 +82,7 @@ private function initializeTransactionDataContainer(): TransactionDataContainer
private function checkForMinimumPurchaseAmount(TransactionDataContainer $container): void {
$minimumPurchaseAmount = $container->installmentCost;
if ($container->amount < $minimumPurchaseAmount) {
throw new TransactionAmountNotEnoughException("Minimum purchase amount not reached for {$container->device->device_serial}");
throw new TransactionAmountNotEnoughException("Minimum purchase amount not reached for appliance with serial id:{$container->transaction->message}");
}
}

Expand Down
20 changes: 11 additions & 9 deletions src/backend/app/Jobs/TokenProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,17 @@ private function saveToken(array $tokenData): void {
private function handlePaymentEvents(Token $token): void {
$owner = $this->transactionContainer->device->person;

event(new PaymentSuccessEvent(
amount: (int) $this->transactionContainer->transaction->amount,
paymentService: $this->transactionContainer->transaction->original_transaction_type,
paymentType: 'energy',
sender: $this->transactionContainer->transaction->sender,
paidFor: $token,
payer: $owner,
transaction: $this->transactionContainer->transaction,
));
if (Token::TYPE_ENERGY == $token->token_type) {
event(new PaymentSuccessEvent(
amount: (int) $this->transactionContainer->transaction->amount,
paymentService: $this->transactionContainer->transaction->original_transaction_type,
paymentType: 'energy',
sender: $this->transactionContainer->transaction->sender,
paidFor: $token,
payer: $owner,
transaction: $this->transactionContainer->transaction,
));
}

event(new TransactionSuccessfulEvent($this->transactionContainer->transaction));
}
Expand Down
9 changes: 8 additions & 1 deletion src/backend/app/Models/Transaction/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,17 @@ public function device(): HasOne {
/**
* @return HasOne<AppliancePerson, $this>
*/
public function appliance(): HasOne {
public function paygoAppliance(): HasOne {
return $this->hasOne(AppliancePerson::class, 'device_serial', 'message');
}

/**
* @return HasOne<AppliancePerson, $this>
*/
public function nonPaygoAppliance(): HasOne {
return $this->hasOne(AppliancePerson::class, 'id', 'message');
}

/**
* @return array<int, array<string, mixed>>
*/
Expand Down
90 changes: 24 additions & 66 deletions src/backend/app/Services/AppliancePaymentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,22 @@

namespace App\Services;

use App\DTO\TransactionDataContainer;
use App\Events\NewLogEvent;
use App\Events\PaymentSuccessEvent;
use App\Exceptions\PaymentAmountBiggerThanTotalRemainingAmount;
use App\Exceptions\PaymentAmountSmallerThanZero;
use App\Models\AppliancePerson;
use App\Models\ApplianceRate;
use App\Models\Device;
use App\Models\MainSettings;
use App\Models\Token;
use App\Models\Transaction\Transaction;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class AppliancePaymentService {
private float $paymentAmount;
public bool $applianceInstallmentsFullFilled = false;

public function __construct(private CashTransactionService $cashTransactionService, private MainSettings $mainSettings, private AppliancePersonService $appliancePersonService, private DeviceService $deviceService) {}

public function getPaymentForAppliance(Request $request, AppliancePerson $appliancePerson): AppliancePerson {
$creatorId = auth('api')->user()->id;
$this->paymentAmount = $amount = (float) $request->input('amount');
$applianceDetail = $this->appliancePersonService->getApplianceDetails($appliancePerson->id);
$this->validateAmount($applianceDetail, $amount);
$deviceSerial = $applianceDetail->device_serial;
$applianceOwner = $appliancePerson->person;

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);
$totalRemainingAmount = $applianceDetail->rates->sum('remaining');
$this->applianceInstallmentsFullFilled = $totalRemainingAmount <= $amount;
$applianceDetail->rates->map(fn (ApplianceRate $installment) => $this->payInstallment(
$installment,
$appliancePerson, // Changed from $applianceOwner to $appliancePerson
$transaction
));
if ($applianceDetail->device_serial) {
$this->processPaymentForDevice($deviceSerial, $transaction, $applianceDetail);
} else {
$this->createPaymentLog($appliancePerson, $amount, $creatorId);
}

return $appliancePerson;
}
public function __construct(private MainSettings $mainSettings) {}

public function updateRateRemaining(int $id, float $amount): ApplianceRate {
$applianceRate = ApplianceRate::query()->findOrFail($id);
Expand Down Expand Up @@ -87,7 +50,7 @@ public function createPaymentHistory(float $amount, AppliancePerson $buyer, Appl
));
}

private function validateAmount(AppliancePerson $applianceDetail, float $amount): void {
public function validateAmount(AppliancePerson $applianceDetail, float $amount): void {
$totalRemainingAmount = $applianceDetail->rates->sum('remaining');
$installmentCost = $applianceDetail->rates[1]['rate_cost'] ?? 0;

Expand Down Expand Up @@ -118,33 +81,6 @@ public function payInstallment(ApplianceRate $installment, AppliancePerson $appl
}
}

private function processPaymentForDevice(string $deviceSerial, Transaction $transaction, AppliancePerson $applianceDetail): void {
$device = $this->deviceService->getBySerialNumber($deviceSerial);

if (!$device instanceof Device) {
throw new ModelNotFoundException("No device found with $deviceSerial");
}

$manufacturer = $device->device->manufacturer;
$installments = $applianceDetail->rates;
// Use this because we do not want to get down payment as installment
$secondInstallment = $applianceDetail->rates[1];
$installmentCost = $secondInstallment ? $secondInstallment['rate_cost']
: 0;
$dayDiff = $this->getDayDifferenceBetweenTwoInstallments($installments);
$transactionData = TransactionDataContainer::initialize($transaction);
$transactionData->installmentCost = $installmentCost;
$transactionData->dayDifferenceBetweenTwoInstallments = $dayDiff;
$transactionData->appliancePerson = $applianceDetail;
$manufacturerApi = resolve($manufacturer->api_name);
$transactionData->applianceInstallmentsFullFilled = $this->applianceInstallmentsFullFilled;

$tokenData = $manufacturerApi->chargeDevice($transactionData);
$token = Token::query()->make($tokenData);
$token->transaction()->associate($transactionData->transaction);
$token->save();
}

/**
* @param Collection<int, ApplianceRate> $installments
*/
Expand Down Expand Up @@ -176,4 +112,26 @@ public function getDayDifferenceBetweenTwoInstallments(Collection $installments)
public function setPaymentAmount(float $amount): void {
$this->paymentAmount = $amount;
}

/**
* @return array<string, mixed>
*/
public function checkPaymentStatus(int $transactionId): array {
$transaction = Transaction::query()->find($transactionId);

if (!$transaction) {
return [
'status' => 'not_found',
'processed' => false,
];
}

$hasPaymentHistories = $transaction->paymentHistories()->exists();

return [
'status' => $hasPaymentHistories ? 'processed' : 'processing',
'processed' => $hasPaymentHistories,
'transaction_id' => $transactionId,
];
}
}
2 changes: 1 addition & 1 deletion src/backend/app/Services/AppliancePersonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function getLoansForCustomerId(int $customerId) {
}

public function getById(int $id): AppliancePerson {
throw new \Exception('Method getById() not yet implemented.');
return $this->appliancePerson->newQuery()->findOrFail($id);
}

/**
Expand Down
Loading
Loading