Skip to content
Merged
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
17 changes: 11 additions & 6 deletions src/backend/app/DTO/TransactionDataContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,17 @@ private function handleAppliancePayments(Transaction $transaction): void {
}

if ($this->appliancePerson) {
$installments = $this->appliancePerson->rates;
$appliancePaymentService = app()->make(AppliancePaymentService::class);
$secondInstallment = $installments[1] ?? null;
$this->installmentCost = $secondInstallment ? $secondInstallment['rate_cost'] : 0;
$this->dayDifferenceBetweenTwoInstallments =
$appliancePaymentService->getDayDifferenceBetweenTwoInstallments($installments);
if ($this->appliancePerson->isEnergyService()) {
$this->installmentCost = $this->appliancePerson->price_per_day ?? 0;
$this->dayDifferenceBetweenTwoInstallments = 1;
} else {
$installments = $this->appliancePerson->rates;
$appliancePaymentService = app()->make(AppliancePaymentService::class);
$secondInstallment = $installments[1] ?? null;
$this->installmentCost = $secondInstallment ? $secondInstallment['rate_cost'] : 0;
$this->dayDifferenceBetweenTwoInstallments =
$appliancePaymentService->getDayDifferenceBetweenTwoInstallments($installments);
}
}
}
}
167 changes: 100 additions & 67 deletions src/backend/app/Http/Controllers/AppliancePersonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use App\Models\Appliance;
use App\Models\AppliancePerson;
use App\Models\Person\Person;
use App\Models\Transaction\Transaction;
use App\Models\User;
use App\Services\AddressesService;
use App\Services\AddressGeographicalInformationService;
use App\Services\AppliancePersonService;
Expand Down Expand Up @@ -36,88 +38,34 @@ public function __construct(
private ApplianceRateService $applianceRateService,
) {}

/**
* Store a newly created resource in storage.
*/
public function store(
Appliance $appliance,
Request $request,
): ApiResource {
try {
$userId = $request->input('user_id');
$applianceId = $request->input('id');
$personId = $request->input('person_id');
$cost = (int) $request->input('cost');
$installmentCount = (int) $request->input('rate');
$downPayment = (float) $request->input('down_payment');
$user = $this->userService->getById($request->input('user_id'));
$appliance = $this->applianceService->getById($request->input('id'));
$paymentType = $request->input('payment_type', AppliancePerson::PAYMENT_TYPE_INSTALLMENT);
$isEnergyService = $paymentType === AppliancePerson::PAYMENT_TYPE_ENERGY_SERVICE;
$downPayment = (float) $request->input('down_payment', 0);
$deviceSerial = $request->input('device_serial');
$addressData = $request->input('address');
$user = $this->userService->getById($userId);
$appliance = $this->applianceService->getById($applianceId);
$installmentType = $request->input('rate_type');
$points = $request->input('points');

DB::connection('tenant')->beginTransaction();

$appliancePerson = $this->appliancePersonService->make([
'appliance_id' => $applianceId,
'person_id' => $personId,
'total_cost' => $cost,
'rate_count' => $installmentCount,
'down_payment' => $downPayment,
'device_serial' => $deviceSerial,
]);
$this->userAppliancePersonService->setAssigned($appliancePerson);
$this->userAppliancePersonService->setAssignee($user);
$this->userAppliancePersonService->assign();
$this->appliancePersonService->save($appliancePerson);
$preferredPrice = $appliance->price;
if ($cost !== $preferredPrice) {
$this->appliancePersonService->createLogForSoldAppliance($appliancePerson, $cost, $preferredPrice);
$appliancePerson = $this->createAppliancePerson($request, $user, $paymentType);

if (!$isEnergyService) {
$this->createInstallmentRates($appliancePerson, $appliance, $request->input('rate_type'));
}
$this->applianceRateService->create($appliancePerson, $installmentType);

if ($deviceSerial) {
$device = $this->deviceService->getBySerialNumber($deviceSerial);
$this->deviceService->update($device, ['person_id' => $personId]);
$appliancePerson->device_serial = $deviceSerial;

$address = $this->addressesService->make([
'street' => $addressData['street'],
'city_id' => $addressData['city_id'],
]);

// Attach the new address to the person rather than the device.
$this->addressesService->assignAddressToOwner($appliancePerson->person, $address);

$geographicalInformation = $this->geographicalInformationService->make([
'points' => $points,
]);
$this->addressGeographicalInformationService->setAssigned($geographicalInformation);
$this->addressGeographicalInformationService->setAssignee($address);
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geographicalInformation);
$this->assignDevice($appliancePerson, $request);
}

if ($downPayment > 0) {
$sender = isset($addressData) ? $addressData['phone'] : '-';
$transaction = $this->cashTransactionService->createCashTransaction(
$user->id,
$downPayment,
$sender,
$deviceSerial
);
$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));
$this->processDownPayment($appliancePerson, $user, $downPayment, $request);
}

DB::connection('tenant')->commit();

return ApiResource::make($appliancePerson);
Expand All @@ -127,6 +75,91 @@ public function store(
}
}

private function createAppliancePerson(Request $request, ?User $user, string $paymentType): AppliancePerson {
$isEnergyService = $paymentType === AppliancePerson::PAYMENT_TYPE_ENERGY_SERVICE;

$appliancePerson = $this->appliancePersonService->make([
'appliance_id' => $request->input('id'),
'person_id' => $request->input('person_id'),
'total_cost' => $isEnergyService ? 0 : (int) $request->input('cost'),
'rate_count' => $isEnergyService ? 0 : (int) $request->input('rate'),
'down_payment' => (float) $request->input('down_payment', 0),
'device_serial' => $request->input('device_serial'),
'payment_type' => $paymentType,
'minimum_payable_amount' => $isEnergyService ? $request->input('minimum_payable_amount') : null,
'price_per_day' => $isEnergyService ? $request->input('price_per_day') : null,
]);

$this->userAppliancePersonService->setAssigned($appliancePerson);
$this->userAppliancePersonService->setAssignee($user);
$this->userAppliancePersonService->assign();
$this->appliancePersonService->save($appliancePerson);

return $appliancePerson;
}

private function createInstallmentRates(AppliancePerson $appliancePerson, Appliance $appliance, string $installmentType): void {
$cost = (int) $appliancePerson->total_cost;
$preferredPrice = $appliance->price;

if ($cost !== $preferredPrice) {
$this->appliancePersonService->createLogForSoldAppliance($appliancePerson, $cost, $preferredPrice);
}

$this->applianceRateService->create($appliancePerson, $installmentType);
}

private function assignDevice(AppliancePerson $appliancePerson, Request $request): void {
$deviceSerial = $request->input('device_serial');
$addressData = $request->input('address');
$points = $request->input('points');

$device = $this->deviceService->getBySerialNumber($deviceSerial);
$this->deviceService->update($device, ['person_id' => $appliancePerson->person_id]);

$address = $this->addressesService->make([
'street' => $addressData['street'],
'city_id' => $addressData['city_id'],
]);

$this->addressesService->assignAddressToOwner($appliancePerson->person, $address);

$geographicalInformation = $this->geographicalInformationService->make([
'points' => $points,
]);
$this->addressGeographicalInformationService->setAssigned($geographicalInformation);
$this->addressGeographicalInformationService->setAssignee($address);
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geographicalInformation);
}

private function processDownPayment(AppliancePerson $appliancePerson, ?User $user, float $downPayment, Request $request): void {
$addressData = $request->input('address');
$deviceSerial = $request->input('device_serial');
$sender = isset($addressData) ? $addressData['phone'] : '-';

$transaction = $this->cashTransactionService->createCashTransaction(
$user->id,
$downPayment,
$sender,
$deviceSerial,
type: Transaction::TYPE_DOWN_PAYMENT,
);

$applianceRate = $this->applianceRateService->createPaidRate($appliancePerson, $downPayment);

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));
}

/**
* Display the specified resource.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ public function authorize(): bool {
public function rules(): array {
return [
'person_id' => ['required'],
'down_payment' => ['required', 'numeric'],
'tenure' => ['required', 'numeric', 'min:0'],
'first_payment_date' => ['required'],
'payment_type' => ['nullable', 'string', 'in:installment,energy_service'],
'down_payment' => ['required_unless:payment_type,energy_service', 'numeric'],
'tenure' => ['required_unless:payment_type,energy_service', 'numeric', 'min:0'],
'first_payment_date' => ['required_unless:payment_type,energy_service'],
'agent_assigned_appliance_id' => ['required'],
'device_serial' => ['nullable', 'string'],
'address' => ['nullable', 'array'],
'points' => ['nullable', 'string'],
'minimum_payable_amount' => ['nullable', 'integer', 'min:0'],
'price_per_day' => ['nullable', 'integer', 'min:0'],
];
}
}
44 changes: 40 additions & 4 deletions src/backend/app/Jobs/ApplianceTransactionProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Jobs;

use App\DTO\TransactionDataContainer;
use App\Events\PaymentSuccessEvent;
use App\Events\TransactionFailedEvent;
use App\Events\TransactionSuccessfulEvent;
use App\Exceptions\ApplianceTokenNotProcessedException;
Expand All @@ -11,12 +12,12 @@
use App\Models\Appliance;
use App\Models\Transaction\Transaction;
use App\Services\AppliancePaymentService;
use App\Services\ApplianceRateService;
use App\Utils\ApplianceInstallmentPayer;
use Illuminate\Support\Facades\Log;

class ApplianceTransactionProcessor extends AbstractJob {
private Transaction $transaction;
protected const TYPE = 'deferred_payment';

public function __construct(int $companyId, private int $transactionId) {
$this->onConnection('redis');
Expand Down Expand Up @@ -66,7 +67,16 @@ private function processTransactionPayment(TransactionDataContainer $container,

private function initializeTransaction(): void {
$this->transaction = Transaction::query()->find($this->transactionId);
$this->transaction->type = 'deferred_payment';

if ($this->transaction->type !== Transaction::TYPE_DOWN_PAYMENT) {
$appliancePerson = $this->transaction->paygoAppliance()->first()
?? $this->transaction->nonPaygoAppliance()->first();

$this->transaction->type = ($appliancePerson && $appliancePerson->isEnergyService())
? Transaction::TYPE_EAAS_RATE
: Transaction::TYPE_DEFERRED_PAYMENT;
}

$this->transaction->save();
}

Expand All @@ -80,13 +90,39 @@ private function initializeTransactionDataContainer(): TransactionDataContainer
}

private function checkForMinimumPurchaseAmount(TransactionDataContainer $container): void {
$minimumPurchaseAmount = $container->installmentCost;
if ($container->amount < $minimumPurchaseAmount) {
$minimumPurchaseAmount = $container->appliancePerson->isEnergyService()
? ($container->appliancePerson->minimum_payable_amount ?? 0)
: $container->installmentCost;

if ($minimumPurchaseAmount > 0 && $container->amount < $minimumPurchaseAmount) {
throw new TransactionAmountNotEnoughException("Minimum purchase amount not reached for appliance with serial id:{$container->transaction->message}");
}
}

private function payApplianceInstallments(TransactionDataContainer $container): TransactionDataContainer {
if ($container->appliancePerson->isEnergyService()) {
$applianceRateService = resolve(ApplianceRateService::class);
$paidRate = $applianceRateService->createPaidRate($container->appliancePerson, $container->amount);
$container->paidRates = [
['appliance_rate_id' => $paidRate->id, 'paid' => $container->amount],
];

$container->applianceInstallmentsFullFilled = false;
// Success payment event means the client will update the ui as updated
// before the token get's generated.
event(new PaymentSuccessEvent(
amount: (int) $container->amount,
paymentService: $this->transaction->original_transaction_type,
paymentType: Transaction::TYPE_EAAS_RATE,
sender: $this->transaction->sender,
paidFor: $paidRate,
payer: $container->appliancePerson->person,
transaction: $this->transaction,
));

return $container;
}

$applianceInstallmentPayer = resolve(ApplianceInstallmentPayer::class);
$applianceInstallmentPayer->initialize($container);
$applianceInstallmentPayer->payInstallmentsForDevice($container);
Expand Down
6 changes: 5 additions & 1 deletion src/backend/app/Jobs/TokenProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Events\PaymentSuccessEvent;
use App\Events\TransactionFailedEvent;
use App\Events\TransactionSuccessfulEvent;
use App\Models\AppliancePerson;
use App\Models\ApplianceRate;
use App\Models\Token;
use Illuminate\Bus\Queueable;
Expand Down Expand Up @@ -74,7 +75,10 @@ private function handleExistingToken(): ?Token {

private function generateToken(mixed $api): void {
try {
if ($this->transactionContainer->applianceInstallmentsFullFilled) {
$isEnergyService = $this->transactionContainer->appliancePerson instanceof AppliancePerson
&& $this->transactionContainer->appliancePerson->isEnergyService();

if (!$isEnergyService && $this->transactionContainer->applianceInstallmentsFullFilled) {
$tokenData = $api->unlockDevice($this->transactionContainer);
} else {
$tokenData = $api->chargeDevice($this->transactionContainer);
Expand Down
10 changes: 10 additions & 0 deletions src/backend/app/Models/AppliancePerson.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
* @property float|null $down_payment
* @property Carbon|null $first_payment_date
* @property string|null $device_serial
* @property string $payment_type
* @property int|null $minimum_payable_amount
* @property int|null $price_per_day
* @property-read Appliance|null $appliance
* @property-read Model $creator
* @property-read Device|null $device
Expand All @@ -36,11 +39,18 @@
* @property-read Collection<int, ApplianceRate> $rates
*/
class AppliancePerson extends BaseModel {
public const PAYMENT_TYPE_INSTALLMENT = 'installment';
public const PAYMENT_TYPE_ENERGY_SERVICE = 'energy_service';

/** @var array<string, string> */
protected $dispatchesEvents = [
'created' => AppliancePersonCreated::class,
];

public function isEnergyService(): bool {
return $this->payment_type === self::PAYMENT_TYPE_ENERGY_SERVICE;
}

/**
* @return BelongsTo<Person, $this>
*/
Expand Down
5 changes: 5 additions & 0 deletions src/backend/app/Models/Transaction/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
*/
class Transaction extends BaseModel {
public const RELATION_NAME = 'transaction';
public const TYPE_ENERGY = 'energy';
public const TYPE_DEFERRED_PAYMENT = 'deferred_payment';
public const TYPE_EAAS_RATE = 'eaas_rate';
public const TYPE_DOWN_PAYMENT = 'down_payment';
public const TYPE_UNKNOWN = 'unknown';
public const TYPE_IMPORTED = 'imported';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ private function mapTransactionPurpose(?string $transactionType): string {
return match ($transactionType) {
'energy' => 'Paygo Payment',
'deferred_payment', 'loan', 'installment' => 'Loan repayment',
'eaas_rate' => 'Energy service payment',
'down_payment' => 'Down payment',
'access_rate' => 'Access rate payment',
default => 'Paygo Payment',
};
Expand Down
Loading
Loading