From ba30a5fcdf98524a87ffd5e0a4c36a1d80a8a742 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 2 Jul 2026 13:27:53 +0800 Subject: [PATCH] fix: QrCode relation bug and Endroid create() error --- .../Api/V1/Store/StorePortalController.php | 12 ++- app/Models/QrCode.php | 8 +- ..._assigned_to_user_id_to_qr_codes_table.php | 29 ++++++ .../Api/V1/Store/ResidentSaleScanTest.php | 95 +++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_07_02_052549_add_assigned_to_user_id_to_qr_codes_table.php create mode 100644 tests/Feature/Api/V1/Store/ResidentSaleScanTest.php diff --git a/app/Http/Controllers/Api/V1/Store/StorePortalController.php b/app/Http/Controllers/Api/V1/Store/StorePortalController.php index 63db1e7..cd2c6ad 100644 --- a/app/Http/Controllers/Api/V1/Store/StorePortalController.php +++ b/app/Http/Controllers/Api/V1/Store/StorePortalController.php @@ -95,6 +95,7 @@ class StorePortalController extends ApiController ]); if (!$request->household_id && !$request->user_id) { + dump($request->all()); return $this->fail('Either household_id or user_id must be provided.', null, 422); } @@ -121,14 +122,15 @@ class StorePortalController extends ApiController ->get(); $qrData = $codes->map(function ($code) { - $qr = \Endroid\QrCode\QrCode::create($code->serial) - ->setSize(300) - ->setMargin(10); - $writer = new PngWriter; + $qr = \Endroid\QrCode\Builder\Builder::create() + ->data($code->serial) + ->size(300) + ->margin(10) + ->build(); return [ 'serial' => $code->serial, - 'qr_base64' => base64_encode($writer->write($qr)->getString()), + 'qr_base64' => base64_encode($qr->getString()), ]; }); diff --git a/app/Models/QrCode.php b/app/Models/QrCode.php index a40efc9..7b4ecb3 100644 --- a/app/Models/QrCode.php +++ b/app/Models/QrCode.php @@ -23,6 +23,7 @@ class QrCode extends Model 'status', 'replacement_for_id', 'assigned_to_household_id', + 'assigned_to_user_id', 'assigned_to_store_id', 'allocated_at', 'activated_at', @@ -54,7 +55,7 @@ class QrCode extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logOnly(['status', 'assigned_to_household_id', 'assigned_to_store_id', 'used_at_drop_off_id']) + ->logOnly(['status', 'assigned_to_household_id', 'assigned_to_user_id', 'assigned_to_store_id', 'used_at_drop_off_id']) ->logOnlyDirty() ->dontSubmitEmptyLogs() ->useLogName('qr_code'); @@ -70,6 +71,11 @@ class QrCode extends Model return $this->belongsTo(Household::class, 'assigned_to_household_id'); } + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_to_user_id'); + } + public function usedAtDropOff(): BelongsTo { return $this->belongsTo(DropOffPoint::class, 'used_at_drop_off_id'); diff --git a/database/migrations/2026_07_02_052549_add_assigned_to_user_id_to_qr_codes_table.php b/database/migrations/2026_07_02_052549_add_assigned_to_user_id_to_qr_codes_table.php new file mode 100644 index 0000000..376c94a --- /dev/null +++ b/database/migrations/2026_07_02_052549_add_assigned_to_user_id_to_qr_codes_table.php @@ -0,0 +1,29 @@ +foreignId('assigned_to_user_id')->nullable()->after('assigned_to_household_id')->constrained('users')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('qr_codes', function (Blueprint $table) { + $table->dropForeign(['assigned_to_user_id']); + $table->dropColumn('assigned_to_user_id'); + }); + } +}; diff --git a/tests/Feature/Api/V1/Store/ResidentSaleScanTest.php b/tests/Feature/Api/V1/Store/ResidentSaleScanTest.php new file mode 100644 index 0000000..431e28b --- /dev/null +++ b/tests/Feature/Api/V1/Store/ResidentSaleScanTest.php @@ -0,0 +1,95 @@ +seed([RoleSeeder::class, SamplePsgcSeeder::class, SampleDropOffPointsSeeder::class]); + + $admin = User::factory()->create(['role' => User::ROLE_ADMIN, 'status' => 'active']); + $storeOwner = User::factory()->create(['role' => User::ROLE_STORE_PARTNER, 'status' => 'active']); + $resident = User::factory()->create(['role' => User::ROLE_RESIDENT, 'status' => 'active']); + $scannerUser = User::factory()->create(['role' => User::ROLE_SCANNER, 'status' => 'active']); + + $dop = DropOffPoint::first(); + + // 2. Create Store & Issue Wholesale + $store = PartnerStore::factory()->create([ + 'owner_user_id' => $storeOwner->id, + 'status' => 'active', + 'commission_rate_percent' => 10 + ]); + app(StoreOperations::class)->issueWholesale($store, 10, 50000); + + // 3. Store sells 5 QR codes to Resident via API + Sanctum::actingAs($storeOwner); + $saleResponse = $this->postJson("/api/v1/store/sales", [ + 'user_id' => $resident->id, + 'quantity' => 5, + ]); + + if ($saleResponse->status() !== 201) { + dump($saleResponse->json()); + } + + $saleResponse->assertCreated(); + $this->assertEquals(5, $saleResponse->json('data.quantity')); + + // Verify codes are active and assigned to resident + $activeCodes = QrCode::where('assigned_to_user_id', $resident->id) + ->where('status', 'active') + ->get(); + + $this->assertCount(5, $activeCodes); + + // 4. Scanner App scans the 5 QR codes + Sanctum::actingAs($scannerUser); + + $scans = []; + foreach ($activeCodes as $code) { + $scans[] = [ + 'serial' => $code->serial, + 'drop_off_point_id' => $dop->id, + 'lat' => 14.6539, + 'lng' => 121.0685, + ]; + } + + $scanResponse = $this->postJson('/api/v1/scanner/scan/bulk', [ + 'scans' => $scans, + ]); + + $scanResponse->assertOk() + ->assertJsonPath('data.accepted_count', 5) + ->assertJsonPath('data.rejected_count', 0); + + // Verify codes are marked as used + $usedCodes = QrCode::where('assigned_to_user_id', $resident->id) + ->where('status', 'used') + ->count(); + + $this->assertEquals(5, $usedCodes); + + echo "✅ E2E Test Passed!\n"; + echo "1. Issued wholesale to store.\n"; + echo "2. Store sold 5 codes to resident (user_id: {$resident->id}).\n"; + echo "3. Scanner app successfully scanned all 5 codes.\n"; + } +}