Skip to content

Commit a99e94e

Browse files
committed
feat: delete sold appliance plan + unbind/reassign device
1 parent 0d4ab08 commit a99e94e

16 files changed

Lines changed: 353 additions & 12 deletions

File tree

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ private function processDownPayment(AppliancePerson $appliancePerson, float $dow
186186
* Display the specified resource.
187187
*/
188188
public function index(Person $person, Request $request): ApiResource {
189-
$appliances = $this->appliancePerson::with('appliance.applianceType', 'rates.logs', 'logs.owner')
189+
$appliances = $this->appliancePerson::withTrashed()
190+
->with('appliance.applianceType', 'rates.logs', 'logs.owner')
190191
->where('person_id', $person->id)
191192
->get();
192193

@@ -202,11 +203,11 @@ public function show(int $applianceId): ApiResource {
202203
public function getRates(int $appliancePersonId, Request $request): ApiResource {
203204
$perPage = $request->get('per_page', 15);
204205

205-
$appliancePerson = $this->appliancePerson::findOrFail($appliancePersonId);
206+
$appliancePerson = $this->appliancePerson::withTrashed()->findOrFail($appliancePersonId);
206207

207208
return ApiResource::make($appliancePerson->rates()
208209
->with('logs.owner')
209-
->orderBy('due_date', 'asc')
210+
->oldest('due_date')
210211
->paginate($perPage));
211212
}
212213

@@ -239,11 +240,28 @@ public function updateTotalCost(int $appliancePersonId, Request $request): ApiRe
239240
public function getLogs(int $appliancePersonId, Request $request): ApiResource {
240241
$perPage = $request->get('per_page', 10);
241242

242-
$appliancePerson = $this->appliancePerson::findOrFail($appliancePersonId);
243+
$appliancePerson = $this->appliancePerson::withTrashed()->findOrFail($appliancePersonId);
243244

244245
return ApiResource::make($appliancePerson->logs()
245-
->with('owner')
246-
->orderBy('created_at', 'desc')
246+
->with('owner')->latest()
247247
->paginate($perPage));
248248
}
249+
250+
public function destroy(int $appliancePersonId, Request $request): ApiResource {
251+
$creatorId = $request->integer('admin_id');
252+
$appliancePerson = $this->appliancePerson::findOrFail($appliancePersonId);
253+
254+
try {
255+
DB::connection('tenant')->beginTransaction();
256+
$this->appliancePersonService->deleteWithDeviceRelease($appliancePerson, $creatorId);
257+
DB::connection('tenant')->commit();
258+
} catch (\Exception $e) {
259+
DB::connection('tenant')->rollBack();
260+
throw new \Exception($e->getMessage(), $e->getCode(), $e);
261+
}
262+
263+
return ApiResource::make(
264+
$this->appliancePersonService->getApplianceDetails($appliancePersonId)
265+
);
266+
}
249267
}

src/backend/app/Models/AppliancePerson.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Database\Eloquent\Relations\HasMany;
1212
use Illuminate\Database\Eloquent\Relations\MorphMany;
1313
use Illuminate\Database\Eloquent\Relations\MorphTo;
14+
use Illuminate\Database\Eloquent\SoftDeletes;
1415
use Illuminate\Support\Carbon;
1516

1617
/**
@@ -25,6 +26,7 @@
2526
* @property int $creator_id
2627
* @property Carbon|null $created_at
2728
* @property Carbon|null $updated_at
29+
* @property Carbon|null $deleted_at
2830
* @property float|null $down_payment
2931
* @property Carbon|null $first_payment_date
3032
* @property string|null $device_serial
@@ -39,6 +41,8 @@
3941
* @property-read Collection<int, ApplianceRate> $rates
4042
*/
4143
class AppliancePerson extends BaseModel {
44+
use SoftDeletes;
45+
4246
public const PAYMENT_TYPE_INSTALLMENT = 'installment';
4347
public const PAYMENT_TYPE_ENERGY_SERVICE = 'energy_service';
4448

src/backend/app/Services/AppliancePersonService.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Events\NewLogEvent;
66
use App\Models\AppliancePerson;
7+
use App\Models\Device;
78
use App\Models\MainSettings;
89
use App\Services\Interfaces\IAssociative;
910
use App\Services\Interfaces\IBaseService;
@@ -20,6 +21,8 @@ class AppliancePersonService implements IBaseService, IAssociative {
2021
public function __construct(
2122
private MainSettings $mainSettings,
2223
private AppliancePerson $appliancePerson,
24+
private Device $device,
25+
private UserService $userService,
2326
) {}
2427

2528
/**
@@ -51,13 +54,33 @@ public function getCurrencyFromMainSettings(): string {
5154
}
5255

5356
public function getApplianceDetails(int $applianceId): AppliancePerson {
54-
$appliance = $this->appliancePerson::with('appliance', 'rates.logs', 'logs.owner', 'device')
57+
$appliance = $this->appliancePerson::withTrashed()
58+
->with('appliance', 'rates.logs', 'logs.owner', 'device')
5559
->where('id', '=', $applianceId)
5660
->first();
5761

5862
return $this->sumTotalPaymentsAndTotalRemainingAmount($appliance);
5963
}
6064

65+
public function deleteWithDeviceRelease(AppliancePerson $appliancePerson, int $creatorId): AppliancePerson {
66+
if ($appliancePerson->device_serial) {
67+
$this->device->newQuery()
68+
->where('device_serial', $appliancePerson->device_serial)
69+
->update(['person_id' => null]);
70+
}
71+
72+
$creatorName = $this->userService->getById($creatorId)->name ?? 'Unknown';
73+
event(new NewLogEvent([
74+
'user_id' => $creatorId,
75+
'affected' => $appliancePerson,
76+
'action' => "User {$creatorName} deleted the sold appliance",
77+
]));
78+
79+
$appliancePerson->delete();
80+
81+
return $appliancePerson;
82+
}
83+
6184
private function sumTotalPaymentsAndTotalRemainingAmount(AppliancePerson $appliance): AppliancePerson {
6285
$rates = collect($appliance->rates);
6386
$appliance['totalRemainingAmount'] = 0;
@@ -81,7 +104,7 @@ public function getLoansForCustomerId(int $customerId) {
81104
}
82105

83106
public function getById(int $id): AppliancePerson {
84-
return $this->appliancePerson->newQuery()->findOrFail($id);
107+
return $this->appliancePerson->newQuery()->withTrashed()->findOrFail($id);
85108
}
86109

87110
/**

src/backend/app/Services/ApplianceRateService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function recomputeRatesFromTotalCost(AppliancePerson $appliancePerson, in
6363
$appliancePerson->save();
6464

6565
$currency = $this->getCurrencyFromMainSettings();
66-
$creatorName = $this->userService->getById($creatorId)?->name ?? 'Unknown';
66+
$creatorName = $this->userService->getById($creatorId)->name ?? 'Unknown';
6767
event(new NewLogEvent([
6868
'user_id' => $creatorId,
6969
'affected' => $appliancePerson,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
public function up(): void {
9+
Schema::connection('tenant')->table('appliance_people', function (Blueprint $table) {
10+
$table->softDeletes();
11+
});
12+
}
13+
14+
public function down(): void {
15+
Schema::connection('tenant')->table('appliance_people', function (Blueprint $table) {
16+
$table->dropSoftDeletes();
17+
});
18+
}
19+
};

src/backend/routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
Route::get('/{appliancePersonId}/rates', [AppliancePersonController::class, 'getRates'])->middleware('permission:appliances');
135135
Route::get('/{appliancePersonId}/logs', [AppliancePersonController::class, 'getLogs'])->middleware('permission:appliances');
136136
Route::put('/{appliancePersonId}/total-cost', [AppliancePersonController::class, 'updateTotalCost'])->middleware('permission:appliances');
137+
Route::delete('/{appliancePersonId}', [AppliancePersonController::class, 'destroy'])->middleware('permission:appliances');
137138
});
138139
Route::group(['prefix' => 'types'], function () {
139140
Route::get('/', [ApplianceTypeController::class, 'index'])->middleware('permission:appliances');
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Feature;
6+
7+
use App\Models\Appliance;
8+
use App\Models\AppliancePerson;
9+
use App\Models\ApplianceRate;
10+
use App\Models\Device;
11+
use App\Models\Log;
12+
use Database\Factories\AppliancePersonFactory;
13+
use Database\Factories\ApplianceTypeFactory;
14+
use Database\Factories\Person\PersonFactory;
15+
use Tests\CreateEnvironments;
16+
use Tests\TestCase;
17+
18+
class AppliancePersonDeleteTest extends TestCase {
19+
use CreateEnvironments;
20+
21+
public function testSoftDeletesAppliancePerson(): void {
22+
$this->createTestData();
23+
$appliancePerson = $this->seedAppliance();
24+
25+
$response = $this->actingAs($this->user)->delete(
26+
"/api/appliances/person/{$appliancePerson->id}",
27+
['admin_id' => $this->user->id]
28+
);
29+
30+
$response->assertStatus(200);
31+
$this->assertNull(AppliancePerson::query()->find($appliancePerson->id));
32+
$this->assertNotNull(AppliancePerson::query()->withTrashed()->find($appliancePerson->id));
33+
}
34+
35+
public function testReleasesBoundDevice(): void {
36+
$this->createTestData();
37+
$appliancePerson = $this->seedAppliance(deviceSerial: 'SN-TEST-1');
38+
Device::query()->create([
39+
'device_serial' => 'SN-TEST-1',
40+
'person_id' => $appliancePerson->person_id,
41+
'device_type' => Device::class,
42+
'device_id' => 0,
43+
]);
44+
45+
$this->actingAs($this->user)->delete(
46+
"/api/appliances/person/{$appliancePerson->id}",
47+
['admin_id' => $this->user->id]
48+
)->assertStatus(200);
49+
50+
$device = Device::query()->where('device_serial', 'SN-TEST-1')->first();
51+
$this->assertNotNull($device);
52+
$this->assertNull($device->person_id);
53+
}
54+
55+
public function testAllowsDeleteWhenRatesPaid(): void {
56+
$this->createTestData();
57+
$appliancePerson = $this->seedAppliance();
58+
$appliancePerson->rates()->oldest('due_date')->first()->update(['remaining' => 0]);
59+
60+
$response = $this->actingAs($this->user)->delete(
61+
"/api/appliances/person/{$appliancePerson->id}",
62+
['admin_id' => $this->user->id]
63+
);
64+
65+
$response->assertStatus(200);
66+
$this->assertNotNull(AppliancePerson::query()->withTrashed()->find($appliancePerson->id)->deleted_at);
67+
}
68+
69+
public function testWritesAuditLog(): void {
70+
$this->createTestData();
71+
$appliancePerson = $this->seedAppliance();
72+
73+
$this->actingAs($this->user)->delete(
74+
"/api/appliances/person/{$appliancePerson->id}",
75+
['admin_id' => $this->user->id]
76+
)->assertStatus(200);
77+
78+
$log = Log::query()
79+
->where('affected_type', AppliancePerson::class)
80+
->where('affected_id', $appliancePerson->id)
81+
->latest('id')
82+
->first();
83+
$this->assertNotNull($log);
84+
$this->assertSame($this->user->id, $log->user_id);
85+
$this->assertStringContainsString("User {$this->user->name} deleted the sold appliance", $log->action);
86+
}
87+
88+
public function testShowReturnsTrashedAppliancePersonWithDeletedAt(): void {
89+
$this->createTestData();
90+
$appliancePerson = $this->seedAppliance();
91+
92+
$this->actingAs($this->user)->delete(
93+
"/api/appliances/person/{$appliancePerson->id}",
94+
['admin_id' => $this->user->id]
95+
)->assertStatus(200);
96+
97+
$response = $this->actingAs($this->user)->get(
98+
"/api/appliances/person/people/detail/{$appliancePerson->id}"
99+
);
100+
101+
$response->assertStatus(200);
102+
$response->assertJsonPath('data.id', $appliancePerson->id);
103+
$this->assertNotNull($response->json('data.deleted_at'));
104+
}
105+
106+
private function seedAppliance(?string $deviceSerial = null): AppliancePerson {
107+
$person = PersonFactory::new()->create();
108+
$applianceType = ApplianceTypeFactory::new()->create();
109+
$appliance = Appliance::query()->create([
110+
'name' => 'Test Appliance',
111+
'price' => 1000,
112+
'appliance_type_id' => $applianceType->id,
113+
]);
114+
115+
/** @var AppliancePerson $appliancePerson */
116+
$appliancePerson = AppliancePersonFactory::new()->create([
117+
'appliance_id' => $appliance->id,
118+
'person_id' => $person->id,
119+
'total_cost' => 1000,
120+
'rate_count' => 5,
121+
'down_payment' => 0,
122+
'device_serial' => $deviceSerial,
123+
]);
124+
125+
foreach ([200, 200, 200, 200, 200] as $i => $cost) {
126+
ApplianceRate::query()->create([
127+
'appliance_person_id' => $appliancePerson->id,
128+
'rate_cost' => $cost,
129+
'remaining' => $cost,
130+
'remind' => 0,
131+
'due_date' => now()->addMonths($i + 1),
132+
]);
133+
}
134+
135+
return $appliancePerson->fresh();
136+
}
137+
}

src/frontend/src/assets/locales/ar.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
"deleteMeterNotify": "تم حذف العداد بنجاح | يجب التأكيد على حذف العداد",
209209
"deleteMiniGrid": "حذف المجمّع الكهربائي الصغير",
210210
"deleteMiniGridNotify": "‏{name}‏ أؤكد أنه سيتم حذف",
211+
"deleteSoldAppliance": "حذف الجهاز المباع | تم حذف الجهاز المباع | ‏{name}‏ أؤكد أنه سيتم حذف",
211212
"deleteVillage": "حذف القرية",
212213
"deleteVillageNotify": "‏{name}‏ أؤكد أنه سيتم حذف",
213214
"deliveryReportsUrl": "رابط تقارير التسليم",
@@ -427,6 +428,7 @@
427428
"socialTariffLabels": "الحد الأقصى للطاقة المكدسة | الإعانة اليومية في التعريفة الاجتماعية",
428429
"socialTariffOptions": "إخفاء خيارات التعريفة الاجتماعية | عرض خيارات التعريفة الاجتماعية",
429430
"solarHomeSystemTransaction": "معاملات نظام الطاقة الشمسية المنزلي | معاملة نظام الطاقة الشمسية المنزلي",
431+
"soldAppliancePlanDeleted": "تم حذف خطة الدفع هذه في {date}",
430432
"soldAppliances": "الأجهزة المباعة",
431433
"soldAssets": "الأصول المباعة",
432434
"soldDate": "تاريخ البيع",
@@ -560,6 +562,7 @@
560562
"day": "يومي | يوم",
561563
"deactivate": "تم التعطيل | تعطيل",
562564
"delete": "حذف",
565+
"deleted": "محذوف",
563566
"description": "وصف",
564567
"detail": "تفاصيل | تفصيل",
565568
"device": "جهاز",

src/frontend/src/assets/locales/bu.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
"deleteMeterNotify": "မီတာကို ဖျက်ရန် အတည်ြပ+ရပါမည်။ | မီတာကို ေအာင်ြမင်စွာ ဖျက်လိကု ်သည်",
209209
"deleteMiniGrid": "Mini-Grid ကိုဖျက်ပါ။",
210210
"deleteMiniGridNotify": "{name} ကိုဖျက်ပစ်မည်ဟု အတည်ပြုပါသည်။",
211+
"deleteSoldAppliance": "Delete Sold Appliance | Sold Appliance Deleted | I confirm that {name} will be deleted",
211212
"deleteVillage": "ရွာကိုဖျက်ပါ။",
212213
"deleteVillageNotify": "{name} ကိုဖျက်ပစ်မည်ဟု အတည်ပြုပါသည်။",
213214
"deliveryReportsUrl": "မက်ေဆ့ချက်အမည်",
@@ -427,6 +428,7 @@
427428
"socialTariffLabels": "လမူ6ေရးအခွနအ်ခြဖင့်ေနစ့V်ေထာက်ပံ့ေPကး | အများဆံုး စုပံုထားေသာ စွမ်းအင်",
428429
"socialTariffOptions": "လမူ6ေရးအခွနအ်ခေရွးချယ်မ6များကိုြပပါ။|လမူ6ေရးအေကာက်ခွန် ေရွးချယ်စရာများကို ဝှက်ထားပါ",
429430
"solarHomeSystemTransaction": "ဆိုလာအိမ်စနစ် အရောင်းအဝယ်များ",
431+
"soldAppliancePlanDeleted": "This payment plan was deleted on {date}",
430432
"soldAppliances": "ပစSည်းကရိယာများ ေရာင်းချခဲ့ြခင်း",
431433
"soldAssets": "ပိုင်ဆိုင်မ6များကို ေရာင်းချခဲ့သည်။",
432434
"soldDate": "ေရာင်းချသည့်ေန]စ့ွဲ",
@@ -560,6 +562,7 @@
560562
"day": "ေန့|ေနစ့V်",
561563
"deactivate": "ပိတ်ရန် | ပိတ်ထားသည်။",
562564
"delete": "ဖျက်ပါ။",
565+
"deleted": "Deleted",
563566
"description": "ေဖာ်ြပချက်",
564567
"detail": "အေသးစိတ် | အေသးစိတ်များ",
565568
"device": "device",

src/frontend/src/assets/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
"deleteMeterNotify": "You have to confirm to delete the meter | Meter Deleted successfully",
209209
"deleteMiniGrid": "Delete Mini-Grid",
210210
"deleteMiniGridNotify": "I confirm that {name} will be deleted",
211+
"deleteSoldAppliance": "Delete Sold Appliance | Sold Appliance Deleted | I confirm that {name} will be deleted",
211212
"deleteVillage": "Delete Village",
212213
"deleteVillageNotify": "I confirm that {name} will be deleted",
213214
"deliveryReportsUrl": "Delivery Reports URL",
@@ -427,6 +428,7 @@
427428
"socialTariffLabels": "Daily allowance at social tariff | Maximum stacked energy",
428429
"socialTariffOptions": "Show Social Tariff Options | Hide Social Tariff Options ",
429430
"solarHomeSystemTransaction": "Solar Home System Transaction | Solar Home System Transactions",
431+
"soldAppliancePlanDeleted": "This payment plan was deleted on {date}",
430432
"soldAppliances": "Sold Appliances",
431433
"soldAssets": "Sold Assets",
432434
"soldDate": "Sold Date",
@@ -560,6 +562,7 @@
560562
"day": "Day | Daily",
561563
"deactivate": "Deactivate | Deactivated",
562564
"delete": "Delete",
565+
"deleted": "Deleted",
563566
"description": "Description",
564567
"detail": "Detail | Details",
565568
"device": "Device",

0 commit comments

Comments
 (0)