src/Bus/BookingController.php line 307

Open in your IDE?
  1. <?php
  2. namespace App\Bus;
  3. use App\Entity\Bus\BookingMpesa;
  4. use App\Entity\Bus\BookingSeat;
  5. use App\Entity\Bus\BusLayout;
  6. use App\Entity\Bus\Reservation;
  7. use App\Entity\Bus\RouteFareRule;
  8. use App\Entity\Bus\Seat;
  9. use App\Entity\Bus\Stop;
  10. use App\Entity\Bus\Trip;
  11. use App\Entity\Bus\TripVehicle;
  12. use App\Entity\Mpesa;
  13. use App\Entity\MpesaAuth;
  14. use App\Entity\MpesaPayment;
  15. use App\Entity\MpesaPaymentRequest;
  16. use App\Entity\MpesaResponse;
  17. use App\Entity\User;
  18. use App\Entity\UserStation;
  19. use App\Entity\Vehicle;
  20. use App\Entity\WayBill;
  21. use App\Form\Bus\BookingType;
  22. use App\Form\Bus\ReserveType;
  23. use App\Form\Bus\RouteeType;
  24. use App\Form\Bus\TripType;
  25. use App\Service\SeatHoldReleaser;
  26. use DateTime;
  27. use Doctrine\DBAL\Exception;
  28. use Doctrine\Persistence\ManagerRegistry;
  29. use Doctrine\Persistence\ObjectManager;
  30. use JMS\Serializer\SerializationContext;
  31. use JMS\Serializer\SerializerBuilder;
  32. use Mpdf\Output\Destination;
  33. use Mpdf\QrCode\Output\Png;
  34. use Mpdf\QrCode\QrCode;
  35. use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
  36. use Sasedev\MpdfBundle\Factory\MpdfFactory;
  37. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  38. use Symfony\Component\Form\FormError;
  39. use Symfony\Component\HttpFoundation\JsonResponse;
  40. use Symfony\Component\HttpFoundation\Request;
  41. use Symfony\Component\HttpFoundation\RequestStack;
  42. use Symfony\Component\HttpFoundation\Response;
  43. use Symfony\Component\Routing\Annotation\Route;
  44. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  45. class BookingController extends AbstractController{
  46. private ManagerRegistry $registry;
  47. private ObjectManager $entityManager;
  48. private RequestStack $requestStack;
  49. private SeatHoldReleaser $seatHoldReleaser;
  50. public function __construct(ManagerRegistry $registry, RequestStack $requestStack, SeatHoldReleaser $seatHoldReleaser)
  51. {
  52. $this->entityManager = $registry->getManager();
  53. $this->requestStack = $requestStack;
  54. $this->seatHoldReleaser = $seatHoldReleaser;
  55. }
  56. /** @Route("/routes", name="routesHome") */
  57. public function routesHome(Request $request)
  58. {
  59. $em = $this->entityManager;
  60. /** @var UserStation $userTown */
  61. $userTown = $em->getRepository(UserStation::class)->findOneBy([
  62. 'user' => $this->getUser(),
  63. 'isActive' => true
  64. ], ['id' => 'DESC']);
  65. $route = new \App\Entity\Bus\Route();
  66. $route->setCreatedAt(new DateTime());
  67. $route->setCreatedBy($this->getUser());
  68. $routeForm = $this->createForm(RouteeType::class, $route);
  69. $routeForm->handleRequest($request);
  70. if ($routeForm->isSubmitted() && $routeForm->isValid()) {
  71. dump($route);
  72. $this->entityManager->persist($route);
  73. $this->entityManager->flush();
  74. return $this->redirectToRoute('routesHome');
  75. }
  76. /** @var \App\Entity\Bus\Route[] $routes */
  77. $routes = $this->entityManager->getRepository(\App\Entity\Bus\Route::class)->findAll();
  78. return $this->render('bus/routes.html.twig', [
  79. 'user_town' => $userTown,
  80. 'routes' => $routes,
  81. 'routeForm' => $routeForm->createView()
  82. ]);
  83. }
  84. /** @Route("/routes/{route}/edit", name="routesEdit") */
  85. public function routesEdit(Request $request, \App\Entity\Bus\Route $route)
  86. {
  87. $em = $this->entityManager;
  88. /** @var UserStation $userTown */
  89. $userTown = $em->getRepository(UserStation::class)->findOneBy([
  90. 'user' => $this->getUser(),
  91. 'isActive' => true
  92. ], ['id' => 'DESC']);
  93. $routeForm = $this->createForm(RouteeType::class, $route);
  94. $routeForm->handleRequest($request);
  95. if ($routeForm->isSubmitted() && $routeForm->isValid()) {
  96. // $route->setStops($route->getStops());
  97. $this->entityManager->persist($route);
  98. $this->entityManager->flush();
  99. $this->addFlash('success', 'Route updated successfully');
  100. return $this->redirectToRoute('routesHome');
  101. }
  102. /** @var \App\Entity\Bus\Route[] $routes */
  103. $routes = $this->entityManager->getRepository(\App\Entity\Bus\Route::class)->findAll();
  104. return $this->render('bus/routes_edit.html.twig', [
  105. 'user_town' => $userTown,
  106. 'routes' => $routes,
  107. 'routeForm' => $routeForm->createView()
  108. ]);
  109. }
  110. /** @Route("/", name="tripsHome") */
  111. public function tripsHome(Request $request)
  112. {
  113. $em = $this->entityManager;
  114. $date = $request->get('trip_date');
  115. $today = new \DateTimeImmutable();
  116. if (!$date) {
  117. $date = $today->format('Y-m-d');
  118. }
  119. /** @var UserStation $userTown */
  120. $userTown = $em->getRepository(UserStation::class)->findOneBy([
  121. 'user' => $this->getUser(),
  122. 'isActive' => true
  123. ], ['id' => 'DESC']);
  124. $stationId = $userTown->getStation()->getId();
  125. // MOMBASA and MERU scenario 1
  126. /* if a station origin is in current station show bus route */
  127. // $sql = "SELECT a.id, a.route_name FROM bus_route a WHERE origin = {$stationId}";
  128. // EMBU scenario 2
  129. /* if a station is a stop in a main route and not the destination */
  130. $sql = "SELECT DISTINCT a.id, a.route_name FROM bus_route a
  131. LEFT JOIN bus_stop b ON b.route_id = a.id AND b.route_type = 'MAIN' AND b.station_id = {$stationId}
  132. WHERE (b.station_id = {$stationId} OR a.origin = {$stationId})
  133. AND a.destination != {$stationId}";
  134. $conn = $this->entityManager->getConnection();
  135. $availableRoutes = null;
  136. try {
  137. $stmt = $conn->prepare($sql);
  138. $availableRoutes = $stmt->executeQuery()->fetchAllAssociative();
  139. } catch (\Exception $e) {
  140. }
  141. /** @var Trip[] $trips */
  142. $trips = $this->entityManager->getRepository(Trip::class)->findActiveTrips($date, $userTown->getStation()->getId(), 'MAIN');
  143. if($trips){
  144. return $this->render('bus/trips.html.twig', [
  145. 'user_town' => $userTown,
  146. 'trips' => $trips,
  147. 'date' => $date
  148. ]);
  149. }else {
  150. foreach ($availableRoutes as $index => $availableRoute) {
  151. $route = $em->getRepository(\App\Entity\Bus\Route::class)->findOneBy([
  152. 'id' => $availableRoute['id']
  153. ]);
  154. $this->addTrip($route, $date);
  155. }
  156. }
  157. /** @var Trip[] $trips */
  158. $trips = $this->entityManager->getRepository(Trip::class)->findActiveTrips($date, $userTown->getStation()->getId(), 'MAIN');
  159. return $this->render('bus/trips.html.twig', [
  160. 'user_town' => $userTown,
  161. 'trips' => $trips,
  162. 'date' => $date
  163. ]);
  164. }
  165. /** @Route("/trips/new", name="newTrip") */
  166. public function newTrip(Request $request)
  167. {
  168. $em = $this->entityManager;
  169. /** @var UserStation $userTown */
  170. $userTown = $em->getRepository(UserStation::class)->findOneBy([
  171. 'user' => $this->getUser(),
  172. 'isActive' => true
  173. ], ['id' => 'DESC']);
  174. $trip = new Trip();
  175. $tripForm = $this->createForm(TripType::class, $trip);
  176. $tripForm->handleRequest($request);
  177. if ($tripForm->isSubmitted() && $tripForm->isValid()) {
  178. $date = $trip->getTripDate();
  179. $d = $date->format('Y-m-d');
  180. $sql = "SELECT count(*) total FROM bus_trip a WHERE a.route_id = {$trip->getRoute()->getId()}
  181. AND DATE(a.trip_date) = CAST('{$d}' as date)";
  182. $conn = $this->entityManager->getConnection();
  183. $stmt = $conn->prepare($sql);
  184. $thisDaysTrips = $stmt->executeQuery()->fetchAssociative();
  185. $sameRouteTrips = $thisDaysTrips['total'];
  186. $trip->setTripIndex($sameRouteTrips + 1);
  187. $trip->setCreatedBy($this->getUser());
  188. $trip->setCreatedAt(new DateTime());
  189. /** @var Vehicle $vehicle */
  190. $vehicle = $tripForm->get('vehicle')->getData();
  191. $seats = [];
  192. for ($i = 1; $i <= $vehicle->getPassengerCapacity(); $i++) {
  193. $seat = new Seat();
  194. $seat->setStatus('AVAILABLE');
  195. $seat->setSeatNumber($i);
  196. $seat->setTrip($trip);
  197. $seat->setIsBooked(false);
  198. $seat->setCreatedAt(new DateTime());
  199. $seats[] = $seat;
  200. }
  201. $trip->setSeats($seats);
  202. $tripVehicle = new TripVehicle();
  203. $tripVehicle->setCreatedAt(new DateTime());
  204. $tripVehicle->setTrip($trip);
  205. $tripVehicle->setCreatedBy($this->getUser());
  206. $tripVehicle->setIsCancelled(false);
  207. $tripVehicle->setVehicle($vehicle);
  208. // $this->entityManager->persist($tripVehicle);
  209. // $this->entityManager->persist($trip);
  210. // $this->entityManager->flush();
  211. return $this->redirectToRoute('tripsHome');
  212. }
  213. return $this->render('bus/new_trip.html.twig', [
  214. 'tripForm' => $tripForm->createView(),
  215. 'user_town' => $userTown
  216. ]);
  217. }
  218. /** @Route("/trip/{trip}", name="viewTrip") */
  219. public function bookingTrip(Request $request, Trip $trip)
  220. {
  221. $currentSeat = $request->get('currentSeat') ? $request->get('currentSeat') : -1;
  222. $layout = $this->entityManager->getRepository(BusLayout::class)->findOneBy(['isDefault' => true]);
  223. $doorRow = $layout ? ($layout->getDoorRow() ?? 0) : 0;
  224. $lastRow = $layout ? max(0, $layout->getNumRows() - 1) : 12;
  225. $rows = 0;
  226. /** @var UserStation $userTown */
  227. $userTown = $this->entityManager->getRepository(UserStation::class)->findOneBy([
  228. 'user' => $this->getUser(),
  229. 'isActive' => true
  230. ], ['id' => 'DESC']);
  231. // dump($userTown);
  232. $stop = $this->entityManager->getRepository(Stop::class)->findCurrentStop($trip->getRoute(), $userTown->getStation());
  233. // dump($stop);
  234. // die;
  235. $currentStop = null;
  236. if ($stop) {
  237. $currentStop = $this->entityManager->getRepository(Stop::class)->findOneBy([
  238. 'id' => $stop['id']
  239. ]);
  240. }
  241. if (!$currentStop) {
  242. return $this->redirectToRoute('tripsHome');
  243. }
  244. // Self-heal any abandoned online holds before rendering, same as
  245. // the customer-facing seat map (BusApi\TripController::trip()) -
  246. // otherwise a seat whose hold expired minutes ago would still show
  247. // here as reserved until someone else happens to try booking it.
  248. $releasedCount = $this->seatHoldReleaser->releaseAllStaleForTrip($trip);
  249. if ($releasedCount > 0) {
  250. $this->entityManager->flush();
  251. }
  252. $seats = $this->entityManager->getRepository(BookingSeat::class)->findAllSeats($trip, $currentStop);
  253. // dump($seats);
  254. // dump('currentSeat',$currentSeat);
  255. return $this->render('bus/booking.html.twig', [
  256. 'seats' => $seats,
  257. 'rows' => $rows,
  258. 'doorRow' => $doorRow,
  259. 'lastRow' => $lastRow,
  260. 'workingSeat' => $currentSeat,
  261. 'trip' => $trip,
  262. 'staleHoldMinutes' => SeatHoldReleaser::staleHoldMinutes(),
  263. ]);
  264. }
  265. /** @Route("/trip/{trip}/json", name="viewJsonTrip") */
  266. public function getTripSeatsJson(Request $request, Trip $trip)
  267. {
  268. $context = new SerializationContext();
  269. $context->setSerializeNull(true);
  270. $page = $request->request->get('page') > 1 ? $request->request->get('page') : 1;
  271. $rows = $request->request->get('rows') > 1 ? $request->request->get('rows') : 50;
  272. $offset = ($page - 1) * $rows;
  273. $serializer = SerializerBuilder::create()->build();
  274. $filterRules = $request->request->get('filterRules');
  275. // Same self-heal as bookingTrip() above - this is the datagrid's
  276. // own independent data source (initial load, reload, refilter all
  277. // hit this action), so it needs the same check rather than relying
  278. // on the full-page render to have already done it once.
  279. $releasedCount = $this->seatHoldReleaser->releaseAllStaleForTrip($trip);
  280. if ($releasedCount > 0) {
  281. $this->entityManager->flush();
  282. }
  283. $seats = $this->entityManager->getRepository(BookingSeat::class)->findAllTripSeats($filterRules, $offset, $rows, $trip);
  284. return new JsonResponse($seats);
  285. }
  286. /** @Route("/trip/{trip}/manifest/pdf", name="viewTripManifest") */
  287. public function getManifestBySearch(Request $request, Trip $trip, MpdfFactory $mpdfFactory)
  288. {
  289. $context = new SerializationContext();
  290. $context->setSerializeNull(true);
  291. $page = $request->request->get('page') > 1 ? $request->request->get('page') : 1;
  292. $rows = $request->request->get('rows') > 1 ? $request->request->get('rows') : 50;
  293. $offset = ($page - 1) * $rows;
  294. $filterRules = $request->request->get('filterRules');
  295. $seats = $this->entityManager->getRepository(BookingSeat::class)->findAllTripSeats($filterRules, $offset, $rows, $trip);
  296. $data = $this->jsonTestData();
  297. // dump($fontData);die;
  298. $mPdf = $mpdfFactory->createMpdfObject([
  299. 'format' => [80, 500],
  300. 'margin_header' => 5,
  301. 'margin_footer' => 5,
  302. 'margin_left' => 5,
  303. 'margin_right' => 5,
  304. 'margin_top' => 0,
  305. 'margin_bottom' => 0,
  306. 'autoPageBreak' => false,
  307. 'orientation' => 'P'
  308. ]);
  309. $date = new DateTime();
  310. $time = $date->getTimestamp();
  311. try {
  312. $receiptType = 'MANIFEST';
  313. $logoImage = file_get_contents('../public/bus/bus_1.png');
  314. $logoBase64 = base64_encode($logoImage);
  315. $mPdf->WriteHTML($this->renderView('bus/manifest/manifest.html.twig', [
  316. 'passengers' => $seats,
  317. 'trip' => $trip,
  318. 'busLogo' => $logoBase64
  319. ]));
  320. $file = "../manifests/manifest_.pdf";
  321. // return $MpdfFactory->createDownloadResponse($mPdf, "ticket_{$ticketId}.pdf", Response::HTTP_OK, ["Set-Cookie", "fileDownload=true; path=/"]);
  322. return $mpdfFactory->createInlineResponse($mPdf, $file);
  323. } catch (\Exception $e) {
  324. return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST);
  325. }
  326. // return new JsonResponse($seats);
  327. }
  328. /** @Route("/trip/{trip}/stops", name="viewTripStopsSearchCombo") */
  329. public function getTripStops(Trip $trip)
  330. {
  331. $em = $this->entityManager;
  332. /** @var \App\Entity\Bus\Route $route */
  333. $route = $em->getRepository(\App\Entity\Bus\Route::class)->findOneBy([
  334. 'id' => $trip->getRoute()->getId()
  335. ]);
  336. $sql = "SELECT a.id,b.station_name FROM bus_stop a
  337. JOIN station b ON b.id = a.station_id
  338. WHERE a.route_id = {$route->getId()}";
  339. $conn = $em->getConnection();
  340. $stmt = $conn->prepare($sql);
  341. $stops = $stmt->executeQuery()->fetchAllAssociative();
  342. array_push($stops, ['id' => '', 'station_name' => 'ALL STOPS']);
  343. return new JsonResponse($stops);
  344. }
  345. /** @Route("/trip/{trip}/seat/{seat}", name="viewTripSeat") */
  346. public function bookingTripSeat(Request $request, $seat, Trip $trip)
  347. {
  348. $em = $this->entityManager;
  349. /** @var Seat $busSeat */
  350. $busSeat = $em->getRepository(Seat::class)->findOneBy([
  351. 'trip' => $trip,
  352. 'seatNumber' => $seat
  353. ]);
  354. $seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
  355. 'seat' => $busSeat
  356. ]);
  357. $bookingUserStation = null;
  358. if ($seatBooking) {
  359. /** @var UserStation $userTown */
  360. $bookingUserStation = $this->entityManager->getRepository(UserStation::class)->findOneBy([
  361. 'user' => $seatBooking->getCreatedBy(),
  362. 'isActive' => true
  363. ], ['id' => 'DESC']);
  364. }
  365. /** @var User $user */
  366. $user = $this->getUser();
  367. /** @var UserStation $userStation */
  368. $userStation = $em->getRepository(UserStation::class)->findOneBy([
  369. 'user' => $user,
  370. 'isActive' => true
  371. ]);
  372. $mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
  373. 'station' => $userStation->getStation(),
  374. 'shortCodeType' => 'IMANI'
  375. ]);
  376. if (!$mpesaAuth) {
  377. $mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
  378. 'station' => 2,
  379. 'shortCodeType' => 'IMANI'
  380. ]);
  381. }
  382. $reserve = $em->getRepository(Reservation::class)->findOneBy([
  383. 'seat' => $busSeat
  384. ]);
  385. $booking = new BookingSeat();
  386. $booking->setCreatedBy($this->getUser());
  387. $booking->setCreatedAt(new DateTime());
  388. $booking->setSeat($busSeat);
  389. $defaultFare = $this->getDatabaseFare($trip);
  390. if ($defaultFare > 0) {
  391. $booking->setAmount($defaultFare);
  392. }
  393. // if ($bookingUserStation) {
  394. // $booking->setDepartureTime($bookingUserStation->getDepartureTime());
  395. // }
  396. $bookingForm = $this->createForm(BookingType::class, $booking, [
  397. 'trip' => $trip,
  398. 'shortCode' => $mpesaAuth->getPaybill(),
  399. 'action' => $this->generateUrl('viewTripSeat', ['seat' => $seat, 'trip' => $trip->getId()])
  400. ]);
  401. $bookingForm->handleRequest($request);
  402. if ($bookingForm->isSubmitted()) {
  403. $dbFare = $this->getDatabaseFare($trip, $booking->getOrigin(), $booking->getDestination());
  404. if ($dbFare > 0) {
  405. $booking->setAmount($dbFare);
  406. }
  407. dump($request->getMethod());
  408. if ($bookingForm->has('mpesa')) {
  409. if ($bookingForm->get('mpesa') && $bookingForm->get('mpesa')->getData()) {
  410. /** @var Mpesa $mpesa */
  411. $mpesa = $bookingForm->get('mpesa')->getData();
  412. if ($booking->getPaidVia() == 'MPESA' || $booking->getPaidVia() == 'MPESA_EXPRESS') {
  413. dump($mpesa);
  414. if ($booking->getAmount() > $mpesa->getTransactionAmount()) {
  415. $bookingForm->get('amount')->addError(new FormError('Mpesa amount not enough'));
  416. }
  417. } else if ($booking->getPaidVia() == 'CASH_MPESA') {
  418. if ($mpesa->getTransactionAmount() >= $booking->getAmount()) {
  419. $bookingForm->get('amount')->addError(new FormError('Please check the amounts'));
  420. }
  421. }
  422. }
  423. }
  424. if ($bookingForm->isValid()) {
  425. if ($bookingForm->has('mpesa') && $bookingForm->get('mpesa') && $bookingForm->get('mpesa')->getData()) {
  426. // dump($bookingForm->get('mpesa')->getData());
  427. /** @var Mpesa $mpesa */
  428. $mpesa = $bookingForm->get('mpesa')->getData();
  429. $bookingMpesa = new BookingMpesa();
  430. $bookingMpesa->setBooking($booking);
  431. $bookingMpesa->setCreatedAt(new DateTime());
  432. $bookingMpesa->setCreatedBy($this->getUser());
  433. $bookingMpesa->setAmount($booking->getAmount());
  434. $bookingMpesa->setBalance(0);
  435. $bookingMpesa->setMpesa($mpesa);
  436. $mpesa->setIsUsed(true);
  437. $this->entityManager->persist($bookingMpesa);
  438. }
  439. $departureTime = $booking->getOrigin()->getDepartureTime();
  440. $eta = $booking->getOrigin()->getEta();
  441. $booking->setDepartureTime($departureTime);
  442. $booking->setEta($eta);
  443. $booking->getSeat()->setStatus('booked');
  444. $this->entityManager->persist($booking);
  445. $this->entityManager->flush();
  446. // return $this->redirectToRoute("@viewTrip?currentSeat={$seat}", ['trip' => $trip->getId()]);
  447. return $this->redirect("/bus/trip/{$trip->getId()}?currentSeat={$seat}");
  448. }
  449. }
  450. return $this->render('bus/seat_detail.html.twig', [
  451. 'seat' => $busSeat,
  452. 'trip' => $trip,
  453. 'booking' => $seatBooking,
  454. 'bookingForm' => $bookingForm->createView(),
  455. 'bookingStation' => $bookingUserStation,
  456. 'reservation' => $reserve,
  457. 'user_station' => $userStation
  458. ]);
  459. }
  460. /** @Route("/trip/{trip}/seat/{seat}/book_reserve_opt", name="viewReserveBookOpt") */
  461. public function bookingReserveOption(Request $request, $seat, Trip $trip)
  462. {
  463. $em = $this->entityManager;
  464. /** @var Seat $busSeat */
  465. $busSeat = $em->getRepository(Seat::class)->findOneBy([
  466. 'trip' => $trip,
  467. 'seatNumber' => $seat
  468. ]);
  469. $reserve = $em->getRepository(Reservation::class)->findOneBy([
  470. 'seat' => $busSeat,
  471. 'canceledAt' => null
  472. ]);
  473. $seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
  474. 'seat' => $busSeat,
  475. ]);
  476. if ($seatBooking || $reserve) {
  477. return $this->redirectToRoute('viewTripSeat', ['trip' => $trip->getId(), 'seat' => $busSeat->getSeatNumber()]);
  478. }
  479. return $this->render('bus/book_reserve.html.twig', [
  480. 'seat' => $busSeat,
  481. 'trip' => $trip,
  482. 'booking' => $seatBooking
  483. ]);
  484. }
  485. /** @Route("/trip/{trip}/seat/{seat}/cancel_reservation", name="cancelReservation") */
  486. public function cancelReservation(Request $request, $seat, Trip $trip)
  487. {
  488. $em = $this->entityManager;
  489. /** @var Seat $busSeat */
  490. $busSeat = $em->getRepository(Seat::class)->findOneBy([
  491. 'trip' => $trip,
  492. 'seatNumber' => $seat
  493. ]);
  494. $seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
  495. 'seat' => $busSeat
  496. ]);
  497. $reserve = $em->getRepository(Reservation::class)->findOneBy([
  498. 'seat' => $busSeat,
  499. 'canceledAt' => null
  500. ]);
  501. $busSeat->setStatus('AVAILABLE');
  502. $reserve->setCanceledAt(new DateTime());
  503. $reserve->setCanceledBy($this->getUser());
  504. $em->flush();
  505. return $this->redirectToRoute('viewTrip', ['trip' => $trip->getId()]);
  506. }
  507. /** @Route("/trip/{trip}/seat/{seat}/reserve", name="viewReserve") */
  508. public function reserve(Request $request, $seat, Trip $trip)
  509. {
  510. $em = $this->entityManager;
  511. /** @var UserStation $userStation */
  512. $userStation = $em->getRepository(UserStation::class)->findOneBy([
  513. 'user' => $this->getUser(),
  514. 'isActive' => true
  515. ]);
  516. /** @var Seat $busSeat */
  517. $busSeat = $em->getRepository(Seat::class)->findOneBy([
  518. 'trip' => $trip,
  519. 'seatNumber' => $seat
  520. ]);
  521. $seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
  522. 'seat' => $busSeat
  523. ]);
  524. $reservation = new Reservation();
  525. $reservation->setReservedAt(new DateTime());
  526. $reservation->setReservedBy($this->getUser());
  527. $reservation->setReservingStation($userStation->getStation());
  528. $reservation->setSeat($busSeat);
  529. $reserveForm = $this->createForm(ReserveType::class, $reservation);
  530. $reserveForm->handleRequest($request);
  531. if ($reserveForm->isSubmitted() && $reserveForm->isValid()) {
  532. $em->persist($reservation);
  533. $busSeat->setStatus('RESERVED');
  534. $em->flush();
  535. return $this->redirectToRoute('viewTrip', ['trip' => $trip->getId()]);
  536. }
  537. return $this->render('bus/reserve.html.twig', [
  538. 'seat' => $busSeat,
  539. 'trip' => $trip,
  540. 'booking' => $seatBooking,
  541. 'reserveForm' => $reserveForm->createView(),
  542. 'action' => $this->generateUrl('viewReserve', ['trip' => $trip->getId(), 'seat' => $busSeat->getSeatNumber()])
  543. ]);
  544. }
  545. /**
  546. * @Route("/ticket/{ticketId}", methods={"GET"}, name="generateTicket")
  547. */
  548. public function generateTicket(Request $request, $ticketId, MpdfFactory $MpdfFactory): Response
  549. {
  550. $em = $this->entityManager;
  551. $booking = $em->getRepository(BookingSeat::class)->findOneBy([
  552. 'id' => $ticketId
  553. ]);
  554. $payment = null;
  555. if ($booking->getPaidVia() == 'MPESA' || $booking->getPaidVia() == 'CASH_MPESA') {
  556. /** @var BookingMpesa $payment */
  557. $payment = $em->getRepository(BookingMpesa::class)->findOneBy([
  558. 'booking' => $booking
  559. ]);
  560. }
  561. $defaultConfig = $MpdfFactory->getDefaultConfigVariables();
  562. $fontDirs = $defaultConfig['fontDir'];
  563. $defaultFontConfig = $MpdfFactory->getDefaultFontVariables();
  564. $fontData = $defaultFontConfig['fontdata'];
  565. // dump($fontData);die;
  566. $mPdf = $MpdfFactory->createMpdfObject([
  567. /* 'fontDir' => array_merge($fontDirs, [
  568. '../../public/fos/fonts/Tangerine',
  569. '../../public/fos/fonts/OdibeeSans',
  570. '../../public/fos/fonts/Mogra',
  571. '../../public/fos/fonts/Libre_Baskerville',
  572. ]),
  573. 'fontdata' => $fontData + [
  574. 'tangerine' => [
  575. 'B' => 'Tangerine-Bold.ttf',
  576. 'R' => 'Tangerine-Regular.ttf'
  577. ],
  578. 'odibeesans' => [
  579. 'R' => 'OdibeeSans-Regular.ttf',
  580. 'I' => 'OdibeeSans-Regular.ttf'
  581. ],
  582. 'mogra' => [
  583. 'R' => 'Mogra-Bold.ttf',
  584. 'I' => 'Mogra-Regular.ttf',
  585. ],
  586. 'librebaskerville' => [
  587. 'R' => 'LibreBaskerville-Regular.ttf',
  588. 'B' => 'LibreBaskerville-Bold.ttf',
  589. 'I' => 'LibreBaskerville-Italic.ttf',
  590. ]
  591. ],*/
  592. 'mode' => 'utf-8',
  593. 'format' => [80, 250],
  594. 'margin_header' => 5,
  595. 'margin_footer' => 5,
  596. 'orientation' => 'P'
  597. ]);
  598. $date = new DateTime();
  599. $time = $date->getTimestamp();
  600. // $mPdf->SetTopMargin("1");
  601. // $qrCode = new QrCode('');
  602. try {
  603. $receiptType = 'TICKET';
  604. /* $timsQRCode = 'ticket';
  605. $timsqrCode = new QrCode($timsQRCode);
  606. $output = new Png();
  607. $data = $output->output($qrCode, 100, [255, 255, 255], [0, 0, 0]);
  608. $timsQRCodeData = $output->output($timsqrCode, 150, [255, 255, 255], [0, 0, 0]);
  609. file_put_contents('assets/qrcode/qrcode.png', $data);
  610. file_put_contents('assets/qrcode/timsQRCode.png', $timsQRCodeData);*/
  611. dump($payment);
  612. $idBase64 = base64_encode($booking->getId());
  613. $logoImage = file_get_contents('../public/bus/bus_1.png');
  614. $logoBase64 = base64_encode($logoImage);
  615. $mPdf->WriteHTML($this->renderView('bus/ticket/ticket.html.twig', [
  616. 'booking' => $booking,
  617. 'receiptType' => $receiptType,
  618. 'payment' => $payment,
  619. 'id' => $idBase64,
  620. 'logo' => $logoBase64
  621. ]));
  622. $file = "../tickets/ticket_{$ticketId}.pdf";
  623. // return $MpdfFactory->createDownloadResponse($mPdf, "ticket_{$ticketId}.pdf", Response::HTTP_OK, ["Set-Cookie", "fileDownload=true; path=/"]);
  624. return $MpdfFactory->createInlineResponse($mPdf, $file);
  625. } catch (\Exception $e) {
  626. return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST);
  627. }
  628. // return new Response("error occurred");
  629. }
  630. /**
  631. * @Route("/daily_account", methods={"GET","POST"}, name="busDailyAccount")
  632. */
  633. public function dailyAccount(Request $request): Response
  634. {
  635. if ($request->isMethod('POST')) {
  636. $em = $this->entityManager;
  637. $today = new \DateTimeImmutable();
  638. $date = $today->format('Y-m-d');
  639. $user_id = $this->getUser()->getId();
  640. /** @var UserStation $userTown */
  641. $userTown = $this->entityManager->getRepository(UserStation::class)->findOneBy([
  642. 'user' => $this->getUser(),
  643. 'isActive' => true
  644. ], ['id' => 'DESC']);
  645. // Status filter added (ai_changes/BE-12): a bus_booking_seat row
  646. // used to always mean "paid" - now that pending/failed online
  647. // bookings can create one before payment succeeds, both queries
  648. // below need to exclude those explicitly. A NULL booking_id
  649. // still means "counter booking, always paid", so those stay in
  650. // unconditionally via the LEFT JOIN + IS NULL check.
  651. $sql = " SELECT d.route_name,a.paid_via,sum(a.amount) amount,c.trip_date FROM bus_booking_seat a
  652. JOIN bus_seat b ON b.id = a.seat_id
  653. JOIN bus_trip c ON c.id = b.trip_id
  654. JOIN bus_route d ON d.id = c.route_id
  655. LEFT JOIN bus_booking bk ON bk.id = a.booking_id
  656. WHERE date(a.created_at) = cast('2026-02-06' as date)
  657. AND (bk.id IS NULL OR bk.status = 'confirmed')
  658. GROUP BY paid_via, d.id,b.trip_id";
  659. $sqlTotal = "SELECT sum(a.amount) as total FROM bus_booking_seat a
  660. LEFT JOIN bus_booking bk ON bk.id = a.booking_id
  661. WHERE a.created_by = {$user_id} AND date(a.created_at) = cast('2026-02-06' as date)
  662. AND (bk.id IS NULL OR bk.status = 'confirmed')";
  663. $conn = $em->getConnection();
  664. try {
  665. $stmt = $conn->prepare($sql);
  666. $amounts = $stmt->executeQuery()->fetchAllAssociative();
  667. $stmtTotal = $conn->prepare($sqlTotal);
  668. $total = $stmtTotal->executeQuery()->fetchAssociative();
  669. return $this->render('bus/daily_account.html.twig', [
  670. 'date' => $date,
  671. 'amounts' => $amounts,
  672. 'total' => $total,
  673. 'user_town' => $userTown
  674. ]);
  675. } catch (Exception $e) {
  676. }
  677. }
  678. return $this->render('bus/daily_account.html.twig');
  679. }
  680. /**
  681. * @Route("/daily_account/list", methods={"POST", "GET"}, name="busDailyAccountList")
  682. */
  683. public function dailyAccountList(Request $request): JsonResponse
  684. {
  685. $em = $this->entityManager;
  686. $page = (int)($request->request->get('page') ?? $request->query->get('page') ?? 1);
  687. $rows = (int)($request->request->get('rows') ?? $request->query->get('rows') ?? 30);
  688. $offset = max(0, ($page - 1) * $rows);
  689. $searchDate = $request->request->get('date') ?? $request->query->get('date');
  690. if (!$searchDate) {
  691. $searchDate = (new \DateTime())->format('Y-m-d');
  692. }
  693. $params = ['searchDate' => $searchDate];
  694. $sqlTotal = "SELECT COUNT(a.id) as count_total, SUM(a.amount) as sum_amount
  695. FROM bus_booking_seat a
  696. LEFT JOIN bus_booking bk ON bk.id = a.booking_id
  697. WHERE date(a.created_at) = :searchDate
  698. AND (bk.id IS NULL OR bk.status = 'confirmed')";
  699. $sqlRows = "SELECT a.id, a.passenger, a.phone, a.amount, a.paid_via,
  700. DATE_FORMAT(a.created_at, '%Y-%m-%d %H:%i') as created_at,
  701. b.seat_number,
  702. DATE_FORMAT(c.trip_date, '%Y-%m-%d') as trip_date,
  703. d.route_name,
  704. COALESCE(st1.station_name, 'N/A') as origin_name,
  705. COALESCE(st2.station_name, 'N/A') as dest_name,
  706. COALESCE(u.username, 'System') as clerk_name
  707. FROM bus_booking_seat a
  708. JOIN bus_seat b ON b.id = a.seat_id
  709. JOIN bus_trip c ON c.id = b.trip_id
  710. JOIN bus_route d ON d.id = c.route_id
  711. LEFT JOIN bus_stop orig ON orig.id = a.origin
  712. LEFT JOIN station st1 ON st1.id = orig.station_id
  713. LEFT JOIN bus_stop dest ON dest.id = a.destination
  714. LEFT JOIN station st2 ON st2.id = dest.station_id
  715. LEFT JOIN user u ON u.id = a.created_by
  716. LEFT JOIN bus_booking bk ON bk.id = a.booking_id
  717. WHERE date(a.created_at) = :searchDate
  718. AND (bk.id IS NULL OR bk.status = 'confirmed')
  719. ORDER BY a.id DESC
  720. LIMIT {$rows} OFFSET {$offset}";
  721. $conn = $em->getConnection();
  722. $totalData = $conn->fetchAssociative($sqlTotal, $params);
  723. $totalCount = (int)($totalData['count_total'] ?? 0);
  724. $sumAmount = (float)($totalData['sum_amount'] ?? 0);
  725. $rowsData = $conn->fetchAllAssociative($sqlRows, $params);
  726. return new JsonResponse([
  727. 'total' => $totalCount,
  728. 'rows' => $rowsData,
  729. 'footer' => [
  730. [
  731. 'passenger' => 'TOTAL FARES',
  732. 'amount' => $sumAmount
  733. ]
  734. ]
  735. ]);
  736. }
  737. /**
  738. * @Route("/booking/seat_details/{id}", methods={"GET", "POST"}, name="busBookingSeatDetails")
  739. */
  740. public function getBookingSeatDetails($id): Response
  741. {
  742. $em = $this->entityManager;
  743. $conn = $em->getConnection();
  744. $sql = "SELECT a.id as ticket_id, a.passenger, a.phone, a.amount, a.paid_via,
  745. DATE_FORMAT(a.created_at, '%Y-%m-%d %H:%i:%s') as created_at,
  746. b.seat_number, b.status as seat_status,
  747. DATE_FORMAT(c.trip_date, '%Y-%m-%d') as trip_date,
  748. d.route_name,
  749. COALESCE(st1.station_name, 'N/A') as origin_name,
  750. COALESCE(st2.station_name, 'N/A') as dest_name,
  751. COALESCE(u.username, 'System') as clerk_name,
  752. bk.id as booking_group_id,
  753. bk.status as booking_status,
  754. m.transaction_id as mpesa_receipt
  755. FROM bus_booking_seat a
  756. JOIN bus_seat b ON b.id = a.seat_id
  757. JOIN bus_trip c ON c.id = b.trip_id
  758. JOIN bus_route d ON d.id = c.route_id
  759. LEFT JOIN bus_stop orig ON orig.id = a.origin
  760. LEFT JOIN station st1 ON st1.id = orig.station_id
  761. LEFT JOIN bus_stop dest ON dest.id = a.destination
  762. LEFT JOIN station st2 ON st2.id = dest.station_id
  763. LEFT JOIN user u ON u.id = a.created_by
  764. LEFT JOIN bus_booking bk ON bk.id = a.booking_id
  765. LEFT JOIN bus_booking_mpesa bbm ON bbm.bus_booking_id = bk.id
  766. LEFT JOIN mpesa m ON m.id = bbm.mpesa_id
  767. WHERE a.id = :id
  768. LIMIT 1";
  769. $details = $conn->fetchAssociative($sql, ['id' => $id]);
  770. if (!$details) {
  771. return new Response('<div class="alert alert-warning p-2">Details not found for ticket #' . htmlspecialchars((string)$id) . '</div>');
  772. }
  773. return $this->render('bus/seat_detail_panel.html.twig', [
  774. 'd' => $details
  775. ]);
  776. }
  777. private function addTrip(\App\Entity\Bus\Route $route, string $date)
  778. {
  779. $em = $this->entityManager;
  780. $dateTime = DateTime::createFromFormat('Y-m-d', $date);
  781. if (!$dateTime) {
  782. $dateTime = new DateTime($date);
  783. }
  784. /** @var RouteFareRule $fareRule */
  785. $fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
  786. 'route' => $route,
  787. ]);
  788. $trip = new Trip();
  789. $trip->setCreatedBy($this->getUser());
  790. $trip->setCreatedAt(new DateTime());
  791. $trip->setTripDate($dateTime);
  792. $trip->setRoute($route);
  793. $trip->setDestination($route->getDestination());
  794. $trip->setOrigin($route->getOrigin());
  795. $trip->setTripIndex(1);
  796. if ($fareRule) {
  797. $trip->setFare($fareRule->getNormalFare());
  798. }
  799. $departureStored = $route->getDepartureTime();
  800. $arrivalStored = $route->getEta();
  801. $baseDate = clone $dateTime;
  802. $departure = (clone $baseDate)->setTime(
  803. (int)$departureStored->format('H'),
  804. (int)$departureStored->format('i'),
  805. (int)$departureStored->format('s')
  806. );
  807. $arrival = (clone $baseDate)->setTime(
  808. (int)$arrivalStored->format('H'),
  809. (int)$arrivalStored->format('i'),
  810. (int)$arrivalStored->format('s')
  811. );
  812. if ($arrival <= $departure) {
  813. $arrival->modify('+1 day');
  814. }
  815. $trip->setDepartureTime($departure);
  816. $trip->setEta($arrival);
  817. /** @var Vehicle $vehicle */
  818. $vehicle = $em->getRepository(Vehicle::class)->findOneBy([
  819. 'regNumber' => 'KCE990Q'
  820. ]);
  821. $seats = [];
  822. for ($i = 1; $i <= $vehicle->getPassengerCapacity(); $i++) {
  823. $seat = new Seat();
  824. $seat->setStatus('AVAILABLE');
  825. $seat->setSeatNumber($i);
  826. $seat->setTrip($trip);
  827. $seat->setIsBooked(false);
  828. $seat->setCreatedAt(new DateTime());
  829. $seats[] = $seat;
  830. }
  831. $trip->setSeats($seats);
  832. $trip->setVehicle($vehicle);
  833. $tripVehicle = new TripVehicle();
  834. $tripVehicle->setCreatedAt(new DateTime());
  835. $tripVehicle->setTrip($trip);
  836. $tripVehicle->setCreatedBy($this->getUser());
  837. $tripVehicle->setIsCancelled(false);
  838. $tripVehicle->setVehicle($vehicle);
  839. $this->entityManager->persist($tripVehicle);
  840. $this->entityManager->persist($trip);
  841. $this->entityManager->flush();
  842. }
  843. private
  844. function jsonTestData()
  845. {
  846. $jsonData = '[{
  847. "id": 741421,
  848. "account_date": "2023-10-09",
  849. "expenses": 0,
  850. "created_at": "2023-10-09 19:59:54",
  851. "station_name": "MOMBASA",
  852. "sender_name": "patrick kinyua",
  853. "sender_phone_number": "0727 044 524",
  854. "receiver_name": "luke njoroge",
  855. "receiver_phone_number": "0705 179 108",
  856. "amount": 2100,
  857. "is_cancelled": 0
  858. },
  859. {
  860. "id": 741405,
  861. "account_date": "2023-10-09",
  862. "expenses": 0,
  863. "created_at": "2023-10-09 19:19:03",
  864. "station_name": "MERU",
  865. "sender_name": "phineas kirimi",
  866. "sender_phone_number": "0791 713 924",
  867. "receiver_name": "patrick munoru",
  868. "receiver_phone_number": "0720 949 054",
  869. "amount": 200,
  870. "is_cancelled": 0
  871. },
  872. {
  873. "id": 741395,
  874. "account_date": "2023-10-09",
  875. "expenses": 0,
  876. "created_at": "2023-10-09 19:09:05",
  877. "station_name": "kitui",
  878. "sender_name": "scout shop",
  879. "sender_phone_number": "0712 907 877",
  880. "receiver_name": "winfred ndolo",
  881. "receiver_phone_number": "0724 528 806",
  882. "amount": 200,
  883. "is_cancelled": 0
  884. },
  885. {
  886. "id": 741393,
  887. "account_date": "2023-10-09",
  888. "expenses": 0,
  889. "created_at": "2023-10-09 19:07:04",
  890. "station_name": "SIAKAGO",
  891. "sender_name": "james gichuki",
  892. "sender_phone_number": "0722 352 992",
  893. "receiver_name": "richard waitherero",
  894. "receiver_phone_number": "0728 454 129",
  895. "amount": 300,
  896. "is_cancelled": 0
  897. },
  898. {
  899. "id": 741378,
  900. "account_date": "2023-10-09",
  901. "expenses": 0,
  902. "created_at": "2023-10-09 18:55:25",
  903. "station_name": "NAIROBI",
  904. "sender_name": "denis munene",
  905. "sender_phone_number": "0740 270 344",
  906. "receiver_name": "tom ochieng",
  907. "receiver_phone_number": "0715 553 018",
  908. "amount": 200,
  909. "is_cancelled": 0
  910. },
  911. {
  912. "id": 741370,
  913. "account_date": "2023-10-09",
  914. "expenses": 0,
  915. "created_at": "2023-10-09 18:43:13",
  916. "station_name": "NAIROBI",
  917. "sender_name": "peter njagi",
  918. "sender_phone_number": "0722 883 774",
  919. "receiver_name": "jackson kanambiu",
  920. "receiver_phone_number": "0714 203 693",
  921. "amount": 200,
  922. "is_cancelled": 0
  923. },
  924. {
  925. "id": 741355,
  926. "account_date": "2023-10-09",
  927. "expenses": 0,
  928. "created_at": "2023-10-09 18:32:02",
  929. "station_name": "MOMBASA",
  930. "sender_name": "mary mbugi",
  931. "sender_phone_number": "0727 897 097",
  932. "receiver_name": "elizabeth nyawira",
  933. "receiver_phone_number": "0706 221 351",
  934. "amount": 300,
  935. "is_cancelled": 0
  936. },
  937. {
  938. "id": 741351,
  939. "account_date": "2023-10-09",
  940. "expenses": 0,
  941. "created_at": "2023-10-09 18:25:01",
  942. "station_name": "NAIROBI",
  943. "sender_name": "david thairu",
  944. "sender_phone_number": "0722 280 846",
  945. "receiver_name": "daniel kimondo",
  946. "receiver_phone_number": "0724 215 522",
  947. "amount": 200,
  948. "is_cancelled": 0
  949. },
  950. {
  951. "id": 741330,
  952. "account_date": "2023-10-09",
  953. "expenses": 0,
  954. "created_at": "2023-10-09 17:58:08",
  955. "station_name": "NAIROBI",
  956. "sender_name": "karen karimi",
  957. "sender_phone_number": "0714 509 299",
  958. "receiver_name": "victor muthoni",
  959. "receiver_phone_number": "0710 329 231",
  960. "amount": 200,
  961. "is_cancelled": 0
  962. },
  963. {
  964. "id": 741328,
  965. "account_date": "2023-10-09",
  966. "expenses": 0,
  967. "created_at": "2023-10-09 17:56:42",
  968. "station_name": "KATHWANA",
  969. "sender_name": "samwel",
  970. "sender_phone_number": "0722 595 844",
  971. "receiver_name": "dirangu ndungu",
  972. "receiver_phone_number": "0721 207 371",
  973. "amount": 250,
  974. "is_cancelled": 0
  975. },
  976. {
  977. "id": 741301,
  978. "account_date": "2023-10-09",
  979. "expenses": 0,
  980. "created_at": "2023-10-09 17:35:27",
  981. "station_name": "MOMBASA",
  982. "sender_name": "edith kimani",
  983. "sender_phone_number": "0714 567 205",
  984. "receiver_name": "okumu",
  985. "receiver_phone_number": "0714 567 205",
  986. "amount": 300,
  987. "is_cancelled": 0
  988. },
  989. {
  990. "id": 741289,
  991. "account_date": "2023-10-09",
  992. "expenses": 0,
  993. "created_at": "2023-10-09 17:26:09",
  994. "station_name": "MTWAPA",
  995. "sender_name": "caroline waweru",
  996. "sender_phone_number": "0712 353 926",
  997. "receiver_name": "GLADYS WAWERU",
  998. "receiver_phone_number": "0723 950 679",
  999. "amount": 300,
  1000. "is_cancelled": 0
  1001. },
  1002. {
  1003. "id": 741270,
  1004. "account_date": "2023-10-09",
  1005. "expenses": 0,
  1006. "created_at": "2023-10-09 17:03:24",
  1007. "station_name": "MWEA",
  1008. "sender_name": "FOCUS EMBU",
  1009. "sender_phone_number": "0716 447 206",
  1010. "receiver_name": "FOCUS MWEA",
  1011. "receiver_phone_number": "0721 606 746",
  1012. "amount": 150,
  1013. "is_cancelled": 0
  1014. },
  1015. {
  1016. "id": 741266,
  1017. "account_date": "2023-10-09",
  1018. "expenses": 0,
  1019. "created_at": "2023-10-09 16:58:43",
  1020. "station_name": "THIKA",
  1021. "sender_name": "KIMATHI KELVIN",
  1022. "sender_phone_number": "0726 116 760",
  1023. "receiver_name": "JAMES NGINGI",
  1024. "receiver_phone_number": "0728 171 491",
  1025. "amount": 200,
  1026. "is_cancelled": 0
  1027. },
  1028. {
  1029. "id": 741261,
  1030. "account_date": "2023-10-09",
  1031. "expenses": 0,
  1032. "created_at": "2023-10-09 16:54:46",
  1033. "station_name": "NAIROBI",
  1034. "sender_name": "ROSALINE WAWIRA",
  1035. "sender_phone_number": "0728 288 950",
  1036. "receiver_name": "eras karimi",
  1037. "receiver_phone_number": "0724 867 838",
  1038. "amount": 200,
  1039. "is_cancelled": 0
  1040. },
  1041. {
  1042. "id": 741248,
  1043. "account_date": "2023-10-09",
  1044. "expenses": 0,
  1045. "created_at": "2023-10-09 16:47:18",
  1046. "station_name": "MOMBASA",
  1047. "sender_name": "jemmimah",
  1048. "sender_phone_number": "0711 485 954",
  1049. "receiver_name": "rose khaidi",
  1050. "receiver_phone_number": "0706 749 991",
  1051. "amount": 300,
  1052. "is_cancelled": 0
  1053. },
  1054. {
  1055. "id": 741209,
  1056. "account_date": "2023-10-09",
  1057. "expenses": 0,
  1058. "created_at": "2023-10-09 16:05:38",
  1059. "station_name": "kitui",
  1060. "sender_name": "eunice mambiro",
  1061. "sender_phone_number": "0720 997 508",
  1062. "receiver_name": "samwel muturi",
  1063. "receiver_phone_number": "0715 143 362",
  1064. "amount": 200,
  1065. "is_cancelled": 0
  1066. },
  1067. {
  1068. "id": 741185,
  1069. "account_date": "2023-10-09",
  1070. "expenses": 0,
  1071. "created_at": "2023-10-09 15:49:11",
  1072. "station_name": "NAIROBI",
  1073. "sender_name": "johnson njeru",
  1074. "sender_phone_number": "0724 586 321",
  1075. "receiver_name": "jane njoki njeru",
  1076. "receiver_phone_number": "0722 398 853",
  1077. "amount": 300,
  1078. "is_cancelled": 0
  1079. },
  1080. {
  1081. "id": 741175,
  1082. "account_date": "2023-10-09",
  1083. "expenses": 0,
  1084. "created_at": "2023-10-09 15:36:39",
  1085. "station_name": "NAIROBI",
  1086. "sender_name": "alexande david",
  1087. "sender_phone_number": "0795 762 309",
  1088. "receiver_name": "peter aunga",
  1089. "receiver_phone_number": "0715 044 187",
  1090. "amount": 200,
  1091. "is_cancelled": 0
  1092. },
  1093. {
  1094. "id": 741172,
  1095. "account_date": "2023-10-09",
  1096. "expenses": 0,
  1097. "created_at": "2023-10-09 15:33:17",
  1098. "station_name": "NAIROBI",
  1099. "sender_name": "j kimondo",
  1100. "sender_phone_number": "0722 830 014",
  1101. "receiver_name": "fred muiruri",
  1102. "receiver_phone_number": "0705 945 561",
  1103. "amount": 200,
  1104. "is_cancelled": 0
  1105. }
  1106. ]';
  1107. $arrayedFromJson = json_decode($jsonData, true);
  1108. return $arrayedFromJson;
  1109. }
  1110. /**
  1111. * @Route("/stk-push/initiate", methods={"POST"}, name="bus_stk_push_initiate")
  1112. */
  1113. public function initiateStkPush(Request $request): JsonResponse
  1114. {
  1115. $em = $this->entityManager;
  1116. $phone = $request->request->get('phone');
  1117. $amount = (float) $request->request->get('amount');
  1118. $seatNumber = $request->request->get('seat');
  1119. $tripId = $request->request->get('tripId');
  1120. if (!$phone || !$amount) {
  1121. return new JsonResponse(['success' => false, 'message' => 'Phone and amount are required.'], Response::HTTP_BAD_REQUEST);
  1122. }
  1123. $phone = preg_replace('/[^0-9]/', '', (string)$phone);
  1124. if (strpos($phone, '0') === 0) {
  1125. $phone = '254' . substr($phone, 1);
  1126. } elseif (strpos($phone, '7') === 0 || strpos($phone, '1') === 0) {
  1127. $phone = '254' . $phone;
  1128. }
  1129. /** @var MpesaAuth $mpesaAuth */
  1130. $mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
  1131. 'paybill' => '4005975'
  1132. ]);
  1133. if (!$mpesaAuth) {
  1134. $mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
  1135. 'shortCodeType' => 'BUS_STK'
  1136. ]);
  1137. }
  1138. if (!$mpesaAuth) {
  1139. return new JsonResponse(['success' => false, 'message' => 'M-Pesa STK configuration not found.'], Response::HTTP_INTERNAL_SERVER_ERROR);
  1140. }
  1141. $token = $this->getMpesaStkToken($mpesaAuth);
  1142. if (!$token) {
  1143. return new JsonResponse(['success' => false, 'message' => 'Failed to generate M-Pesa OAuth Access Token.'], Response::HTTP_INTERNAL_SERVER_ERROR);
  1144. }
  1145. $timestamp = (new DateTime())->format('YmdHis');
  1146. $businessShortCode = $mpesaAuth->getPaybill();
  1147. $passKey = $mpesaAuth->getPassKey();
  1148. $password = base64_encode($businessShortCode . $passKey . $timestamp);
  1149. $callbackUrl = $this->generateUrl('bus_stk_push_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
  1150. $postData = [
  1151. 'BusinessShortCode' => $businessShortCode,
  1152. 'Password' => $password,
  1153. 'Timestamp' => $timestamp,
  1154. 'TransactionType' => 'CustomerPayBillOnline',
  1155. 'Amount' => (int) $amount,
  1156. 'PartyA' => $phone,
  1157. 'PartyB' => $businessShortCode,
  1158. 'PhoneNumber' => $phone,
  1159. 'CallBackURL' => $callbackUrl,
  1160. 'AccountReference' => 'SEAT' . ($seatNumber ? '_' . $seatNumber : ''),
  1161. 'TransactionDesc' => 'Bus Booking Seat ' . $seatNumber
  1162. ];
  1163. $curl = curl_init();
  1164. curl_setopt_array($curl, [
  1165. CURLOPT_URL => "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
  1166. CURLOPT_HTTPHEADER => [
  1167. "Content-Type: application/json",
  1168. "Authorization: Bearer {$token}"
  1169. ],
  1170. CURLOPT_RETURNTRANSFER => true,
  1171. CURLOPT_POST => true,
  1172. CURLOPT_POSTFIELDS => json_encode($postData),
  1173. CURLOPT_SSL_VERIFYPEER => false,
  1174. CURLOPT_TIMEOUT => 30,
  1175. ]);
  1176. $responseRaw = curl_exec($curl);
  1177. curl_close($curl);
  1178. $res = json_decode($responseRaw, true);
  1179. if (isset($res['ResponseCode']) && $res['ResponseCode'] == '0') {
  1180. $checkoutRequestId = $res['CheckoutRequestID'] ?? '';
  1181. $merchantRequestId = $res['MerchantRequestID'] ?? '';
  1182. $mpesaReq = new MpesaPaymentRequest();
  1183. $mpesaReq->setMerchantRequestId($merchantRequestId);
  1184. $mpesaReq->setCheckoutRequestId($checkoutRequestId);
  1185. $mpesaReq->setDescription("Bus Booking Seat {$seatNumber} Trip {$tripId}");
  1186. $mpesaReq->setMessage($res['CustomerMessage'] ?? $res['ResponseDescription'] ?? '');
  1187. $mpesaReq->setPhone((int) $phone);
  1188. $mpesaReq->setCreatedAt(new DateTime());
  1189. $em->persist($mpesaReq);
  1190. $em->flush();
  1191. return new JsonResponse([
  1192. 'success' => true,
  1193. 'checkoutRequestId' => $checkoutRequestId,
  1194. 'message' => 'STK Push sent to ' . $phone . '. Please enter PIN on phone.'
  1195. ]);
  1196. } else {
  1197. $errorMsg = $res['errorMessage'] ?? $res['ResponseDescription'] ?? 'Failed to send STK Push prompt.';
  1198. return new JsonResponse([
  1199. 'success' => false,
  1200. 'message' => $errorMsg
  1201. ], Response::HTTP_BAD_REQUEST);
  1202. }
  1203. }
  1204. /**
  1205. * @Route("/stk-push/callback", methods={"POST"}, name="bus_stk_push_callback")
  1206. */
  1207. public function callbackStkPush(Request $request): JsonResponse
  1208. {
  1209. $em = $this->entityManager;
  1210. $content = $request->getContent();
  1211. $data = json_decode($content, true);
  1212. $stkCallback = $data['Body']['stkCallback'] ?? null;
  1213. if ($stkCallback) {
  1214. $merchantRequestId = $stkCallback['MerchantRequestID'] ?? '';
  1215. $checkoutRequestId = $stkCallback['CheckoutRequestID'] ?? '';
  1216. $resultCode = (int) ($stkCallback['ResultCode'] ?? -1);
  1217. $resultDesc = $stkCallback['ResultDesc'] ?? '';
  1218. $mpesaResp = new MpesaResponse();
  1219. $mpesaResp->setMerchantRequestId($merchantRequestId);
  1220. $mpesaResp->setCheckoutRequestId($checkoutRequestId);
  1221. $mpesaResp->setCode($resultCode);
  1222. $mpesaResp->setDescription($resultDesc);
  1223. $mpesaResp->setCreatedAt(new DateTime());
  1224. $em->persist($mpesaResp);
  1225. if ($resultCode === 0) {
  1226. $items = $stkCallback['CallbackMetadata']['Item'] ?? [];
  1227. $receipt = '';
  1228. $amount = 0;
  1229. $phone = '';
  1230. foreach ($items as $item) {
  1231. if (($item['Name'] ?? '') === 'MpesaReceiptNumber') {
  1232. $receipt = (string) $item['Value'];
  1233. }
  1234. if (($item['Name'] ?? '') === 'Amount') {
  1235. $amount = (float) $item['Value'];
  1236. }
  1237. if (($item['Name'] ?? '') === 'PhoneNumber') {
  1238. $phone = (string) $item['Value'];
  1239. }
  1240. }
  1241. if ($receipt) {
  1242. $existing = $em->getRepository(Mpesa::class)->findOneBy(['transactionId' => $receipt]);
  1243. if (!$existing) {
  1244. $mpesa = new Mpesa();
  1245. $mpesa->setTransactionId($receipt);
  1246. $mpesa->setTransactionType('CustomerPayBillOnline');
  1247. $mpesa->setTransactionTime(date('Y-m-d H:i:s'));
  1248. $mpesa->setTransactionAmount($amount);
  1249. $mpesa->setRefNumber($checkoutRequestId);
  1250. $mpesa->setShortCode(4005975);
  1251. $mpesa->setBalance(0);
  1252. $mpesa->setMsisdn($phone);
  1253. $mpesa->setFirstName('M-Pesa');
  1254. $mpesa->setLastName('Customer');
  1255. $mpesa->setMiddleName('');
  1256. $mpesa->setCreatedAt(new DateTime());
  1257. $mpesa->setIsUsed(false);
  1258. $em->persist($mpesa);
  1259. }
  1260. }
  1261. }
  1262. $em->flush();
  1263. }
  1264. return new JsonResponse(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
  1265. }
  1266. /**
  1267. * @Route("/stk-push/status/{checkoutRequestId}", methods={"GET"}, name="bus_stk_push_status")
  1268. */
  1269. public function checkStkPushStatus(string $checkoutRequestId): JsonResponse
  1270. {
  1271. $em = $this->entityManager;
  1272. $response = $em->getRepository(MpesaResponse::class)->findOneBy([
  1273. 'checkoutRequestId' => $checkoutRequestId
  1274. ]);
  1275. if ($response) {
  1276. if ($response->getCode() === 0) {
  1277. $mpesa = $em->getRepository(Mpesa::class)->findOneBy([
  1278. 'refNumber' => $checkoutRequestId
  1279. ]);
  1280. if (!$mpesa) {
  1281. $mpesa = $em->getRepository(Mpesa::class)->findOneBy([
  1282. 'shortCode' => 4005975,
  1283. 'isUsed' => false
  1284. ], ['id' => 'DESC']);
  1285. }
  1286. return new JsonResponse([
  1287. 'status' => 'SUCCESS',
  1288. 'mpesaId' => $mpesa ? $mpesa->getId() : null,
  1289. 'receipt' => $mpesa ? $mpesa->getTransactionId() : '',
  1290. 'amount' => $mpesa ? $mpesa->getTransactionAmount() : 0,
  1291. 'message' => 'Payment received successfully!'
  1292. ]);
  1293. } else {
  1294. return new JsonResponse([
  1295. 'status' => 'FAILED',
  1296. 'message' => $response->getDescription() ?: 'Payment failed or cancelled by customer.'
  1297. ]);
  1298. }
  1299. }
  1300. return new JsonResponse(['status' => 'PENDING']);
  1301. }
  1302. private function getMpesaStkToken(MpesaAuth $mpesaAuth): ?string
  1303. {
  1304. if ($mpesaAuth->getToken() && $mpesaAuth->getTokenUpdatedAt()) {
  1305. $diff = (new DateTime())->getTimestamp() - $mpesaAuth->getTokenUpdatedAt()->getTimestamp();
  1306. if ($diff < 3000) {
  1307. return $mpesaAuth->getToken();
  1308. }
  1309. }
  1310. $url = "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials";
  1311. $credentials = base64_encode("{$mpesaAuth->getConsumerKey()}:{$mpesaAuth->getConsumerSecret()}");
  1312. $curl = curl_init();
  1313. curl_setopt_array($curl, [
  1314. CURLOPT_URL => $url,
  1315. CURLOPT_HTTPHEADER => [
  1316. "Content-Type: application/json",
  1317. "Authorization: Basic {$credentials}"
  1318. ],
  1319. CURLOPT_RETURNTRANSFER => true,
  1320. CURLOPT_HTTPGET => true,
  1321. CURLOPT_SSL_VERIFYPEER => false,
  1322. CURLOPT_TIMEOUT => 15,
  1323. ]);
  1324. $response = curl_exec($curl);
  1325. curl_close($curl);
  1326. if ($response) {
  1327. $result = json_decode($response, true);
  1328. if (isset($result['access_token'])) {
  1329. $token = $result['access_token'];
  1330. $mpesaAuth->setToken($token);
  1331. $mpesaAuth->setTokenUpdatedAt(new DateTime());
  1332. $this->entityManager->flush();
  1333. return $token;
  1334. }
  1335. }
  1336. return $mpesaAuth->getToken();
  1337. }
  1338. /**
  1339. * @Route("/trip/{trip}/fare", methods={"GET"}, name="getTripFare")
  1340. */
  1341. public function getTripFare(Request $request, Trip $trip): JsonResponse
  1342. {
  1343. $em = $this->entityManager;
  1344. $originId = $request->query->get('origin');
  1345. $destinationId = $request->query->get('destination');
  1346. $originStop = $originId ? $em->getRepository(Stop::class)->find($originId) : null;
  1347. $destStop = $destinationId ? $em->getRepository(Stop::class)->find($destinationId) : null;
  1348. $fare = $this->getDatabaseFare($trip, $originStop, $destStop);
  1349. return new JsonResponse(['fare' => $fare]);
  1350. }
  1351. private function getDatabaseFare(Trip $trip, ?Stop $originStop = null, ?Stop $destinationStop = null): float
  1352. {
  1353. $em = $this->entityManager;
  1354. $originStation = $originStop ? $originStop->getStation() : null;
  1355. $destStation = $destinationStop ? $destinationStop->getStation() : null;
  1356. if ($originStation && $destStation) {
  1357. $fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
  1358. 'route' => $trip->getRoute(),
  1359. 'origin' => $originStation,
  1360. 'destination' => $destStation
  1361. ]);
  1362. if ($fareRule && $fareRule->getNormalFare() > 0) {
  1363. return (float) $fareRule->getNormalFare();
  1364. }
  1365. }
  1366. $fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
  1367. 'route' => $trip->getRoute()
  1368. ]);
  1369. if ($fareRule && $fareRule->getNormalFare() > 0) {
  1370. return (float) $fareRule->getNormalFare();
  1371. }
  1372. return (float) ($trip->getFare() ?: 0);
  1373. }
  1374. }