src/Controller/Api/UtilityBillController.php line 38

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api;
  3. use App\Entity\ApartmentPayment;
  4. use App\Entity\PaymentBill;
  5. use App\Entity\User;
  6. use App\Helpers\ResponseCode;
  7. use App\Repository\ApartmentDataRepository;
  8. use App\Repository\CounterInfoRepository;
  9. use App\Repository\PaymentBillRepository;
  10. use App\Repository\UtilityBillItemRepository;
  11. use App\Utils\Fondy;
  12. use App\Utils\Mercure;
  13. use App\Utils\Paginator;
  14. use App\Utils\ResponseTrait;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\DependencyInjection\ContainerInterface;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\Routing\Annotation\Route;
  19. use Symfony\Contracts\Translation\TranslatorInterface;
  20. class UtilityBillController extends AbstractController
  21. {
  22.     use ResponseTrait {
  23.         ResponseTrait::__construct as private traitConstruct;
  24.     }
  25.     public function __construct(ContainerInterface $container)
  26.     {
  27.         $this->traitConstruct($container);
  28.     }
  29.     /**
  30.      * @Route(path="/api/utility/dashboard", methods={"GET"})
  31.      */
  32.     public function getDashboard(Request $request)
  33.     {
  34.         /** @var User $user */
  35.         $user $this->getUser();
  36.         
  37.         $em $this->getDoctrine()->getManager();
  38.         $apartmentDataRepository $em->getRepository(\App\Entity\ApartmentData::class);
  39.         $utilityBillItemRepository $em->getRepository(\App\Entity\UtilityBillItem::class);
  40.         
  41.         // Get persAcc or type filter from query parameter (optional)
  42.         // persAcc takes precedence over type for more precise filtering
  43.         $persAcc $request->query->get('persAcc');
  44.         $type $request->query->get('type');
  45.         
  46.         // Get all apartments for user
  47.         $allApartments $apartmentDataRepository->findAllByUser($user);
  48.         
  49.         if (empty($allApartments)) {
  50.             return $this->statusOk(nullnull, [
  51.                 'user' => [
  52.                     'name' => $user->getFullName(),
  53.                     'apartment' => $user->getApartment(),
  54.                 ],
  55.                 'apartments' => [],
  56.                 'balance' => 0,
  57.                 'counters' => [],
  58.                 'bills' => [],
  59.             ]);
  60.         }
  61.         // If persAcc is specified, find that specific apartment
  62.         $apartmentData null;
  63.         if ($persAcc) {
  64.             $apartmentData $apartmentDataRepository->findByPersAcc($persAcc);
  65.             // Verify it belongs to this user
  66.             if ($apartmentData && $apartmentData->getUser() !== $user) {
  67.                 $apartmentData null;
  68.             }
  69.         } elseif ($type) {
  70.             // Fallback to type filter if persAcc not provided
  71.             // Normalize "boxing" to "box" for compatibility
  72.             $normalizedType = ($type === 'boxing') ? 'box' $type;
  73.             foreach ($allApartments as $apt) {
  74.                 $aptType $apt->getType();
  75.                 // Normalize "boxing" to "box" for comparison
  76.                 if ($aptType === 'boxing') {
  77.                     $aptType 'box';
  78.                 }
  79.                 if ($aptType === $normalizedType) {
  80.                     $apartmentData $apt;
  81.                     break;
  82.                 }
  83.             }
  84.         } else {
  85.             // Default to first apartment (or apartment type if available)
  86.             $apartmentData $allApartments[0];
  87.             foreach ($allApartments as $apt) {
  88.                 if ($apt->getType() === 'apartment') {
  89.                     $apartmentData $apt;
  90.                     break;
  91.                 }
  92.             }
  93.         }
  94.         // Format all apartments for selection screen
  95.         $apartments = [];
  96.         foreach ($allApartments as $apt) {
  97.             // Use total_balance from entity, fallback to calculated balance if not available
  98.             // Negate the value to match calculateBalance format (negative = red/debt)
  99.             $aptBalance $apt->getTotalBalance() !== null 
  100.                 ? -(float)$apt->getTotalBalance() 
  101.                 : $this->calculateBalance($apt$utilityBillItemRepository);
  102.             $aptNumber $apt->getNumber() ?? '';
  103.             
  104.             // Format number display
  105.             $displayNumber $aptNumber;
  106.             if (preg_match('/№\s*(\d+)/'$aptNumber$matches)) {
  107.                 $displayNumber '№' $matches[1];
  108.             }
  109.             
  110.             $apartments[] = [
  111.                 'persAcc' => $apt->getPersAcc(),
  112.                 'type' => $apt->getType(),
  113.                 'number' => $displayNumber,
  114.                 'balance' => $aptBalance,
  115.                 'fio' => $apt->getFio(),
  116.             ];
  117.         }
  118.         // Get utility counters (only for apartments with counters)
  119.         $counters = [];
  120.         if ($apartmentData && $apartmentData->getType() === 'apartment') {
  121.             $counters $this->formatCounters($apartmentData->getCounters()->toArray());
  122.         }
  123.         // Get bills grouped by month for selected apartment
  124.         $bills = [];
  125.         if ($apartmentData) {
  126.             $bills $this->getBillsGroupedByMonth($apartmentData$utilityBillItemRepository);
  127.         }
  128.         // Get payments for selected apartment or all apartments
  129.         $payments = [];
  130.         if ($persAcc) {
  131.             // Get payments for specific apartment
  132.             if ($apartmentData) {
  133.                 $payments $this->formatPayments($apartmentData->getPayments()->toArray());
  134.             }
  135.         } else {
  136.             // Get all payments from all apartments
  137.             foreach ($allApartments as $apt) {
  138.                 $aptPayments $this->formatPayments($apt->getPayments()->toArray());
  139.                 $payments array_merge($payments$aptPayments);
  140.             }
  141.             // Sort by date descending
  142.             usort($payments, function($a$b) {
  143.                 return strcmp($b['paymentDate'], $a['paymentDate']);
  144.             });
  145.         }
  146.         // Get balance for selected apartment - use total_balance from entity, fallback to calculated balance
  147.         // Negate the value to match calculateBalance format (negative = red/debt)
  148.         $balance 0;
  149.         if ($apartmentData) {
  150.             $balance $apartmentData->getTotalBalance() !== null 
  151.                 ? -(float)$apartmentData->getTotalBalance() 
  152.                 : $this->calculateBalance($apartmentData$utilityBillItemRepository);
  153.         }
  154.         // Get user name from first apartment or user
  155.         $userName $user->getFullName();
  156.         if (!empty($allApartments) && $allApartments[0]->getFio()) {
  157.             $userName $allApartments[0]->getFio();
  158.         }
  159.         return $this->statusOk(nullnull, [
  160.             'user' => [
  161.                 'name' => $userName,
  162.                 'apartment' => $apartmentData && $apartmentData->getNumber() 
  163.                     ? $apartmentData->getNumber() 
  164.                     : ($user->getApartment() ? 'Апартаменти №' $user->getApartment() : null),
  165.             ],
  166.             'apartments' => $apartments,
  167.             'balance' => $balance,
  168.             'counters' => $counters,
  169.             'bills' => $bills,
  170.             'payments' => $payments,
  171.         ]);
  172.     }
  173.     /**
  174.      * @Route(path="/api/utility/bills/{year}/{month}", methods={"GET"})
  175.      */
  176.     public function getBillsForMonth(int $yearint $monthRequest $request)
  177.     {
  178.         /** @var User $user */
  179.         $user $this->getUser();
  180.         
  181.         $em $this->getDoctrine()->getManager();
  182.         $apartmentDataRepository $em->getRepository(\App\Entity\ApartmentData::class);
  183.         $utilityBillItemRepository $em->getRepository(\App\Entity\UtilityBillItem::class);
  184.         
  185.         // Get type filter from query parameter (optional)
  186.         $type $request->query->get('type');
  187.         
  188.         // Get all apartments for user
  189.         $allApartments $apartmentDataRepository->findAllByUser($user);
  190.         
  191.         // Find apartment by type or use first one
  192.         $apartmentData null;
  193.         if ($type) {
  194.             foreach ($allApartments as $apt) {
  195.                 if ($apt->getType() === $type) {
  196.                     $apartmentData $apt;
  197.                     break;
  198.                 }
  199.             }
  200.         } else {
  201.             // Default to first apartment (or apartment type if available)
  202.             if (!empty($allApartments)) {
  203.                 $apartmentData $allApartments[0];
  204.                 foreach ($allApartments as $apt) {
  205.                     if ($apt->getType() === 'apartment') {
  206.                         $apartmentData $apt;
  207.                         break;
  208.                     }
  209.                 }
  210.             }
  211.         }
  212.         
  213.         if (!$apartmentData) {
  214.             return $this->statusOk(nullnull, [
  215.                 'period' => sprintf('%04d-%02d'$year$month),
  216.                 'total' => 0,
  217.                 'items' => [],
  218.             ]);
  219.         }
  220.         $period = new \DateTime();
  221.         $period->setDate($year$month1);
  222.         $period->setTime(000);
  223.         $billItems $utilityBillItemRepository->createQueryBuilder('bi')
  224.             ->where('bi.apartmentData = :apartmentData')
  225.             ->andWhere('bi.period = :period')
  226.             ->setParameter('apartmentData'$apartmentData)
  227.             ->setParameter('period'$period)
  228.             ->getQuery()
  229.             ->getResult();
  230.         $total 0;
  231.         $items = [];
  232.         foreach ($billItems as $item) {
  233.             $amount = (float)$item->getAmount();
  234.             $total += $amount;
  235.             
  236.             $items[] = [
  237.                 'service' => $item->getService(),
  238.                 'org' => $item->getOrg(),
  239.                 'amount' => number_format($amount2','' '),
  240.             ];
  241.         }
  242.         return $this->statusOk(nullnull, [
  243.             'period' => sprintf('%04d-%02d'$year$month),
  244.             'total' => number_format($total2','' '),
  245.             'items' => $items,
  246.         ]);
  247.     }
  248.     /**
  249.      * @Route(path="/api/utility/payments", methods={"GET"})
  250.      */
  251.     public function getPaymentHistory(Request $requestPaginator $paginator)
  252.     {
  253.         /** @var User $user */
  254.         $user $this->getUser();
  255.         $em $this->getDoctrine()->getManager();
  256.         $paymentBillRepository $em->getRepository(\App\Entity\PaymentBill::class);
  257.         $query $paymentBillRepository->getByUser($user);
  258.         $payments $paginator->paginate(
  259.             $query,
  260.             $request->query->getInt('page'1),
  261.             $request->query->getInt('limit'10)
  262.         );
  263.         $formattedPayments = [];
  264.         foreach ($payments['items'] as $payment) {
  265.             $formattedPayments[] = [
  266.                 'id' => $payment->getId(),
  267.                 'amount' => number_format($payment->getCost(), 2','' '),
  268.                 'date' => $payment->getCreatedAt()->format('d.m.Y H:i'),
  269.                 'status' => $payment->getStatus(),
  270.             ];
  271.         }
  272.         $payments['items'] = $formattedPayments;
  273.         return $this->statusOk(nullnull$payments);
  274.     }
  275.     /**
  276.      * @Route(path="/api/utility/payment", methods={"POST"})
  277.      */
  278.     public function payUtilityBill(
  279.         Request $request,
  280.         TranslatorInterface $translator,
  281.         Fondy $fondy,
  282.         Mercure $mercure
  283.     ) {
  284.         try {
  285.             $data json_decode($request->getContent(), true);
  286.             
  287.             /** @var User $user */
  288.             $user $this->getUser();
  289.             
  290.             $em $this->getDoctrine()->getManager();
  291.         
  292.             // Validate input
  293.             $year = isset($data['year']) ? (int)$data['year'] : null;
  294.             $month = isset($data['month']) ? (int)$data['month'] : null;
  295.             $amount = isset($data['amount']) ? (float)$data['amount'] : null;
  296.             
  297.             if (!$year || !$month || !$amount || $amount <= 0) {
  298.                 return $this->statusConflict(
  299.                     $translator->trans('invalid_payment_data'),
  300.                     ResponseCode::BILLS_NOT_PROVIDED
  301.                 );
  302.             }
  303.             
  304.             // Check if user has valid card token
  305.             $recToken $user->getRecToken();
  306.             $rectokenLifetime $user->getRectokenLifetime();
  307.             
  308.             if (!$recToken || !$rectokenLifetime || $rectokenLifetime->format('Y-m-d') < date('Y-m-d')) {
  309.                 return $this->statusConflict(
  310.                     $translator->trans('need_card'),
  311.                     ResponseCode::BILLS_NOT_PROVIDED
  312.                 );
  313.             }
  314.             
  315.             // Get apartment data
  316.             $apartmentDataRepository $em->getRepository(\App\Entity\ApartmentData::class);
  317.             $apartmentData $apartmentDataRepository->findByUser($user);
  318.             
  319.             if (!$apartmentData) {
  320.                 return $this->statusConflict(
  321.                     $translator->trans('apartment_not_found'),
  322.                     ResponseCode::BILLS_NOT_PROVIDED
  323.                 );
  324.             }
  325.             
  326.             // Get utility bill items for the specified period
  327.             $period = new \DateTime();
  328.             $period->setDate($year$month1);
  329.             $period->setTime(000);
  330.             
  331.             $utilityBillItemRepository $em->getRepository(\App\Entity\UtilityBillItem::class);
  332.             $billItems $utilityBillItemRepository->createQueryBuilder('bi')
  333.                 ->where('bi.apartmentData = :apartmentData')
  334.                 ->andWhere('bi.period = :period')
  335.                 ->setParameter('apartmentData'$apartmentData)
  336.                 ->setParameter('period'$period)
  337.                 ->getQuery()
  338.                 ->getResult();
  339.             
  340.             if (empty($billItems)) {
  341.                 return $this->statusConflict(
  342.                     $translator->trans('bills_not_found_for_period'),
  343.                     ResponseCode::BILLS_NOT_PROVIDED
  344.                 );
  345.             }
  346.             
  347.             // Create payment
  348.             $payment = new PaymentBill();
  349.             $payment->setUser($user);
  350.             $payment->setCost($amount);
  351.             $em->persist($payment);
  352.             $em->flush();
  353.             
  354.             // Process payment through Fondy
  355.             $result $fondy->initiatePayment(
  356.                 ceil($amount 100), // Convert to kopecks
  357.                 $user->getRecToken(),
  358.                 'Utility_Payment_' $payment->getId()
  359.             );
  360.             
  361.             // Update payment with Fondy response
  362.             $payment->setOrderId($result['order_id'] ?? '')
  363.                 ->setStatus($result['order_status'] ?? '')
  364.                 ->setPaymentId($result['payment_id'] ?? '');
  365.             $em->persist($payment);
  366.             $em->flush();
  367.             
  368.             // Send Mercure notification
  369.             $mercure->sendMessageAboutBillPayment($payment);
  370.             
  371.             return $this->statusOk(
  372.                 $translator->trans('payment_done'),
  373.                 null,
  374.                 [
  375.                     'payment_id' => $payment->getPaymentId(),
  376.                     'order_id' => $payment->getOrderId(),
  377.                     'status' => $payment->getStatus(),
  378.                     'amount' => $amount,
  379.                 ]
  380.             );
  381.         } catch (\Exception $e) {
  382.             return $this->statusConflict(
  383.                 'Error: ' $e->getMessage() . ' in ' $e->getFile() . ':' $e->getLine(),
  384.                 ResponseCode::BILLS_NOT_PROVIDED
  385.             );
  386.         }
  387.     }
  388.     /**
  389.      * Format utility counters for mobile app
  390.      *
  391.      * @param array $counters
  392.      * @return array
  393.      */
  394.     private function formatCounters(array $counters): array
  395.     {
  396.         $formatted = [];
  397.         $counterMap = [
  398.             'Електроенергія' => ['type' => 'light''label' => 'Світло'],
  399.             'Холодна вода' => ['type' => 'cold_water''label' => 'Холодна вода'],
  400.             'Гаряча вода' => ['type' => 'hot_water''label' => 'Гаряча вода'],
  401.             'Опалення' => ['type' => 'heating''label' => 'Опалення'],
  402.         ];
  403.         foreach ($counters as $counter) {
  404.             $counterName $counter->getCounter();
  405.             foreach ($counterMap as $key => $info) {
  406.                 if (strpos($counterName$key) !== false) {
  407.                     $formatted[] = [
  408.                         'type' => $info['type'],
  409.                         'label' => $info['label'],
  410.                         'indication' => $counter->getIndication(),
  411.                         'counter' => $counterName,
  412.                     ];
  413.                     break;
  414.                 }
  415.             }
  416.         }
  417.         // Ensure all types are present
  418.         $types = ['light''cold_water''hot_water''heating'];
  419.         $existingTypes array_column($formatted'type');
  420.         
  421.         foreach ($types as $type) {
  422.             if (!in_array($type$existingTypes)) {
  423.                 $labelMap = [
  424.                     'light' => 'Світло',
  425.                     'cold_water' => 'Холодна вода',
  426.                     'hot_water' => 'Гаряча вода',
  427.                     'heating' => 'Опалення',
  428.                 ];
  429.                 $formatted[] = [
  430.                     'type' => $type,
  431.                     'label' => $labelMap[$type],
  432.                     'indication' => 0,
  433.                     'counter' => '',
  434.                 ];
  435.             }
  436.         }
  437.         return $formatted;
  438.     }
  439.     /**
  440.      * Get bills grouped by month
  441.      *
  442.      * @param $apartmentData
  443.      * @param UtilityBillItemRepository $utilityBillItemRepository
  444.      * @return array
  445.      */
  446.     private function getBillsGroupedByMonth($apartmentDataUtilityBillItemRepository $utilityBillItemRepository): array
  447.     {
  448.         $billItems $utilityBillItemRepository->createQueryBuilder('bi')
  449.             ->where('bi.apartmentData = :apartmentData')
  450.             ->setParameter('apartmentData'$apartmentData)
  451.             ->orderBy('bi.period''DESC')
  452.             ->getQuery()
  453.             ->getResult();
  454.         $grouped = [];
  455.         $ukrainianMonths = [
  456.             => 'Січень'=> 'Лютий'=> 'Березень',
  457.             => 'Квітень'=> 'Травень'=> 'Червень',
  458.             => 'Липень'=> 'Серпень'=> 'Вересень',
  459.             10 => 'Жовтень'11 => 'Листопад'12 => 'Грудень',
  460.         ];
  461.         foreach ($billItems as $item) {
  462.             $period $item->getPeriod();
  463.             $key $period->format('Y-m');
  464.             $year = (int)$period->format('Y');
  465.             $month = (int)$period->format('m');
  466.             if (!isset($grouped[$key])) {
  467.                 $grouped[$key] = [
  468.                     'period' => $key,
  469.                     'month' => $ukrainianMonths[$month] ?? $month,
  470.                     'year' => $year,
  471.                     'total' => 0,
  472.                     'items' => [],
  473.                 ];
  474.             }
  475.             $amount = (float)$item->getAmount();
  476.             $grouped[$key]['total'] += $amount;
  477.             $grouped[$key]['items'][] = [
  478.                 'service' => $item->getService(),
  479.                 'amount' => $amount,
  480.             ];
  481.         }
  482.         // Format totals and sort by period descending
  483.         $result = [];
  484.         foreach ($grouped as $key => $data) {
  485.             $result[] = [
  486.                 'period' => $key,
  487.                 'month' => $data['month'],
  488.                 'year' => $data['year'],
  489.                 'total' => number_format($data['total'], 2','' '),
  490.                 'items' => array_map(function($item) {
  491.                     return [
  492.                         'service' => $item['service'],
  493.                         'amount' => number_format($item['amount'], 2','' '),
  494.                     ];
  495.                 }, $data['items']),
  496.             ];
  497.         }
  498.         // Sort by period descending
  499.         usort($result, function($a$b) {
  500.             return strcmp($b['period'], $a['period']);
  501.         });
  502.         return $result;
  503.     }
  504.     /**
  505.      * Map service name to short name for mobile display
  506.      *
  507.      * @param string $service
  508.      * @return string
  509.      */
  510.     private function mapServiceToShortName(string $service): string
  511.     {
  512.         $mapping = [
  513.             'електроенергі' => 'Світло',
  514.             'холодн' => 'Холодна вода',
  515.             'гаряч' => 'Гаряча вода',
  516.             'опаленн' => 'Опалення',
  517.         ];
  518.         $serviceLower mb_strtolower($service);
  519.         foreach ($mapping as $key => $label) {
  520.             if (mb_strpos($serviceLower$key) !== false) {
  521.                 return $label;
  522.             }
  523.         }
  524.         return $service;
  525.     }
  526.     /**
  527.      * Calculate balance (negative sum of unpaid bills)
  528.      *
  529.      * @param $apartmentData
  530.      * @param UtilityBillItemRepository $utilityBillItemRepository
  531.      * @return float
  532.      */
  533.     private function calculateBalance($apartmentDataUtilityBillItemRepository $utilityBillItemRepository): float
  534.     {
  535.         // Get all unpaid bill items
  536.         $billItems $utilityBillItemRepository->createQueryBuilder('bi')
  537.             ->where('bi.apartmentData = :apartmentData')
  538.             ->setParameter('apartmentData'$apartmentData)
  539.             ->getQuery()
  540.             ->getResult();
  541.         $total 0;
  542.         foreach ($billItems as $item) {
  543.             $total += (float)$item->getAmount();
  544.         }
  545.         // Return negative balance
  546.         return -$total;
  547.     }
  548.     /**
  549.      * @Route(path="/api/utility/counters", methods={"GET"})
  550.      */
  551.     public function getCounters(Request $request)
  552.     {
  553.         /** @var User $user */
  554.         $user $this->getUser();
  555.         
  556.         $em $this->getDoctrine()->getManager();
  557.         $apartmentDataRepository $em->getRepository(\App\Entity\ApartmentData::class);
  558.         $counterInfoRepository $em->getRepository(\App\Entity\CounterInfo::class);
  559.         
  560.         // Get persAcc from query parameter (optional)
  561.         $persAcc $request->query->get('persAcc');
  562.         
  563.         // Get all apartments for user
  564.         $allApartments $apartmentDataRepository->findAllByUser($user);
  565.         
  566.         // Find apartment by persAcc if provided, otherwise use first apartment
  567.         $apartmentData null;
  568.         if ($persAcc) {
  569.             $apartmentData $apartmentDataRepository->findByPersAcc($persAcc);
  570.             // Verify it belongs to this user
  571.             if ($apartmentData && $apartmentData->getUser() !== $user) {
  572.                 $apartmentData null;
  573.             }
  574.         } else {
  575.             // Default to first apartment if no persAcc specified
  576.             if (!empty($allApartments)) {
  577.                 $apartmentData $allApartments[0];
  578.             }
  579.         }
  580.         
  581.         if (!$apartmentData) {
  582.             return $this->statusOk(nullnull, [
  583.                 'counters' => [],
  584.             ]);
  585.         }
  586.         $counterInfos $counterInfoRepository->findByPersAcc($apartmentData->getPersAcc());
  587.         
  588.         $formattedCounters = [];
  589.         foreach ($counterInfos as $counterInfo) {
  590.             $counterType $this->getCounterType($counterInfo->getCounter());
  591.             $indications = [];
  592.             
  593.             foreach ($counterInfo->getIndications() as $indication) {
  594.                 $indications[] = [
  595.                     'period' => $indication->getPeriod()->format('Y-m-d'),
  596.                     'previous' => $indication->getIndiPrev(),
  597.                     'current' => $indication->getIndiCur(),
  598.                     'difference' => $indication->getIndiCur() - $indication->getIndiPrev(),
  599.                     'updated' => $indication->getCreatedAt()->format('d.m.Y'),
  600.                 ];
  601.             }
  602.             
  603.             // Sort indications by period descending
  604.             usort($indications, function($a$b) {
  605.                 return strcmp($b['period'], $a['period']);
  606.             });
  607.             
  608.             // Extract meter number from counter string (e.g., "10622721/Електроенергія" -> "10622721")
  609.             $meterNumber $this->extractMeterNumber($counterInfo->getCounter());
  610.             
  611.             $formattedCounters[] = [
  612.                 'type' => $counterType['type'],
  613.                 'label' => $counterType['label'],
  614.                 'counter' => $counterInfo->getCounter(),
  615.                 'meterNumber' => $meterNumber,
  616.                 'persAcc' => $counterInfo->getPersAcc(),
  617.                 'dateStart' => $counterInfo->getDateStart() ? $counterInfo->getDateStart()->format('Y-m-d') : null,
  618.                 'dateEnd' => $counterInfo->getDateEnd() ? $counterInfo->getDateEnd()->format('Y-m-d') : null,
  619.                 'indications' => $indications,
  620.             ];
  621.         }
  622.         return $this->statusOk(nullnull, [
  623.             'counters' => $formattedCounters,
  624.         ]);
  625.     }
  626.     /**
  627.      * @Route(path="/api/utility/counters/{type}", methods={"GET"})
  628.      */
  629.     public function getCounterByType(string $typeRequest $request)
  630.     {
  631.         /** @var User $user */
  632.         $user $this->getUser();
  633.         
  634.         $em $this->getDoctrine()->getManager();
  635.         $apartmentDataRepository $em->getRepository(\App\Entity\ApartmentData::class);
  636.         $counterInfoRepository $em->getRepository(\App\Entity\CounterInfo::class);
  637.         
  638.         // Get persAcc from query parameter (optional)
  639.         $persAcc $request->query->get('persAcc');
  640.         
  641.         // Get all apartments for user
  642.         $allApartments $apartmentDataRepository->findAllByUser($user);
  643.         
  644.         // Find apartment by persAcc if provided, otherwise use first apartment
  645.         $apartmentData null;
  646.         if ($persAcc) {
  647.             $apartmentData $apartmentDataRepository->findByPersAcc($persAcc);
  648.             // Verify it belongs to this user
  649.             if ($apartmentData && $apartmentData->getUser() !== $user) {
  650.                 $apartmentData null;
  651.             }
  652.         } else {
  653.             // Default to first apartment if no persAcc specified
  654.             if (!empty($allApartments)) {
  655.                 $apartmentData $allApartments[0];
  656.             }
  657.         }
  658.         
  659.         if (!$apartmentData) {
  660.             return $this->statusOk(nullnull, [
  661.                 'type' => $type,
  662.                 'counter' => null,
  663.                 'indications' => [],
  664.             ]);
  665.         }
  666.         $counterInfos $counterInfoRepository->findByPersAcc($apartmentData->getPersAcc());
  667.         
  668.         $typeMap = [
  669.             'light' => 'Електроенергія',
  670.             'cold_water' => 'Холодна вода',
  671.             'hot_water' => 'Гаряча вода',
  672.             'heating' => 'Опалення',
  673.         ];
  674.         
  675.         $searchKey $typeMap[$type] ?? null;
  676.         if (!$searchKey) {
  677.             return $this->statusOk(nullnull, [
  678.                 'type' => $type,
  679.                 'counter' => null,
  680.                 'indications' => [],
  681.             ]);
  682.         }
  683.         
  684.         $counterInfo null;
  685.         foreach ($counterInfos as $ci) {
  686.             if (strpos($ci->getCounter(), $searchKey) !== false) {
  687.                 $counterInfo $ci;
  688.                 break;
  689.             }
  690.         }
  691.         
  692.         if (!$counterInfo) {
  693.             return $this->statusOk(nullnull, [
  694.                 'type' => $type,
  695.                 'counter' => null,
  696.                 'indications' => [],
  697.             ]);
  698.         }
  699.         
  700.         $indications = [];
  701.         foreach ($counterInfo->getIndications() as $indication) {
  702.             $indications[] = [
  703.                 'period' => $indication->getPeriod()->format('Y-m-d'),
  704.                 'previous' => $indication->getIndiPrev(),
  705.                 'current' => $indication->getIndiCur(),
  706.                 'difference' => $indication->getIndiCur() - $indication->getIndiPrev(),
  707.                 'updated' => $indication->getCreatedAt()->format('d.m.Y'),
  708.             ];
  709.         }
  710.         
  711.         // Sort indications by period descending
  712.         usort($indications, function($a$b) {
  713.             return strcmp($b['period'], $a['period']);
  714.         });
  715.         
  716.         $counterType $this->getCounterType($counterInfo->getCounter());
  717.         
  718.         // Extract meter number from counter string (e.g., "10622721/Електроенергія" -> "10622721")
  719.         $meterNumber $this->extractMeterNumber($counterInfo->getCounter());
  720.         
  721.         return $this->statusOk(nullnull, [
  722.             'type' => $type,
  723.             'label' => $counterType['label'],
  724.             'counter' => $counterInfo->getCounter(),
  725.             'meterNumber' => $meterNumber,
  726.             'persAcc' => $counterInfo->getPersAcc(),
  727.             'dateStart' => $counterInfo->getDateStart() ? $counterInfo->getDateStart()->format('Y-m-d') : null,
  728.             'dateEnd' => $counterInfo->getDateEnd() ? $counterInfo->getDateEnd()->format('Y-m-d') : null,
  729.             'indications' => $indications,
  730.         ]);
  731.     }
  732.     /**
  733.      * Get counter type from counter name
  734.      *
  735.      * @param string $counterName
  736.      * @return array
  737.      */
  738.     private function getCounterType(string $counterName): array
  739.     {
  740.         $counterMap = [
  741.             'Електроенергія' => ['type' => 'light''label' => 'Світло'],
  742.             'Холодна вода' => ['type' => 'cold_water''label' => 'Холодна вода'],
  743.             'Гаряча вода' => ['type' => 'hot_water''label' => 'Гаряча вода'],
  744.             'Опалення' => ['type' => 'heating''label' => 'Опалення'],
  745.         ];
  746.         foreach ($counterMap as $key => $info) {
  747.             if (strpos($counterName$key) !== false) {
  748.                 return $info;
  749.             }
  750.         }
  751.         return ['type' => 'unknown''label' => $counterName];
  752.     }
  753.     /**
  754.      * Extract meter number from counter string
  755.      * Example: "10622721/Електроенергія" -> "10622721"
  756.      *
  757.      * @param string $counter
  758.      * @return string
  759.      */
  760.     private function extractMeterNumber(string $counter): string
  761.     {
  762.         $parts explode('/'$counter);
  763.         return $parts[0] ?? '';
  764.     }
  765.     /**
  766.      * Format payments for mobile app
  767.      *
  768.      * @param array $payments
  769.      * @return array
  770.      */
  771.     private function formatPayments(array $payments): array
  772.     {
  773.         $formatted = [];
  774.         foreach ($payments as $payment) {
  775.             $formatted[] = [
  776.                 'paymentDate' => $payment->getPaymentDate()->format('Y-m-d\TH:i:s'),
  777.                 'paymentAmount' => number_format((float)$payment->getPaymentAmount(), 2','' '),
  778.             ];
  779.         }
  780.         return $formatted;
  781.     }
  782. }