Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 79 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
| PricesController | |
0.00% |
0 / 79 |
|
0.00% |
0 / 4 |
156 | |
0.00% |
0 / 1 |
| byPostalCode | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
| listServices | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| buildMatch | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
6 | |||
| normalizeRegion | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Controllers; |
| 4 | |
| 5 | use App\Models\PricePostalCode; |
| 6 | use App\Models\PriceScoreTierService; |
| 7 | use App\Models\PriceService; |
| 8 | use Illuminate\Http\Request; |
| 9 | use Illuminate\Support\Facades\Log; |
| 10 | |
| 11 | class PricesController extends Controller |
| 12 | { |
| 13 | /** |
| 14 | * FIRE-933: Return the full price list for a given postal code. |
| 15 | * FIRE-935: Returns a `matches` array to support postal codes that exist |
| 16 | * across multiple regions. Always returns an array (1+ elements) on success. |
| 17 | */ |
| 18 | public function byPostalCode(Request $request, string $postalCode) |
| 19 | { |
| 20 | try { |
| 21 | // FIRE-935: Validate postal code format before hitting the DB. |
| 22 | if (!preg_match('/^\d{5}$/', $postalCode)) { |
| 23 | return response([ |
| 24 | 'message' => 'KO', |
| 25 | 'error' => 'Invalid postal code format', |
| 26 | ], 400); |
| 27 | } |
| 28 | |
| 29 | $region = $this->normalizeRegion($request->query('region')); |
| 30 | |
| 31 | $query = PricePostalCode::with('zone') |
| 32 | ->where('postal_code', $postalCode); |
| 33 | |
| 34 | if ($region) { |
| 35 | $query->whereHas('zone', fn ($q) => $q->where('region', $region)); |
| 36 | } |
| 37 | |
| 38 | $rows = $query->get(); |
| 39 | |
| 40 | if ($rows->isEmpty()) { |
| 41 | return response([ |
| 42 | 'message' => 'KO', |
| 43 | 'error' => 'Postal code not found in price catalog', |
| 44 | ], 404); |
| 45 | } |
| 46 | |
| 47 | $matches = $rows->map(fn ($ppc) => $this->buildMatch($ppc))->values(); |
| 48 | |
| 49 | return response([ |
| 50 | 'message' => 'OK', |
| 51 | 'data' => [ |
| 52 | 'postal_code' => $postalCode, |
| 53 | 'matches' => $matches, |
| 54 | ], |
| 55 | ]); |
| 56 | } catch (\Exception $e) { |
| 57 | Log::channel('third-party')->error('[prices:api] byPostalCode failed: ' . $e->getMessage()); |
| 58 | |
| 59 | return response([ |
| 60 | 'message' => 'KO', |
| 61 | 'error' => 'Internal error', |
| 62 | ], 500); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * FIRE-933: Return the active service catalog for frontend dropdowns. |
| 68 | */ |
| 69 | public function listServices() |
| 70 | { |
| 71 | try { |
| 72 | $services = PriceService::where('is_active', true) |
| 73 | ->orderBy('priority') |
| 74 | ->get(['id', 'name', 'description', 'category', 'unit', 'priority']); |
| 75 | |
| 76 | return response([ |
| 77 | 'message' => 'OK', |
| 78 | 'data' => $services, |
| 79 | ]); |
| 80 | } catch (\Exception $e) { |
| 81 | Log::channel('third-party')->error('[prices:api] listServices failed: ' . $e->getMessage()); |
| 82 | |
| 83 | return response([ |
| 84 | 'message' => 'KO', |
| 85 | 'error' => 'Internal error', |
| 86 | ], 500); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * FIRE-935: Build a single-region "match" entry for a postal code row. |
| 92 | * Fetches the tier prices for the row's region + score_cp combination. |
| 93 | */ |
| 94 | private function buildMatch(PricePostalCode $ppc): array |
| 95 | { |
| 96 | $region = $ppc->zone->region; |
| 97 | $prices = []; |
| 98 | |
| 99 | if ($ppc->score_cp !== null) { |
| 100 | $prices = PriceScoreTierService::with('service') |
| 101 | ->where('region', $region) |
| 102 | ->where('score_cp', $ppc->score_cp) |
| 103 | ->whereNull('effective_from') |
| 104 | ->whereHas('service', fn ($q) => $q->where('is_active', true)) |
| 105 | ->get() |
| 106 | ->map(fn ($tier) => [ |
| 107 | 'service_id' => $tier->service->id, |
| 108 | 'service_name' => $tier->service->name, |
| 109 | 'description' => $tier->service->description, |
| 110 | 'category' => $tier->service->category, |
| 111 | 'unit' => $tier->service->unit, |
| 112 | 'price' => (float) $tier->price, |
| 113 | ]) |
| 114 | ->sortBy(fn ($p) => $p['service_id']) |
| 115 | ->values() |
| 116 | ->all(); |
| 117 | } |
| 118 | |
| 119 | return [ |
| 120 | 'region' => $region, |
| 121 | 'zone' => [ |
| 122 | 'id' => $ppc->zone->id, |
| 123 | 'name' => $ppc->zone->zone_name, |
| 124 | 'type' => $ppc->zone->zone_type, |
| 125 | ], |
| 126 | 'score_cp' => $ppc->score_cp, |
| 127 | 'num_clients' => $ppc->num_clients, |
| 128 | 'weighted_clients' => $ppc->weighted_clients, |
| 129 | 'prices' => $prices, |
| 130 | ]; |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Normalize a region string: urldecode + handle Catalunya -> Cataluña. |
| 135 | */ |
| 136 | private function normalizeRegion(?string $region): ?string |
| 137 | { |
| 138 | if (!$region) { |
| 139 | return null; |
| 140 | } |
| 141 | |
| 142 | $region = urldecode($region); |
| 143 | |
| 144 | return $region === 'Catalunya' ? 'Cataluña' : $region; |
| 145 | } |
| 146 | } |