Skip to content

Commit 68bae98

Browse files
authored
Feat: Field App operations improvment (#1472)
* agent app: add agent register endpoint that all agents registerating customers * agent app: refactor transaction creation for agents on behalf of customers * agent app: add settings field to agent info endpoint * agent app: fetch agent customers using endpoint * agent app: fetch transaction status for token generated for a payment * Apply quality check and unit test fixes
1 parent 9324553 commit 68bae98

13 files changed

Lines changed: 460 additions & 17 deletions

src/backend/app/Http/Controllers/AgentAuthController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace App\Http\Controllers;
44

55
use App\Models\Agent;
6+
use App\Models\MainSettings;
67
use App\Services\AgentService;
8+
use App\Services\MainSettingsService;
79
use App\Utils\DemoCompany;
810
use Dedoc\Scramble\Attributes\BodyParameter;
911
use Dedoc\Scramble\Attributes\Group;
@@ -18,6 +20,7 @@ class AgentAuthController extends Controller {
1820
*/
1921
public function __construct(
2022
private AgentService $agentService,
23+
private MainSettingsService $mainSettingsService,
2124
) {}
2225

2326
/**
@@ -83,6 +86,7 @@ public function me(): JsonResponse {
8386
'agent' => $agent,
8487
'roles' => $roles,
8588
'permissions' => $permissions,
89+
'settings' => $this->mainSettings(),
8690
]);
8791
}
8892

@@ -128,6 +132,24 @@ protected function respondWithToken($token) {
128132
'agent' => $agent,
129133
'roles' => $roles,
130134
'permissions' => $permissions,
135+
'settings' => $this->mainSettings(),
131136
]);
132137
}
138+
139+
/**
140+
* @return array<string, mixed>|null
141+
*/
142+
private function mainSettings(): ?array {
143+
$settings = $this->mainSettingsService->getAll()->first();
144+
if (!$settings instanceof MainSettings) {
145+
return null;
146+
}
147+
148+
return [
149+
'currency' => $settings->currency,
150+
'country' => $settings->country,
151+
'language' => $settings->language,
152+
'company_name' => $settings->company_name,
153+
];
154+
}
133155
}

src/backend/app/Http/Controllers/AgentCustomerController.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22

33
namespace App\Http\Controllers;
44

5+
use App\Http\Requests\CreateAgentCustomerRequest;
56
use App\Http\Resources\ApiResource;
67
use App\Services\AgentCustomerService;
78
use App\Services\AgentService;
9+
use Illuminate\Http\JsonResponse;
810
use Illuminate\Http\Request;
11+
use Illuminate\Support\Facades\DB;
12+
use Illuminate\Support\Facades\Log;
13+
use Illuminate\Validation\ValidationException;
914

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

32+
public function show(int $customerId): ApiResource {
33+
$agent = $this->agentService->getByAuthenticatedUser();
34+
35+
return ApiResource::make($this->agentCustomerService->findForAgent($agent, $customerId));
36+
}
37+
2738
public function search(Request $request): ApiResource {
2839
$term = $request->input('term');
2940
$limit = $request->input('paginate', 15);
3041
$agent = $this->agentService->getByAuthenticatedUser();
3142

3243
return ApiResource::make($this->agentCustomerService->search($term, $limit, $agent));
3344
}
45+
46+
public function store(CreateAgentCustomerRequest $request): JsonResponse {
47+
$agent = $this->agentService->getByAuthenticatedUser();
48+
49+
try {
50+
DB::connection('tenant')->beginTransaction();
51+
$person = $this->agentCustomerService->register($agent, $request);
52+
DB::connection('tenant')->commit();
53+
54+
return ApiResource::make($person)->response()->setStatusCode(201);
55+
} catch (ValidationException $e) {
56+
DB::connection('tenant')->rollBack();
57+
throw $e;
58+
} catch (\Exception $e) {
59+
DB::connection('tenant')->rollBack();
60+
Log::critical('Error while an agent was registering a customer', ['message' => $e->getMessage()]);
61+
throw $e;
62+
}
63+
}
3464
}

src/backend/app/Http/Controllers/AgentTransactionsController.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Http\Controllers;
44

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

2829
return ApiResource::make($this->agentTransactionService->getByCustomerId($agent->id, $customerId));
2930
}
31+
32+
/**
33+
* Return the token generated for one of the agent's transactions, if any.
34+
* Token generation is asynchronous (queued in ProcessPayment), so the
35+
* field app polls this endpoint after a successful POST until the token
36+
* appears or it gives up.
37+
*/
38+
public function token(int $transactionId): ApiResource {
39+
$agent = $this->agentService->getByAuthenticatedUser();
40+
$transaction = $this->agentTransactionService->findForAgent($agent->id, $transactionId);
41+
42+
if (!$transaction instanceof Transaction) {
43+
abort(404, 'Transaction not found.');
44+
}
45+
46+
return ApiResource::make([
47+
'transaction_id' => $transaction->id,
48+
'token' => $transaction->token,
49+
]);
50+
}
3051
}

src/backend/app/Http/Controllers/TransactionController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function show(int $id): ApiResource {
2727
return ApiResource::make($transaction);
2828
}
2929

30-
public function store(Request $request): void {
30+
public function store(Request $request): ApiResource {
3131
/**
3232
* @var ITransactionProvider $transactionProvider
3333
*/
@@ -44,5 +44,9 @@ public function store(Request $request): void {
4444
Log::warning('Company ID not found in request attributes. Payment transaction job not triggered for transaction '.$transaction->id);
4545
}
4646
}
47+
48+
return ApiResource::make([
49+
'id' => $transaction->id ?? null,
50+
]);
4751
}
4852
}

src/backend/app/Http/Middleware/AgentBalanceMiddleware.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function handle($request, \Closure $next) {
4848
throw new DownPaymentBiggerThanAmountException('Down payment is bigger than amount');
4949
}
5050
}
51-
if ($routeName === 'agent-transaction') {
51+
if (in_array($routeName, ['agent-transaction', 'agent-app-transaction'], true)) {
5252
if ($transactionAmount = $request->input('amount')) {
5353
$agentBalance -= $transactionAmount;
5454
} else {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class CreateAgentCustomerRequest extends FormRequest {
8+
public function authorize(): bool {
9+
return true;
10+
}
11+
12+
/**
13+
* @return array<string, mixed>
14+
*/
15+
public function rules(): array {
16+
return [
17+
'name' => ['required', 'string', 'min:3'],
18+
'surname' => ['required', 'string', 'min:3'],
19+
'phone' => ['required', 'string', 'phone:INTERNATIONAL'],
20+
'city_id' => ['required', 'integer', 'exists:tenant.cities,id'],
21+
'geo_points' => ['nullable', 'string'],
22+
];
23+
}
24+
}

src/backend/app/Http/Requests/CreateAgentSoldApplianceRequest.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace App\Http\Requests;
44

5+
use App\Models\AgentAssignedAppliances;
6+
use App\Models\ApplianceType;
57
use Illuminate\Foundation\Http\FormRequest;
68

79
class CreateAgentSoldApplianceRequest extends FormRequest {
@@ -18,18 +20,42 @@ public function authorize(): bool {
1820
* @return array<string, mixed>
1921
*/
2022
public function rules(): array {
23+
$deviceSerialRules = $this->isShsAppliance()
24+
? ['required', 'string']
25+
: ['nullable', 'string'];
26+
2127
return [
2228
'person_id' => ['required'],
2329
'payment_type' => ['nullable', 'string', 'in:installment,energy_service'],
2430
'down_payment' => ['required_unless:payment_type,energy_service', 'numeric'],
2531
'tenure' => ['required_unless:payment_type,energy_service', 'numeric', 'min:0'],
2632
'first_payment_date' => ['required_unless:payment_type,energy_service'],
2733
'agent_assigned_appliance_id' => ['required'],
28-
'device_serial' => ['nullable', 'string'],
34+
'device_serial' => $deviceSerialRules,
2935
'address' => ['nullable', 'array'],
3036
'points' => ['nullable', 'string'],
3137
'minimum_payable_amount' => ['nullable', 'integer', 'min:0'],
3238
'price_per_day' => ['nullable', 'integer', 'min:0'],
3339
];
3440
}
41+
42+
/**
43+
* @return array<string, string>
44+
*/
45+
public function messages(): array {
46+
return [
47+
'device_serial.required' => 'Device serial is required for solar home system sales.',
48+
];
49+
}
50+
51+
private function isShsAppliance(): bool {
52+
$assignedId = $this->input('agent_assigned_appliance_id');
53+
if (!$assignedId) {
54+
return false;
55+
}
56+
57+
$assigned = AgentAssignedAppliances::with('appliance')->find($assignedId);
58+
59+
return $assigned?->appliance?->appliance_type_id === ApplianceType::APPLIANCE_TYPE_SHS;
60+
}
3561
}

src/backend/app/Services/AgentCustomerService.php

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,78 @@
22

33
namespace App\Services;
44

5+
use App\Http\Requests\CreateAgentCustomerRequest;
56
use App\Models\Agent;
7+
use App\Models\City;
68
use App\Models\Person\Person;
79
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
10+
use Illuminate\Database\Eloquent\Builder;
11+
use Illuminate\Validation\ValidationException;
812

913
class AgentCustomerService {
10-
public function __construct(private Person $person) {}
14+
public function __construct(
15+
private Person $person,
16+
private City $city,
17+
private PersonService $personService,
18+
private GeographicalInformationService $geographicalInformationService,
19+
private AddressGeographicalInformationService $addressGeographicalInformationService,
20+
) {}
21+
22+
public function register(Agent $agent, CreateAgentCustomerRequest $request): Person {
23+
$cityId = $request->integer('city_id');
24+
$phone = $request->string('phone')->toString();
25+
$geoPoints = $request->string('geo_points')->toString();
26+
27+
$city = $this->city->newQuery()->findOrFail($cityId);
28+
if ($city->mini_grid_id !== $agent->mini_grid_id) {
29+
throw ValidationException::withMessages(['city_id' => ['Selected city does not belong to the agent\'s mini-grid.']]);
30+
}
31+
32+
if ($this->personService->getByPhoneNumber($phone) instanceof Person) {
33+
throw ValidationException::withMessages(['phone' => ['A customer with this phone number already exists.']]);
34+
}
35+
36+
$request->merge(['is_customer' => 1]);
37+
$person = $this->personService->createFromRequest($request);
38+
39+
if ($geoPoints !== '') {
40+
$address = $person->addresses()->where('is_primary', 1)->first();
41+
$geographicalInformation = $this->geographicalInformationService->make([
42+
'points' => $geoPoints,
43+
]);
44+
$this->addressGeographicalInformationService->setAssigned($geographicalInformation);
45+
$this->addressGeographicalInformationService->setAssignee($address);
46+
$this->addressGeographicalInformationService->assign();
47+
$this->geographicalInformationService->save($geographicalInformation);
48+
}
49+
50+
return $person->fresh(['addresses.city', 'addresses.geo']);
51+
}
1152

1253
/**
1354
* @return LengthAwarePaginator<int, Person>
1455
*/
1556
public function list(Agent $agent): LengthAwarePaginator {
16-
$miniGridId = $agent->mini_grid_id;
57+
return $this->scopedQuery($agent)->paginate(config('settings.paginate'));
58+
}
59+
60+
public function findForAgent(Agent $agent, int $customerId): Person {
61+
return $this->scopedQuery($agent)->findOrFail($customerId);
62+
}
1763

64+
/**
65+
* @return Builder<Person>
66+
*/
67+
private function scopedQuery(Agent $agent): Builder {
1868
return $this->person->newQuery()->with([
1969
'devices',
2070
'addresses' => fn ($q) => $q->where('is_primary', 1)->with('city'),
2171
])
2272
->where('is_customer', 1)
2373
->whereHas(
2474
'addresses',
25-
fn ($q) => $q->whereHas('city', fn ($q) => $q->where('mini_grid_id', $miniGridId))
26-
)
27-
->paginate(config('settings.paginate'));
75+
fn ($q) => $q->whereHas('city', fn ($q) => $q->where('mini_grid_id', $agent->mini_grid_id))
76+
);
2877
}
2978

3079
/**

src/backend/app/Services/AgentSoldApplianceService.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public function processSaleFromRequest(AgentSoldAppliance $agentSoldAppliance, a
205205
'street' => $addressFromCustomer->street,
206206
'city_id' => $addressFromCustomer->city_id,
207207
];
208-
$points = $requestData['points'] ?? $addressFromCustomer->geo()->first()->points;
208+
$points = $requestData['points'] ?? $addressFromCustomer->geo()->first()?->points;
209209

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

221-
$geoInfo = $this->geographicalInformationService->make([
222-
'points' => $points,
223-
]);
221+
if ($points) {
222+
$geoInfo = $this->geographicalInformationService->make([
223+
'points' => $points,
224+
]);
224225

225-
$this->addressGeographicalInformationService->setAssigned($geoInfo);
226-
$this->addressGeographicalInformationService->setAssignee($address);
227-
$this->addressGeographicalInformationService->assign();
228-
$this->geographicalInformationService->save($geoInfo);
226+
$this->addressGeographicalInformationService->setAssigned($geoInfo);
227+
$this->addressGeographicalInformationService->setAssignee($address);
228+
$this->addressGeographicalInformationService->assign();
229+
$this->geographicalInformationService->save($geoInfo);
230+
}
229231
}
230232

231233
// initalize appliance Rates

src/backend/app/Services/AgentTransactionService.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ public function getById(int $id): AgentTransaction {
7070
throw new \Exception('Method getById() not yet implemented.');
7171
}
7272

73+
/**
74+
* Find a transaction owned by the given agent and eager-load its token.
75+
* Returns null if the transaction doesn't exist or doesn't belong to the agent.
76+
*/
77+
public function findForAgent(int $agentId, int $transactionId): ?Transaction {
78+
return $this->transaction->newQuery()
79+
->with(['token'])
80+
->whereHasMorph(
81+
'originalTransaction',
82+
[AgentTransaction::class],
83+
fn ($q) => $q->where('agent_id', $agentId)
84+
)
85+
->where('id', $transactionId)
86+
->first();
87+
}
88+
7389
/**
7490
* @param array<string, mixed> $transactionData
7591
*/

0 commit comments

Comments
 (0)