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
22 changes: 22 additions & 0 deletions src/backend/app/Http/Controllers/AgentAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace App\Http\Controllers;

use App\Models\Agent;
use App\Models\MainSettings;
use App\Services\AgentService;
use App\Services\MainSettingsService;
use App\Utils\DemoCompany;
use Dedoc\Scramble\Attributes\BodyParameter;
use Dedoc\Scramble\Attributes\Group;
Expand All @@ -18,6 +20,7 @@ class AgentAuthController extends Controller {
*/
public function __construct(
private AgentService $agentService,
private MainSettingsService $mainSettingsService,
) {}

/**
Expand Down Expand Up @@ -83,6 +86,7 @@ public function me(): JsonResponse {
'agent' => $agent,
'roles' => $roles,
'permissions' => $permissions,
'settings' => $this->mainSettings(),
]);
}

Expand Down Expand Up @@ -128,6 +132,24 @@ protected function respondWithToken($token) {
'agent' => $agent,
'roles' => $roles,
'permissions' => $permissions,
'settings' => $this->mainSettings(),
]);
}

/**
* @return array<string, mixed>|null
*/
private function mainSettings(): ?array {
$settings = $this->mainSettingsService->getAll()->first();
if (!$settings instanceof MainSettings) {
return null;
}

return [
'currency' => $settings->currency,
'country' => $settings->country,
'language' => $settings->language,
'company_name' => $settings->company_name,
];
}
}
30 changes: 30 additions & 0 deletions src/backend/app/Http/Controllers/AgentCustomerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

namespace App\Http\Controllers;

use App\Http\Requests\CreateAgentCustomerRequest;
use App\Http\Resources\ApiResource;
use App\Services\AgentCustomerService;
use App\Services\AgentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class AgentCustomerController extends Controller {
public function __construct(
Expand All @@ -24,11 +29,36 @@ public function index(Request $request) {
return ApiResource::make($this->agentCustomerService->list($agent));
}

public function show(int $customerId): ApiResource {
$agent = $this->agentService->getByAuthenticatedUser();

return ApiResource::make($this->agentCustomerService->findForAgent($agent, $customerId));
}

public function search(Request $request): ApiResource {
$term = $request->input('term');
$limit = $request->input('paginate', 15);
$agent = $this->agentService->getByAuthenticatedUser();

return ApiResource::make($this->agentCustomerService->search($term, $limit, $agent));
}

public function store(CreateAgentCustomerRequest $request): JsonResponse {
$agent = $this->agentService->getByAuthenticatedUser();

try {
DB::connection('tenant')->beginTransaction();
$person = $this->agentCustomerService->register($agent, $request);
DB::connection('tenant')->commit();

return ApiResource::make($person)->response()->setStatusCode(201);
} catch (ValidationException $e) {
DB::connection('tenant')->rollBack();
throw $e;
} catch (\Exception $e) {
DB::connection('tenant')->rollBack();
Log::critical('Error while an agent was registering a customer', ['message' => $e->getMessage()]);
throw $e;
}
}
}
21 changes: 21 additions & 0 deletions src/backend/app/Http/Controllers/AgentTransactionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http\Controllers;

use App\Http\Resources\ApiResource;
use App\Models\Transaction\Transaction;
use App\Services\AgentService;
use App\Services\AgentTransactionService;
use Illuminate\Http\Request;
Expand All @@ -27,4 +28,24 @@ public function show(int $customerId, Request $request): ApiResource {

return ApiResource::make($this->agentTransactionService->getByCustomerId($agent->id, $customerId));
}

/**
* Return the token generated for one of the agent's transactions, if any.
* Token generation is asynchronous (queued in ProcessPayment), so the
* field app polls this endpoint after a successful POST until the token
* appears or it gives up.
*/
public function token(int $transactionId): ApiResource {
$agent = $this->agentService->getByAuthenticatedUser();
$transaction = $this->agentTransactionService->findForAgent($agent->id, $transactionId);

if (!$transaction instanceof Transaction) {
abort(404, 'Transaction not found.');
}

return ApiResource::make([
'transaction_id' => $transaction->id,
'token' => $transaction->token,
]);
}
}
6 changes: 5 additions & 1 deletion src/backend/app/Http/Controllers/TransactionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function show(int $id): ApiResource {
return ApiResource::make($transaction);
}

public function store(Request $request): void {
public function store(Request $request): ApiResource {
/**
* @var ITransactionProvider $transactionProvider
*/
Expand All @@ -44,5 +44,9 @@ public function store(Request $request): void {
Log::warning('Company ID not found in request attributes. Payment transaction job not triggered for transaction '.$transaction->id);
}
}

return ApiResource::make([
'id' => $transaction->id ?? null,
]);
}
}
2 changes: 1 addition & 1 deletion src/backend/app/Http/Middleware/AgentBalanceMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function handle($request, \Closure $next) {
throw new DownPaymentBiggerThanAmountException('Down payment is bigger than amount');
}
}
if ($routeName === 'agent-transaction') {
if (in_array($routeName, ['agent-transaction', 'agent-app-transaction'], true)) {
if ($transactionAmount = $request->input('amount')) {
$agentBalance -= $transactionAmount;
} else {
Expand Down
24 changes: 24 additions & 0 deletions src/backend/app/Http/Requests/CreateAgentCustomerRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateAgentCustomerRequest extends FormRequest {
public function authorize(): bool {
return true;
}

/**
* @return array<string, mixed>
*/
public function rules(): array {
return [
'name' => ['required', 'string', 'min:3'],
'surname' => ['required', 'string', 'min:3'],
'phone' => ['required', 'string', 'phone:INTERNATIONAL'],
'city_id' => ['required', 'integer', 'exists:tenant.cities,id'],
'geo_points' => ['nullable', 'string'],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Http\Requests;

use App\Models\AgentAssignedAppliances;
use App\Models\ApplianceType;
use Illuminate\Foundation\Http\FormRequest;

class CreateAgentSoldApplianceRequest extends FormRequest {
Expand All @@ -18,18 +20,42 @@ public function authorize(): bool {
* @return array<string, mixed>
*/
public function rules(): array {
$deviceSerialRules = $this->isShsAppliance()
? ['required', 'string']
: ['nullable', 'string'];

return [
'person_id' => ['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'],
'device_serial' => $deviceSerialRules,
'address' => ['nullable', 'array'],
'points' => ['nullable', 'string'],
'minimum_payable_amount' => ['nullable', 'integer', 'min:0'],
'price_per_day' => ['nullable', 'integer', 'min:0'],
];
}

/**
* @return array<string, string>
*/
public function messages(): array {
return [
'device_serial.required' => 'Device serial is required for solar home system sales.',
];
}

private function isShsAppliance(): bool {
$assignedId = $this->input('agent_assigned_appliance_id');
if (!$assignedId) {
return false;
}

$assigned = AgentAssignedAppliances::with('appliance')->find($assignedId);

return $assigned?->appliance?->appliance_type_id === ApplianceType::APPLIANCE_TYPE_SHS;
}
}
59 changes: 54 additions & 5 deletions src/backend/app/Services/AgentCustomerService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,78 @@

namespace App\Services;

use App\Http\Requests\CreateAgentCustomerRequest;
use App\Models\Agent;
use App\Models\City;
use App\Models\Person\Person;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;

class AgentCustomerService {
public function __construct(private Person $person) {}
public function __construct(
private Person $person,
private City $city,
private PersonService $personService,
private GeographicalInformationService $geographicalInformationService,
private AddressGeographicalInformationService $addressGeographicalInformationService,
) {}

public function register(Agent $agent, CreateAgentCustomerRequest $request): Person {
$cityId = $request->integer('city_id');
$phone = $request->string('phone')->toString();
$geoPoints = $request->string('geo_points')->toString();

$city = $this->city->newQuery()->findOrFail($cityId);
if ($city->mini_grid_id !== $agent->mini_grid_id) {
throw ValidationException::withMessages(['city_id' => ['Selected city does not belong to the agent\'s mini-grid.']]);
}

if ($this->personService->getByPhoneNumber($phone) instanceof Person) {
throw ValidationException::withMessages(['phone' => ['A customer with this phone number already exists.']]);
}

$request->merge(['is_customer' => 1]);
$person = $this->personService->createFromRequest($request);

if ($geoPoints !== '') {
$address = $person->addresses()->where('is_primary', 1)->first();
$geographicalInformation = $this->geographicalInformationService->make([
'points' => $geoPoints,
]);
$this->addressGeographicalInformationService->setAssigned($geographicalInformation);
$this->addressGeographicalInformationService->setAssignee($address);
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geographicalInformation);
}

return $person->fresh(['addresses.city', 'addresses.geo']);
}

/**
* @return LengthAwarePaginator<int, Person>
*/
public function list(Agent $agent): LengthAwarePaginator {
$miniGridId = $agent->mini_grid_id;
return $this->scopedQuery($agent)->paginate(config('settings.paginate'));
}

public function findForAgent(Agent $agent, int $customerId): Person {
return $this->scopedQuery($agent)->findOrFail($customerId);
}

/**
* @return Builder<Person>
*/
private function scopedQuery(Agent $agent): Builder {
return $this->person->newQuery()->with([
'devices',
'addresses' => fn ($q) => $q->where('is_primary', 1)->with('city'),
])
->where('is_customer', 1)
->whereHas(
'addresses',
fn ($q) => $q->whereHas('city', fn ($q) => $q->where('mini_grid_id', $miniGridId))
)
->paginate(config('settings.paginate'));
fn ($q) => $q->whereHas('city', fn ($q) => $q->where('mini_grid_id', $agent->mini_grid_id))
);
}

/**
Expand Down
18 changes: 10 additions & 8 deletions src/backend/app/Services/AgentSoldApplianceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public function processSaleFromRequest(AgentSoldAppliance $agentSoldAppliance, a
'street' => $addressFromCustomer->street,
'city_id' => $addressFromCustomer->city_id,
];
$points = $requestData['points'] ?? $addressFromCustomer->geo()->first()->points;
$points = $requestData['points'] ?? $addressFromCustomer->geo()->first()?->points;

$device = $this->deviceService->getBySerialNumber($deviceSerial);
$this->deviceService->update($device, ['person_id' => $requestData['person_id']]);
Expand All @@ -218,14 +218,16 @@ public function processSaleFromRequest(AgentSoldAppliance $agentSoldAppliance, a
// Attach the new address to the buyer (person) rather than the device.
$this->addressesService->assignAddressToOwner($appliancePerson->person, $address);

$geoInfo = $this->geographicalInformationService->make([
'points' => $points,
]);
if ($points) {
$geoInfo = $this->geographicalInformationService->make([
'points' => $points,
]);

$this->addressGeographicalInformationService->setAssigned($geoInfo);
$this->addressGeographicalInformationService->setAssignee($address);
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geoInfo);
$this->addressGeographicalInformationService->setAssigned($geoInfo);
$this->addressGeographicalInformationService->setAssignee($address);
$this->addressGeographicalInformationService->assign();
$this->geographicalInformationService->save($geoInfo);
}
}

// initalize appliance Rates
Expand Down
16 changes: 16 additions & 0 deletions src/backend/app/Services/AgentTransactionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ public function getById(int $id): AgentTransaction {
throw new \Exception('Method getById() not yet implemented.');
}

/**
* Find a transaction owned by the given agent and eager-load its token.
* Returns null if the transaction doesn't exist or doesn't belong to the agent.
*/
public function findForAgent(int $agentId, int $transactionId): ?Transaction {
return $this->transaction->newQuery()
->with(['token'])
->whereHasMorph(
'originalTransaction',
[AgentTransaction::class],
fn ($q) => $q->where('agent_id', $agentId)
)
->where('id', $transactionId)
->first();
}

/**
* @param array<string, mixed> $transactionData
*/
Expand Down
Loading
Loading